From: David Bremner Date: Mon, 23 Jun 2025 15:18:23 +0000 (-0300) Subject: Merge branch 'release' X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=HEAD;hp=d6e4ca6a7a3a1a18eee7be3aa5fb8d0987e0894d Merge branch 'release' --- diff --git a/.gitignore b/.gitignore index 3edd1768..eda6d9cf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,11 +10,15 @@ /bindings/python-cffi/build/ /lib/libnotmuch*.dylib /lib/libnotmuch.so* +/nmbug /notmuch +/notmuch-git /notmuch-shared /releases /sh.config /sphinx.config /version.stamp +/bindings/python-cffi/_notmuch_config.py TAGS tags +__pycache__ diff --git a/Makefile.global b/Makefile.global index fe79121d..258c379c 100644 --- a/Makefile.global +++ b/Makefile.global @@ -47,12 +47,10 @@ DEB_TAR_FILE=$(PACKAGE)_$(VERSION).orig.tar.xz SHA256_FILE=$(TAR_FILE).sha256.asc DETACHED_SIG_FILE=$(TAR_FILE).asc -PV_FILE=bindings/python/notmuch/version.py - # Smash together user's values with our extra values -FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(extra_cflags) $(CONFIGURE_CFLAGS) -FINAL_CXXFLAGS = $(CPPFLAGS) $(CXXFLAGS) $(WARN_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) $(CONFIGURE_CXXFLAGS) -FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Lutil -lnotmuch_util -Llib -lnotmuch +FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(WARN_CFLAGS) $(extra_cflags) $(CPPFLAGS) $(CONFIGURE_CFLAGS) $(CFLAGS) +FINAL_CXXFLAGS = $(WARN_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) $(CPPFLAGS) $(CONFIGURE_CXXFLAGS) $(CXXFLAGS) +FINAL_NOTMUCH_LDFLAGS = -Lutil -lnotmuch_util -Llib -lnotmuch $(LDFLAGS) ifeq ($(LIBDIR_IN_LDCONFIG),0) FINAL_NOTMUCH_LDFLAGS += $(RPATH_LDFLAGS) endif diff --git a/Makefile.local b/Makefile.local index e12b94cd..2cc9bd29 100644 --- a/Makefile.local +++ b/Makefile.local @@ -1,7 +1,7 @@ # -*- makefile-gmake -*- .PHONY: all -all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings +all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings notmuch-git nmbug ifeq ($(MAKECMDGOALS),) ifeq ($(shell cat .first-build-message 2>/dev/null),) @NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all @@ -45,16 +45,22 @@ $(SHA256_FILE): $(TAR_FILE) $(DETACHED_SIG_FILE): $(TAR_FILE) gpg --armor --detach-sign $^ +CLEAN := $(CLEAN) notmuch-git +notmuch-git: notmuch-git.py + cp $< $@ + chmod ugo+x $@ + +CLEAN := $(CLEAN) nmbug +nmbug: notmuch-git + ln -s $< $@ + .PHONY: dist dist: $(TAR_FILE) .PHONY: update-versions update-versions: - sed -i -e "s/^__VERSION__[[:blank:]]*=.*$$/__VERSION__ = \'${VERSION}\'/" \ - -e "s/^SOVERSION[[:blank:]]*=.*$$/SOVERSION = \'${LIBNOTMUCH_VERSION_MAJOR}\'/" \ - ${PV_FILE} - cp version.txt bindings/python-cffi + @echo nothing to do # We invoke make recursively only to force ordering of our phony # targets in the case of parallel invocation of make (-j). @@ -74,7 +80,7 @@ release: verify-source-tree-and-version ln -sf $(TAR_FILE) $(DEB_TAR_FILE) pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG) mkdir -p releases - mv $(TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) releases + mv $(TAR_FILE) $(DEB_TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) releases $(MAKE) VERSION=$(VERSION) release-message > $(PACKAGE)-$(VERSION).announce ifeq ($(REALLY_UPLOAD),yes) git push origin $(VERSION) release pristine-tar @@ -93,7 +99,11 @@ pre-release: ln -sf $(TAR_FILE) $(DEB_TAR_FILE) pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG) mkdir -p releases - mv $(TAR_FILE) $(DEB_TAR_FILE) releases + mv $(TAR_FILE) $(DEB_TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) releases +ifeq ($(REALLY_UPLOAD),yes) + git push origin $(UPSTREAM_TAG) release pristine-tar + cd releases && scp $(TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) $(RELEASE_HOST):$(RELEASE_DIR) +endif .PHONY: debian-snapshot debian-snapshot: @@ -121,8 +131,7 @@ release-message: @echo "Which can be verified with:" @echo "" @echo " $(RELEASE_URL)/$(SHA256_FILE)" - @echo -n " " - @cat releases/$(SHA256_FILE) + @sed "s/^/ /" releases/$(SHA256_FILE) @echo "" @echo " $(RELEASE_URL)/$(DETACHED_SIG_FILE)" @echo " (signed by `getent passwd "$$USER" | cut -d: -f 5 | cut -d, -f 1`)" @@ -170,7 +179,7 @@ release-checks: .PHONY: verify-newer verify-newer: - @echo -n "Checking that no $(VERSION) release already exists..." + @printf %s "Checking that no $(VERSION) release already exists..." @wget -q --no-check-certificate -O /dev/null $(RELEASE_URL)/$(TAR_FILE) ; \ case $$? in \ 8) echo "Good." ;; \ diff --git a/NEWS b/NEWS index d08183bf..0d284b11 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,426 @@ +Notmuch 0.39 (2025-03-15) +========================= + +General +------- + +Allow to customize the separator between individual and matched and +non-matched authors when showing threads. See +`search.authors_separator` and `search.authors_matched_separator` in +notmuch-config(1). + +Library +------- + +Make the s-expression query parser thread-safe. + +Emacs +----- + +The new variable `untrusted-content` is now (conditionally) set in +relevant modes. + +Add `notmuch-mua-subject-regexp` to help avoid sending erroneous +subjects. + +Allow toggling display of excluded messages. + +Fix for text/calendar display. Fix for subject sanitizing in tree +mode. Fix for hl-line location in search and tree mode. Fix for +interaction with which-func-mode and imenu. Fix for saving hidden +headers in drafts. + +Support for Emacs older than 27.1 is deprecated with this release. + +Legacy Python Bindings +---------------------- + +The legacy python bindings previously in `bindings/python` are no +longer supported. These bindings have been deprecated since 2020. As +of this release they are no longer built or tested, and patches for +them will no longer be accepted. They have been moved to `contrib` to +smooth the transition. Thanks very much to Sebastian Spaeth and all of +the other contributors to these bindings over the years. + +Python Bindings +--------------- + +Adapt python-cffi version handling to stricter version constraints in +setuptools. + +Fix iterator related segfaults. + +Ruby Bindings +------------- + +Fix for warnings about "undefining the allocator of T_DATA". + +Test Suite +---------- + +Adapt test suite for changes in Emacs 30.1 and GnuPG 2.4 series. + +Notmuch 0.38.3 (2024-03-09) +=========================== + +CLI +--- + +Fix a bug in configuration code that caused the notmuch command to +erroneously report "Error: could not locate database" under some +circumstances. + +Notmuch 0.38.2 (2023-12-01) +=========================== + +Library +------- + +Make sorting of string maps lexicographic on (key,value) pairs. This +avoids some test failures due to variation in message property output +order. + +Emacs +----- + +Avoid extra separators after the last address in `notmuch-emacs-mua`. + + +Notmuch 0.38.1 (2023-10-26) +=========================== + +CLI +--- + +Report parse errors in config files. + +Emacs +----- + +Fix image toggling for Emacs >= 29.1. + +notmuch-mutt +------------ + +Fix syntax error in script. + +Notmuch 0.38 (2023-09-12) +========================= + +General +------- + +Support relative lastmod queries (see notmuch-sexp-queries(7) and +notmuch-search-terms(7) for details). + +Support indexing of designated attachments as text (see +notmuch-config(1) for details). + +CLI +--- + +Add options --offset and --limit to notmuch-show(1). + +Emacs +----- + +New commands notmuch-search-edit-search and notmuch-tree-edit-search. + +Introduce notmuch-tree-outline-mode. + +Some compatibility fixes for Emacs 29. At least one issue (hiding +images) remains in 0.38. + +Support completion when piping to external command. + +Fix regression in updating tag display introduced by 0.37. + +Library +------- + +Fix bug creating database when database.path is not set. + +Incremental performance improvements for message deletion. + +Catch Xapian exceptions when deleting messages. + +Sync removed message properties to the database. + +Replace use of thread-unsafe Query::MatchAll in the infix query +parser. + +Notmuch-Mutt +------------ + +Be more careful when clearing the results directory. + +Ruby +---- + +Use `database_open_with_config`, and provide compatible path search +semantics. + +Bugfix for query.get_sort + +Test Suite +---------- + +Support testing installed version of notmuch. + +Adapt to some breaking changes in glib handling of init files. + +Replace OpenPGP key used in test suite. + +Performance Tests +----------------- + +Update signatures for performance test corpus. + +Notmuch 0.37 (2022-08-21) +========================= + +Library +------- + +Fix uninitialized field in message objects. + +Improve exception handling and error propagation for message objects. + +Sexp Queries +------------ + +Add one sided lastmod ranges for sexp queries. + +Expand macro parameters inside regex and wildcard modifiers. + +Command Line Interface +---------------------- + +`notmuch help` now works for external commands. + +`NOTMUCH_CONFIG` is now passed to external commands and hooks. + +Promote the development tool `nmbug` to a user facing tool +`notmuch-git`. See notmuch-git(1) for details. + +Emacs +----- + +The function `notmuch-mua-mail` now moves point depending on the +provided arguments. + +Restrict what mime types are inlined in replies and on refresh. + +The functions in notmuch-query.el are now obsolete and may be removed +in a future version of Notmuch. + +Add some controls for lazy display of message bodies (See "Dealing +with large messages and threads" in the notmuch-emacs documentation). + +Allow the user to select (with '%') a different duplicate message file +to display. + +Use `message-dont-reply-to-names` in `notmuch-message-mode`. + +Support custom header-line format for notmuch-show mode. + +Notmuch 0.36 (2022-04-25) +========================= + +Library +------- + +Add the `sexp` prefix to the infix (traditional) query parser. This +allows specific subqueries to be parsed by the sexp parser (with +appropriate quoting). See `notmuch-search-terms(7)` for details. + +Add another heuristic to regexp fields to prevent phrase parsing of +bracketed sub-expressions. + +Command Line Interface +---------------------- + +Envelope from ("From ") headers are now escaped as X-Envelope-From: in +input to `notmuch-insert`. This prevents creating mbox files when +calling `notmuch-insert` from e.g. `postfix`. + +Python (CFFI) Bindings +---------------------- + +Use the `config_pairs` API in ConfigIterator. This returns all +matching key-value pairs, not just those that happen to be stored in +the database. + +Documentation +------------- + +Reorganize documentation for `notmuch-config`. Add a few links from +other man pages. + +Emacs +----- + +Bind the usual undo key sequences to new command +"notmuch-tag-undo". This allows transparent undo of tagging +operations. + +Tests +----- + +Fix smime.4 with newer gmime. Unset `XDG_DATA_HOME` and `MAILDIR` for tests. + +New add-on tool: notmuch-web +----------------------------- + +The new devel/ tool `notmuch-web` is a very thin web client. It +supports a full search interface for one user: there is no facility +for multiple users provided today. See the notmuch-web README file +for more information. + +Be careful about running it on a network-connected system: it will +expose a web interface that requires no authentication but exposes +your mail store. + +Notmuch 0.35 (2022-02-06) +========================= + +Library +------- + +Implement the `date` and `lastmod` fields in the S-expression parser. + +Ignore trailing `/` for pathnames in both query parsers. + +Rename configuration option `built_with.sexpr_query` to +`built_with.sexp_queries`. + +Do not assume a default mail root in split (e.g. XDG) configurations. + +Fix some small memory leaks in `notmuch_database_open_with_config`. + +CLI +--- + +Improve handling of leading/trailing punctuation and space for +configuration lists. + +Only ignore `.notmuch` at the top level in `notmuch new`. + +Optionally show extra headers in `notmuch show`. See +`show.extra_headers` in notmuch-config(1). + +Emacs +----- + +Drop `C-TAB` binding in hello mode, document `backtab`. + +Fix visual glitch in search mode by running `notmuch-search-hook` +lazily. + +Don't add space to completion candidates, improves compatibility with +third party completion frameworks. + +Make citation formatting more robust against whitespace. + +Use `--excludes=false` when generating the 'All tags' section. + +Use cached copy of message body for `Fcc`, avoiding variant bodies for +signed and/or encrypted messages. + +Add notmuch-logo.svg and use it in notmuch-hello view, replacing +the .png version. + +Make header line in show buffers optional. + +Add customizable names for search buffers. + +Build +----- + +Fix out-of-tree build for `python-cffi` bindings. + +Rearrange position of {C,CXX,CPP,LD}FLAGS, prevent some clashes with +installed version of notmuch. + +Ignore more configure options. + +Test Suite +---------- + +Replace some uses of `gdb` in the test suite with `LD_PRELOAD` based +shims. + +Use `--with-colons` for gpgsm, fix compatibility with newer gnupg. + +Python bindings +--------------- + +Add `matched` property to message objects. + +Users are reminded that the old python bindings in bindings/python are +deprecated; this will probably be the last major release that ships +them. + +Completion +---------- + +Use `database.mail_root` for path completion in bash/zsh. + +Notmuch 0.34.3 (2022-01-09) +=========================== + +Library +------- + +Do not crash when presented with a .notmuch directory without a +xapian/ subdirectory. + +Python Bindings (notmuch2) +-------------------------- + +Database constructor now searches for configuration by default. Pass +`config=Database.CONFIG.EMPTY` to disable. + +The `Message.replies()` method now returns OwnedMessage objects, to +prevent certain memory de-allocation errors. + +Fix for importing `notmuch2` module when building bindings +documentation. + +Notmuch 0.34.2 (2021-12-09) +=========================== + +Library +------- + +Fix a bug that wrongly resolved conflict between the `database_path` +parameter to `notmuch_database_open_with_config` and configuration +item `database.path` in favour of the latter. + +Python Bindings (notmuch2) +-------------------------- + +When building the documentation for the `notmuch2` python module, +import from the built module, not a system wide installed one. + +The notmuch2.Database constructor now uses the library function +`notmuch_database_open_with_config` to support the same configuration +and database location options as the library does. + +Fix some unprintable exception objects. + +Notmuch 0.34.1 (2021-11-03) +=========================== + +Library +------- + +Fix for deallocation and nulling of output parameter for +notmuch_database_{open_with,create_with,load}_config when errors +occur. This change fixes a potential use-after-free bug that has been +present since 0.32. This release also improves the documentation of +status returns for the same 3 functions. + Notmuch 0.34 (2021-10-20) ========================= @@ -298,7 +721,7 @@ Notmuch 0.31.2 (2020-11-08) Build ----- -Catch one more occurence of "version" in the build system, which +Catch one more occurrence of "version" in the build system, which caused the file to be regenerated in the release tarball. Notmuch 0.31.1 (2020-11-08) diff --git a/bindings/Makefile.local b/bindings/Makefile.local index 3672e69f..72fe9850 100644 --- a/bindings/Makefile.local +++ b/bindings/Makefile.local @@ -3,21 +3,26 @@ dir := bindings # force the shared library to be built -ruby-bindings: lib/$(LINKER_NAME) +ruby-bindings: $(dir)/ruby.stamp + +$(dir)/ruby.stamp: lib/$(LINKER_NAME) ifeq ($(HAVE_RUBY_DEV),1) cd $(dir)/ruby && \ - EXTRA_LDFLAGS="$(NO_UNDEFINED_LDFLAGS)" \ + EXTRA_LDFLAGS="$(NO_UNDEFINED_LDFLAGS) $(TALLOC_LDFLAGS)" \ + EXTRA_INCFLAGS="-I$(NOTMUCH_SRCDIR)/lib $(TALLOC_CFLAGS)" \ LIBNOTMUCH="../../lib/$(LINKER_NAME)" \ - NOTMUCH_SRCDIR='$(NOTMUCH_SRCDIR)' \ $(RUBY) extconf.rb --vendor - $(MAKE) -C $(dir)/ruby CFLAGS="$(CFLAGS) -pipe -fno-plt -fPIC" + $(MAKE) -C $(dir)/ruby CFLAGS="$(CFLAGS) -pipe -fno-plt -fPIC" && touch $@ endif -python-cffi-bindings: lib/$(LINKER_NAME) +python-cffi-bindings: $(dir)/python-cffi.stamp + +$(dir)/python-cffi.stamp: lib/$(LINKER_NAME) ifeq ($(HAVE_PYTHON3_CFFI),1) cd $(dir)/python-cffi && \ ${PYTHON} setup.py build --build-lib build/stage && \ - mkdir -p build/stage/tests && cp tests/*.py build/stage/tests + mkdir -p build/stage/tests && cp tests/*.py build/stage/tests && \ + touch ../python-cffi.stamp endif CLEAN += $(patsubst %,$(dir)/ruby/%, \ @@ -26,6 +31,10 @@ CLEAN += $(patsubst %,$(dir)/ruby/%, \ init.o message.o messages.o mkmf.log notmuch.so query.o \ status.o tags.o thread.o threads.o) -CLEAN += bindings/ruby/.vendorarchdir.time +CLEAN += bindings/ruby/.vendorarchdir.time $(dir)/ruby.stamp + +CLEAN += bindings/python-cffi/build $(dir)/python-cffi.stamp +CLEAN += bindings/python-cffi/__pycache__ -CLEAN += bindings/python-cffi/build +DISTCLEAN += bindings/python-cffi/_notmuch_config.py \ + bindings/python-cffi/notmuch2.egg-info diff --git a/bindings/python-cffi/notmuch2/__init__.py b/bindings/python-cffi/notmuch2/__init__.py index f281edc1..978e01c2 100644 --- a/bindings/python-cffi/notmuch2/__init__.py +++ b/bindings/python-cffi/notmuch2/__init__.py @@ -22,7 +22,7 @@ Libnotmuch uses a hierarchical memory allocator, this means all objects have a strict parent-child relationship and when the parent is freed all the children are freed as well. This has some implications for these Python bindings as parent objects need to be kept alive. -This is normally schielded entirely from the user however and the +This is normally shielded entirely from the user however and the Python objects automatically make sure the right references are kept alive. It is however the reason the :class:`BaseObject` exists as it defines the API all Python objects need to implement to work @@ -37,9 +37,9 @@ In these python bindings they have sometimes been exposed as like :class:`collections.abc.Set` or :class:`collections.abc.Mapping` etc. This gives a more natural API to work with, e.g. being able to treat tags as sets. However it does mean that the -:meth:`__contains__`, :meth:`__len__` and frieds methods on these are -usually more and essentially O(n) rather than O(1) as you might -usually expect from Python containers. +:meth:`__contains__`, :meth:`__len__` and similar methods on these are +usually more expensive and essentially O(n) rather than O(1) as you might +expect from Python containers. """ from notmuch2 import _capi diff --git a/bindings/python-cffi/notmuch2/_base.py b/bindings/python-cffi/notmuch2/_base.py index 1cf03c88..1b0b5b7e 100644 --- a/bindings/python-cffi/notmuch2/_base.py +++ b/bindings/python-cffi/notmuch2/_base.py @@ -28,7 +28,7 @@ class NotmuchObject(metaclass=abc.ABCMeta): However during some peculiar situations, e.g. interpreter shutdown, it is possible for the :meth:`__del__` method to have - been called, whele there are still references to an object. This + been called, while there are still references to an object. This could result in child objects asking their memory to be freed after the parent has already freed the memory, making things rather unhappy as double frees are not taken lightly in C. To @@ -167,14 +167,14 @@ class NotmuchIter(NotmuchObject, collections.abc.Iterator): It is tempting to use a generator function instead, but this would not correctly respect the :class:`NotmuchObject` memory handling protocol and in some unsuspecting cornercases cause memory - trouble. You probably want to sublcass this in order to wrap the + trouble. You probably want to subclass this in order to wrap the value returned by :meth:`__next__`. :param parent: The parent object. :type parent: NotmuchObject :param iter_p: The CFFI pointer to the C iterator. :type iter_p: cffi.cdata - :param fn_destory: The CFFI notmuch_*_destroy function. + :param fn_destroy: The CFFI notmuch_*_destroy function. :param fn_valid: The CFFI notmuch_*_valid function. :param fn_get: The CFFI notmuch_*_get function. :param fn_next: The CFFI notmuch_*_move_to_next function. @@ -223,7 +223,6 @@ class NotmuchIter(NotmuchObject, collections.abc.Iterator): def __next__(self): if not self._fn_valid(self._iter_p): - self._destroy() raise StopIteration() obj_p = self._fn_get(self._iter_p) self._fn_next(self._iter_p) diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py index 24df939e..65d7dcb6 100644 --- a/bindings/python-cffi/notmuch2/_build.py +++ b/bindings/python-cffi/notmuch2/_build.py @@ -1,5 +1,5 @@ import cffi - +from _notmuch_config import * ffibuilder = cffi.FFI() ffibuilder.set_source( @@ -16,8 +16,8 @@ ffibuilder.set_source( #ERROR libnotmuch version < 5.1 not supported #endif """, - include_dirs=['../../lib'], - library_dirs=['../../lib'], + include_dirs=[NOTMUCH_INCLUDE_DIR], + library_dirs=[NOTMUCH_LIB_DIR], libraries=['notmuch'], ) ffibuilder.cdef( @@ -54,6 +54,8 @@ ffibuilder.cdef( NOTMUCH_STATUS_NO_DATABASE, NOTMUCH_STATUS_DATABASE_EXISTS, NOTMUCH_STATUS_BAD_QUERY_SYNTAX, + NOTMUCH_STATUS_NO_MAIL_ROOT, + NOTMUCH_STATUS_CLOSED_DATABASE, NOTMUCH_STATUS_LAST_STATUS } notmuch_status_t; typedef enum { @@ -96,27 +98,25 @@ ffibuilder.cdef( typedef struct _notmuch_string_map_iterator notmuch_message_properties_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_pairs notmuch_config_pairs_t; typedef struct _notmuch_indexopts notmuch_indexopts_t; const char * notmuch_status_to_string (notmuch_status_t status); notmuch_status_t - notmuch_database_create_verbose (const char *path, - notmuch_database_t **database, - char **error_message); - notmuch_status_t - notmuch_database_create (const char *path, notmuch_database_t **database); + notmuch_database_create_with_config (const char *database_path, + const char *config_path, + const char *profile, + notmuch_database_t **database, + char **error_message); notmuch_status_t - notmuch_database_open_verbose (const char *path, - notmuch_database_mode_t mode, - notmuch_database_t **database, - char **error_message); - 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); notmuch_status_t notmuch_database_close (notmuch_database_t *database); notmuch_status_t @@ -326,18 +326,18 @@ ffibuilder.cdef( notmuch_database_set_config (notmuch_database_t *db, const char *key, const char *value); notmuch_status_t notmuch_database_get_config (notmuch_database_t *db, const char *key, char **value); - notmuch_status_t - notmuch_database_get_config_list (notmuch_database_t *db, const char *prefix, notmuch_config_list_t **out); + notmuch_config_pairs_t * + notmuch_config_get_pairs (notmuch_database_t *db, const char *prefix); notmuch_bool_t - notmuch_config_list_valid (notmuch_config_list_t *config_list); + notmuch_config_pairs_valid (notmuch_config_pairs_t *config_list); const char * - notmuch_config_list_key (notmuch_config_list_t *config_list); + notmuch_config_pairs_key (notmuch_config_pairs_t *config_list); const char * - notmuch_config_list_value (notmuch_config_list_t *config_list); + notmuch_config_pairs_value (notmuch_config_pairs_t *config_list); void - notmuch_config_list_move_to_next (notmuch_config_list_t *config_list); + notmuch_config_pairs_move_to_next (notmuch_config_pairs_t *config_list); void - notmuch_config_list_destroy (notmuch_config_list_t *config_list); + notmuch_config_pairs_destroy (notmuch_config_pairs_t *config_list); """ ) diff --git a/bindings/python-cffi/notmuch2/_config.py b/bindings/python-cffi/notmuch2/_config.py index 29de6495..4df03bd0 100644 --- a/bindings/python-cffi/notmuch2/_config.py +++ b/bindings/python-cffi/notmuch2/_config.py @@ -13,27 +13,42 @@ class ConfigIter(base.NotmuchIter): def __init__(self, parent, iter_p): super().__init__( parent, iter_p, - fn_destroy=capi.lib.notmuch_config_list_destroy, - fn_valid=capi.lib.notmuch_config_list_valid, - fn_get=capi.lib.notmuch_config_list_key, - fn_next=capi.lib.notmuch_config_list_move_to_next) + fn_destroy=capi.lib.notmuch_config_pairs_destroy, + fn_valid=capi.lib.notmuch_config_pairs_valid, + fn_get=capi.lib.notmuch_config_pairs_key, + fn_next=capi.lib.notmuch_config_pairs_move_to_next) def __next__(self): - item = super().__next__() - return base.BinString.from_cffi(item) - + # skip pairs whose value is NULL + while capi.lib.notmuch_config_pairs_valid(super()._iter_p): + val_p = capi.lib.notmuch_config_pairs_value(super()._iter_p) + key_p = capi.lib.notmuch_config_pairs_key(super()._iter_p) + if key_p == capi.ffi.NULL: + # this should never happen + raise errors.NullPointerError + key = base.BinString.from_cffi(key_p) + capi.lib.notmuch_config_pairs_move_to_next(super()._iter_p) + if val_p != capi.ffi.NULL and base.BinString.from_cffi(val_p) != "": + return key + self._destroy() + raise StopIteration class ConfigMapping(base.NotmuchObject, collections.abc.MutableMapping): - """The config key/value pairs stored in the database. + """The config key/value pairs loaded from the database, config file, + and/or defaults. The entries are exposed as a :class:`collections.abc.MutableMapping` object. Note that setting a value to an empty string is the same as deleting it. + Mutating (deleting or updating values) in the map persists only in + the database, which can be shadowed by config files. + :param parent: the parent object :param ptr_name: the name of the attribute on the parent which will return the memory pointer. This allows this object to access the pointer via the parent's descriptor and thus trigger :class:`MemoryPointer`'s memory safety. + """ def __init__(self, parent, ptr_name): @@ -77,11 +92,10 @@ class ConfigMapping(base.NotmuchObject, collections.abc.MutableMapping): :raises NullPointerError: If the iterator can not be created. """ - configlist_pp = capi.ffi.new('notmuch_config_list_t**') - ret = capi.lib.notmuch_database_get_config_list(self._ptr(), b'', configlist_pp) - if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: - raise errors.NotmuchError(ret) - return ConfigIter(self._parent, configlist_pp[0]) + config_pairs_p = capi.lib.notmuch_config_get_pairs(self._ptr(), b'') + if config_pairs_p == capi.ffi.NULL: + raise KeyError + return ConfigIter(self._parent, config_pairs_p) def __len__(self): return sum(1 for t in self) diff --git a/bindings/python-cffi/notmuch2/_database.py b/bindings/python-cffi/notmuch2/_database.py index 868f4408..f1451bf1 100644 --- a/bindings/python-cffi/notmuch2/_database.py +++ b/bindings/python-cffi/notmuch2/_database.py @@ -15,7 +15,7 @@ import notmuch2._query as querymod import notmuch2._tags as tags -__all__ = ['Database', 'AtomicContext', 'DbRevision'] +__all__ = ['Database', 'AtomicContext', 'DbRevision', 'IndexOptions'] def _config_pathname(): @@ -31,6 +31,9 @@ class Mode(enum.Enum): READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE +class ConfigFile(enum.Enum): + EMPTY = b'' + SEARCH = capi.ffi.NULL class QuerySortOrder(enum.Enum): OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST @@ -71,6 +74,9 @@ class Database(base.NotmuchObject): :cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``, ``FLAG``, ``FALSE`` or ``ALL``. See the query documentation for details. + :cvar CONFIG: Control loading of config file. Enumeration of + ``EMPTY`` (don't load a config file), and ``SEARCH`` (search as + in :ref:`config_search`) :cvar AddedMessage: A namedtuple ``(msg, dup)`` used by :meth:`add` as return value. :cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items. @@ -81,9 +87,8 @@ class Database(base.NotmuchObject): still open. :param path: The directory of where the database is stored. If - ``None`` the location will be read from the user's - configuration file, respecting the ``NOTMUCH_CONFIG`` - environment variable if set. + ``None`` the location will be searched according to + :ref:`database` :type path: str, bytes, os.PathLike or pathlib.Path :param mode: The mode to open the database in. One of :attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`. For @@ -91,6 +96,8 @@ class Database(base.NotmuchObject): :attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`. :type mode: :attr:`MODE` or str. + :param config: Where to load the configuration from, if any. + :type config: :attr:`CONFIG.EMPTY`, :attr:`CONFIG.SEARCH`, str, bytes, os.PathLike, pathlib.Path :raises KeyError: if an unknown mode string is used. :raises OSError: or subclasses if the configuration file can not be opened. @@ -102,6 +109,7 @@ class Database(base.NotmuchObject): MODE = Mode SORT = QuerySortOrder EXCLUDE = QueryExclude + CONFIG = ConfigFile AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup']) _db_p = base.MemoryPointer() STR_MODE_MAP = { @@ -109,18 +117,40 @@ class Database(base.NotmuchObject): 'rw': MODE.READ_WRITE, } - def __init__(self, path=None, mode=MODE.READ_ONLY): + @staticmethod + def _cfg_path_encode(path): + if isinstance(path,ConfigFile): + path = path.value + elif path is None: + path = capi.ffi.NULL + elif not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): + path = bytes(path) + else: + path = os.fsencode(path) + return path + + @staticmethod + def _db_path_encode(path): + if path is None: + path = capi.ffi.NULL + elif not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): + path = bytes(path) + else: + path = os.fsencode(path) + return path + + def __init__(self, path=None, mode=MODE.READ_ONLY, config=CONFIG.SEARCH): if isinstance(mode, str): mode = self.STR_MODE_MAP[mode] self.mode = mode - if path is None: - path = self.default_path() - if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): - path = bytes(path) + db_pp = capi.ffi.new('notmuch_database_t **') cmsg = capi.ffi.new('char**') - ret = capi.lib.notmuch_database_open_verbose(os.fsencode(path), - mode.value, db_pp, cmsg) + ret = capi.lib.notmuch_database_open_with_config(self._db_path_encode(path), + mode.value, + self._cfg_path_encode(config), + capi.ffi.NULL, + db_pp, cmsg) if cmsg[0]: msg = capi.ffi.string(cmsg[0]).decode(errors='replace') capi.lib.free(cmsg[0]) @@ -132,18 +162,20 @@ class Database(base.NotmuchObject): self.closed = False @classmethod - def create(cls, path=None): + def create(cls, path=None, config=ConfigFile.EMPTY): """Create and open database in READ_WRITE mode. This is creates a new notmuch database and returns an opened instance in :attr:`MODE.READ_WRITE` mode. - :param path: The directory of where the database is stored. If - ``None`` the location will be read from the user's - configuration file, respecting the ``NOTMUCH_CONFIG`` - environment variable if set. + :param path: The directory of where the database is stored. + If ``None`` the location will be read searched by the + notmuch library (see notmuch(3)::notmuch_open_with_config). :type path: str, bytes or os.PathLike + :param config: The pathname of the notmuch configuration file. + :type config: :attr:`CONFIG.EMPTY`, :attr:`CONFIG.SEARCH`, str, bytes, os.PathLike, pathlib.Path + :raises OSError: or subclasses if the configuration file can not be opened. :raises configparser.Error: or subclasses if the configuration @@ -154,14 +186,13 @@ class Database(base.NotmuchObject): :returns: The newly created instance. """ - if path is None: - path = cls.default_path() - if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): - path = bytes(path) + db_pp = capi.ffi.new('notmuch_database_t **') cmsg = capi.ffi.new('char**') - ret = capi.lib.notmuch_database_create_verbose(os.fsencode(path), - db_pp, cmsg) + ret = capi.lib.notmuch_database_create_with_config(cls._db_path_encode(path), + cls._cfg_path_encode(config), + capi.ffi.NULL, + db_pp, cmsg) if cmsg[0]: msg = capi.ffi.string(cmsg[0]).decode(errors='replace') capi.lib.free(cmsg[0]) @@ -176,7 +207,7 @@ class Database(base.NotmuchObject): ret = capi.lib.notmuch_database_destroy(db_pp[0]) if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: raise errors.NotmuchError(ret) - return cls(path, cls.MODE.READ_WRITE) + return cls(path, cls.MODE.READ_WRITE, config=config) @staticmethod def default_path(cfg_path=None): @@ -187,8 +218,8 @@ class Database(base.NotmuchObject): :param cfg_path: The pathname of the notmuch configuration file. If not specified tries to use the pathname provided in the - :env:`NOTMUCH_CONFIG` environment variable and falls back - to :file:`~/.notmuch-config. + :envvar:`NOTMUCH_CONFIG` environment variable and falls back + to :file:`~/.notmuch-config`. :type cfg_path: str, bytes, os.PathLike or pathlib.Path. :returns: The path of the database, which does not necessarily @@ -198,8 +229,11 @@ class Database(base.NotmuchObject): be opened. :raises configparser.Error: or subclasses if the configuration file can not be parsed. - :raises NotmuchError if the config file does not have the + :raises NotmuchError: if the config file does not have the database.path setting. + + .. deprecated:: 0.35 + Use the ``config`` parameter to :meth:`__init__` or :meth:`__create__` instead. """ if not cfg_path: cfg_path = _config_pathname() @@ -317,7 +351,7 @@ class Database(base.NotmuchObject): The returned context manager can be used to perform atomic operations on the database. - .. note:: Unlinke a traditional RDBMS transaction this does + .. note:: Unlike a traditional RDBMS transaction this does not imply durability, it only ensures the changes are performed atomically. @@ -382,7 +416,7 @@ class Database(base.NotmuchObject): :raises XapianError: A Xapian exception occurred. :raises FileError: The file referred to by ``pathname`` could not be opened. - :raises FileNotEmailError: The file referreed to by + :raises FileNotEmailError: The file referred to by ``pathname`` is not recognised as an email message. :raises ReadOnlyDatabaseError: The database is opened in READ_ONLY mode. diff --git a/bindings/python-cffi/notmuch2/_errors.py b/bindings/python-cffi/notmuch2/_errors.py index f55cc96b..17c3ad9c 100644 --- a/bindings/python-cffi/notmuch2/_errors.py +++ b/bindings/python-cffi/notmuch2/_errors.py @@ -83,7 +83,8 @@ class NotmuchError(Exception): if self.message: return self.message elif self.status: - return capi.lib.notmuch_status_to_string(self.status) + char_str = capi.lib.notmuch_status_to_string(self.status) + return capi.ffi.string(char_str).decode(errors='replace') else: return 'Unknown error' diff --git a/bindings/python-cffi/notmuch2/_message.py b/bindings/python-cffi/notmuch2/_message.py index 2f232076..79485238 100644 --- a/bindings/python-cffi/notmuch2/_message.py +++ b/bindings/python-cffi/notmuch2/_message.py @@ -26,7 +26,7 @@ class Message(base.NotmuchObject): package from Python's standard library. You could e.g. create this as such:: - notmuch_msg = db.get_message(msgid) # or from a query + notmuch_msg = db.find(msgid) # or from a query parser = email.parser.BytesParser(policy=email.policy.default) with notmuch_msg.path.open('rb) as fp: email_msg = parser.parse(fp) @@ -80,12 +80,6 @@ class Message(base.NotmuchObject): def messageid(self): """The message ID as a string. - The message ID is decoded with the ignore error handler. This - is fine as long as the message ID is well formed. If it is - not valid ASCII then this will be lossy. So if you need to be - able to write the exact same message ID back you should use - :attr:`messageidb`. - Note that notmuch will decode the message ID value and thus strip off the surrounding ``<`` and ``>`` characters. This is different from Python's :mod:`email` package behaviour which @@ -205,6 +199,20 @@ class Message(base.NotmuchObject): self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED) return bool(ret) + @property + def matched(self): + """Indicates whether this message was matched by the query. + + When a thread is created from a search, some of the + messages may not match the original query. This property + is set to *True* for those that do match. + + :raises ObjectDestroyedError: if used after destroyed. + """ + ret = capi.lib.notmuch_message_get_flag( + self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_MATCH) + return bool(ret) + @property def date(self): """The message date as an integer. @@ -232,8 +240,7 @@ class Message(base.NotmuchObject): :param header: Case-insensitive header name to retrieve. :type header: str or bytes - :returns: The header value, an empty string if the header is - not present. + :returns: The header value. :rtype: str :raises LookupError: if the header is not present. @@ -335,9 +342,9 @@ class Message(base.NotmuchObject): and thus are only recommended if you know there to be only one value. - Instead the map has an additional :meth:`PropertiesMap.all` + Instead the map has an additional :meth:`PropertiesMap.getall` method which can be used to retrieve all properties of a given - key. This method also allows iterating of a a subset of the + key. This method also allows iterating of a subset of the keys starting with a given prefix. """ try: @@ -357,14 +364,14 @@ class Message(base.NotmuchObject): This method will only work if the message was created from a thread. Otherwise it will yield no results. - :returns: An iterator yielding :class:`Message` instances. + :returns: An iterator yielding :class:`OwnedMessage` instances. :rtype: MessageIter """ # The notmuch_messages_valid call accepts NULL and this will # become an empty iterator, raising StopIteration immediately. # Hence no return value checking here. msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p) - return MessageIter(self, msgs_p, db=self._db) + return MessageIter(self, msgs_p, db=self._db, msg_cls=OwnedMessage) def __hash__(self): return hash(self.messageid) @@ -421,7 +428,7 @@ class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping): """A mutable mapping to manage properties. Both keys and values of properties are supposed to be UTF-8 - strings in libnotmuch. However since the uderlying API uses + strings in libnotmuch. However since the underlying API uses bytestrings you can use either str or bytes to represent keys and all returned keys and values use :class:`BinString`. @@ -479,7 +486,7 @@ class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping): return len(list(it)) def __getitem__(self, key): - """Return **the first** peroperty associated with a key.""" + """Return **the first** property associated with a key.""" if isinstance(key, str): key = key.encode('utf-8') value_pp = capi.ffi.new('char**') @@ -502,7 +509,7 @@ class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping): """Return a :class:`collections.abc.ItemsView` for this map. The ItemsView treats a ``(key, value)`` pair as unique, so - dupcliate ``(key, value)`` pairs will be merged together. + duplicate ``(key, value)`` pairs will be merged together. However duplicate keys with different values will be returned. """ items = set() @@ -517,7 +524,7 @@ class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping): return PropertiesItemsView(items) def values(self): - """Return a :class:`collecions.abc.ValuesView` for this map. + """Return a :class:`collections.abc.ValuesView` for this map. All unique property values are included in the view. """ @@ -596,7 +603,7 @@ class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping): def getall(self, prefix='', *, exact=False): """Return an iterator yielding all properties for a given key prefix. - The returned iterator yields all peroperties which start with + The returned iterator yields all properties which start with a given key prefix as ``(key, value)`` namedtuples. If called with ``exact=True`` then only properties which exactly match the prefix are returned, those a key longer than the prefix @@ -641,7 +648,6 @@ class PropertiesIter(base.NotmuchIter): def __next__(self): if not self._fn_valid(self._iter_p): - self._destroy() raise StopIteration key = capi.lib.notmuch_message_properties_key(self._iter_p) value = capi.lib.notmuch_message_properties_value(self._iter_p) diff --git a/bindings/python-cffi/notmuch2/_query.py b/bindings/python-cffi/notmuch2/_query.py index 1db6ec96..169fcf27 100644 --- a/bindings/python-cffi/notmuch2/_query.py +++ b/bindings/python-cffi/notmuch2/_query.py @@ -52,7 +52,7 @@ class Query(base.NotmuchObject): This executes the query and returns an iterator over the :class:`Message` objects found. """ - msgs_pp = capi.ffi.new('notmuch_messages_t**') + msgs_pp = capi.ffi.new('notmuch_messages_t **') ret = capi.lib.notmuch_query_search_messages(self._query_p, msgs_pp) if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: raise errors.NotmuchError(ret) diff --git a/bindings/python-cffi/notmuch2/_tags.py b/bindings/python-cffi/notmuch2/_tags.py index ee5d2a34..28528831 100644 --- a/bindings/python-cffi/notmuch2/_tags.py +++ b/bindings/python-cffi/notmuch2/_tags.py @@ -69,7 +69,7 @@ class ImmutableTagSet(base.NotmuchObject, collections.abc.Set): return self.iter(encoding='utf-8', errors='ignore') def iter(self, *, encoding=None, errors='strict'): - """Aternate iterator constructor controlling string decoding. + """Alternate iterator constructor controlling string decoding. Tags are stored as bytes in the notmuch database, in Python it's easier to work with unicode strings and thus is what the @@ -87,7 +87,7 @@ class ImmutableTagSet(base.NotmuchObject, collections.abc.Set): """ # self._cffi_fn should point either to # notmuch_database_get_all_tags, notmuch_thread_get_tags or - # notmuch_message_get_tags. nothmuch.h suggests these never + # notmuch_message_get_tags. notmuch.h suggests these never # fail, let's handle NULL anyway. tags_p = self._cffi_fn(self._ptr()) if tags_p == capi.ffi.NULL: @@ -155,7 +155,7 @@ class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet): Tags are bytestrings and calling ``iter()`` will return an iterator yielding bytestrings. However the :meth:`iter` method can be used to return tags as unicode strings, while all other - operations accept either byestrings or unicode strings. In case + operations accept either bytestrings or unicode strings. In case unicode strings are used they will be encoded using utf-8 before being passed to notmuch. """ diff --git a/bindings/python-cffi/setup.py b/bindings/python-cffi/setup.py index cda52338..3719c86c 100644 --- a/bindings/python-cffi/setup.py +++ b/bindings/python-cffi/setup.py @@ -1,7 +1,8 @@ import setuptools +from _notmuch_config import * -with open('version.txt') as fp: - VERSION = fp.read().strip() +with open(NOTMUCH_VERSION_FILE) as fp: + VERSION = fp.read().strip().replace('~', '') setuptools.setup( name='notmuch2', diff --git a/bindings/python-cffi/tests/conftest.py b/bindings/python-cffi/tests/conftest.py index 6835fd30..fe90c787 100644 --- a/bindings/python-cffi/tests/conftest.py +++ b/bindings/python-cffi/tests/conftest.py @@ -43,7 +43,7 @@ def notmuch(maildir): env = os.environ.copy() env['NOTMUCH_CONFIG'] = str(cfg_fname) proc = subprocess.run(cmd, - timeout=5, + timeout=120, env=env) proc.check_returncode() return run diff --git a/bindings/python-cffi/tests/test_config.py b/bindings/python-cffi/tests/test_config.py index 1b2695f5..2a7f42f0 100644 --- a/bindings/python-cffi/tests/test_config.py +++ b/bindings/python-cffi/tests/test_config.py @@ -23,9 +23,9 @@ class TestIter: def test_set_get(self, maildir): # Ensure get-set works from different db objects - with dbmod.Database.create(maildir.path) as db0: + with dbmod.Database.create(maildir.path, config=dbmod.Database.CONFIG.EMPTY) as db0: db0.config['spam'] = 'ham' - with dbmod.Database(maildir.path) as db1: + with dbmod.Database(maildir.path, config=dbmod.Database.CONFIG.EMPTY) as db1: assert db1.config['spam'] == 'ham' def test_get_keyerror(self, db): @@ -34,20 +34,24 @@ class TestIter: print(repr(val)) def test_iter(self, db): - assert list(db.config) == [] - db.config['spam'] = 'ham' - db.config['eggs'] = 'bacon' - assert set(db.config) == {'spam', 'eggs'} - assert set(db.config.keys()) == {'spam', 'eggs'} - assert set(db.config.values()) == {'ham', 'bacon'} - assert set(db.config.items()) == {('spam', 'ham'), ('eggs', 'bacon')} + def has_prefix(x): + return x.startswith('TEST.') + + assert [ x for x in db.config if has_prefix(x) ] == [] + db.config['TEST.spam'] = 'TEST.ham' + db.config['TEST.eggs'] = 'TEST.bacon' + assert { x for x in db.config if has_prefix(x) } == {'TEST.spam', 'TEST.eggs'} + assert { x for x in db.config.keys() if has_prefix(x) } == {'TEST.spam', 'TEST.eggs'} + assert { x for x in db.config.values() if has_prefix(x) } == {'TEST.ham', 'TEST.bacon'} + assert { (x, y) for (x,y) in db.config.items() if has_prefix(x) } == \ + {('TEST.spam', 'TEST.ham'), ('TEST.eggs', 'TEST.bacon')} def test_len(self, db): - assert len(db.config) == 0 + defaults = len(db.config) db.config['spam'] = 'ham' - assert len(db.config) == 1 + assert len(db.config) == defaults + 1 db.config['eggs'] = 'bacon' - assert len(db.config) == 2 + assert len(db.config) == defaults + 2 def test_del(self, db): db.config['spam'] = 'ham' diff --git a/bindings/python-cffi/tests/test_database.py b/bindings/python-cffi/tests/test_database.py index 9b3219c0..1557235d 100644 --- a/bindings/python-cffi/tests/test_database.py +++ b/bindings/python-cffi/tests/test_database.py @@ -13,7 +13,7 @@ import notmuch2._message as message @pytest.fixture def db(maildir): - with dbmod.Database.create(maildir.path) as db: + with dbmod.Database.create(maildir.path, config=notmuch2.Database.CONFIG.EMPTY) as db: yield db @@ -293,7 +293,7 @@ class TestQuery: maildir.deliver(body='baz', headers=[('In-Reply-To', '<{}>'.format(msgid))]) notmuch('new') - with dbmod.Database(maildir.path, 'rw') as db: + with dbmod.Database(maildir.path, 'rw', config=notmuch2.Database.CONFIG.EMPTY) as db: yield db def test_count_messages(self, db): @@ -303,6 +303,18 @@ class TestQuery: msgs = db.messages('*') assert isinstance(msgs, collections.abc.Iterator) + def test_messages_iterator(self, db): + for msg in db.messages('*'): + assert isinstance(msg, notmuch2.Message) + assert isinstance(msg.messageid, str) + + def test_messages_iterator_list(self, db): + msgs = list(db.messages('*')) + assert len(msgs) == 3 + for msg in msgs: + assert isinstance(msg, notmuch2.Message) + assert isinstance(msg.messageid, str) + def test_message_no_results(self, db): msgs = db.messages('not_a_matching_query') with pytest.raises(StopIteration): @@ -320,6 +332,25 @@ class TestQuery: threads = db.threads('*') assert isinstance(threads, collections.abc.Iterator) + def test_threads_iterator(self, db): + for t in db.threads('*'): + assert isinstance(t, notmuch2.Thread) + assert isinstance(t.threadid, str) + for msg in t: + assert isinstance(msg, notmuch2.Message) + assert isinstance(msg.messageid, str) + + def test_threads_iterator_list(self, db): + threads = list(db.threads('*')) + assert len(threads) == 2 + for t in threads: + assert isinstance(t, notmuch2.Thread) + assert isinstance(t.threadid, str) + msgs = list(t) + for msg in msgs: + assert isinstance(msg, notmuch2.Message) + assert isinstance(msg.messageid, str) + def test_threads_no_match(self, db): threads = db.threads('not_a_matching_query') with pytest.raises(StopIteration): diff --git a/bindings/python-cffi/tests/test_errors.py b/bindings/python-cffi/tests/test_errors.py new file mode 100644 index 00000000..c2519f86 --- /dev/null +++ b/bindings/python-cffi/tests/test_errors.py @@ -0,0 +1,8 @@ +from notmuch2 import _capi as capi +from notmuch2 import _errors as errors + +def test_status_no_message(): + exc = errors.NotmuchError(capi.lib.NOTMUCH_STATUS_PATH_ERROR) + assert exc.status == capi.lib.NOTMUCH_STATUS_PATH_ERROR + assert exc.message is None + assert str(exc) == 'Path supplied is illegal for this function' diff --git a/bindings/python-cffi/tests/test_message.py b/bindings/python-cffi/tests/test_message.py index 532bf921..3b103530 100644 --- a/bindings/python-cffi/tests/test_message.py +++ b/bindings/python-cffi/tests/test_message.py @@ -97,6 +97,9 @@ class TestMessage: def test_ghost_no(self, msg): assert not msg.ghost + def test_matched_no(self,msg): + assert not msg.matched + def test_date(self, msg): # XXX Someone seems to treat things as local time instead of # UTC or the other way around. @@ -215,6 +218,20 @@ class TestProperties: props.add('foo', 'a') assert set(props.getall('foo')) == {('foo', 'a')} + def test_getall_iter(self, props): + props.add('foo', 'a') + props.add('foobar', 'b') + for prop in props.getall('foo'): + assert isinstance(prop.value, str) + + def test_getall_iter_list(self, props): + props.add('foo', 'a') + props.add('foobar', 'b') + res = list(props.getall('foo')) + assert len(res) == 2 + for prop in res: + assert isinstance(prop.value, str) + def test_getall_prefix(self, props): props.add('foo', 'a') props.add('foobar', 'b') diff --git a/bindings/python-cffi/tests/test_tags.py b/bindings/python-cffi/tests/test_tags.py index faf3947b..f2c6209d 100644 --- a/bindings/python-cffi/tests/test_tags.py +++ b/bindings/python-cffi/tests/test_tags.py @@ -23,7 +23,7 @@ class TestImmutable: """ maildir.deliver() notmuch('new') - with database.Database(maildir.path) as db: + with database.Database(maildir.path, config=database.Database.CONFIG.EMPTY) as db: yield db.tags def test_type(self, tagset): @@ -33,7 +33,7 @@ class TestImmutable: def test_hash(self, tagset, maildir, notmuch): h0 = hash(tagset) notmuch('tag', '+foo', '*') - with database.Database(maildir.path) as db: + with database.Database(maildir.path, config=database.Database.CONFIG.EMPTY) as db: h1 = hash(db.tags) assert h0 != h1 @@ -42,7 +42,7 @@ class TestImmutable: def test_neq(self, tagset, maildir, notmuch): notmuch('tag', '+foo', '*') - with database.Database(maildir.path) as db: + with database.Database(maildir.path, config=database.Database.CONFIG.EMPTY) as db: assert tagset != db.tags def test_contains(self, tagset): @@ -159,7 +159,8 @@ class TestMutableTagset: _, pathname = maildir.deliver() notmuch('new') with database.Database(maildir.path, - mode=database.Mode.READ_WRITE) as db: + mode=database.Mode.READ_WRITE, + config=database.Database.CONFIG.EMPTY) as db: msg = db.get(pathname) yield msg.tags @@ -195,7 +196,8 @@ class TestMutableTagset: _, pathname = maildir.deliver(flagged=True) notmuch('new') with database.Database(maildir.path, - mode=database.Mode.READ_WRITE) as db: + mode=database.Mode.READ_WRITE, + config=database.Database.CONFIG.EMPTY) as db: msg = db.get(pathname) msg.tags.discard('flagged') msg.tags.from_maildir_flags() @@ -205,7 +207,8 @@ class TestMutableTagset: _, pathname = maildir.deliver(flagged=True) notmuch('new') with database.Database(maildir.path, - mode=database.Mode.READ_WRITE) as db: + mode=database.Mode.READ_WRITE, + config=database.Database.CONFIG.EMPTY) as db: msg = db.get(pathname) flags = msg.path.name.split(',')[-1] assert 'F' in flags diff --git a/bindings/python-cffi/tests/test_thread.py b/bindings/python-cffi/tests/test_thread.py index 1f44b35d..619d2aac 100644 --- a/bindings/python-cffi/tests/test_thread.py +++ b/bindings/python-cffi/tests/test_thread.py @@ -13,7 +13,7 @@ def thread(maildir, notmuch): maildir.deliver(body='bar', headers=[('In-Reply-To', '<{}>'.format(msgid))]) notmuch('new') - with notmuch2.Database(maildir.path) as db: + with notmuch2.Database(maildir.path, config=notmuch2.Database.CONFIG.EMPTY) as db: yield next(db.threads('foo')) @@ -57,6 +57,13 @@ def test_iter(thread): def test_matched(thread): assert thread.matched == 1 +def test_matched_iter(thread): + count = 0 + msgs = list(iter(thread)) + for msg in msgs: + if msg.matched: + count += 1 + assert count == thread.matched def test_authors_type(thread): assert isinstance(thread.authors, notmuch2.BinString) diff --git a/bindings/python-cffi/version.txt b/bindings/python-cffi/version.txt deleted file mode 100644 index 61d2f357..00000000 --- a/bindings/python-cffi/version.txt +++ /dev/null @@ -1 +0,0 @@ -0.34 diff --git a/bindings/python/.gitignore b/bindings/python/.gitignore deleted file mode 100644 index 601acdd7..00000000 --- a/bindings/python/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.py[co] -/docs/build -/docs/html -/build/ diff --git a/bindings/python/MANIFEST.in b/bindings/python/MANIFEST.in deleted file mode 100644 index c83be4b1..00000000 --- a/bindings/python/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include notmuch -#recursive-include docs/html * \ No newline at end of file diff --git a/bindings/python/README b/bindings/python/README deleted file mode 100644 index 5bf076d2..00000000 --- a/bindings/python/README +++ /dev/null @@ -1,17 +0,0 @@ -notmuch -- The python interface to notmuch -========================================== - -This module makes the functionality of the notmuch library -(`https://notmuchmail.org`_) available to python. Successful import of -this module depends on a libnotmuch.so|dll being available on the -user's system. - -If you have downloaded the full source tarball, you can create the -documentation with sphinx installed, go to the docs directory and -"make html". A static version of the documentation is available at: - - https://notmuch.readthedocs.io/projects/notmuch-python/ - -To build the python bindings, do - - python setup.py install --prefix=path/to/your/preferred/location diff --git a/bindings/python/docs/COPYING b/bindings/python/docs/COPYING deleted file mode 100644 index e6000869..00000000 --- a/bindings/python/docs/COPYING +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/bindings/python/docs/Makefile b/bindings/python/docs/Makefile deleted file mode 100644 index bd6de38c..00000000 --- a/bindings/python/docs/Makefile +++ /dev/null @@ -1,88 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf build/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) html - @echo - @echo "Build finished. The HTML pages are in html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) build/dirhtml - @echo - @echo "Build finished. The HTML pages are in build/dirhtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in build/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) build/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in build/qthelp, like this:" - @echo "# qcollectiongenerator build/qthelp/pyDNS.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile build/qthelp/pyDNS.qhc" - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex - @echo - @echo "Build finished; the LaTeX files are in build/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes - @echo - @echo "The overview file is in build/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in build/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) build/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in build/doctest/output.txt." diff --git a/bindings/python/docs/source/conf.py b/bindings/python/docs/source/conf.py deleted file mode 100644 index 8b43c5ca..00000000 --- a/bindings/python/docs/source/conf.py +++ /dev/null @@ -1,209 +0,0 @@ -# -*- coding: utf-8 -*- -# -# pyDNS documentation build configuration file, created by -# sphinx-quickstart on Tue Feb 2 10:00:47 2010. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -from unittest.mock import Mock - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0,os.path.abspath('../..')) - -MOCK_MODULES = [ - 'ctypes', -] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() - - -from notmuch import __VERSION__,__AUTHOR__ -# -- General configuration ----------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo'] -autoclass_content = "both" - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'notmuch' -copyright = u'2010-2012, ' + __AUTHOR__ - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = __VERSION__ -# The full version, including alpha/beta/rc tags. -release = __VERSION__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of documents that shouldn't be included in the build. -#unused_docs = [] - -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = [] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = False - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -html_use_modindex = False - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'notmuchdoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'notmuch.tex', u'notmuch Documentation', - u'notmuch contributors', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_use_modindex = True - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/bindings/python/docs/source/database.rst b/bindings/python/docs/source/database.rst deleted file mode 100644 index 660de91d..00000000 --- a/bindings/python/docs/source/database.rst +++ /dev/null @@ -1,54 +0,0 @@ -:class:`Database` -- The underlying notmuch database -==================================================== - -.. currentmodule:: notmuch - -.. autoclass:: Database([path=None[, create=False[, mode=MODE.READ_ONLY]]]) - - .. automethod:: create - - .. automethod:: open(path, status=MODE.READ_ONLY) - - .. automethod:: close - - .. automethod:: get_path - - .. automethod:: get_version - - .. automethod:: needs_upgrade - - .. automethod:: upgrade - - .. automethod:: begin_atomic - - .. automethod:: end_atomic - - .. automethod:: get_directory - - .. automethod:: index_file - - .. automethod:: remove_message - - .. automethod:: find_message - - .. automethod:: find_message_by_filename - - .. automethod:: get_all_tags - - .. automethod:: create_query - - .. automethod:: get_config - - .. automethod:: get_configs - - .. automethod:: set_config - - .. attribute:: Database.MODE - - Defines constants that are used as the mode in which to open a database. - - MODE.READ_ONLY - Open the database in read-only mode - - MODE.READ_WRITE - Open the database in read-write mode diff --git a/bindings/python/docs/source/filesystem.rst b/bindings/python/docs/source/filesystem.rst deleted file mode 100644 index 13fe1194..00000000 --- a/bindings/python/docs/source/filesystem.rst +++ /dev/null @@ -1,32 +0,0 @@ -Files and directories -===================== - -.. currentmodule:: notmuch - -:class:`Filenames` -- An iterator over filenames ------------------------------------------------- - -.. autoclass:: Filenames - - .. method:: Filenames.__len__ - .. warning:: - :meth:`__len__` was removed in version 0.22 as it exhausted the - iterator and broke list(Filenames()). Use `len(list(names))` - instead. - -:class:`Directory` -- A directory entry in the database -------------------------------------------------------- - -.. autoclass:: Directory - - .. automethod:: Directory.get_child_files - - .. automethod:: Directory.get_child_directories - - .. automethod:: Directory.get_mtime - - .. automethod:: Directory.set_mtime - - .. autoattribute:: Directory.mtime - - .. autoattribute:: Directory.path diff --git a/bindings/python/docs/source/index.rst b/bindings/python/docs/source/index.rst deleted file mode 100644 index bef7e60d..00000000 --- a/bindings/python/docs/source/index.rst +++ /dev/null @@ -1,36 +0,0 @@ -Welcome to :mod:`notmuch`'s documentation -========================================= - -.. currentmodule:: notmuch - -The :mod:`notmuch` module provides an interface to the `notmuch -`_ functionality, directly interfacing to a -shared notmuch library. Within :mod:`notmuch`, the classes -:class:`Database`, :class:`Query` provide most of the core -functionality, returning :class:`Threads`, :class:`Messages` and -:class:`Tags`. - -.. moduleauthor:: Sebastian Spaeth - -:License: This module is covered under the GNU GPL v3 (or later). - -.. toctree:: - :maxdepth: 1 - - quickstart - notes - status_and_errors - database - query - messages - message - tags - threads - thread - filesystem - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`search` diff --git a/bindings/python/docs/source/message.rst b/bindings/python/docs/source/message.rst deleted file mode 100644 index b0033924..00000000 --- a/bindings/python/docs/source/message.rst +++ /dev/null @@ -1,54 +0,0 @@ -:class:`Message` -- A single message -==================================== - -.. currentmodule:: notmuch - -.. autoclass:: Message - - .. automethod:: get_message_id - - .. automethod:: get_thread_id - - .. automethod:: get_replies - - .. automethod:: get_filename - - .. automethod:: get_filenames - - .. attribute:: FLAG - - FLAG.MATCH - This flag is automatically set by a - Query.search_threads on those messages that match the - query. This allows us to distinguish matches from the rest - of the messages in that thread. - - .. automethod:: get_flag - - .. automethod:: set_flag - - .. automethod:: get_date - - .. automethod:: get_header - - .. automethod:: get_tags - - .. automethod:: get_property - - .. automethod:: get_properties - - .. automethod:: maildir_flags_to_tags - - .. automethod:: tags_to_maildir_flags - - .. automethod:: remove_tag - - .. automethod:: add_tag - - .. automethod:: remove_all_tags - - .. automethod:: freeze - - .. automethod:: thaw - - .. automethod:: __str__ diff --git a/bindings/python/docs/source/messages.rst b/bindings/python/docs/source/messages.rst deleted file mode 100644 index 3ccf505c..00000000 --- a/bindings/python/docs/source/messages.rst +++ /dev/null @@ -1,15 +0,0 @@ -:class:`Messages` -- A bunch of messages -======================================== - -.. currentmodule:: notmuch - -.. autoclass:: Messages - - .. automethod:: collect_tags - - .. method:: __len__() - - .. warning:: - - :meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke - list(Messages()). Use the :meth:`Query.count_messages` function or use `len(list(msgs))`. diff --git a/bindings/python/docs/source/notes.rst b/bindings/python/docs/source/notes.rst deleted file mode 100644 index a7927485..00000000 --- a/bindings/python/docs/source/notes.rst +++ /dev/null @@ -1,6 +0,0 @@ -Interfacing with notmuch -======================== - -.. todo:: move the note about talloc out of the code - -.. automodule:: notmuch diff --git a/bindings/python/docs/source/query.rst b/bindings/python/docs/source/query.rst deleted file mode 100644 index 785e984b..00000000 --- a/bindings/python/docs/source/query.rst +++ /dev/null @@ -1,43 +0,0 @@ -:class:`Query` -- A search query -================================ - -.. currentmodule:: notmuch - -.. autoclass:: Query - - .. automethod:: create - - .. attribute:: Query.SORT - - Defines constants that are used as the mode in which to open a database. - - SORT.OLDEST_FIRST - Sort by message date, oldest first. - - SORT.NEWEST_FIRST - Sort by message date, newest first. - - SORT.MESSAGE_ID - Sort by email message ID. - - SORT.UNSORTED - Do not apply a special sort order (returns results in document id - order). - - .. automethod:: set_sort - - .. attribute:: sort - - Instance attribute :attr:`sort` contains the sort order (see - :attr:`Query.SORT`) if explicitly specified via - :meth:`set_sort`. By default it is set to `None`. - - .. automethod:: exclude_tag - - .. automethod:: search_threads - - .. automethod:: search_messages - - .. automethod:: count_messages - - .. automethod:: count_threads diff --git a/bindings/python/docs/source/quickstart.rst b/bindings/python/docs/source/quickstart.rst deleted file mode 100644 index 609f42e1..00000000 --- a/bindings/python/docs/source/quickstart.rst +++ /dev/null @@ -1,19 +0,0 @@ -Quickstart and examples -======================= - -.. todo:: write a nice introduction -.. todo:: improve the examples - -Notmuch can be imported as:: - - import notmuch - -or:: - - from notmuch import Query, Database - - db = Database('path', create=True) - msgs = Query(db, 'from:myself').search_messages() - - for msg in msgs: - print(msg) diff --git a/bindings/python/docs/source/status_and_errors.rst b/bindings/python/docs/source/status_and_errors.rst deleted file mode 100644 index 68913f16..00000000 --- a/bindings/python/docs/source/status_and_errors.rst +++ /dev/null @@ -1,57 +0,0 @@ -.. currentmodule:: notmuch - -Status and Errors -================= - -Some methods return a status, indicating if an operation was successful and what the error was. Most of these status codes are expressed as a specific value, the :class:`notmuch.STATUS`. - -.. note:: - - Prior to version 0.12 the exception classes and the enumeration - :class:`notmuch.STATUS` were defined in `notmuch.globals`. They - have since then been moved into `notmuch.errors`. - -:class:`STATUS` -- Notmuch operation return value --------------------------------------------------- - -.. autoclass:: notmuch.STATUS - :inherited-members: - -.. automethod:: notmuch.STATUS.status2str - -:exc:`NotmuchError` -- A Notmuch execution error ------------------------------------------------- -Whenever an error occurs, we throw a special Exception :exc:`NotmuchError`, or a more fine grained Exception which is derived from it. This means it is always safe to check for NotmuchErrors if you want to catch all errors. If you are interested in more fine grained exceptions, you can use those below. - -.. autoexception:: NotmuchError - -The following exceptions are all directly derived from NotmuchError. Each of them corresponds to a specific :class:`notmuch.STATUS` value. You can either check the :attr:`status` attribute of a NotmuchError to see if a specific error has occurred, or you can directly check for the following Exception types: - -.. autoexception:: OutOfMemoryError(message=None) - :members: -.. autoexception:: ReadOnlyDatabaseError(message=None) - :members: -.. autoexception:: XapianError(message=None) - :members: -.. autoexception:: FileError(message=None) - :members: -.. autoexception:: FileNotEmailError(message=None) - :members: -.. autoexception:: DuplicateMessageIdError(message=None) - :members: -.. autoexception:: NullPointerError(message=None) - :members: -.. autoexception:: TagTooLongError(message=None) - :members: -.. autoexception:: UnbalancedFreezeThawError(message=None) - :members: -.. autoexception:: UnbalancedAtomicError(message=None) - :members: -.. autoexception:: UnsupportedOperationError(message=None) - :members: -.. autoexception:: UpgradeRequiredError(message=None) - :members: -.. autoexception:: PathError(message=None) - :members: -.. autoexception:: NotInitializedError(message=None) - :members: diff --git a/bindings/python/docs/source/tags.rst b/bindings/python/docs/source/tags.rst deleted file mode 100644 index 31527d4b..00000000 --- a/bindings/python/docs/source/tags.rst +++ /dev/null @@ -1,17 +0,0 @@ -:class:`Tags` -- Notmuch tags ------------------------------ - -.. currentmodule:: notmuch - -.. autoclass:: Tags - :members: - - .. method:: __len__ - - .. warning:: - - :meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke - list(Tags()). Use :meth:`len(list(msgs))` instead if you need to know the number of - tags. - - .. automethod:: __str__ diff --git a/bindings/python/docs/source/thread.rst b/bindings/python/docs/source/thread.rst deleted file mode 100644 index 40678725..00000000 --- a/bindings/python/docs/source/thread.rst +++ /dev/null @@ -1,26 +0,0 @@ -:class:`Thread` -- A single thread -================================== - -.. currentmodule:: notmuch - -.. autoclass:: Thread - - .. automethod:: get_thread_id - - .. automethod:: get_total_messages - - .. automethod:: get_toplevel_messages - - .. automethod:: get_matched_messages - - .. automethod:: get_authors - - .. automethod:: get_subject - - .. automethod:: get_oldest_date - - .. automethod:: get_newest_date - - .. automethod:: get_tags - - .. automethod:: __str__ diff --git a/bindings/python/docs/source/threads.rst b/bindings/python/docs/source/threads.rst deleted file mode 100644 index 46ce5be5..00000000 --- a/bindings/python/docs/source/threads.rst +++ /dev/null @@ -1,14 +0,0 @@ -:class:`Threads` -- Threads iterator -==================================== - -.. currentmodule:: notmuch - -.. autoclass:: Threads - - .. method:: __len__ - .. warning:: - :meth:`__len__` was removed in version 0.22 as it exhausted the - iterator and broke list(Threads()). Use `len(list(msgs))` - instead. - - .. automethod:: __str__ diff --git a/bindings/python/notmuch/__init__.py b/bindings/python/notmuch/__init__.py deleted file mode 100644 index cf627ffa..00000000 --- a/bindings/python/notmuch/__init__.py +++ /dev/null @@ -1,84 +0,0 @@ -"""The :mod:`notmuch` module provides most of the functionality that a user is -likely to need. - -.. note:: The underlying notmuch library is build on a hierarchical - memory allocator called talloc. All objects derive from a - top-level :class:`Database` object. - - This means that as soon as an object is deleted, all underlying - derived objects such as Queries, Messages, Message, and Tags will - be freed by the underlying library as well. Accessing these - objects will then lead to segfaults and other unexpected behavior. - - We implement reference counting, so that parent objects can be - automatically freed when they are not needed anymore. For - example:: - - db = Database('path',create=True) - msgs = Query(db,'from:myself').search_messages() - - This returns :class:`Messages` which internally contains a - reference to its parent :class:`Query` object. Otherwise the - Query() would be immediately freed, taking our *msgs* down with - it. - - In this case, the above Query() object will be automatically freed - whenever we delete all derived objects, ie in our case: - `del(msgs)` would also delete the parent Query. It would not - delete the parent Database() though, as that is still referenced - from the variable *db* in which it is stored. - - Pretty much the same is valid for all other objects in the - hierarchy, such as :class:`Query`, :class:`Messages`, - :class:`Message`, and :class:`Tags`. -""" - -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010-2011 Sebastian Spaeth -""" -from .database import Database -from .directory import Directory -from .filenames import Filenames -from .message import Message -from .messages import Messages -from .query import Query -from .tag import Tags -from .thread import Thread -from .threads import Threads -from .globals import nmlib -from .errors import ( - STATUS, - NotmuchError, - OutOfMemoryError, - ReadOnlyDatabaseError, - XapianError, - FileError, - FileNotEmailError, - DuplicateMessageIdError, - NullPointerError, - TagTooLongError, - UnbalancedFreezeThawError, - UnbalancedAtomicError, - NotInitializedError, - UnsupportedOperationError, - UpgradeRequiredError, - PathError, -) -from .version import __VERSION__ -__LICENSE__ = "GPL v3+" -__AUTHOR__ = 'Sebastian Spaeth ' diff --git a/bindings/python/notmuch/compat.py b/bindings/python/notmuch/compat.py deleted file mode 100644 index c931329e..00000000 --- a/bindings/python/notmuch/compat.py +++ /dev/null @@ -1,71 +0,0 @@ -''' -This file is part of notmuch. - -This module handles differences between python2.x and python3.x and -allows the notmuch bindings to support both version families with one -source tree. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -Copyright 2012 Justus Winter <4winter@informatik.uni-hamburg.de> -''' - -import sys - -if sys.version_info[0] == 2: - from ConfigParser import SafeConfigParser - - class Python3StringMixIn(object): - def __str__(self): - return unicode(self).encode('utf-8') - - def encode_utf8(value): - ''' - Ensure a nicely utf-8 encoded string to pass to wrapped - libnotmuch functions. - - C++ code expects strings to be well formatted and unicode - strings to have no null bytes. - ''' - if not isinstance(value, basestring): - raise TypeError('Expected str or unicode, got %s' % type(value)) - - if isinstance(value, unicode): - return value.encode('utf-8', 'replace') - - return value -else: - from configparser import SafeConfigParser - - class Python3StringMixIn(object): - def __str__(self): - return self.__unicode__() - - def encode_utf8(value): - ''' - Ensure a nicely utf-8 encoded string to pass to wrapped - libnotmuch functions. - - C++ code expects strings to be well formatted and unicode - strings to have no null bytes. - ''' - if not isinstance(value, str): - raise TypeError('Expected str, got %s' % type(value)) - - return value.encode('utf-8', 'replace') - -# We import the SafeConfigParser class on behalf of other code to cope -# with the differences between Python 2 and 3. -SafeConfigParser # avoid warning about unused import diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py deleted file mode 100644 index 8fb507fa..00000000 --- a/bindings/python/notmuch/database.py +++ /dev/null @@ -1,789 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -""" - -import os -import codecs -import warnings -from ctypes import c_char_p, c_void_p, c_uint, byref, POINTER -from .compat import SafeConfigParser -from .globals import ( - nmlib, - Enum, - _str, - NotmuchConfigListP, - NotmuchDatabaseP, - NotmuchDirectoryP, - NotmuchIndexoptsP, - NotmuchMessageP, - NotmuchTagsP, -) -from .errors import ( - STATUS, - FileError, - NotmuchError, - NullPointerError, - NotInitializedError, -) -from .message import Message -from .tag import Tags -from .query import Query -from .directory import Directory - -class Database(object): - """The :class:`Database` is the highest-level object that notmuch - provides. It references a notmuch database, and can be opened in - read-only or read-write mode. A :class:`Query` can be derived from - or be applied to a specific database to find messages. Also adding - and removing messages to the database happens via this - object. Modifications to the database are not atmic by default (see - :meth:`begin_atomic`) and once a database has been modified, all - other database objects pointing to the same data-base will throw an - :exc:`XapianError` as the underlying database has been - modified. Close and reopen the database to continue working with it. - - :class:`Database` objects implement the context manager protocol - so you can use the :keyword:`with` statement to ensure that the - database is properly closed. See :meth:`close` for more - information. - - .. note:: - - Any function in this class can and will throw an - :exc:`NotInitializedError` if the database was not initialized - properly. - """ - _std_db_path = None - """Class attribute to cache user's default database""" - - MODE = Enum(['READ_ONLY', 'READ_WRITE']) - """Constants: Mode in which to open the database""" - - DECRYPTION_POLICY = Enum(['FALSE', 'TRUE', 'AUTO', 'NOSTASH']) - """Constants: policies for decrypting messages during indexing""" - - """notmuch_database_get_directory""" - _get_directory = nmlib.notmuch_database_get_directory - _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)] - _get_directory.restype = c_uint - - """notmuch_database_get_path""" - _get_path = nmlib.notmuch_database_get_path - _get_path.argtypes = [NotmuchDatabaseP] - _get_path.restype = c_char_p - - """notmuch_database_get_version""" - _get_version = nmlib.notmuch_database_get_version - _get_version.argtypes = [NotmuchDatabaseP] - _get_version.restype = c_uint - - """notmuch_database_get_revision""" - _get_revision = nmlib.notmuch_database_get_revision - _get_revision.argtypes = [NotmuchDatabaseP, POINTER(c_char_p)] - _get_revision.restype = c_uint - - """notmuch_database_open""" - _open = nmlib.notmuch_database_open - _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)] - _open.restype = c_uint - - """notmuch_database_upgrade""" - _upgrade = nmlib.notmuch_database_upgrade - _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p] - _upgrade.restype = c_uint - - """ notmuch_database_find_message""" - _find_message = nmlib.notmuch_database_find_message - _find_message.argtypes = [NotmuchDatabaseP, c_char_p, - POINTER(NotmuchMessageP)] - _find_message.restype = c_uint - - """notmuch_database_find_message_by_filename""" - _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename - _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p, - POINTER(NotmuchMessageP)] - _find_message_by_filename.restype = c_uint - - """notmuch_database_get_all_tags""" - _get_all_tags = nmlib.notmuch_database_get_all_tags - _get_all_tags.argtypes = [NotmuchDatabaseP] - _get_all_tags.restype = NotmuchTagsP - - """notmuch_database_create""" - _create = nmlib.notmuch_database_create - _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)] - _create.restype = c_uint - - def __init__(self, path = None, create = False, - mode = MODE.READ_ONLY): - """If *path* is `None`, we will try to read a users notmuch - configuration and use his configured database. The location of the - configuration file can be specified through the environment variable - *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`. - - If *create* is `True`, the database will always be created in - :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY. - - :param path: Directory to open/create the database in (see - above for behavior if `None`) - :type path: `str` or `None` - :param create: Pass `False` to open an existing, `True` to create a new - database. - :type create: bool - :param mode: Mode to open a database in. Is always - :attr:`MODE`.READ_WRITE when creating a new one. - :type mode: :attr:`MODE` - :raises: :exc:`NotmuchError` or derived exception in case of - failure. - """ - self._db = None - self.mode = mode - if path is None: - # no path specified. use a user's default database - if Database._std_db_path is None: - #the following line throws a NotmuchError if it fails - Database._std_db_path = self._get_user_default_db() - path = Database._std_db_path - - if create == False: - self.open(path, mode) - else: - self.create(path) - - _destroy = nmlib.notmuch_database_destroy - _destroy.argtypes = [NotmuchDatabaseP] - _destroy.restype = c_uint - - def __del__(self): - if self._db: - status = self._destroy(self._db) - if status != STATUS.SUCCESS: - raise NotmuchError(status) - - def _assert_db_is_initialized(self): - """Raises :exc:`NotInitializedError` if self._db is `None`""" - if not self._db: - raise NotInitializedError() - - def create(self, path): - """Creates a new notmuch database - - This function is used by __init__() and usually does not need - to be called directly. It wraps the underlying - *notmuch_database_create* function and creates a new notmuch - database at *path*. It will always return a database in :attr:`MODE` - .READ_WRITE mode as creating an empty database for - reading only does not make a great deal of sense. - - :param path: A directory in which we should create the database. - :type path: str - :raises: :exc:`NotmuchError` in case of any failure - (possibly after printing an error message on stderr). - """ - if self._db: - raise NotmuchError(message="Cannot create db, this Database() " - "already has an open one.") - - db = NotmuchDatabaseP() - status = Database._create(_str(path), byref(db)) - - if status != STATUS.SUCCESS: - raise NotmuchError(status) - self._db = db - return status - - def open(self, path, mode=0): - """Opens an existing database - - This function is used by __init__() and usually does not need - to be called directly. It wraps the underlying - *notmuch_database_open* function. - - :param status: Open the database in read-only or read-write mode - :type status: :attr:`MODE` - :raises: Raises :exc:`NotmuchError` in case of any failure - (possibly after printing an error message on stderr). - """ - db = NotmuchDatabaseP() - status = Database._open(_str(path), mode, byref(db)) - - if status != STATUS.SUCCESS: - raise NotmuchError(status) - self._db = db - return status - - _close = nmlib.notmuch_database_close - _close.argtypes = [NotmuchDatabaseP] - _close.restype = c_uint - - def close(self): - ''' - Closes the notmuch database. - - .. warning:: - - This function closes the notmuch database. From that point - on every method invoked on any object ever derived from - the closed database may cease to function and raise a - NotmuchError. - ''' - if self._db: - status = self._close(self._db) - if status != STATUS.SUCCESS: - raise NotmuchError(status) - - def __enter__(self): - ''' - Implements the context manager protocol. - ''' - return self - - def __exit__(self, exc_type, exc_value, traceback): - ''' - Implements the context manager protocol. - ''' - self.close() - - def get_path(self): - """Returns the file path of an open database""" - self._assert_db_is_initialized() - return Database._get_path(self._db).decode('utf-8') - - def get_version(self): - """Returns the database format version - - :returns: The database version as positive integer - """ - self._assert_db_is_initialized() - return Database._get_version(self._db) - - def get_revision (self): - """Returns the committed database revision and UUID - - :returns: (revision, uuid) The database revision as a positive integer - and the UUID of the database. - """ - self._assert_db_is_initialized() - uuid = c_char_p () - revision = Database._get_revision(self._db, byref (uuid)) - return (revision, uuid.value.decode ('utf-8')) - - _needs_upgrade = nmlib.notmuch_database_needs_upgrade - _needs_upgrade.argtypes = [NotmuchDatabaseP] - _needs_upgrade.restype = bool - - def needs_upgrade(self): - """Does this database need to be upgraded before writing to it? - - If this function returns `True` then no functions that modify the - database (:meth:`index_file`, - :meth:`Message.add_tag`, :meth:`Directory.set_mtime`, - etc.) will work unless :meth:`upgrade` is called successfully first. - - :returns: `True` or `False` - """ - self._assert_db_is_initialized() - return self._needs_upgrade(self._db) - - def upgrade(self): - """Upgrades the current database - - After opening a database in read-write mode, the client should - check if an upgrade is needed (notmuch_database_needs_upgrade) and - if so, upgrade with this function before making any modifications. - - NOT IMPLEMENTED: The optional progress_notify callback can be - used by the caller to provide progress indication to the - user. If non-NULL it will be called periodically with - 'progress' as a floating-point value in the range of [0.0..1.0] - indicating the progress made so far in the upgrade process. - - :TODO: catch exceptions, document return values and etc... - """ - self._assert_db_is_initialized() - status = Database._upgrade(self._db, None, None) - # TODO: catch exceptions, document return values and etc - return status - - _begin_atomic = nmlib.notmuch_database_begin_atomic - _begin_atomic.argtypes = [NotmuchDatabaseP] - _begin_atomic.restype = c_uint - - def begin_atomic(self): - """Begin an atomic database operation - - Any modifications performed between a successful - :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to - the database atomically. Note that, unlike a typical database - transaction, this only ensures atomicity, not durability; - neither begin nor end necessarily flush modifications to disk. - - :returns: :attr:`STATUS`.SUCCESS or raises - :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION - Xapian exception occurred; atomic section not entered. - - *Added in notmuch 0.9*""" - self._assert_db_is_initialized() - status = self._begin_atomic(self._db) - if status != STATUS.SUCCESS: - raise NotmuchError(status) - return status - - _end_atomic = nmlib.notmuch_database_end_atomic - _end_atomic.argtypes = [NotmuchDatabaseP] - _end_atomic.restype = c_uint - - def end_atomic(self): - """Indicate the end of an atomic database operation - - See :meth:`begin_atomic` for details. - - :returns: :attr:`STATUS`.SUCCESS or raises - - :raises: - :exc:`NotmuchError`: - :attr:`STATUS`.XAPIAN_EXCEPTION - A Xapian exception occurred; atomic section not - ended. - :attr:`STATUS`.UNBALANCED_ATOMIC: - end_atomic has been called more times than begin_atomic. - - *Added in notmuch 0.9*""" - self._assert_db_is_initialized() - status = self._end_atomic(self._db) - if status != STATUS.SUCCESS: - raise NotmuchError(status) - return status - - def get_directory(self, path): - """Returns a :class:`Directory` of path, - - :param path: An unicode string containing the path relative to the path - of database (see :meth:`get_path`), or else should be an absolute - path with initial components that match the path of 'database'. - :returns: :class:`Directory` or raises an exception. - :raises: :exc:`FileError` if path is not relative database or absolute - with initial components same as database. - """ - self._assert_db_is_initialized() - - # sanity checking if path is valid, and make path absolute - if path and path[0] == os.sep: - # we got an absolute path - if not path.startswith(self.get_path()): - # but its initial components are not equal to the db path - raise FileError('Database().get_directory() called ' - 'with a wrong absolute path') - abs_dirpath = path - else: - #we got a relative path, make it absolute - abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path)) - - dir_p = NotmuchDirectoryP() - status = Database._get_directory(self._db, _str(path), byref(dir_p)) - - if status != STATUS.SUCCESS: - raise NotmuchError(status) - if not dir_p: - return None - - # return the Directory, init it with the absolute path - return Directory(abs_dirpath, dir_p, self) - - _get_default_indexopts = nmlib.notmuch_database_get_default_indexopts - _get_default_indexopts.argtypes = [NotmuchDatabaseP] - _get_default_indexopts.restype = NotmuchIndexoptsP - - _indexopts_set_decrypt_policy = nmlib.notmuch_indexopts_set_decrypt_policy - _indexopts_set_decrypt_policy.argtypes = [NotmuchIndexoptsP, c_uint] - _indexopts_set_decrypt_policy.restype = None - - _indexopts_destroy = nmlib.notmuch_indexopts_destroy - _indexopts_destroy.argtypes = [NotmuchIndexoptsP] - _indexopts_destroy.restype = None - - _index_file = nmlib.notmuch_database_index_file - _index_file.argtypes = [NotmuchDatabaseP, c_char_p, - c_void_p, - POINTER(NotmuchMessageP)] - _index_file.restype = c_uint - - def index_file(self, filename, sync_maildir_flags=False, decrypt_policy=None): - """Adds a new message to the database - - :param filename: should be a path relative to the path of the - open database (see :meth:`get_path`), or else should be an - absolute filename with initial components that match the - path of the database. - - The file should be a single mail message (not a - multi-message mbox) that is expected to remain at its - current location, since the notmuch database will reference - the filename, and will not copy the entire contents of the - file. - - :param sync_maildir_flags: If the message contains Maildir - flags, we will -depending on the notmuch configuration- sync - those tags to initial notmuch tags, if set to `True`. It is - `False` by default to remain consistent with the libnotmuch - API. You might want to look into the underlying method - :meth:`Message.maildir_flags_to_tags`. - - :param decrypt_policy: If the message contains any encrypted - parts, and decrypt_policy is set to - :attr:`DECRYPTION_POLICY`.TRUE, notmuch will try to - decrypt the message and index the cleartext, stashing any - discovered session keys. If it is set to - :attr:`DECRYPTION_POLICY`.FALSE, it will never try to - decrypt during indexing. If it is set to - :attr:`DECRYPTION_POLICY`.AUTO, then it will try to use - any stashed session keys it knows about, but will not try - to access the user's secret keys. - :attr:`DECRYPTION_POLICY`.NOSTASH behaves the same as - :attr:`DECRYPTION_POLICY`.TRUE except that no session keys - are stashed in the database. If decrypt_policy is set to - None (the default), then the database itself will decide - whether to decrypt, based on the `index.decrypt` - configuration setting (see notmuch-config(1)). - - :returns: On success, we return - - 1) a :class:`Message` object that can be used for things - such as adding tags to the just-added message. - 2) one of the following :attr:`STATUS` values: - - :attr:`STATUS`.SUCCESS - Message successfully added to database. - :attr:`STATUS`.DUPLICATE_MESSAGE_ID - Message has the same message ID as another message already - in the database. The new filename was successfully added - to the list of the filenames for the existing message. - - :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`) - - :raises: Raises a :exc:`NotmuchError` with the following meaning. - If such an exception occurs, nothing was added to the database. - - :attr:`STATUS`.FILE_ERROR - An error occurred trying to open the file, (such as - permission denied, or file not found, etc.). - :attr:`STATUS`.FILE_NOT_EMAIL - The contents of filename don't look like an email - message. - :attr:`STATUS`.READ_ONLY_DATABASE - Database was opened in read-only mode so no message can - be added. - """ - self._assert_db_is_initialized() - msg_p = NotmuchMessageP() - indexopts = c_void_p(None) - if decrypt_policy is not None: - indexopts = self._get_default_indexopts(self._db) - self._indexopts_set_decrypt_policy(indexopts, decrypt_policy) - - status = self._index_file(self._db, _str(filename), indexopts, byref(msg_p)) - - if indexopts: - self._indexopts_destroy(indexopts) - - if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]: - raise NotmuchError(status) - - #construct Message() and return - msg = Message(msg_p, self) - #automatic sync initial tags from Maildir flags - if sync_maildir_flags: - msg.maildir_flags_to_tags() - return (msg, status) - - def add_message(self, filename, sync_maildir_flags=False): - """Deprecated alias for :meth:`index_file` - """ - warnings.warn( - "This function is deprecated and will be removed in the future, use index_file.", DeprecationWarning) - - return self.index_file(filename, sync_maildir_flags=sync_maildir_flags) - - _remove_message = nmlib.notmuch_database_remove_message - _remove_message.argtypes = [NotmuchDatabaseP, c_char_p] - _remove_message.restype = c_uint - - def remove_message(self, filename): - """Removes a message (filename) from the given notmuch database - - Note that only this particular filename association is removed from - the database. If the same message (as determined by the message ID) - is still available via other filenames, then the message will - persist in the database for those filenames. When the last filename - is removed for a particular message, the database content for that - message will be entirely removed. - - :returns: A :attr:`STATUS` value with the following meaning: - - :attr:`STATUS`.SUCCESS - The last filename was removed and the message was removed - from the database. - :attr:`STATUS`.DUPLICATE_MESSAGE_ID - This filename was removed but the message persists in the - database with at least one other filename. - - :raises: Raises a :exc:`NotmuchError` with the following meaning. - If such an exception occurs, nothing was removed from the - database. - - :attr:`STATUS`.READ_ONLY_DATABASE - Database was opened in read-only mode so no message can be - removed. - """ - self._assert_db_is_initialized() - status = self._remove_message(self._db, _str(filename)) - if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]: - raise NotmuchError(status) - return status - - def find_message(self, msgid): - """Returns a :class:`Message` as identified by its message ID - - Wraps the underlying *notmuch_database_find_message* function. - - :param msgid: The message ID - :type msgid: unicode or str - :returns: :class:`Message` or `None` if no message is found. - :raises: - :exc:`OutOfMemoryError` - If an Out-of-memory occurred while constructing the message. - :exc:`XapianError` - In case of a Xapian Exception. These exceptions - include "Database modified" situations, e.g. when the - notmuch database has been modified by another program - in the meantime. In this case, you should close and - reopen the database and retry. - :exc:`NotInitializedError` if - the database was not initialized. - """ - self._assert_db_is_initialized() - msg_p = NotmuchMessageP() - status = Database._find_message(self._db, _str(msgid), byref(msg_p)) - if status != STATUS.SUCCESS: - raise NotmuchError(status) - return msg_p and Message(msg_p, self) or None - - def find_message_by_filename(self, filename): - """Find a message with the given filename - - :returns: If the database contains a message with the given - filename, then a class:`Message:` is returned. This - function returns None if no message is found with the given - filename. - - :raises: :exc:`OutOfMemoryError` if an Out-of-memory occurred while - constructing the message. - :raises: :exc:`XapianError` in case of a Xapian Exception. - These exceptions include "Database modified" - situations, e.g. when the notmuch database has been - modified by another program in the meantime. In this - case, you should close and reopen the database and - retry. - :raises: :exc:`NotInitializedError` if the database was not - initialized. - - *Added in notmuch 0.9*""" - self._assert_db_is_initialized() - - msg_p = NotmuchMessageP() - status = Database._find_message_by_filename(self._db, _str(filename), - byref(msg_p)) - if status != STATUS.SUCCESS: - raise NotmuchError(status) - return msg_p and Message(msg_p, self) or None - - def get_all_tags(self): - """Returns :class:`Tags` with a list of all tags found in the database - - :returns: :class:`Tags` - :exception: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER - on error - """ - self._assert_db_is_initialized() - tags_p = Database._get_all_tags(self._db) - if not tags_p: - raise NullPointerError() - return Tags(tags_p, self) - - def create_query(self, querystring): - """Returns a :class:`Query` derived from this database - - This is a shorthand method for doing:: - - # short version - # Automatically frees the Database() when 'q' is deleted - - q = Database(dbpath).create_query('from:"Biene Maja"') - - # long version, which is functionally equivalent but will keep the - # Database in the 'db' variable around after we delete 'q': - - db = Database(dbpath) - q = Query(db,'from:"Biene Maja"') - - This function is a python extension and not in the underlying C API. - """ - return Query(self, querystring) - - """notmuch_database_status_string""" - _status_string = nmlib.notmuch_database_status_string - _status_string.argtypes = [NotmuchDatabaseP] - _status_string.restype = c_char_p - - def status_string(self): - """Returns the status string of the database - - This is sometimes used for additional error reporting - """ - self._assert_db_is_initialized() - s = Database._status_string(self._db) - if s: - return s.decode('utf-8', 'ignore') - return s - - def __repr__(self): - return "'Notmuch DB " + self.get_path() + "'" - - def _get_user_default_db(self): - """ Reads a user's notmuch config and returns his db location - - Throws a NotmuchError if it cannot find it""" - config = SafeConfigParser() - conf_f = os.getenv('NOTMUCH_CONFIG', - os.path.expanduser('~/.notmuch-config')) - config.readfp(codecs.open(conf_f, 'r', 'utf-8')) - if not config.has_option('database', 'path'): - raise NotmuchError(message="No DB path specified" - " and no user default found") - db_path = config.get('database', 'path') - if not os.path.isabs(db_path): - db_path = os.path.expanduser(os.path.join("~", db_path)) - return db_path - - """notmuch_database_get_config""" - _get_config = nmlib.notmuch_database_get_config - _get_config.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(c_char_p)] - _get_config.restype = c_uint - - def get_config(self, key): - """Return the value of the given config key. - - Note that only config values that are stored in the database are - searched and returned. The config file is not read. - - :param key: the config key under which a value should be looked up, it - should probably be in the form "section.key" - :type key: str - :returns: the config value or the empty string if no value is present - for that key - :rtype: str - :raises: :exc:`NotmuchError` in case of failure. - - """ - self._assert_db_is_initialized() - return_string = c_char_p() - status = self._get_config(self._db, _str(key), byref(return_string)) - if status != STATUS.SUCCESS: - raise NotmuchError(status) - return return_string.value.decode('utf-8') - - """notmuch_database_get_config_list""" - _get_config_list = nmlib.notmuch_database_get_config_list - _get_config_list.argtypes = [NotmuchDatabaseP, c_char_p, - POINTER(NotmuchConfigListP)] - _get_config_list.restype = c_uint - - _config_list_valid = nmlib.notmuch_config_list_valid - _config_list_valid.argtypes = [NotmuchConfigListP] - _config_list_valid.restype = bool - - _config_list_key = nmlib.notmuch_config_list_key - _config_list_key.argtypes = [NotmuchConfigListP] - _config_list_key.restype = c_char_p - - _config_list_value = nmlib.notmuch_config_list_value - _config_list_value.argtypes = [NotmuchConfigListP] - _config_list_value.restype = c_char_p - - _config_list_move_to_next = nmlib.notmuch_config_list_move_to_next - _config_list_move_to_next.argtypes = [NotmuchConfigListP] - _config_list_move_to_next.restype = None - - _config_list_destroy = nmlib.notmuch_config_list_destroy - _config_list_destroy.argtypes = [NotmuchConfigListP] - _config_list_destroy.restype = None - - def get_configs(self, prefix=''): - """Return a generator of key, value pairs where the start of key - matches the given prefix - - Note that only config values that are stored in the database are - searched and returned. The config file is not read. If no `prefix` is - given all config values are returned. - - This could be used to get all named queries into a dict for example:: - - queries = {k[6:]: v for k, v in db.get_configs('query.')} - - :param prefix: a string by which the keys should be selected - :type prefix: str - :yields: all key-value pairs where `prefix` matches the beginning - of the key - :ytype: pairs of str - :raises: :exc:`NotmuchError` in case of failure. - - """ - self._assert_db_is_initialized() - config_list_p = NotmuchConfigListP() - status = self._get_config_list(self._db, _str(prefix), - byref(config_list_p)) - if status != STATUS.SUCCESS: - raise NotmuchError(status) - while self._config_list_valid(config_list_p): - key = self._config_list_key(config_list_p).decode('utf-8') - value = self._config_list_value(config_list_p).decode('utf-8') - yield key, value - self._config_list_move_to_next(config_list_p) - - """notmuch_database_set_config""" - _set_config = nmlib.notmuch_database_set_config - _set_config.argtypes = [NotmuchDatabaseP, c_char_p, c_char_p] - _set_config.restype = c_uint - - def set_config(self, key, value): - """Set a config value in the notmuch database. - - If an empty string is provided as `value` the `key` is unset! - - :param key: the key to set - :type key: str - :param value: the value to store under `key` - :type value: str - :returns: None - :raises: :exc:`NotmuchError` in case of failure. - - """ - self._assert_db_is_initialized() - status = self._set_config(self._db, _str(key), _str(value)) - if status != STATUS.SUCCESS: - raise NotmuchError(status) diff --git a/bindings/python/notmuch/directory.py b/bindings/python/notmuch/directory.py deleted file mode 100644 index b30c9e35..00000000 --- a/bindings/python/notmuch/directory.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -""" - -from ctypes import c_uint, c_long -from .globals import ( - nmlib, - NotmuchDirectoryP, - NotmuchFilenamesP -) -from .errors import ( - STATUS, - NotmuchError, - NotInitializedError, -) -from .filenames import Filenames - -class Directory(object): - """Represents a directory entry in the notmuch directory - - Modifying attributes of this object will modify the - database, not the real directory attributes. - - The Directory object is usually derived from another object - e.g. via :meth:`Database.get_directory`, and will automatically be - become invalid whenever that parent is deleted. You should - therefore initialized this object handing it a reference to the - parent, preventing the parent from automatically being garbage - collected. - """ - - """notmuch_directory_get_mtime""" - _get_mtime = nmlib.notmuch_directory_get_mtime - _get_mtime.argtypes = [NotmuchDirectoryP] - _get_mtime.restype = c_long - - """notmuch_directory_set_mtime""" - _set_mtime = nmlib.notmuch_directory_set_mtime - _set_mtime.argtypes = [NotmuchDirectoryP, c_long] - _set_mtime.restype = c_uint - - """notmuch_directory_get_child_files""" - _get_child_files = nmlib.notmuch_directory_get_child_files - _get_child_files.argtypes = [NotmuchDirectoryP] - _get_child_files.restype = NotmuchFilenamesP - - """notmuch_directory_get_child_directories""" - _get_child_directories = nmlib.notmuch_directory_get_child_directories - _get_child_directories.argtypes = [NotmuchDirectoryP] - _get_child_directories.restype = NotmuchFilenamesP - - def _assert_dir_is_initialized(self): - """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED) - if dir_p is None""" - if not self._dir_p: - raise NotInitializedError() - - def __init__(self, path, dir_p, parent): - """ - :param path: The absolute path of the directory object. - :param dir_p: The pointer to an internal notmuch_directory_t object. - :param parent: The object this Directory is derived from - (usually a :class:`Database`). We do not directly use - this, but store a reference to it as long as - this Directory object lives. This keeps the - parent object alive. - """ - self._path = path - self._dir_p = dir_p - self._parent = parent - - def set_mtime(self, mtime): - """Sets the mtime value of this directory in the database - - The intention is for the caller to use the mtime to allow efficient - identification of new messages to be added to the database. The - recommended usage is as follows: - - * Read the mtime of a directory from the filesystem - - * Call :meth:`Database.index_file` for all mail files in - the directory - - * Call notmuch_directory_set_mtime with the mtime read from the - filesystem. Then, when wanting to check for updates to the - directory in the future, the client can call :meth:`get_mtime` - and know that it only needs to add files if the mtime of the - directory and files are newer than the stored timestamp. - - .. note:: - - :meth:`get_mtime` function does not allow the caller to - distinguish a timestamp of 0 from a non-existent timestamp. So - don't store a timestamp of 0 unless you are comfortable with - that. - - :param mtime: A (time_t) timestamp - :raises: :exc:`XapianError` a Xapian exception occurred, mtime - not stored - :raises: :exc:`ReadOnlyDatabaseError` the database was opened - in read-only mode so directory mtime cannot be modified - :raises: :exc:`NotInitializedError` the directory object has not - been initialized - """ - self._assert_dir_is_initialized() - status = Directory._set_mtime(self._dir_p, mtime) - - if status != STATUS.SUCCESS: - raise NotmuchError(status) - - def get_mtime(self): - """Gets the mtime value of this directory in the database - - Retrieves a previously stored mtime for this directory. - - :param mtime: A (time_t) timestamp - :raises: :exc:`NotmuchError`: - - :attr:`STATUS`.NOT_INITIALIZED - The directory has not been initialized - """ - self._assert_dir_is_initialized() - return Directory._get_mtime(self._dir_p) - - # Make mtime attribute a property of Directory() - mtime = property(get_mtime, set_mtime, doc="""Property that allows getting - and setting of the Directory *mtime* (read-write) - - See :meth:`get_mtime` and :meth:`set_mtime` for usage and - possible exceptions.""") - - def get_child_files(self): - """Gets a Filenames iterator listing all the filenames of - messages in the database within the given directory. - - The returned filenames will be the basename-entries only (not - complete paths. - """ - self._assert_dir_is_initialized() - files_p = Directory._get_child_files(self._dir_p) - return Filenames(files_p, self) - - def get_child_directories(self): - """Gets a :class:`Filenames` iterator listing all the filenames of - sub-directories in the database within the given directory - - The returned filenames will be the basename-entries only (not - complete paths. - """ - self._assert_dir_is_initialized() - files_p = Directory._get_child_directories(self._dir_p) - return Filenames(files_p, self) - - @property - def path(self): - """Returns the absolute path of this Directory (read-only)""" - return self._path - - def __repr__(self): - """Object representation""" - return "" % self._path - - _destroy = nmlib.notmuch_directory_destroy - _destroy.argtypes = [NotmuchDirectoryP] - _destroy.restype = None - - def __del__(self): - """Close and free the Directory""" - if self._dir_p: - self._destroy(self._dir_p) diff --git a/bindings/python/notmuch/errors.py b/bindings/python/notmuch/errors.py deleted file mode 100644 index b7684ef6..00000000 --- a/bindings/python/notmuch/errors.py +++ /dev/null @@ -1,204 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -""" - -from ctypes import c_char_p, c_int - -from .globals import ( - nmlib, - Enum, - Python3StringMixIn, -) - -class Status(Enum): - """Enum with a string representation of a notmuch_status_t value.""" - _status2str = nmlib.notmuch_status_to_string - _status2str.restype = c_char_p - _status2str.argtypes = [c_int] - - def __init__(self, statuslist): - """It is initialized with a list of strings that are available as - Status().string1 - Status().stringn attributes. - """ - super(Status, self).__init__(statuslist) - - @classmethod - def status2str(self, status): - """Get a (unicode) string representation of a notmuch_status_t value.""" - # define strings for custom error messages - if status == STATUS.NOT_INITIALIZED: - return "Operation on uninitialized object impossible." - return unicode(Status._status2str(status)) - -STATUS = Status(['SUCCESS', - 'OUT_OF_MEMORY', - 'READ_ONLY_DATABASE', - 'XAPIAN_EXCEPTION', - 'FILE_ERROR', - 'FILE_NOT_EMAIL', - 'DUPLICATE_MESSAGE_ID', - 'NULL_POINTER', - 'TAG_TOO_LONG', - 'UNBALANCED_FREEZE_THAW', - 'UNBALANCED_ATOMIC', - 'UNSUPPORTED_OPERATION', - 'UPGRADE_REQUIRED', - 'PATH_ERROR', - 'NOT_INITIALIZED']) -"""STATUS is a class, whose attributes provide constants that serve as return -indicators for notmuch functions. Currently the following ones are defined. For -possible return values and specific meaning for each method, see the method -description. - - * SUCCESS - * OUT_OF_MEMORY - * READ_ONLY_DATABASE - * XAPIAN_EXCEPTION - * FILE_ERROR - * FILE_NOT_EMAIL - * DUPLICATE_MESSAGE_ID - * NULL_POINTER - * TAG_TOO_LONG - * UNBALANCED_FREEZE_THAW - * UNBALANCED_ATOMIC - * UNSUPPORTED_OPERATION - * UPGRADE_REQUIRED - * PATH_ERROR - * NOT_INITIALIZED - -Invoke the class method `notmuch.STATUS.status2str` with a status value as -argument to receive a human readable string""" -STATUS.__name__ = 'STATUS' - - -class NotmuchError(Exception, Python3StringMixIn): - """Is initiated with a (notmuch.STATUS[, message=None]). It will not - return an instance of the class NotmuchError, but a derived instance - of a more specific Error Message, e.g. OutOfMemoryError. Each status - but SUCCESS has a corresponding subclassed Exception.""" - - @classmethod - def get_exc_subclass(cls, status): - """Returns a fine grained Exception() type, - detailing the error status""" - subclasses = { - STATUS.OUT_OF_MEMORY: OutOfMemoryError, - STATUS.READ_ONLY_DATABASE: ReadOnlyDatabaseError, - STATUS.XAPIAN_EXCEPTION: XapianError, - STATUS.FILE_ERROR: FileError, - STATUS.FILE_NOT_EMAIL: FileNotEmailError, - STATUS.DUPLICATE_MESSAGE_ID: DuplicateMessageIdError, - STATUS.NULL_POINTER: NullPointerError, - STATUS.TAG_TOO_LONG: TagTooLongError, - STATUS.UNBALANCED_FREEZE_THAW: UnbalancedFreezeThawError, - STATUS.UNBALANCED_ATOMIC: UnbalancedAtomicError, - STATUS.UNSUPPORTED_OPERATION: UnsupportedOperationError, - STATUS.UPGRADE_REQUIRED: UpgradeRequiredError, - STATUS.PATH_ERROR: PathError, - STATUS.NOT_INITIALIZED: NotInitializedError, - } - assert 0 < status <= len(subclasses) - return subclasses[status] - - def __new__(cls, *args, **kwargs): - """Return a correct subclass of NotmuchError if needed - - We return a NotmuchError instance if status is None (or 0) and a - subclass that inherits from NotmuchError depending on the - 'status' parameter otherwise.""" - # get 'status'. Passed in as arg or kwarg? - status = args[0] if len(args) else kwargs.get('status', None) - # no 'status' or cls is subclass already, return 'cls' instance - if not status or cls != NotmuchError: - return super(NotmuchError, cls).__new__(cls) - subclass = cls.get_exc_subclass(status) # which class to use? - return subclass.__new__(subclass, *args, **kwargs) - - def __init__(self, status=None, message=None): - self.status = status - self.message = message - - def __unicode__(self): - if self.message is not None: - return self.message - elif self.status is not None: - return STATUS.status2str(self.status) - else: - return 'Unknown error' - - -# List of Subclassed exceptions that correspond to STATUS values and are -# subclasses of NotmuchError. -class OutOfMemoryError(NotmuchError): - status = STATUS.OUT_OF_MEMORY - - -class ReadOnlyDatabaseError(NotmuchError): - status = STATUS.READ_ONLY_DATABASE - - -class XapianError(NotmuchError): - status = STATUS.XAPIAN_EXCEPTION - - -class FileError(NotmuchError): - status = STATUS.FILE_ERROR - - -class FileNotEmailError(NotmuchError): - status = STATUS.FILE_NOT_EMAIL - - -class DuplicateMessageIdError(NotmuchError): - status = STATUS.DUPLICATE_MESSAGE_ID - - -class NullPointerError(NotmuchError): - status = STATUS.NULL_POINTER - - -class TagTooLongError(NotmuchError): - status = STATUS.TAG_TOO_LONG - - -class UnbalancedFreezeThawError(NotmuchError): - status = STATUS.UNBALANCED_FREEZE_THAW - - -class UnbalancedAtomicError(NotmuchError): - status = STATUS.UNBALANCED_ATOMIC - - -class UnsupportedOperationError(NotmuchError): - status = STATUS.UNSUPPORTED_OPERATION - - -class UpgradeRequiredError(NotmuchError): - status = STATUS.UPGRADE_REQUIRED - - -class PathError(NotmuchError): - status = STATUS.PATH_ERROR - - -class NotInitializedError(NotmuchError): - """Derived from NotmuchError, this occurs if the underlying data - structure (e.g. database is not initialized (yet) or an iterator has - been exhausted. You can test for NotmuchError with .status = - STATUS.NOT_INITIALIZED""" - status = STATUS.NOT_INITIALIZED diff --git a/bindings/python/notmuch/filenames.py b/bindings/python/notmuch/filenames.py deleted file mode 100644 index 3bbc22b9..00000000 --- a/bindings/python/notmuch/filenames.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -""" -from ctypes import c_char_p -from .globals import ( - nmlib, - NotmuchFilenamesP, - Python3StringMixIn, -) -from .errors import ( - NullPointerError, - NotInitializedError, -) - - -class Filenames(Python3StringMixIn): - """Represents a list of filenames as returned by notmuch - - Objects of this class implement the iterator protocol. - - .. note:: - - The underlying library only provides a one-time iterator (it - cannot reset the iterator to the start). Thus iterating over - the function will "exhaust" the list of tags, and a subsequent - iteration attempt will raise a - :exc:`NotInitializedError`. Also note, that any function that - uses iteration (nearly all) will also exhaust the tags. So - both:: - - for name in filenames: print name - - as well as:: - - list_of_names = list(names) - - and even a simple:: - - #str() iterates over all tags to construct a space separated list - print(str(filenames)) - - will "exhaust" the Filenames. However, you can use - :meth:`Message.get_filenames` repeatedly to get fresh - Filenames objects to perform various actions on filenames. - """ - - #notmuch_filenames_get - _get = nmlib.notmuch_filenames_get - _get.argtypes = [NotmuchFilenamesP] - _get.restype = c_char_p - - def __init__(self, files_p, parent): - """ - :param files_p: A pointer to an underlying *notmuch_tags_t* - structure. These are not publicly exposed, so a user - will almost never instantiate a :class:`Tags` object - herself. They are usually handed back as a result, - e.g. in :meth:`Database.get_all_tags`. *tags_p* must be - valid, we will raise an :exc:`NullPointerError` - if it is `None`. - :type files_p: :class:`ctypes.c_void_p` - :param parent: The parent object (ie :class:`Message` these - filenames are derived from, and saves a - reference to it, so we can automatically delete the db object - once all derived objects are dead. - """ - if not files_p: - raise NullPointerError() - - self._files_p = files_p - #save reference to parent object so we keep it alive - self._parent = parent - - def __iter__(self): - """ Make Filenames an iterator """ - return self - - _valid = nmlib.notmuch_filenames_valid - _valid.argtypes = [NotmuchFilenamesP] - _valid.restype = bool - - _move_to_next = nmlib.notmuch_filenames_move_to_next - _move_to_next.argtypes = [NotmuchFilenamesP] - _move_to_next.restype = None - - def __next__(self): - if not self._files_p: - raise NotInitializedError() - - if not self._valid(self._files_p): - self._files_p = None - raise StopIteration - - file_ = Filenames._get(self._files_p) - self._move_to_next(self._files_p) - return file_.decode('utf-8', 'ignore') - next = __next__ # python2.x iterator protocol compatibility - - def __unicode__(self): - """Represent Filenames() as newline-separated list of full paths - - .. note:: - - This method exhausts the iterator object, so you will not be able to - iterate over them again. - """ - return "\n".join(self) - - _destroy = nmlib.notmuch_filenames_destroy - _destroy.argtypes = [NotmuchFilenamesP] - _destroy.restype = None - - def __del__(self): - """Close and free the notmuch filenames""" - if self._files_p: - self._destroy(self._files_p) diff --git a/bindings/python/notmuch/globals.py b/bindings/python/notmuch/globals.py deleted file mode 100644 index 11e328b7..00000000 --- a/bindings/python/notmuch/globals.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -""" - -from ctypes import CDLL, Structure, POINTER -from notmuch.version import SOVERSION - -#----------------------------------------------------------------------------- -#package-global instance of the notmuch library -try: - from os import uname - if uname()[0] == 'Darwin': - nmlib = CDLL("libnotmuch.{0:s}.dylib".format(SOVERSION)) - else: - nmlib = CDLL("libnotmuch.so.{0:s}".format(SOVERSION)) -except: - raise ImportError("Could not find shared 'notmuch' library.") - -from .compat import Python3StringMixIn, encode_utf8 as _str - -# We import these on behalf of other modules. Silence warning about -# these symbols not being used. -Python3StringMixIn -_str - -class Enum(object): - """Provides ENUMS as "code=Enum(['a','b','c'])" where code.a=0 etc...""" - def __init__(self, names): - for number, name in enumerate(names): - setattr(self, name, number) - - -class NotmuchDatabaseS(Structure): - pass -NotmuchDatabaseP = POINTER(NotmuchDatabaseS) - - -class NotmuchQueryS(Structure): - pass -NotmuchQueryP = POINTER(NotmuchQueryS) - - -class NotmuchThreadsS(Structure): - pass -NotmuchThreadsP = POINTER(NotmuchThreadsS) - - -class NotmuchThreadS(Structure): - pass -NotmuchThreadP = POINTER(NotmuchThreadS) - - -class NotmuchMessagesS(Structure): - pass -NotmuchMessagesP = POINTER(NotmuchMessagesS) - - -class NotmuchMessageS(Structure): - pass -NotmuchMessageP = POINTER(NotmuchMessageS) - - -class NotmuchMessagePropertiesS(Structure): - pass -NotmuchMessagePropertiesP = POINTER(NotmuchMessagePropertiesS) - - -class NotmuchTagsS(Structure): - pass -NotmuchTagsP = POINTER(NotmuchTagsS) - - -class NotmuchDirectoryS(Structure): - pass -NotmuchDirectoryP = POINTER(NotmuchDirectoryS) - - -class NotmuchFilenamesS(Structure): - pass -NotmuchFilenamesP = POINTER(NotmuchFilenamesS) - - -class NotmuchConfigListS(Structure): - pass -NotmuchConfigListP = POINTER(NotmuchConfigListS) - - -class NotmuchIndexoptsS(Structure): - pass -NotmuchIndexoptsP = POINTER(NotmuchIndexoptsS) diff --git a/bindings/python/notmuch/message.py b/bindings/python/notmuch/message.py deleted file mode 100644 index e71dbe3e..00000000 --- a/bindings/python/notmuch/message.py +++ /dev/null @@ -1,721 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth - Jesse Rosenthal -""" - - -from ctypes import c_char_p, c_long, c_uint, c_int, POINTER, byref -from datetime import date -from .globals import ( - nmlib, - Enum, - _str, - Python3StringMixIn, - NotmuchTagsP, - NotmuchMessageP, - NotmuchMessagesP, - NotmuchMessagePropertiesP, - NotmuchFilenamesP, -) -from .errors import ( - STATUS, - NotmuchError, - NullPointerError, - NotInitializedError, -) -from .tag import Tags -from .filenames import Filenames - -import email -import sys - - -class Message(Python3StringMixIn): - r"""Represents a single Email message - - Technically, this wraps the underlying *notmuch_message_t* - structure. A user will usually not create these objects themselves - but get them as search results. - - As it implements :meth:`__cmp__`, it is possible to compare two - :class:`Message`\s using `if msg1 == msg2: ...`. - """ - - """notmuch_message_get_filename (notmuch_message_t *message)""" - _get_filename = nmlib.notmuch_message_get_filename - _get_filename.argtypes = [NotmuchMessageP] - _get_filename.restype = c_char_p - - """return all filenames for a message""" - _get_filenames = nmlib.notmuch_message_get_filenames - _get_filenames.argtypes = [NotmuchMessageP] - _get_filenames.restype = NotmuchFilenamesP - - """notmuch_message_get_flag""" - _get_flag = nmlib.notmuch_message_get_flag - _get_flag.argtypes = [NotmuchMessageP, c_uint] - _get_flag.restype = bool - - """notmuch_message_set_flag""" - _set_flag = nmlib.notmuch_message_set_flag - _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int] - _set_flag.restype = None - - """notmuch_message_get_message_id (notmuch_message_t *message)""" - _get_message_id = nmlib.notmuch_message_get_message_id - _get_message_id.argtypes = [NotmuchMessageP] - _get_message_id.restype = c_char_p - - """notmuch_message_get_thread_id""" - _get_thread_id = nmlib.notmuch_message_get_thread_id - _get_thread_id.argtypes = [NotmuchMessageP] - _get_thread_id.restype = c_char_p - - """notmuch_message_get_replies""" - _get_replies = nmlib.notmuch_message_get_replies - _get_replies.argtypes = [NotmuchMessageP] - _get_replies.restype = NotmuchMessagesP - - """notmuch_message_get_tags (notmuch_message_t *message)""" - _get_tags = nmlib.notmuch_message_get_tags - _get_tags.argtypes = [NotmuchMessageP] - _get_tags.restype = NotmuchTagsP - - _get_date = nmlib.notmuch_message_get_date - _get_date.argtypes = [NotmuchMessageP] - _get_date.restype = c_long - - _get_header = nmlib.notmuch_message_get_header - _get_header.argtypes = [NotmuchMessageP, c_char_p] - _get_header.restype = c_char_p - - """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)""" - _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags - _tags_to_maildir_flags.argtypes = [NotmuchMessageP] - _tags_to_maildir_flags.restype = c_int - - """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)""" - _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags - _maildir_flags_to_tags.argtypes = [NotmuchMessageP] - _maildir_flags_to_tags.restype = c_int - - """notmuch_message_get_property""" - _get_property = nmlib.notmuch_message_get_property - _get_property.argtypes = [NotmuchMessageP, c_char_p, POINTER(c_char_p)] - _get_property.restype = c_int - - """notmuch_message_get_properties""" - _get_properties = nmlib.notmuch_message_get_properties - _get_properties.argtypes = [NotmuchMessageP, c_char_p, c_int] - _get_properties.restype = NotmuchMessagePropertiesP - - """notmuch_message_properties_valid""" - _properties_valid = nmlib.notmuch_message_properties_valid - _properties_valid.argtypes = [NotmuchMessagePropertiesP] - _properties_valid.restype = bool - - """notmuch_message_properties_value""" - _properties_value = nmlib.notmuch_message_properties_value - _properties_value.argtypes = [NotmuchMessagePropertiesP] - _properties_value.restype = c_char_p - - """notmuch_message_properties_key""" - _properties_key = nmlib.notmuch_message_properties_key - _properties_key.argtypes = [NotmuchMessagePropertiesP] - _properties_key.restype = c_char_p - - """notmuch_message_properties_move_to_next""" - _properties_move_to_next = nmlib.notmuch_message_properties_move_to_next - _properties_move_to_next.argtypes = [NotmuchMessagePropertiesP] - _properties_move_to_next.restype = None - - #Constants: Flags that can be set/get with set_flag - FLAG = Enum(['MATCH']) - - def __init__(self, msg_p, parent=None): - """ - :param msg_p: A pointer to an internal notmuch_message_t - Structure. If it is `None`, we will raise an - :exc:`NullPointerError`. - - :param parent: A 'parent' object is passed which this message is - derived from. We save a reference to it, so we can - automatically delete the parent object once all derived - objects are dead. - """ - if not msg_p: - raise NullPointerError() - self._msg = msg_p - #keep reference to parent, so we keep it alive - self._parent = parent - - def get_message_id(self): - """Returns the message ID - - :returns: String with a message ID - :raises: :exc:`NotInitializedError` if the message - is not initialized. - """ - if not self._msg: - raise NotInitializedError() - return Message._get_message_id(self._msg).decode('utf-8', 'ignore') - - def get_thread_id(self): - """Returns the thread ID - - The returned string belongs to 'message' will only be valid for as - long as the message is valid. - - This function will not return `None` since Notmuch ensures that every - message belongs to a single thread. - - :returns: String with a thread ID - :raises: :exc:`NotInitializedError` if the message - is not initialized. - """ - if not self._msg: - raise NotInitializedError() - - return Message._get_thread_id(self._msg).decode('utf-8', 'ignore') - - def get_replies(self): - """Gets all direct replies to this message as :class:`Messages` - iterator - - .. note:: - - This call only makes sense if 'message' was ultimately obtained from - a :class:`Thread` object, (such as by coming directly from the - result of calling :meth:`Thread.get_toplevel_messages` or by any - number of subsequent calls to :meth:`get_replies`). If this message - was obtained through some non-thread means, (such as by a call to - :meth:`Query.search_messages`), then this function will return - an empty Messages iterator. - - :returns: :class:`Messages`. - :raises: :exc:`NotInitializedError` if the message - is not initialized. - """ - if not self._msg: - raise NotInitializedError() - - msgs_p = Message._get_replies(self._msg) - - from .messages import Messages, EmptyMessagesResult - - if not msgs_p: - return EmptyMessagesResult(self) - - return Messages(msgs_p, self) - - def get_date(self): - """Returns time_t of the message date - - For the original textual representation of the Date header from the - message call notmuch_message_get_header() with a header value of - "date". - - :returns: A time_t timestamp. - :rtype: c_unit64 - :raises: :exc:`NotInitializedError` if the message - is not initialized. - """ - if not self._msg: - raise NotInitializedError() - return Message._get_date(self._msg) - - def get_header(self, header): - """Get the value of the specified header. - - The value will be read from the actual message file, not from - the notmuch database. The header name is case insensitive. - - Returns an empty string ("") if the message does not contain a - header line matching 'header'. - - :param header: The name of the header to be retrieved. - It is not case-sensitive. - :type header: str - :returns: The header value as string - :raises: :exc:`NotInitializedError` if the message is not - initialized - :raises: :exc:`NullPointerError` if any error occurred - """ - if not self._msg: - raise NotInitializedError() - - #Returns NULL if any error occurs. - header = Message._get_header(self._msg, _str(header)) - if header == None: - raise NullPointerError() - return header.decode('UTF-8', 'ignore') - - def get_filename(self): - """Returns the file path of the message file - - :returns: Absolute file path & name of the message file - :raises: :exc:`NotInitializedError` if the message - is not initialized. - """ - if not self._msg: - raise NotInitializedError() - return Message._get_filename(self._msg).decode('utf-8', 'ignore') - - def get_filenames(self): - """Get all filenames for the email corresponding to 'message' - - Returns a Filenames() generator with all absolute filepaths for - messages recorded to have the same Message-ID. These files must - not necessarily have identical content.""" - if not self._msg: - raise NotInitializedError() - - files_p = Message._get_filenames(self._msg) - - return Filenames(files_p, self) - - def get_flag(self, flag): - """Checks whether a specific flag is set for this message - - The method :meth:`Query.search_threads` sets - *Message.FLAG.MATCH* for those messages that match the - query. This method allows us to get the value of this flag. - - :param flag: One of the :attr:`Message.FLAG` values (currently only - *Message.FLAG.MATCH* - :returns: An unsigned int (0/1), indicating whether the flag is set. - :raises: :exc:`NotInitializedError` if the message - is not initialized. - """ - if not self._msg: - raise NotInitializedError() - return Message._get_flag(self._msg, flag) - - def set_flag(self, flag, value): - """Sets/Unsets a specific flag for this message - - :param flag: One of the :attr:`Message.FLAG` values (currently only - *Message.FLAG.MATCH* - :param value: A bool indicating whether to set or unset the flag. - - :raises: :exc:`NotInitializedError` if the message - is not initialized. - """ - if not self._msg: - raise NotInitializedError() - self._set_flag(self._msg, flag, value) - - def get_tags(self): - """Returns the message tags - - :returns: A :class:`Tags` iterator. - :raises: :exc:`NotInitializedError` if the message is not - initialized - :raises: :exc:`NullPointerError` if any error occurred - """ - if not self._msg: - raise NotInitializedError() - - tags_p = Message._get_tags(self._msg) - if not tags_p: - raise NullPointerError() - return Tags(tags_p, self) - - _add_tag = nmlib.notmuch_message_add_tag - _add_tag.argtypes = [NotmuchMessageP, c_char_p] - _add_tag.restype = c_uint - - def add_tag(self, tag, sync_maildir_flags=False): - """Adds a tag to the given message - - Adds a tag to the current message. The maximal tag length is defined in - the notmuch library and is currently 200 bytes. - - :param tag: String with a 'tag' to be added. - - :param sync_maildir_flags: If notmuch configuration is set to do - this, add maildir flags corresponding to notmuch tags. See - underlying method :meth:`tags_to_maildir_flags`. Use False - if you want to add/remove many tags on a message without - having to physically rename the file every time. Do note, - that this will do nothing when a message is frozen, as tag - changes will not be committed to the database yet. - - :returns: STATUS.SUCCESS if the tag was successfully added. - Raises an exception otherwise. - :raises: :exc:`NullPointerError` if the `tag` argument is NULL - :raises: :exc:`TagTooLongError` if the length of `tag` exceeds - Message.NOTMUCH_TAG_MAX) - :raises: :exc:`ReadOnlyDatabaseError` if the database was opened - in read-only mode so message cannot be modified - :raises: :exc:`NotInitializedError` if message has not been - initialized - """ - if not self._msg: - raise NotInitializedError() - - status = self._add_tag(self._msg, _str(tag)) - - # bail out on failure - if status != STATUS.SUCCESS: - raise NotmuchError(status) - - if sync_maildir_flags: - self.tags_to_maildir_flags() - return STATUS.SUCCESS - - _remove_tag = nmlib.notmuch_message_remove_tag - _remove_tag.argtypes = [NotmuchMessageP, c_char_p] - _remove_tag.restype = c_uint - - def remove_tag(self, tag, sync_maildir_flags=False): - """Removes a tag from the given message - - If the message has no such tag, this is a non-operation and - will report success anyway. - - :param tag: String with a 'tag' to be removed. - :param sync_maildir_flags: If notmuch configuration is set to do - this, add maildir flags corresponding to notmuch tags. See - underlying method :meth:`tags_to_maildir_flags`. Use False - if you want to add/remove many tags on a message without - having to physically rename the file every time. Do note, - that this will do nothing when a message is frozen, as tag - changes will not be committed to the database yet. - - :returns: STATUS.SUCCESS if the tag was successfully removed or if - the message had no such tag. - Raises an exception otherwise. - :raises: :exc:`NullPointerError` if the `tag` argument is NULL - :raises: :exc:`TagTooLongError` if the length of `tag` exceeds - Message.NOTMUCH_TAG_MAX) - :raises: :exc:`ReadOnlyDatabaseError` if the database was opened - in read-only mode so message cannot be modified - :raises: :exc:`NotInitializedError` if message has not been - initialized - """ - if not self._msg: - raise NotInitializedError() - - status = self._remove_tag(self._msg, _str(tag)) - # bail out on error - if status != STATUS.SUCCESS: - raise NotmuchError(status) - - if sync_maildir_flags: - self.tags_to_maildir_flags() - return STATUS.SUCCESS - - _remove_all_tags = nmlib.notmuch_message_remove_all_tags - _remove_all_tags.argtypes = [NotmuchMessageP] - _remove_all_tags.restype = c_uint - - def remove_all_tags(self, sync_maildir_flags=False): - """Removes all tags from the given message. - - See :meth:`freeze` for an example showing how to safely - replace tag values. - - - :param sync_maildir_flags: If notmuch configuration is set to do - this, add maildir flags corresponding to notmuch tags. See - :meth:`tags_to_maildir_flags`. Use False if you want to - add/remove many tags on a message without having to - physically rename the file every time. Do note, that this - will do nothing when a message is frozen, as tag changes - will not be committed to the database yet. - - :returns: STATUS.SUCCESS if the tags were successfully removed. - Raises an exception otherwise. - :raises: :exc:`ReadOnlyDatabaseError` if the database was opened - in read-only mode so message cannot be modified - :raises: :exc:`NotInitializedError` if message has not been - initialized - """ - if not self._msg: - raise NotInitializedError() - - status = self._remove_all_tags(self._msg) - - # bail out on error - if status != STATUS.SUCCESS: - raise NotmuchError(status) - - if sync_maildir_flags: - self.tags_to_maildir_flags() - return STATUS.SUCCESS - - _freeze = nmlib.notmuch_message_freeze - _freeze.argtypes = [NotmuchMessageP] - _freeze.restype = c_uint - - def get_property(self, prop): - """ Retrieve the value for a single property key - - :param prop: The name of the property to get. - :returns: String with the property value or None if there is no such - key. In the case of multiple values for the given key, the - first one is retrieved. - :raises: :exc:`NotInitializedError` if message has not been - initialized - """ - if not self._msg: - raise NotInitializedError() - - value = c_char_p() - status = Message._get_property(self._msg, _str(prop), byref(value)) - if status != 0: - raise NotmuchError(status) - - if value is None or value.value is None: - return None - return value.value.decode('utf-8') - - def get_properties(self, prop="", exact=False): - """ Get the properties of the message, returning a generator of - name, value pairs. - - The generator will yield once per value. There might be more than one - value on each name, so the generator might yield the same name several - times. - - :param prop: The name of the property to get. Otherwise it will return - the full list of properties of the message. - :param exact: if True, require exact match with key. Otherwise - treat as prefix. - :yields: Each property values as a pair of `name, value` - :ytype: pairs of str - :raises: :exc:`NotInitializedError` if message has not been - initialized - """ - if not self._msg: - raise NotInitializedError() - - properties = Message._get_properties(self._msg, _str(prop), exact) - while Message._properties_valid(properties): - key = Message._properties_key(properties) - value = Message._properties_value(properties) - yield key.decode("utf-8"), value.decode("utf-8") - Message._properties_move_to_next(properties) - - def freeze(self): - """Freezes the current state of 'message' within the database - - This means that changes to the message state, (via :meth:`add_tag`, - :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be - committed to the database until the message is :meth:`thaw` ed. - - Multiple calls to freeze/thaw are valid and these calls will - "stack". That is there must be as many calls to thaw as to freeze - before a message is actually thawed. - - The ability to do freeze/thaw allows for safe transactions to - change tag values. For example, explicitly setting a message to - have a given set of tags might look like this:: - - msg.freeze() - msg.remove_all_tags(False) - for tag in new_tags: - msg.add_tag(tag, False) - msg.thaw() - msg.tags_to_maildir_flags() - - With freeze/thaw used like this, the message in the database is - guaranteed to have either the full set of original tag values, or - the full set of new tag values, but nothing in between. - - Imagine the example above without freeze/thaw and the operation - somehow getting interrupted. This could result in the message being - left with no tags if the interruption happened after - :meth:`remove_all_tags` but before :meth:`add_tag`. - - :returns: STATUS.SUCCESS if the message was successfully frozen. - Raises an exception otherwise. - :raises: :exc:`ReadOnlyDatabaseError` if the database was opened - in read-only mode so message cannot be modified - :raises: :exc:`NotInitializedError` if message has not been - initialized - """ - if not self._msg: - raise NotInitializedError() - - status = self._freeze(self._msg) - - if STATUS.SUCCESS == status: - # return on success - return status - - raise NotmuchError(status) - - _thaw = nmlib.notmuch_message_thaw - _thaw.argtypes = [NotmuchMessageP] - _thaw.restype = c_uint - - def thaw(self): - """Thaws the current 'message' - - Thaw the current 'message', synchronizing any changes that may have - occurred while 'message' was frozen into the notmuch database. - - See :meth:`freeze` for an example of how to use this - function to safely provide tag changes. - - Multiple calls to freeze/thaw are valid and these calls with - "stack". That is there must be as many calls to thaw as to freeze - before a message is actually thawed. - - :returns: STATUS.SUCCESS if the message was successfully frozen. - Raises an exception otherwise. - :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made - to thaw an unfrozen message. That is, there have been - an unbalanced number of calls to :meth:`freeze` and - :meth:`thaw`. - :raises: :exc:`NotInitializedError` if message has not been - initialized - """ - if not self._msg: - raise NotInitializedError() - - status = self._thaw(self._msg) - - if STATUS.SUCCESS == status: - # return on success - return status - - raise NotmuchError(status) - - def is_match(self): - """(Not implemented)""" - return self.get_flag(Message.FLAG.MATCH) - - def tags_to_maildir_flags(self): - """Synchronize notmuch tags to file Maildir flags - - 'D' if the message has the "draft" tag - 'F' if the message has the "flagged" tag - 'P' if the message has the "passed" tag - 'R' if the message has the "replied" tag - 'S' if the message does not have the "unread" tag - - Any existing flags unmentioned in the list above will be - preserved in the renaming. - - Also, if this filename is in a directory named "new", rename it - to be within the neighboring directory named "cur". - - Do note that calling this method while a message is frozen might - not work yet, as the modified tags have not been committed yet - to the database. - - :returns: a :class:`STATUS` value. In short, you want to see - notmuch.STATUS.SUCCESS here. See there for details.""" - if not self._msg: - raise NotInitializedError() - return Message._tags_to_maildir_flags(self._msg) - - def maildir_flags_to_tags(self): - """Synchronize file Maildir flags to notmuch tags - - Flag Action if present - ---- ----------------- - 'D' Adds the "draft" tag to the message - 'F' Adds the "flagged" tag to the message - 'P' Adds the "passed" tag to the message - 'R' Adds the "replied" tag to the message - 'S' Removes the "unread" tag from the message - - For each flag that is not present, the opposite action - (add/remove) is performed for the corresponding tags. If there - are multiple filenames associated with this message, the flag is - considered present if it appears in one or more filenames. (That - is, the flags from the multiple filenames are combined with the - logical OR operator.) - - As a convenience, you can set the sync_maildir_flags parameter in - :meth:`Database.index_file` to implicitly call this. - - :returns: a :class:`STATUS`. In short, you want to see - notmuch.STATUS.SUCCESS here. See there for details.""" - if not self._msg: - raise NotInitializedError() - return Message._maildir_flags_to_tags(self._msg) - - def __repr__(self): - """Represent a Message() object by str()""" - return self.__str__() - - def __unicode__(self): - format = "%s (%s) (%s)" - return format % (self.get_header('from'), - self.get_tags(), - date.fromtimestamp(self.get_date()), - ) - - def get_message_parts(self): - """Output like notmuch show""" - fp = open(self.get_filename(), 'rb') - if sys.version_info[0] < 3: - email_msg = email.message_from_file(fp) - else: - email_msg = email.message_from_binary_file(fp) - fp.close() - - out = [] - for msg in email_msg.walk(): - if not msg.is_multipart(): - out.append(msg) - return out - - def get_part(self, num): - """Returns the nth message body part""" - parts = self.get_message_parts() - if (num <= 0 or num > len(parts)): - return "" - else: - out_part = parts[(num - 1)] - return out_part.get_payload(decode=True) - - def __hash__(self): - """Implement hash(), so we can use Message() sets""" - file = self.get_filename() - if not file: - return None - return hash(file) - - def __cmp__(self, other): - """Implement cmp(), so we can compare Message()s - - 2 messages are considered equal if they point to the same - Message-Id and if they point to the same file names. If 2 - Messages derive from different queries where some files have - been added or removed, the same messages would not be considered - equal (as they do not point to the same set of files - any more).""" - res = cmp(self.get_message_id(), other.get_message_id()) - if res: - res = cmp(list(self.get_filenames()), list(other.get_filenames())) - return res - - _destroy = nmlib.notmuch_message_destroy - _destroy.argtypes = [NotmuchMessageP] - _destroy.restype = None - - def __del__(self): - """Close and free the notmuch Message""" - if self._msg: - self._destroy(self._msg) diff --git a/bindings/python/notmuch/messages.py b/bindings/python/notmuch/messages.py deleted file mode 100644 index 3801c666..00000000 --- a/bindings/python/notmuch/messages.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth - Jesse Rosenthal -""" - -from .globals import ( - nmlib, - NotmuchTagsP, - NotmuchMessageP, - NotmuchMessagesP, -) -from .errors import ( - NullPointerError, - NotInitializedError, -) -from .tag import Tags -from .message import Message - -class Messages(object): - r"""Represents a list of notmuch messages - - This object provides an iterator over a list of notmuch messages - (Technically, it provides a wrapper for the underlying - *notmuch_messages_t* structure). Do note that the underlying library - only provides a one-time iterator (it cannot reset the iterator to - the start). Thus iterating over the function will "exhaust" the list - of messages, and a subsequent iteration attempt will raise a - :exc:`NotInitializedError`. If you need to - re-iterate over a list of messages you will need to retrieve a new - :class:`Messages` object or cache your :class:`Message`\s in a list - via:: - - msglist = list(msgs) - - You can store and reuse the single :class:`Message` objects as often - as you want as long as you keep the parent :class:`Messages` object - around. (Due to hierarchical memory allocation, all derived - :class:`Message` objects will be invalid when we delete the parent - :class:`Messages` object, even if it was already exhausted.) So - this works:: - - db = Database() - msgs = Query(db,'').search_messages() #get a Messages() object - msglist = list(msgs) - - # msgs is "exhausted" now and msgs.next() will raise an exception. - # However it will be kept alive until all retrieved Message() - # objects are also deleted. If you do e.g. an explicit del(msgs) - # here, the following lines would fail. - - # You can reiterate over *msglist* however as often as you want. - # It is simply a list with :class:`Message`s. - - print (msglist[0].get_filename()) - print (msglist[1].get_filename()) - print (msglist[0].get_message_id()) - - - As :class:`Message` implements both __hash__() and __cmp__(), it is - possible to make sets out of :class:`Messages` and use set - arithmetic (this happens in python and will of course be *much* - slower than redoing a proper query with the appropriate filters:: - - s1, s2 = set(msgs1), set(msgs2) - s.union(s2) - s1 -= s2 - ... - - Be careful when using set arithmetic between message sets derived - from different Databases (ie the same database reopened after - messages have changed). If messages have added or removed associated - files in the meantime, it is possible that the same message would be - considered as a different object (as it points to a different file). - """ - - #notmuch_messages_get - _get = nmlib.notmuch_messages_get - _get.argtypes = [NotmuchMessagesP] - _get.restype = NotmuchMessageP - - _collect_tags = nmlib.notmuch_messages_collect_tags - _collect_tags.argtypes = [NotmuchMessagesP] - _collect_tags.restype = NotmuchTagsP - - def __init__(self, msgs_p, parent=None): - """ - :param msgs_p: A pointer to an underlying *notmuch_messages_t* - structure. These are not publicly exposed, so a user - will almost never instantiate a :class:`Messages` object - herself. They are usually handed back as a result, - e.g. in :meth:`Query.search_messages`. *msgs_p* must be - valid, we will raise an :exc:`NullPointerError` if it is - `None`. - :type msgs_p: :class:`ctypes.c_void_p` - :param parent: The parent object - (ie :class:`Query`) these tags are derived from. It saves - a reference to it, so we can automatically delete the db - object once all derived objects are dead. - :TODO: Make the iterator work more than once and cache the tags in - the Python object.(?) - """ - if not msgs_p: - raise NullPointerError() - - self._msgs = msgs_p - #store parent, so we keep them alive as long as self is alive - self._parent = parent - - def collect_tags(self): - """Return the unique :class:`Tags` in the contained messages - - :returns: :class:`Tags` - :exceptions: :exc:`NotInitializedError` if not init'ed - - .. note:: - - :meth:`collect_tags` will iterate over the messages and therefore - will not allow further iterations. - """ - if not self._msgs: - raise NotInitializedError() - - # collect all tags (returns NULL on error) - tags_p = Messages._collect_tags(self._msgs) - #reset _msgs as we iterated over it and can do so only once - self._msgs = None - - if not tags_p: - raise NullPointerError() - return Tags(tags_p, self) - - def __iter__(self): - """ Make Messages an iterator """ - return self - - _valid = nmlib.notmuch_messages_valid - _valid.argtypes = [NotmuchMessagesP] - _valid.restype = bool - - _move_to_next = nmlib.notmuch_messages_move_to_next - _move_to_next.argtypes = [NotmuchMessagesP] - _move_to_next.restype = None - - def __next__(self): - if not self._msgs: - raise NotInitializedError() - - if not self._valid(self._msgs): - self._msgs = None - raise StopIteration - - msg = Message(Messages._get(self._msgs), self) - self._move_to_next(self._msgs) - return msg - next = __next__ # python2.x iterator protocol compatibility - - def __nonzero__(self): - ''' - Implement truth value testing. If __nonzero__ is not - implemented, the python runtime would fall back to `len(..) > - 0` thus exhausting the iterator. - - :returns: True if the wrapped iterator has at least one more object - left. - ''' - return self._msgs and self._valid(self._msgs) - - _destroy = nmlib.notmuch_messages_destroy - _destroy.argtypes = [NotmuchMessagesP] - _destroy.restype = None - - def __del__(self): - """Close and free the notmuch Messages""" - if self._msgs: - self._destroy(self._msgs) - -class EmptyMessagesResult(Messages): - def __init__(self, parent): - self._msgs = None - self._parent = parent - - def __next__(self): - raise StopIteration() - next = __next__ diff --git a/bindings/python/notmuch/query.py b/bindings/python/notmuch/query.py deleted file mode 100644 index ffb86df1..00000000 --- a/bindings/python/notmuch/query.py +++ /dev/null @@ -1,237 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -""" - -from ctypes import c_char_p, c_uint, POINTER, byref -from .globals import ( - nmlib, - Enum, - _str, - NotmuchQueryP, - NotmuchThreadsP, - NotmuchDatabaseP, - NotmuchMessagesP, -) -from .errors import ( - NotmuchError, - NullPointerError, - NotInitializedError, -) -from .threads import Threads -from .messages import Messages - - -class Query(object): - """Represents a search query on an opened :class:`Database`. - - A query selects and filters a subset of messages from the notmuch - database we derive from. - - :class:`Query` provides an instance attribute :attr:`sort`, which - contains the sort order (if specified via :meth:`set_sort`) or - `None`. - - Any function in this class may throw an :exc:`NotInitializedError` - in case the underlying query object was not set up correctly. - - .. note:: Do remember that as soon as we tear down this object, - all underlying derived objects such as threads, - messages, tags etc will be freed by the underlying library - as well. Accessing these objects will lead to segfaults and - other unexpected behavior. See above for more details. - """ - # constants - SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED']) - """Constants: Sort order in which to return results""" - - def __init__(self, db, querystr): - """ - :param db: An open database which we derive the Query from. - :type db: :class:`Database` - :param querystr: The query string for the message. - :type querystr: utf-8 encoded str or unicode - """ - self._db = None - self._query = None - self.sort = None - self.create(db, querystr) - - def _assert_query_is_initialized(self): - """Raises :exc:`NotInitializedError` if self._query is `None`""" - if not self._query: - raise NotInitializedError() - - """notmuch_query_create""" - _create = nmlib.notmuch_query_create - _create.argtypes = [NotmuchDatabaseP, c_char_p] - _create.restype = NotmuchQueryP - - def create(self, db, querystr): - """Creates a new query derived from a Database - - This function is utilized by __init__() and usually does not need to - be called directly. - - :param db: Database to create the query from. - :type db: :class:`Database` - :param querystr: The query string - :type querystr: utf-8 encoded str or unicode - :raises: - :exc:`NullPointerError` if the query creation failed - (e.g. too little memory). - :exc:`NotInitializedError` if the underlying db was not - initialized. - """ - db._assert_db_is_initialized() - # create reference to parent db to keep it alive - self._db = db - # create query, return None if too little mem available - query_p = Query._create(db._db, _str(querystr)) - if not query_p: - raise NullPointerError - self._query = query_p - - _set_sort = nmlib.notmuch_query_set_sort - _set_sort.argtypes = [NotmuchQueryP, c_uint] - _set_sort.restype = None - - def set_sort(self, sort): - """Set the sort order future results will be delivered in - - :param sort: Sort order (see :attr:`Query.SORT`) - """ - self._assert_query_is_initialized() - self.sort = sort - self._set_sort(self._query, sort) - - _exclude_tag = nmlib.notmuch_query_add_tag_exclude - _exclude_tag.argtypes = [NotmuchQueryP, c_char_p] - _exclude_tag.restype = None - - def exclude_tag(self, tagname): - """Add a tag that will be excluded from the query results by default. - - This exclusion will be overridden if this tag appears explicitly in the - query. - - :param tagname: Name of the tag to be excluded - """ - self._assert_query_is_initialized() - self._exclude_tag(self._query, _str(tagname)) - - """notmuch_query_search_threads""" - _search_threads = nmlib.notmuch_query_search_threads - _search_threads.argtypes = [NotmuchQueryP, POINTER(NotmuchThreadsP)] - _search_threads.restype = c_uint - - def search_threads(self): - r"""Execute a query for threads - - Execute a query for threads, returning a :class:`Threads` iterator. - The returned threads are owned by the query and as such, will only be - valid until the Query is deleted. - - The method sets :attr:`Message.FLAG`\.MATCH for those messages that - match the query. The method :meth:`Message.get_flag` allows us - to get the value of this flag. - - :returns: :class:`Threads` - :raises: :exc:`NullPointerError` if search_threads failed - """ - self._assert_query_is_initialized() - threads_p = NotmuchThreadsP() # == NULL - status = Query._search_threads(self._query, byref(threads_p)) - if status != 0: - raise NotmuchError(status) - - if not threads_p: - raise NullPointerError - return Threads(threads_p, self) - - """notmuch_query_search_messages_st""" - _search_messages = nmlib.notmuch_query_search_messages - _search_messages.argtypes = [NotmuchQueryP, POINTER(NotmuchMessagesP)] - _search_messages.restype = c_uint - - def search_messages(self): - """Filter messages according to the query and return - :class:`Messages` in the defined sort order - - :returns: :class:`Messages` - :raises: :exc:`NullPointerError` if search_messages failed - """ - self._assert_query_is_initialized() - msgs_p = NotmuchMessagesP() # == NULL - status = Query._search_messages(self._query, byref(msgs_p)) - if status != 0: - raise NotmuchError(status) - - if not msgs_p: - raise NullPointerError - return Messages(msgs_p, self) - - _count_messages = nmlib.notmuch_query_count_messages - _count_messages.argtypes = [NotmuchQueryP, POINTER(c_uint)] - _count_messages.restype = c_uint - - def count_messages(self): - ''' - This function performs a search and returns Xapian's best - guess as to the number of matching messages. - - :returns: the estimated number of messages matching this query - :rtype: int - ''' - self._assert_query_is_initialized() - count = c_uint(0) - status = Query._count_messages(self._query, byref(count)) - if status != 0: - raise NotmuchError(status) - return count.value - - _count_threads = nmlib.notmuch_query_count_threads - _count_threads.argtypes = [NotmuchQueryP, POINTER(c_uint)] - _count_threads.restype = c_uint - - def count_threads(self): - ''' - This function performs a search and returns the number of - unique thread IDs in the matching messages. This is the same - as number of threads matching a search. - - Note that this is a significantly heavier operation than - meth:`Query.count_messages`. - - :returns: the number of threads returned by this query - :rtype: int - ''' - self._assert_query_is_initialized() - count = c_uint(0) - status = Query._count_threads(self._query, byref(count)) - if status != 0: - raise NotmuchError(status) - return count.value - - _destroy = nmlib.notmuch_query_destroy - _destroy.argtypes = [NotmuchQueryP] - _destroy.restype = None - - def __del__(self): - """Close and free the Query""" - if self._query: - self._destroy(self._query) diff --git a/bindings/python/notmuch/tag.py b/bindings/python/notmuch/tag.py deleted file mode 100644 index fbb18ce4..00000000 --- a/bindings/python/notmuch/tag.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -""" -from ctypes import c_char_p -from .globals import ( - nmlib, - Python3StringMixIn, - NotmuchTagsP, -) -from .errors import ( - NullPointerError, - NotInitializedError, -) - - -class Tags(Python3StringMixIn): - """Represents a list of notmuch tags - - This object provides an iterator over a list of notmuch tags (which - are unicode instances). - - Do note that the underlying library only provides a one-time - iterator (it cannot reset the iterator to the start). Thus iterating - over the function will "exhaust" the list of tags, and a subsequent - iteration attempt will raise a :exc:`NotInitializedError`. - Also note, that any function that uses iteration (nearly all) will - also exhaust the tags. So both:: - - for tag in tags: print tag - - as well as:: - - number_of_tags = len(tags) - - and even a simple:: - - #str() iterates over all tags to construct a space separated list - print(str(tags)) - - will "exhaust" the Tags. If you need to re-iterate over a list of - tags you will need to retrieve a new :class:`Tags` object. - """ - - #notmuch_tags_get - _get = nmlib.notmuch_tags_get - _get.argtypes = [NotmuchTagsP] - _get.restype = c_char_p - - def __init__(self, tags_p, parent=None): - """ - :param tags_p: A pointer to an underlying *notmuch_tags_t* - structure. These are not publicly exposed, so a user - will almost never instantiate a :class:`Tags` object - herself. They are usually handed back as a result, - e.g. in :meth:`Database.get_all_tags`. *tags_p* must be - valid, we will raise an :exc:`NullPointerError` if it is - `None`. - :type tags_p: :class:`ctypes.c_void_p` - :param parent: The parent object (ie :class:`Database` or - :class:`Message` these tags are derived from, and saves a - reference to it, so we can automatically delete the db object - once all derived objects are dead. - :TODO: Make the iterator optionally work more than once by - cache the tags in the Python object(?) - """ - if not tags_p: - raise NullPointerError() - - self._tags = tags_p - #save reference to parent object so we keep it alive - self._parent = parent - - def __iter__(self): - """ Make Tags an iterator """ - return self - - _valid = nmlib.notmuch_tags_valid - _valid.argtypes = [NotmuchTagsP] - _valid.restype = bool - - _move_to_next = nmlib.notmuch_tags_move_to_next - _move_to_next.argtypes = [NotmuchTagsP] - _move_to_next.restype = None - - def __next__(self): - if not self._tags: - raise NotInitializedError() - if not self._valid(self._tags): - self._tags = None - raise StopIteration - tag = Tags._get(self._tags).decode('UTF-8') - self._move_to_next(self._tags) - return tag - next = __next__ # python2.x iterator protocol compatibility - - def __nonzero__(self): - ''' - Implement truth value testing. If __nonzero__ is not - implemented, the python runtime would fall back to `len(..) > - 0` thus exhausting the iterator. - - :returns: True if the wrapped iterator has at least one more object - left. - ''' - return self._tags and self._valid(self._tags) - - def __unicode__(self): - """string representation of :class:`Tags`: a space separated list of tags - - .. note:: - - As this iterates over the tags, we will not be able to iterate over - them again (as in retrieve them)! If the tags have been exhausted - already, this will raise a :exc:`NotInitializedError`on subsequent - attempts. - """ - return " ".join(self) - - _destroy = nmlib.notmuch_tags_destroy - _destroy.argtypes = [NotmuchTagsP] - _destroy.restype = None - - def __del__(self): - """Close and free the notmuch tags""" - if self._tags: - self._destroy(self._tags) diff --git a/bindings/python/notmuch/thread.py b/bindings/python/notmuch/thread.py deleted file mode 100644 index 9aa0e08c..00000000 --- a/bindings/python/notmuch/thread.py +++ /dev/null @@ -1,281 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -""" - -from ctypes import c_char_p, c_long, c_int -from .globals import ( - nmlib, - NotmuchThreadP, - NotmuchMessagesP, - NotmuchTagsP, -) -from .errors import ( - NullPointerError, - NotInitializedError, -) -from .messages import Messages -from .tag import Tags -from datetime import date - -class Thread(object): - """Represents a single message thread.""" - - """notmuch_thread_get_thread_id""" - _get_thread_id = nmlib.notmuch_thread_get_thread_id - _get_thread_id.argtypes = [NotmuchThreadP] - _get_thread_id.restype = c_char_p - - """notmuch_thread_get_authors""" - _get_authors = nmlib.notmuch_thread_get_authors - _get_authors.argtypes = [NotmuchThreadP] - _get_authors.restype = c_char_p - - """notmuch_thread_get_subject""" - _get_subject = nmlib.notmuch_thread_get_subject - _get_subject.argtypes = [NotmuchThreadP] - _get_subject.restype = c_char_p - - """notmuch_thread_get_toplevel_messages""" - _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages - _get_toplevel_messages.argtypes = [NotmuchThreadP] - _get_toplevel_messages.restype = NotmuchMessagesP - - _get_newest_date = nmlib.notmuch_thread_get_newest_date - _get_newest_date.argtypes = [NotmuchThreadP] - _get_newest_date.restype = c_long - - _get_oldest_date = nmlib.notmuch_thread_get_oldest_date - _get_oldest_date.argtypes = [NotmuchThreadP] - _get_oldest_date.restype = c_long - - """notmuch_thread_get_tags""" - _get_tags = nmlib.notmuch_thread_get_tags - _get_tags.argtypes = [NotmuchThreadP] - _get_tags.restype = NotmuchTagsP - - def __init__(self, thread_p, parent=None): - """ - :param thread_p: A pointer to an internal notmuch_thread_t - Structure. These are not publicly exposed, so a user - will almost never instantiate a :class:`Thread` object - herself. They are usually handed back as a result, - e.g. when iterating through :class:`Threads`. *thread_p* - must be valid, we will raise an :exc:`NullPointerError` - if it is `None`. - - :param parent: A 'parent' object is passed which this message is - derived from. We save a reference to it, so we can - automatically delete the parent object once all derived - objects are dead. - """ - if not thread_p: - raise NullPointerError() - self._thread = thread_p - #keep reference to parent, so we keep it alive - self._parent = parent - - def get_thread_id(self): - """Get the thread ID of 'thread' - - The returned string belongs to 'thread' and will only be valid - for as long as the thread is valid. - - :returns: String with a message ID - :raises: :exc:`NotInitializedError` if the thread - is not initialized. - """ - if not self._thread: - raise NotInitializedError() - return Thread._get_thread_id(self._thread).decode('utf-8', 'ignore') - - _get_total_messages = nmlib.notmuch_thread_get_total_messages - _get_total_messages.argtypes = [NotmuchThreadP] - _get_total_messages.restype = c_int - - def get_total_messages(self): - """Get the total number of messages in 'thread' - - :returns: The number of all messages in the database - belonging to this thread. Contrast with - :meth:`get_matched_messages`. - :raises: :exc:`NotInitializedError` if the thread - is not initialized. - """ - if not self._thread: - raise NotInitializedError() - return self._get_total_messages(self._thread) - - def get_toplevel_messages(self): - """Returns a :class:`Messages` iterator for the top-level messages in - 'thread' - - This iterator will not necessarily iterate over all of the messages - in the thread. It will only iterate over the messages in the thread - which are not replies to other messages in the thread. - - :returns: :class:`Messages` - :raises: :exc:`NotInitializedError` if query is not initialized - :raises: :exc:`NullPointerError` if search_messages failed - """ - if not self._thread: - raise NotInitializedError() - - msgs_p = Thread._get_toplevel_messages(self._thread) - - if not msgs_p: - raise NullPointerError() - - return Messages(msgs_p, self) - - """notmuch_thread_get_messages""" - _get_messages = nmlib.notmuch_thread_get_messages - _get_messages.argtypes = [NotmuchThreadP] - _get_messages.restype = NotmuchMessagesP - - def get_messages(self): - """Returns a :class:`Messages` iterator for all messages in 'thread' - - :returns: :class:`Messages` - :raises: :exc:`NotInitializedError` if query is not initialized - :raises: :exc:`NullPointerError` if get_messages failed - """ - if not self._thread: - raise NotInitializedError() - - msgs_p = Thread._get_messages(self._thread) - - if not msgs_p: - raise NullPointerError() - - return Messages(msgs_p, self) - - _get_matched_messages = nmlib.notmuch_thread_get_matched_messages - _get_matched_messages.argtypes = [NotmuchThreadP] - _get_matched_messages.restype = c_int - - def get_matched_messages(self): - """Returns the number of messages in 'thread' that matched the query - - :returns: The number of all messages belonging to this thread that - matched the :class:`Query`from which this thread was created. - Contrast with :meth:`get_total_messages`. - :raises: :exc:`NotInitializedError` if the thread - is not initialized. - """ - if not self._thread: - raise NotInitializedError() - return self._get_matched_messages(self._thread) - - def get_authors(self): - """Returns the authors of 'thread' - - The returned string is a comma-separated list of the names of the - authors of mail messages in the query results that belong to this - thread. - - The returned string belongs to 'thread' and will only be valid for - as long as this Thread() is not deleted. - """ - if not self._thread: - raise NotInitializedError() - authors = Thread._get_authors(self._thread) - if not authors: - return None - return authors.decode('UTF-8', 'ignore') - - def get_subject(self): - """Returns the Subject of 'thread' - - The returned string belongs to 'thread' and will only be valid for - as long as this Thread() is not deleted. - """ - if not self._thread: - raise NotInitializedError() - subject = Thread._get_subject(self._thread) - if not subject: - return None - return subject.decode('UTF-8', 'ignore') - - def get_newest_date(self): - """Returns time_t of the newest message date - - :returns: A time_t timestamp. - :rtype: c_unit64 - :raises: :exc:`NotInitializedError` if the message - is not initialized. - """ - if not self._thread: - raise NotInitializedError() - return Thread._get_newest_date(self._thread) - - def get_oldest_date(self): - """Returns time_t of the oldest message date - - :returns: A time_t timestamp. - :rtype: c_unit64 - :raises: :exc:`NotInitializedError` if the message - is not initialized. - """ - if not self._thread: - raise NotInitializedError() - return Thread._get_oldest_date(self._thread) - - def get_tags(self): - """ Returns the message tags - - In the Notmuch database, tags are stored on individual - messages, not on threads. So the tags returned here will be all - tags of the messages which matched the search and which belong to - this thread. - - The :class:`Tags` object is owned by the thread and as such, will only - be valid for as long as this :class:`Thread` is valid (e.g. until the - query from which it derived is explicitly deleted). - - :returns: A :class:`Tags` iterator. - :raises: :exc:`NotInitializedError` if query is not initialized - :raises: :exc:`NullPointerError` if search_messages failed - """ - if not self._thread: - raise NotInitializedError() - - tags_p = Thread._get_tags(self._thread) - if not tags_p: - raise NullPointerError() - return Tags(tags_p, self) - - def __unicode__(self): - frm = "thread:%s %12s [%d/%d] %s; %s (%s)" - - return frm % (self.get_thread_id(), - date.fromtimestamp(self.get_newest_date()), - self.get_matched_messages(), - self.get_total_messages(), - self.get_authors(), - self.get_subject(), - self.get_tags(), - ) - - _destroy = nmlib.notmuch_thread_destroy - _destroy.argtypes = [NotmuchThreadP] - _destroy.restype = None - - def __del__(self): - """Close and free the notmuch Thread""" - if self._thread: - self._destroy(self._thread) diff --git a/bindings/python/notmuch/threads.py b/bindings/python/notmuch/threads.py deleted file mode 100644 index 0c382d5b..00000000 --- a/bindings/python/notmuch/threads.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -""" - -from .globals import ( - nmlib, - Python3StringMixIn, - NotmuchThreadP, - NotmuchThreadsP, -) -from .errors import ( - NullPointerError, - NotInitializedError, -) -from .thread import Thread - -class Threads(Python3StringMixIn): - """Represents a list of notmuch threads - - This object provides an iterator over a list of notmuch threads - (Technically, it provides a wrapper for the underlying - *notmuch_threads_t* structure). Do note that the underlying - library only provides a one-time iterator (it cannot reset the - iterator to the start). Thus iterating over the function will - "exhaust" the list of threads, and a subsequent iteration attempt - will raise a :exc:`NotInitializedError`. Also - note, that any function that uses iteration will also - exhaust the messages. So both:: - - for thread in threads: print thread - - as well as:: - - list_of_threads = list(threads) - - will "exhaust" the threads. If you need to re-iterate over a list of - messages you will need to retrieve a new :class:`Threads` object. - - Things are not as bad as it seems though, you can store and reuse - the single Thread objects as often as you want as long as you - keep the parent Threads object around. (Recall that due to - hierarchical memory allocation, all derived Threads objects will - be invalid when we delete the parent Threads() object, even if it - was already "exhausted".) So this works:: - - db = Database() - threads = Query(db,'').search_threads() #get a Threads() object - threadlist = [] - for thread in threads: - threadlist.append(thread) - - # threads is "exhausted" now. - # However it will be kept around until all retrieved Thread() objects are - # also deleted. If you did e.g. an explicit del(threads) here, the - # following lines would fail. - - # You can reiterate over *threadlist* however as often as you want. - # It is simply a list with Thread objects. - - print (threadlist[0].get_thread_id()) - print (threadlist[1].get_thread_id()) - print (threadlist[0].get_total_messages()) - """ - - #notmuch_threads_get - _get = nmlib.notmuch_threads_get - _get.argtypes = [NotmuchThreadsP] - _get.restype = NotmuchThreadP - - def __init__(self, threads_p, parent=None): - """ - :param threads_p: A pointer to an underlying *notmuch_threads_t* - structure. These are not publicly exposed, so a user - will almost never instantiate a :class:`Threads` object - herself. They are usually handed back as a result, - e.g. in :meth:`Query.search_threads`. *threads_p* must be - valid, we will raise an :exc:`NullPointerError` if it is - `None`. - :type threads_p: :class:`ctypes.c_void_p` - :param parent: The parent object - (ie :class:`Query`) these tags are derived from. It saves - a reference to it, so we can automatically delete the db - object once all derived objects are dead. - :TODO: Make the iterator work more than once and cache the tags in - the Python object.(?) - """ - if not threads_p: - raise NullPointerError() - - self._threads = threads_p - #store parent, so we keep them alive as long as self is alive - self._parent = parent - - def __iter__(self): - """ Make Threads an iterator """ - return self - - _valid = nmlib.notmuch_threads_valid - _valid.argtypes = [NotmuchThreadsP] - _valid.restype = bool - - _move_to_next = nmlib.notmuch_threads_move_to_next - _move_to_next.argtypes = [NotmuchThreadsP] - _move_to_next.restype = None - - def __next__(self): - if not self._threads: - raise NotInitializedError() - - if not self._valid(self._threads): - self._threads = None - raise StopIteration - - thread = Thread(Threads._get(self._threads), self) - self._move_to_next(self._threads) - return thread - next = __next__ # python2.x iterator protocol compatibility - - def __nonzero__(self): - ''' - Implement truth value testing. If __nonzero__ is not - implemented, the python runtime would fall back to `len(..) > - 0` thus exhausting the iterator. - - :returns: True if the wrapped iterator has at least one more object - left. - ''' - return self._threads and self._valid(self._threads) - - _destroy = nmlib.notmuch_threads_destroy - _destroy.argtypes = [NotmuchThreadsP] - _destroy.restype = None - - def __del__(self): - """Close and free the notmuch Threads""" - if self._threads: - self._destroy(self._threads) diff --git a/bindings/python/notmuch/version.py b/bindings/python/notmuch/version.py deleted file mode 100644 index de3faa77..00000000 --- a/bindings/python/notmuch/version.py +++ /dev/null @@ -1,3 +0,0 @@ -# this file should be kept in sync with ../../../version -__VERSION__ = '0.34' -SOVERSION = '5' diff --git a/bindings/python/setup.py b/bindings/python/setup.py deleted file mode 100644 index 6308b9f9..00000000 --- a/bindings/python/setup.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 - -""" -This file is part of notmuch. - -Notmuch is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your -option) any later version. - -Notmuch is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -for more details. - -You should have received a copy of the GNU General Public License -along with notmuch. If not, see . - -Copyright 2010 Sebastian Spaeth -""" - -import os -from distutils.core import setup - -# get the notmuch version number without importing the notmuch module -version_file = os.path.join(os.path.dirname(__file__), - 'notmuch', 'version.py') -exec(compile(open(version_file).read(), version_file, 'exec')) -assert '__VERSION__' in globals(), \ - 'Failed to read the notmuch binding version number' - -setup(name='notmuch', - version=__VERSION__, - description='Python binding of the notmuch mail search and indexing library.', - author='Sebastian Spaeth', - author_email='Sebastian@SSpaeth.de', - url='https://notmuchmail.org/', - download_url='https://notmuchmail.org/releases/notmuch-%s.tar.gz' % __VERSION__, - packages=['notmuch'], - keywords=['library', 'email'], - long_description='''Overview -======== - -The notmuch module provides an interface to the `notmuch -`_ functionality, directly interfacing with a -shared notmuch library. Notmuch provides a maildatabase that allows -for extremely quick searching and filtering of your email according to -various criteria. - -The documentation for the latest notmuch release can be `viewed -online `_. - -Requirements ------------- - -You need to have notmuch installed (or rather libnotmuch.so.1). Also, -notmuch makes use of the ctypes library, and has only been tested with -python >= 2.5. It will not work on earlier python versions. -''', - classifiers=['Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License (GPL)', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3', - 'Topic :: Communications :: Email', - 'Topic :: Software Development :: Libraries' - ], - platforms='', - license='https://www.gnu.org/licenses/gpl-3.0.txt', - ) diff --git a/bindings/ruby/database.c b/bindings/ruby/database.c index 9c3dbd96..ed224ef7 100644 --- a/bindings/ruby/database.c +++ b/bindings/ruby/database.c @@ -58,40 +58,56 @@ notmuch_rb_database_initialize (int argc, VALUE *argv, VALUE self) notmuch_database_t *database; notmuch_status_t ret; + path = NULL; + create = 0; + mode = NOTMUCH_DATABASE_MODE_READ_ONLY; + /* Check arguments */ - rb_scan_args (argc, argv, "11", &pathv, &hashv); + rb_scan_args (argc, argv, "02", &pathv, &hashv); - SafeStringValue (pathv); - path = RSTRING_PTR (pathv); + if (!NIL_P (pathv)) { + SafeStringValue (pathv); + path = RSTRING_PTR (pathv); + } if (!NIL_P (hashv)) { - Check_Type (hashv, T_HASH); - create = RTEST (rb_hash_aref (hashv, ID2SYM (ID_db_create))); - modev = rb_hash_aref (hashv, ID2SYM (ID_db_mode)); - if (NIL_P (modev)) - mode = NOTMUCH_DATABASE_MODE_READ_ONLY; - else if (!FIXNUM_P (modev)) - rb_raise (rb_eTypeError, ":mode isn't a Fixnum"); - else { - mode = FIX2INT (modev); - switch (mode) { - case NOTMUCH_DATABASE_MODE_READ_ONLY: - case NOTMUCH_DATABASE_MODE_READ_WRITE: - break; - default: - rb_raise ( rb_eTypeError, "Invalid mode"); + VALUE rmode, rcreate; + VALUE kwargs[2]; + static ID keyword_ids[2]; + + if (!keyword_ids[0]) { + keyword_ids[0] = rb_intern_const ("mode"); + keyword_ids[1] = rb_intern_const ("create"); + } + + rb_get_kwargs (hashv, keyword_ids, 0, 2, kwargs); + + rmode = kwargs[0]; + rcreate = kwargs[1]; + + if (rmode != Qundef) { + if (!FIXNUM_P (rmode)) + rb_raise (rb_eTypeError, ":mode isn't a Fixnum"); + else { + mode = FIX2INT (rmode); + switch (mode) { + case NOTMUCH_DATABASE_MODE_READ_ONLY: + case NOTMUCH_DATABASE_MODE_READ_WRITE: + break; + default: + rb_raise ( rb_eTypeError, "Invalid mode"); + } } } - } else { - create = 0; - mode = NOTMUCH_DATABASE_MODE_READ_ONLY; + if (rcreate != Qundef) + create = RTEST (rcreate); } rb_check_typeddata (self, ¬much_rb_database_type); if (create) ret = notmuch_database_create (path, &database); else - ret = notmuch_database_open (path, mode, &database); + ret = notmuch_database_open_with_config (path, mode, NULL, NULL, &database, NULL); notmuch_rb_status_raise (ret); DATA_PTR (self) = notmuch_rb_object_create (database, "notmuch_rb_database"); @@ -408,7 +424,7 @@ notmuch_rb_database_get_all_tags (VALUE self) rb_raise (notmuch_rb_eBaseError, "%s", msg); } - return Data_Wrap_Notmuch_Object (notmuch_rb_cTags, ¬much_rb_tags_type, tags); + return notmuch_rb_tags_get (tags); } /* diff --git a/bindings/ruby/defs.h b/bindings/ruby/defs.h index e2541e8f..a2cb38c8 100644 --- a/bindings/ruby/defs.h +++ b/bindings/ruby/defs.h @@ -33,7 +33,6 @@ extern VALUE notmuch_rb_cThreads; extern VALUE notmuch_rb_cThread; extern VALUE notmuch_rb_cMessages; extern VALUE notmuch_rb_cMessage; -extern VALUE notmuch_rb_cTags; extern VALUE notmuch_rb_eBaseError; extern VALUE notmuch_rb_eDatabaseError; @@ -48,8 +47,6 @@ extern VALUE notmuch_rb_eUnbalancedFreezeThawError; extern VALUE notmuch_rb_eUnbalancedAtomicError; extern ID ID_call; -extern ID ID_db_create; -extern ID ID_db_mode; /* RSTRING_PTR() is new in ruby-1.9 */ #if !defined(RSTRING_PTR) @@ -59,7 +56,6 @@ extern ID ID_db_mode; extern const rb_data_type_t notmuch_rb_object_type; extern const rb_data_type_t notmuch_rb_database_type; extern const rb_data_type_t notmuch_rb_directory_type; -extern const rb_data_type_t notmuch_rb_filenames_type; extern const rb_data_type_t notmuch_rb_query_type; extern const rb_data_type_t notmuch_rb_threads_type; extern const rb_data_type_t notmuch_rb_thread_type; @@ -92,9 +88,6 @@ extern const rb_data_type_t notmuch_rb_tags_type; #define Data_Get_Notmuch_Directory(obj, ptr) \ Data_Get_Notmuch_Object ((obj), ¬much_rb_directory_type, (ptr)) -#define Data_Get_Notmuch_FileNames(obj, ptr) \ - Data_Get_Notmuch_Object ((obj), ¬much_rb_filenames_type, (ptr)) - #define Data_Get_Notmuch_Query(obj, ptr) \ Data_Get_Notmuch_Object ((obj), ¬much_rb_query_type, (ptr)) @@ -226,10 +219,7 @@ notmuch_rb_directory_get_child_directories (VALUE self); /* filenames.c */ VALUE -notmuch_rb_filenames_destroy (VALUE self); - -VALUE -notmuch_rb_filenames_each (VALUE self); +notmuch_rb_filenames_get (notmuch_filenames_t *fnames); /* query.c */ VALUE @@ -370,10 +360,7 @@ notmuch_rb_message_thaw (VALUE self); /* tags.c */ VALUE -notmuch_rb_tags_destroy (VALUE self); - -VALUE -notmuch_rb_tags_each (VALUE self); +notmuch_rb_tags_get (notmuch_tags_t *tags); /* init.c */ void diff --git a/bindings/ruby/directory.c b/bindings/ruby/directory.c index 910f0a99..f267d82f 100644 --- a/bindings/ruby/directory.c +++ b/bindings/ruby/directory.c @@ -87,7 +87,7 @@ notmuch_rb_directory_get_child_files (VALUE self) fnames = notmuch_directory_get_child_files (dir); - return Data_Wrap_Notmuch_Object (notmuch_rb_cFileNames, ¬much_rb_filenames_type, fnames); + return notmuch_rb_filenames_get (fnames); } /* @@ -106,5 +106,5 @@ notmuch_rb_directory_get_child_directories (VALUE self) fnames = notmuch_directory_get_child_directories (dir); - return Data_Wrap_Notmuch_Object (notmuch_rb_cFileNames, ¬much_rb_filenames_type, fnames); + return notmuch_rb_filenames_get (fnames); } diff --git a/bindings/ruby/extconf.rb b/bindings/ruby/extconf.rb index d914537c..ba97670f 100644 --- a/bindings/ruby/extconf.rb +++ b/bindings/ruby/extconf.rb @@ -5,21 +5,14 @@ require 'mkmf' -dir = File.join(ENV['NOTMUCH_SRCDIR'], 'lib') - -# includes -$INCFLAGS = "-I#{dir} #{$INCFLAGS}" - -if ENV['EXTRA_LDFLAGS'] - $LDFLAGS += " " + ENV['EXTRA_LDFLAGS'] -end +$INCFLAGS = "#{ENV['EXTRA_INCFLAGS']} #{$INCFLAGS}" +$LDFLAGS = "#{ENV['EXTRA_LDFLAGS']} #{$LDFLAGS}" if not ENV['LIBNOTMUCH'] exit 1 end -$LOCAL_LIBS += ENV['LIBNOTMUCH'] -$LIBS += " -ltalloc" +$LOCAL_LIBS = ENV['LIBNOTMUCH'] # Create Makefile dir_config('notmuch') diff --git a/bindings/ruby/filenames.c b/bindings/ruby/filenames.c index 0dec1952..60c3fb8b 100644 --- a/bindings/ruby/filenames.c +++ b/bindings/ruby/filenames.c @@ -1,53 +1,11 @@ -/* The Ruby interface to the notmuch mail library - * - * Copyright © 2010 Ali Polatel - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/ . - * - * Author: Ali Polatel - */ - #include "defs.h" -/* - * call-seq: FILENAMES.destroy! => nil - * - * Destroys the filenames, freeing all resources allocated for it. - */ -VALUE -notmuch_rb_filenames_destroy (VALUE self) -{ - notmuch_rb_object_destroy (self, ¬much_rb_filenames_type); - - return Qnil; -} - -/* - * call-seq: FILENAMES.each {|item| block } => FILENAMES - * - * Calls +block+ once for each element in +self+, passing that element as a - * parameter. - */ VALUE -notmuch_rb_filenames_each (VALUE self) +notmuch_rb_filenames_get (notmuch_filenames_t *fnames) { - notmuch_filenames_t *fnames; - - Data_Get_Notmuch_FileNames (self, fnames); + VALUE rb_array = rb_ary_new (); for (; notmuch_filenames_valid (fnames); notmuch_filenames_move_to_next (fnames)) - rb_yield (rb_str_new2 (notmuch_filenames_get (fnames))); - - return self; + rb_ary_push (rb_array, rb_str_new2 (notmuch_filenames_get (fnames))); + return rb_array; } diff --git a/bindings/ruby/init.c b/bindings/ruby/init.c index cd9f04cd..10558a33 100644 --- a/bindings/ruby/init.c +++ b/bindings/ruby/init.c @@ -22,13 +22,11 @@ VALUE notmuch_rb_cDatabase; VALUE notmuch_rb_cDirectory; -VALUE notmuch_rb_cFileNames; VALUE notmuch_rb_cQuery; VALUE notmuch_rb_cThreads; VALUE notmuch_rb_cThread; VALUE notmuch_rb_cMessages; VALUE notmuch_rb_cMessage; -VALUE notmuch_rb_cTags; VALUE notmuch_rb_eBaseError; VALUE notmuch_rb_eDatabaseError; @@ -43,8 +41,6 @@ VALUE notmuch_rb_eUnbalancedFreezeThawError; VALUE notmuch_rb_eUnbalancedAtomicError; ID ID_call; -ID ID_db_create; -ID ID_db_mode; const rb_data_type_t notmuch_rb_object_type = { .wrap_struct_name = "notmuch_object", @@ -65,13 +61,11 @@ const rb_data_type_t notmuch_rb_object_type = { define_type (database); define_type (directory); -define_type (filenames); define_type (query); define_type (threads); define_type (thread); define_type (messages); define_type (message); -define_type (tags); /* * Document-module: Notmuch @@ -86,13 +80,11 @@ define_type (tags); * the user: * * - Notmuch::Database - * - Notmuch::FileNames * - Notmuch::Query * - Notmuch::Threads * - Notmuch::Messages * - Notmuch::Thread * - Notmuch::Message - * - Notmuch::Tags */ void @@ -101,8 +93,6 @@ Init_notmuch (void) VALUE mod; ID_call = rb_intern ("call"); - ID_db_create = rb_intern ("create"); - ID_db_mode = rb_intern ("mode"); mod = rb_define_module ("Notmuch"); @@ -297,23 +287,13 @@ Init_notmuch (void) rb_define_method (notmuch_rb_cDirectory, "child_files", notmuch_rb_directory_get_child_files, 0); /* in directory.c */ rb_define_method (notmuch_rb_cDirectory, "child_directories", notmuch_rb_directory_get_child_directories, 0); /* in directory.c */ - /* - * Document-class: Notmuch::FileNames - * - * Notmuch file names - */ - 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 */ - rb_include_module (notmuch_rb_cFileNames, rb_mEnumerable); - /* * Document-class: Notmuch::Query * * Notmuch query */ notmuch_rb_cQuery = rb_define_class_under (mod, "Query", rb_cObject); + rb_undef_alloc_func(notmuch_rb_cQuery); 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 */ @@ -332,6 +312,7 @@ Init_notmuch (void) * Notmuch threads */ notmuch_rb_cThreads = rb_define_class_under (mod, "Threads", rb_cObject); + rb_undef_alloc_func(notmuch_rb_cThreads); 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 */ @@ -343,6 +324,7 @@ Init_notmuch (void) * Notmuch messages */ notmuch_rb_cMessages = rb_define_class_under (mod, "Messages", rb_cObject); + rb_undef_alloc_func(notmuch_rb_cMessages); 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 */ @@ -355,6 +337,7 @@ Init_notmuch (void) * Notmuch thread */ notmuch_rb_cThread = rb_define_class_under (mod, "Thread", rb_cObject); + rb_undef_alloc_func(notmuch_rb_cThread); 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 */ @@ -374,6 +357,7 @@ Init_notmuch (void) * Notmuch message */ notmuch_rb_cMessage = rb_define_class_under (mod, "Message", rb_cObject); + rb_undef_alloc_func(notmuch_rb_cMessage); 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 */ @@ -395,15 +379,4 @@ Init_notmuch (void) rb_define_method (notmuch_rb_cMessage, "tags_to_maildir_flags", notmuch_rb_message_tags_to_maildir_flags, 0); /* in message.c */ rb_define_method (notmuch_rb_cMessage, "freeze", notmuch_rb_message_freeze, 0); /* in message.c */ rb_define_method (notmuch_rb_cMessage, "thaw", notmuch_rb_message_thaw, 0); /* in message.c */ - - /* - * Document-class: Notmuch::Tags - * - * Notmuch tags - */ - 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 */ - rb_include_module (notmuch_rb_cTags, rb_mEnumerable); } diff --git a/bindings/ruby/message.c b/bindings/ruby/message.c index f45c95cc..13c182f6 100644 --- a/bindings/ruby/message.c +++ b/bindings/ruby/message.c @@ -120,7 +120,7 @@ notmuch_rb_message_get_filenames (VALUE self) fnames = notmuch_message_get_filenames (message); - return Data_Wrap_Notmuch_Object (notmuch_rb_cFileNames, ¬much_rb_filenames_type, fnames); + return notmuch_rb_filenames_get (fnames); } /* @@ -221,7 +221,7 @@ notmuch_rb_message_get_tags (VALUE self) if (!tags) rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return Data_Wrap_Notmuch_Object (notmuch_rb_cTags, ¬much_rb_tags_type, tags); + return notmuch_rb_tags_get (tags); } /* diff --git a/bindings/ruby/messages.c b/bindings/ruby/messages.c index ca5b10d0..6369d052 100644 --- a/bindings/ruby/messages.c +++ b/bindings/ruby/messages.c @@ -71,5 +71,5 @@ notmuch_rb_messages_collect_tags (VALUE self) if (!tags) rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return Data_Wrap_Notmuch_Object (notmuch_rb_cTags, ¬much_rb_tags_type, tags); + return notmuch_rb_tags_get (tags); } diff --git a/bindings/ruby/query.c b/bindings/ruby/query.c index 8a2b4d3d..077def02 100644 --- a/bindings/ruby/query.c +++ b/bindings/ruby/query.c @@ -45,7 +45,7 @@ notmuch_rb_query_get_sort (VALUE self) Data_Get_Notmuch_Query (self, query); - return FIX2INT (notmuch_query_get_sort (query)); + return INT2FIX (notmuch_query_get_sort (query)); } /* diff --git a/bindings/ruby/tags.c b/bindings/ruby/tags.c index 2af85e36..b64874d1 100644 --- a/bindings/ruby/tags.c +++ b/bindings/ruby/tags.c @@ -1,56 +1,13 @@ -/* The Ruby interface to the notmuch mail library - * - * Copyright © 2010, 2011 Ali Polatel - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/ . - * - * Author: Ali Polatel - */ - #include "defs.h" -/* - * call-seq: TAGS.destroy! => nil - * - * Destroys the tags, freeing all resources allocated for it. - */ -VALUE -notmuch_rb_tags_destroy (VALUE self) -{ - notmuch_rb_object_destroy (self, ¬much_rb_tags_type); - - return Qnil; -} - -/* - * call-seq: TAGS.each {|item| block } => TAGS - * - * Calls +block+ once for each element in +self+, passing that element as a - * parameter. - */ VALUE -notmuch_rb_tags_each (VALUE self) +notmuch_rb_tags_get (notmuch_tags_t *tags) { - const char *tag; - notmuch_tags_t *tags; - - Data_Get_Notmuch_Tags (self, tags); + VALUE rb_array = rb_ary_new (); for (; notmuch_tags_valid (tags); notmuch_tags_move_to_next (tags)) { - tag = notmuch_tags_get (tags); - rb_yield (rb_str_new2 (tag)); + const char *tag = notmuch_tags_get (tags); + rb_ary_push (rb_array, rb_str_new2 (tag)); } - - return self; + return rb_array; } diff --git a/bindings/ruby/thread.c b/bindings/ruby/thread.c index 7cb2a3dc..b20ed893 100644 --- a/bindings/ruby/thread.c +++ b/bindings/ruby/thread.c @@ -204,5 +204,5 @@ notmuch_rb_thread_get_tags (VALUE self) if (!tags) rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return Data_Wrap_Notmuch_Object (notmuch_rb_cTags, ¬much_rb_tags_type, tags); + return notmuch_rb_tags_get (tags); } diff --git a/compat/have_strcasestr.c b/compat/have_strcasestr.c index 3cd1838d..8e004572 100644 --- a/compat/have_strcasestr.c +++ b/compat/have_strcasestr.c @@ -1,5 +1,6 @@ #define _GNU_SOURCE -#include +#include /* strcasecmp() in POSIX */ +#include /* strcasecmp() in *BSD */ int main () diff --git a/completion/notmuch-completion.bash b/completion/notmuch-completion.bash index 15425697..3748846e 100644 --- a/completion/notmuch-completion.bash +++ b/completion/notmuch-completion.bash @@ -103,12 +103,12 @@ _notmuch_search_terms() COMPREPLY=( $(compgen -P "from:" -W "`_notmuch_email ${cur}`" -- ${cur##from:}) ) ;; path:*) - local path=`notmuch config get database.path` + local path=`notmuch config get database.mail_root` compopt -o nospace COMPREPLY=( $(compgen -d "$path/${cur##path:}" | sed "s|^$path/||" ) ) ;; folder:*) - local path=`notmuch config get database.path` + local path=`notmuch config get database.mail_root` compopt -o nospace COMPREPLY=( $(compgen -d "$path/${cur##folder:}" | \ sed "s|^$path/||" | grep -v "\(^\|/\)\(cur\|new\|tmp\)$" ) ) @@ -281,7 +281,7 @@ _notmuch_insert() $split && case "${prev}" in --folder) - local path=`notmuch config get database.path` + local path=`notmuch config get database.mail_root` compopt -o nospace COMPREPLY=( $(compgen -d "$path/${cur}" | \ sed "s|^$path/||" | grep -v "\(^\|/\)\(cur\|new\|tmp\)$" ) ) @@ -530,7 +530,7 @@ _notmuch_show() ! $split && case "${cur}" in -*) - local options="--entire-thread= --format= --exclude= --body= --format-version= --part= --verify --decrypt= --include-html ${_notmuch_shared_options}" + local options="--entire-thread= --format= --exclude= --body= --format-version= --part= --verify --decrypt= --include-html --limit= --offset= ${_notmuch_shared_options}" compopt -o nospace COMPREPLY=( $(compgen -W "$options" -- ${cur}) ) ;; diff --git a/completion/zsh/_notmuch b/completion/zsh/_notmuch index e920f10b..0bdd7f77 100644 --- a/completion/zsh/_notmuch +++ b/completion/zsh/_notmuch @@ -69,8 +69,8 @@ _notmuch_term_mimetype() { _notmuch_term_path() { local ret=1 expl - local maildir="$(notmuch config get database.path)" - [[ -d $maildir ]] || { _message -e "database.path not found" ; return $ret } + local maildir="$(notmuch config get database.mail_root)" + [[ -d $maildir ]] || { _message -e "database.mail_root not found" ; return $ret } _description notmuch-folder expl 'maildir folder' _files "$expl[@]" -W $maildir -/ && ret=0 @@ -79,8 +79,8 @@ _notmuch_term_path() { _notmuch_term_folder() { local ret=1 expl - local maildir="$(notmuch config get database.path)" - [[ -d $maildir ]] || { _message -e "database.path not found" ; return $ret } + local maildir="$(notmuch config get database.mail_root)" + [[ -d $maildir ]] || { _message -e "database.mail_root not found" ; return $ret } _description notmuch-folder expl 'maildir folder' local ignoredfolders=( '*/(cur|new|tmp)' ) @@ -245,6 +245,8 @@ _notmuch_show() { '--exclude=[respect excluded tags setting]:exclude tags:(true false)' \ '--body=[output body]:output body content:(true false)' \ '--include-html[include text/html parts in the output]' \ + '--limit=[limit the number of displayed results]:limit: ' \ + '--offset=[skip displaying the first N results]:offset: ' \ '*::search term:_notmuch_search_term' } diff --git a/configure b/configure index 6c3a38f1..7afd08c7 100755 --- a/configure +++ b/configure @@ -55,6 +55,8 @@ subdirs="${subdirs} bindings" # the directory structure and copy Makefiles. if [ "$srcdir" != "." ]; then + NOTMUCH_BUILDDIR=$PWD + for dir in . ${subdirs}; do mkdir -p "$dir" cp "$srcdir"/"$dir"/Makefile.local "$dir" @@ -78,6 +80,8 @@ if [ "$srcdir" != "." ]; then "$srcdir"/bindings/python-cffi/notmuch2 \ "$srcdir"/bindings/python-cffi/setup.py \ bindings/python-cffi/ +else + NOTMUCH_BUILDDIR=$NOTMUCH_SRCDIR fi # Set several defaults (optionally specified by the user in @@ -308,12 +312,22 @@ for option; do true elif [ "${option%%=*}" = '--host' ] ; then true + elif [ "${option%%=*}" = '--bindir' ] ; then + true + elif [ "${option%%=*}" = '--sbindir' ] ; then + true elif [ "${option%%=*}" = '--datadir' ] ; then true elif [ "${option%%=*}" = '--localstatedir' ] ; then true + elif [ "${option%%=*}" = '--sharedstatedir' ] ; then + true elif [ "${option%%=*}" = '--libexecdir' ] ; then true + elif [ "${option%%=*}" = '--exec-prefix' ] ; then + true + elif [ "${option%%=*}" = '--program-prefix' ] ; then + true elif [ "${option}" = '--disable-maintainer-mode' ] ; then true elif [ "${option}" = '--disable-dependency-tracking' ] ; then @@ -396,6 +410,30 @@ EOF exit 1 fi +printf "C compiler supports address sanitizer... " +test_cmdline="${CC} ${CFLAGS} ${CPPFLAGS} -fsanitize=address minimal.c ${LDFLAGS} -o minimal" +if ${test_cmdline} >/dev/null 2>&1 && ./minimal +then + printf "Yes.\n" + have_asan=1 +else + printf "Nope, skipping those tests.\n" + have_asan=0 +fi +unset test_cmdline + +printf "C compiler supports thread sanitizer... " +test_cmdline="${CC} ${CFLAGS} ${CPPFLAGS} -fsanitize=thread minimal.c ${LDFLAGS} -o minimal" +if ${test_cmdline} >/dev/null 2>&1 && ./minimal +then + printf "Yes.\n" + have_tsan=1 +else + printf "Nope, skipping those tests.\n" + have_tsan=0 +fi +unset test_cmdline + printf "Reading libnotmuch version from source... " cat > _libversion.c < @@ -499,9 +537,9 @@ EOF printf 'No.\nCould not make tempdir for testing session-key support.\n' errors=$((errors + 1)) elif ${CC} ${CFLAGS} ${gmime_cflags} _check_session_keys.c ${gmime_ldflags} -o _check_session_keys \ - && GNUPGHOME=${TEMP_GPG} gpg --batch --quiet --import < "$srcdir"/test/gnupg-secret-key.asc \ + && GNUPGHOME=${TEMP_GPG} gpg --batch --quiet --import < "$srcdir"/test/openpgp4-secret-key.asc \ && SESSION_KEY=$(GNUPGHOME=${TEMP_GPG} ./_check_session_keys) \ - && [ $SESSION_KEY = 9:0BACD64099D1468AB07C796F0C0AC4851948A658A15B34E803865E9FC635F2F5 ] + && [ $SESSION_KEY = 9:496A0B6D15A5E7BA762FB8E5FE6DEE421D4D9BBFCEAD1CDD0CCF636D07ADE621 ] then printf "OK.\n" else @@ -515,8 +553,8 @@ version of GPGME. Please try to rebuild your version of GMime against a more recent version of GPGME (at least GPGME 1.8.0). EOF - if command -v gpgme-config >/dev/null; then - printf 'Your current GPGME development version is: %s\n' "$(gpgme-config --version)" + if GPGME_VERS="$(pkg-config --modversion gpgme || gpgme-config --version)"; then + printf 'Your current GPGME development version is: %s\n' "$GPGME_VERS" else printf 'You do not have the GPGME development libraries installed.\n' fi @@ -526,11 +564,7 @@ EOF rm -rf "$TEMP_GPG" fi - # see https://github.com/jstedfast/gmime/pull/90 - # should be fixed in GMime in 3.2.7, but some distros might patch - printf "Checking for GMime X.509 certificate validity... " - - cat > _check_x509_validity.c < _check_gmime_cert.c < #include @@ -542,7 +576,6 @@ int main () { GMimeSignature *sig = NULL; GMimeCertificate *cert = NULL; GMimeObject *output = NULL; - GMimeValidity validity = GMIME_VALIDITY_UNKNOWN; int len; g_mime_init (); @@ -563,16 +596,27 @@ int main () { if (sig == NULL) return !! fprintf (stderr, "no GMimeSignature found at position 0\n"); cert = g_mime_signature_get_certificate (sig); if (cert == NULL) return !! fprintf (stderr, "no GMimeCertificate found\n"); - validity = g_mime_certificate_get_id_validity (cert); +#ifdef CHECK_VALIDITY + GMimeValidity validity = g_mime_certificate_get_id_validity (cert); if (validity != GMIME_VALIDITY_FULL) return !! fprintf (stderr, "Got validity %d, expected %d\n", validity, GMIME_VALIDITY_FULL); - +#endif +#ifdef CHECK_EMAIL + const char *email = g_mime_certificate_get_email (cert); + if (! email) return !! fprintf (stderr, "no email returned"); + if (email[0] == '<') return 2; +#endif return 0; } EOF + + # see https://github.com/jstedfast/gmime/pull/90 + # should be fixed in GMime in 3.2.7, but some distros might patch + printf "Checking for GMime X.509 certificate validity... " + if ! TEMP_GPG=$(mktemp -d "${TMPDIR:-/tmp}/notmuch.XXXXXX"); then printf 'No.\nCould not make tempdir for testing X.509 certificate validity support.\n' errors=$((errors + 1)) - elif ${CC} ${CFLAGS} ${gmime_cflags} _check_x509_validity.c ${gmime_ldflags} -o _check_x509_validity \ + elif ${CC} -DCHECK_VALIDITY ${CFLAGS} ${gmime_cflags} _check_gmime_cert.c ${gmime_ldflags} -o _check_x509_validity \ && echo disable-crl-checks > "$TEMP_GPG/gpgsm.conf" \ && echo "4D:E0:FF:63:C0:E9:EC:01:29:11:C8:7A:EE:DA:3A:9A:7F:6E:C1:0D S" >> "$TEMP_GPG/trustlist.txt" \ && GNUPGHOME=${TEMP_GPG} gpgsm --batch --quiet --import < "$srcdir"/test/smime/ca.crt @@ -594,6 +638,15 @@ EOF errors=$((errors + 1)) fi fi + printf "Checking whether GMime emits email addresses with angle brackets... " + if ${CC} -DCHECK_EMAIL ${CFLAGS} ${gmime_cflags} _check_gmime_cert.c ${gmime_ldflags} -o _check_email && + GNUPGHOME=${TEMP_GPG} ./_check_email; then + gmime_emits_angle_brackets=0 + printf "No.\n" + else + gmime_emits_angle_brackets=1 + printf "Yes.\n" + fi else printf 'No.\nFailed to set up gpgsm for testing X.509 certificate validity support.\n' errors=$((errors + 1)) @@ -629,7 +682,7 @@ int main () { body = GMIME_MULTIPART_ENCRYPTED(g_mime_message_get_mime_part (g_mime_parser_construct_message (parser, NULL))); if (body == NULL) return !! fprintf (stderr, "did not find a multipart/encrypted message\n"); - output = g_mime_multipart_encrypted_decrypt (body, GMIME_DECRYPT_NONE, "9:13607E4217515A70EC8DF9DBC16C5327B94577561D98AD1246FA8756659C7899", &result, &error); + output = g_mime_multipart_encrypted_decrypt (body, GMIME_DECRYPT_NONE, "9:9E1CDF53BBF794EA34F894B5B68E1E56FB015EA69F81D2A5EAB7F96C7B65783E", &result, &error); if (error || output == NULL) return !! fprintf (stderr, "decrypt failed\n"); sig_list = g_mime_decrypt_result_get_signatures (result); @@ -650,7 +703,7 @@ EOF printf 'No.\nCould not make tempdir for testing signature verification when decrypting with session keys.\n' errors=$((errors + 1)) elif ${CC} ${CFLAGS} ${gmime_cflags} _verify_sig_with_session_key.c ${gmime_ldflags} -o _verify_sig_with_session_key \ - && GNUPGHOME=${TEMP_GPG} gpg --batch --quiet --import < "$srcdir"/test/gnupg-secret-key.asc \ + && GNUPGHOME=${TEMP_GPG} gpg --batch --quiet --import < "$srcdir"/test/openpgp4-secret-key.asc \ && rm -f ${TEMP_GPG}/private-keys-v1.d/*.key then if GNUPGHOME=${TEMP_GPG} ./_verify_sig_with_session_key; then @@ -734,6 +787,7 @@ if command -v ${BASHCMD} > /dev/null; then printf "Yes (%s).\n" "$bash_absolute" else have_bash=0 + bash_absolute= printf "No. (%s not found)\n" "${BASHCMD}" fi @@ -744,6 +798,7 @@ if command -v ${PERL} > /dev/null; then printf "Yes (%s).\n" "$perl_absolute" else have_perl=0 + perl_absolute= printf "No. (%s not found)\n" "${PERL}" fi @@ -1228,7 +1283,7 @@ for flag in -Wmissing-declarations; do done printf "\n\t%s\n" "${WARN_CFLAGS}" -rm -f minimal minimal.c _time_t.c _libversion.c _libversion _libversion.sh _check_session_keys.c _check_session_keys _check_x509_validity.c _check_x509_validity \ +rm -f minimal minimal.c _time_t.c _libversion.c _libversion _libversion.sh _check_session_keys.c _check_session_keys _check_gmime_cert.c _check_x509_validity _check_email \ _verify_sig_with_session_key.c _verify_sig_with_session_key # construct the Makefile.config @@ -1245,6 +1300,7 @@ cat > Makefile.config < sh.config <) +NOTMUCH_GMIME_EMITS_ANGLE_BRACKETS=${gmime_emits_angle_brackets} + # Whether GMime can verify signatures when decrypting with a session key: NOTMUCH_GMIME_VERIFY_WITH_SESSION_KEY=${gmime_verify_with_session_key} +# Flags needed to compile and link against zlib +NOTMUCH_ZLIB_CFLAGS="${zlib_cflags}" +NOTMUCH_ZLIB_LDFLAGS="${zlib_ldflags}" + +# Does the C compiler support the sanitizers +NOTMUCH_HAVE_ASAN=${have_asan} +NOTMUCH_HAVE_TSAN=${have_tsan} + # do we have man pages? NOTMUCH_HAVE_MAN=$((have_sphinx)) @@ -1564,6 +1639,10 @@ NOTMUCH_HAVE_PYTHON3_PYTEST=${have_python3_pytest} # Is the sfsexp library available? NOTMUCH_HAVE_SFSEXP=${have_sfsexp} +# And if so, flags needed at compile/link time for sfsexp +NOTMUCH_SFSEXP_CFLAGS="${sfsexp_cflags}" +NOTMUCH_SFSEXP_LDFLAGS="${sfsexp_ldflags}" + # Platform we are run on PLATFORM=${platform} EOF @@ -1579,6 +1658,14 @@ EOF printf "rsti_dir = '%s'\n" "$(cd emacs && pwd -P)" } > sphinx.config +cat > bindings/python-cffi/_notmuch_config.py < (Debian package: libmailtools-perl) -- String::ShellQuote - (Debian package: libstring-shellquote-perl) - Term::ReadLine::Gnu (Debian package: libterm-readline-gnu-perl) diff --git a/contrib/notmuch-mutt/notmuch-mutt b/contrib/notmuch-mutt/notmuch-mutt index d1e2c084..b81252c8 100755 --- a/contrib/notmuch-mutt/notmuch-mutt +++ b/contrib/notmuch-mutt/notmuch-mutt @@ -2,7 +2,7 @@ # # notmuch-mutt - notmuch (of a) helper for Mutt # -# Copyright: © 2011-2015 Stefano Zacchiroli +# Copyright: © 2011-2015 Stefano Zacchiroli # License: GNU General Public License (GPL), version 3 or above # # See the bottom of this file for more documentation. @@ -13,11 +13,11 @@ use warnings; use File::Path; use File::Basename; +use File::Find; use Getopt::Long qw(:config no_getopt_compat); use Mail::Header; use Mail::Box::Maildir; use Pod::Usage; -use String::ShellQuote; use Term::ReadLine; use Digest::SHA; @@ -26,9 +26,53 @@ my $xdg_cache_dir = "$ENV{HOME}/.cache"; $xdg_cache_dir = $ENV{XDG_CACHE_HOME} if $ENV{XDG_CACHE_HOME}; my $cache_dir = "$xdg_cache_dir/notmuch/mutt"; +sub die_dir($$) { + my ($maildir, $error) = @_; + die "notmuch-mutt: search cache maildir $maildir $error\n". + "Please ensure that the notmuch-mutt search cache Maildir\n". + "contains no subfolders or real mail data, only symlinks to mail\n"; +} + +sub die_subdir($$$) { + my ($maildir, $subdir, $error) = @_; + die_dir($maildir, "subdir $subdir $error"); +} -# create an empty maildir (if missing) or empty an existing maildir" -sub empty_maildir($) { +# check that the search cache maildir is that and not a real maildir +# otherwise there could be data loss when the search cache is emptied +sub check_search_cache_maildir($) { + my ($maildir) = (@_); + + return unless -e $maildir; + + -d $maildir or die_dir($maildir, 'is not a directory'); + + opendir(my $mdh, $maildir) or die_dir($maildir, "cannot be opened: $!"); + my @contents = grep { !/^\.\.?$/ } readdir $mdh; + closedir $mdh; + + my @required = ('cur', 'new', 'tmp'); + foreach my $d (@required) { + -l "$maildir/$d" and die_dir($maildir, "contains symlink $d"); + -e "$maildir/$d" or die_subdir($maildir, $d, 'is missing'); + -d "$maildir/$d" or die_subdir($maildir, $d, 'is not a directory'); + find(sub { + $_ eq '.' and return; + $_ eq '..' and return; + -l $_ or die_subdir($maildir, $d, "contains non-symlink $_"); + }, "$maildir/$d"); + } + + my %required = map { $_ => 1 } @required; + foreach my $d (@contents) { + -l "$maildir/$d" and die_dir( $maildir, "contains symlink $d"); + -d "$maildir/$d" or die_dir( $maildir, "contains non-directory $d"); + exists($required{$d}) or die_dir( $maildir, "contains directory $d"); + } +} + +# create an empty search cache maildir (if missing) or empty existing one +sub empty_search_cache_maildir($) { my ($maildir) = (@_); rmtree($maildir) if (-d $maildir); my $folder = new Mail::Box::Maildir(folder => $maildir, @@ -46,7 +90,8 @@ sub search($$$) { push @args, "--duplicate=1" if $remove_dups; push @args, $query; - empty_maildir($maildir); + check_search_cache_maildir($maildir); + empty_search_cache_maildir($maildir); open my $pipe, '-|', @args or die "Running @args failed: $!\n"; while (<$pipe>) { chomp; @@ -121,21 +166,23 @@ sub thread_action($$@) { my $mid = get_message_id(); if (! defined $mid) { - empty_maildir($results_dir); die "notmuch-mutt: cannot find Message-Id, abort.\n"; } - my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid"); - my $tid = `$search_cmd`; # get thread id - chomp($tid); - search($results_dir, $remove_dups, $tid); + $mid =~ s/ //g; # notmuch strips spaces before storing Message-Id + $mid =~ s/"/""""/g; # escape all double quote characters twice + + search($results_dir, $remove_dups, qq{thread:"{id:""$mid""}"}); } sub tag_action(@) { my $mid = get_message_id(); defined $mid or die "notmuch-mutt: cannot find Message-Id, abort.\n"; - system("notmuch", "tag", @_, "--", "id:$mid"); + $mid =~ s/ //g; # notmuch strips spaces before storing Message-Id + $mid =~ s/"/""/g; # escape all double quote characters + + system("notmuch", "tag", @_, "--", qq{id:"$mid"}); } sub die_usage() { diff --git a/contrib/python-legacy/.gitignore b/contrib/python-legacy/.gitignore new file mode 100644 index 00000000..601acdd7 --- /dev/null +++ b/contrib/python-legacy/.gitignore @@ -0,0 +1,4 @@ +*.py[co] +/docs/build +/docs/html +/build/ diff --git a/contrib/python-legacy/MANIFEST.in b/contrib/python-legacy/MANIFEST.in new file mode 100644 index 00000000..c83be4b1 --- /dev/null +++ b/contrib/python-legacy/MANIFEST.in @@ -0,0 +1,2 @@ +include notmuch +#recursive-include docs/html * \ No newline at end of file diff --git a/contrib/python-legacy/README b/contrib/python-legacy/README new file mode 100644 index 00000000..5bf076d2 --- /dev/null +++ b/contrib/python-legacy/README @@ -0,0 +1,17 @@ +notmuch -- The python interface to notmuch +========================================== + +This module makes the functionality of the notmuch library +(`https://notmuchmail.org`_) available to python. Successful import of +this module depends on a libnotmuch.so|dll being available on the +user's system. + +If you have downloaded the full source tarball, you can create the +documentation with sphinx installed, go to the docs directory and +"make html". A static version of the documentation is available at: + + https://notmuch.readthedocs.io/projects/notmuch-python/ + +To build the python bindings, do + + python setup.py install --prefix=path/to/your/preferred/location diff --git a/contrib/python-legacy/docs/COPYING b/contrib/python-legacy/docs/COPYING new file mode 100644 index 00000000..e6000869 --- /dev/null +++ b/contrib/python-legacy/docs/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/contrib/python-legacy/docs/Makefile b/contrib/python-legacy/docs/Makefile new file mode 100644 index 00000000..bd6de38c --- /dev/null +++ b/contrib/python-legacy/docs/Makefile @@ -0,0 +1,88 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf build/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) html + @echo + @echo "Build finished. The HTML pages are in html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) build/dirhtml + @echo + @echo "Build finished. The HTML pages are in build/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in build/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) build/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in build/qthelp, like this:" + @echo "# qcollectiongenerator build/qthelp/pyDNS.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile build/qthelp/pyDNS.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex + @echo + @echo "Build finished; the LaTeX files are in build/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes + @echo + @echo "The overview file is in build/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in build/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) build/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in build/doctest/output.txt." diff --git a/contrib/python-legacy/docs/source/conf.py b/contrib/python-legacy/docs/source/conf.py new file mode 100644 index 00000000..bd4c8b17 --- /dev/null +++ b/contrib/python-legacy/docs/source/conf.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# +# pyDNS documentation build configuration file, created by +# sphinx-quickstart on Tue Feb 2 10:00:47 2010. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +from unittest.mock import Mock + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0,os.path.abspath('../..')) + +MOCK_MODULES = [ + 'ctypes', +] +for mod_name in MOCK_MODULES: + sys.modules[mod_name] = Mock() + + +from notmuch import __VERSION__,__AUTHOR__ +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo'] +autoclass_content = "both" + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'notmuch' +copyright = u'2010-2012, ' + __AUTHOR__ + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = __VERSION__ +# The full version, including alpha/beta/rc tags. +release = __VERSION__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = False + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +html_use_modindex = False + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'notmuchdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'notmuch.tex', u'notmuch Documentation', + u'notmuch contributors', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} diff --git a/contrib/python-legacy/docs/source/database.rst b/contrib/python-legacy/docs/source/database.rst new file mode 100644 index 00000000..660de91d --- /dev/null +++ b/contrib/python-legacy/docs/source/database.rst @@ -0,0 +1,54 @@ +:class:`Database` -- The underlying notmuch database +==================================================== + +.. currentmodule:: notmuch + +.. autoclass:: Database([path=None[, create=False[, mode=MODE.READ_ONLY]]]) + + .. automethod:: create + + .. automethod:: open(path, status=MODE.READ_ONLY) + + .. automethod:: close + + .. automethod:: get_path + + .. automethod:: get_version + + .. automethod:: needs_upgrade + + .. automethod:: upgrade + + .. automethod:: begin_atomic + + .. automethod:: end_atomic + + .. automethod:: get_directory + + .. automethod:: index_file + + .. automethod:: remove_message + + .. automethod:: find_message + + .. automethod:: find_message_by_filename + + .. automethod:: get_all_tags + + .. automethod:: create_query + + .. automethod:: get_config + + .. automethod:: get_configs + + .. automethod:: set_config + + .. attribute:: Database.MODE + + Defines constants that are used as the mode in which to open a database. + + MODE.READ_ONLY + Open the database in read-only mode + + MODE.READ_WRITE + Open the database in read-write mode diff --git a/contrib/python-legacy/docs/source/filesystem.rst b/contrib/python-legacy/docs/source/filesystem.rst new file mode 100644 index 00000000..13fe1194 --- /dev/null +++ b/contrib/python-legacy/docs/source/filesystem.rst @@ -0,0 +1,32 @@ +Files and directories +===================== + +.. currentmodule:: notmuch + +:class:`Filenames` -- An iterator over filenames +------------------------------------------------ + +.. autoclass:: Filenames + + .. method:: Filenames.__len__ + .. warning:: + :meth:`__len__` was removed in version 0.22 as it exhausted the + iterator and broke list(Filenames()). Use `len(list(names))` + instead. + +:class:`Directory` -- A directory entry in the database +------------------------------------------------------- + +.. autoclass:: Directory + + .. automethod:: Directory.get_child_files + + .. automethod:: Directory.get_child_directories + + .. automethod:: Directory.get_mtime + + .. automethod:: Directory.set_mtime + + .. autoattribute:: Directory.mtime + + .. autoattribute:: Directory.path diff --git a/contrib/python-legacy/docs/source/index.rst b/contrib/python-legacy/docs/source/index.rst new file mode 100644 index 00000000..bef7e60d --- /dev/null +++ b/contrib/python-legacy/docs/source/index.rst @@ -0,0 +1,36 @@ +Welcome to :mod:`notmuch`'s documentation +========================================= + +.. currentmodule:: notmuch + +The :mod:`notmuch` module provides an interface to the `notmuch +`_ functionality, directly interfacing to a +shared notmuch library. Within :mod:`notmuch`, the classes +:class:`Database`, :class:`Query` provide most of the core +functionality, returning :class:`Threads`, :class:`Messages` and +:class:`Tags`. + +.. moduleauthor:: Sebastian Spaeth + +:License: This module is covered under the GNU GPL v3 (or later). + +.. toctree:: + :maxdepth: 1 + + quickstart + notes + status_and_errors + database + query + messages + message + tags + threads + thread + filesystem + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/contrib/python-legacy/docs/source/message.rst b/contrib/python-legacy/docs/source/message.rst new file mode 100644 index 00000000..b0033924 --- /dev/null +++ b/contrib/python-legacy/docs/source/message.rst @@ -0,0 +1,54 @@ +:class:`Message` -- A single message +==================================== + +.. currentmodule:: notmuch + +.. autoclass:: Message + + .. automethod:: get_message_id + + .. automethod:: get_thread_id + + .. automethod:: get_replies + + .. automethod:: get_filename + + .. automethod:: get_filenames + + .. attribute:: FLAG + + FLAG.MATCH + This flag is automatically set by a + Query.search_threads on those messages that match the + query. This allows us to distinguish matches from the rest + of the messages in that thread. + + .. automethod:: get_flag + + .. automethod:: set_flag + + .. automethod:: get_date + + .. automethod:: get_header + + .. automethod:: get_tags + + .. automethod:: get_property + + .. automethod:: get_properties + + .. automethod:: maildir_flags_to_tags + + .. automethod:: tags_to_maildir_flags + + .. automethod:: remove_tag + + .. automethod:: add_tag + + .. automethod:: remove_all_tags + + .. automethod:: freeze + + .. automethod:: thaw + + .. automethod:: __str__ diff --git a/contrib/python-legacy/docs/source/messages.rst b/contrib/python-legacy/docs/source/messages.rst new file mode 100644 index 00000000..3ccf505c --- /dev/null +++ b/contrib/python-legacy/docs/source/messages.rst @@ -0,0 +1,15 @@ +:class:`Messages` -- A bunch of messages +======================================== + +.. currentmodule:: notmuch + +.. autoclass:: Messages + + .. automethod:: collect_tags + + .. method:: __len__() + + .. warning:: + + :meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke + list(Messages()). Use the :meth:`Query.count_messages` function or use `len(list(msgs))`. diff --git a/contrib/python-legacy/docs/source/notes.rst b/contrib/python-legacy/docs/source/notes.rst new file mode 100644 index 00000000..a7927485 --- /dev/null +++ b/contrib/python-legacy/docs/source/notes.rst @@ -0,0 +1,6 @@ +Interfacing with notmuch +======================== + +.. todo:: move the note about talloc out of the code + +.. automodule:: notmuch diff --git a/contrib/python-legacy/docs/source/query.rst b/contrib/python-legacy/docs/source/query.rst new file mode 100644 index 00000000..785e984b --- /dev/null +++ b/contrib/python-legacy/docs/source/query.rst @@ -0,0 +1,43 @@ +:class:`Query` -- A search query +================================ + +.. currentmodule:: notmuch + +.. autoclass:: Query + + .. automethod:: create + + .. attribute:: Query.SORT + + Defines constants that are used as the mode in which to open a database. + + SORT.OLDEST_FIRST + Sort by message date, oldest first. + + SORT.NEWEST_FIRST + Sort by message date, newest first. + + SORT.MESSAGE_ID + Sort by email message ID. + + SORT.UNSORTED + Do not apply a special sort order (returns results in document id + order). + + .. automethod:: set_sort + + .. attribute:: sort + + Instance attribute :attr:`sort` contains the sort order (see + :attr:`Query.SORT`) if explicitly specified via + :meth:`set_sort`. By default it is set to `None`. + + .. automethod:: exclude_tag + + .. automethod:: search_threads + + .. automethod:: search_messages + + .. automethod:: count_messages + + .. automethod:: count_threads diff --git a/contrib/python-legacy/docs/source/quickstart.rst b/contrib/python-legacy/docs/source/quickstart.rst new file mode 100644 index 00000000..609f42e1 --- /dev/null +++ b/contrib/python-legacy/docs/source/quickstart.rst @@ -0,0 +1,19 @@ +Quickstart and examples +======================= + +.. todo:: write a nice introduction +.. todo:: improve the examples + +Notmuch can be imported as:: + + import notmuch + +or:: + + from notmuch import Query, Database + + db = Database('path', create=True) + msgs = Query(db, 'from:myself').search_messages() + + for msg in msgs: + print(msg) diff --git a/contrib/python-legacy/docs/source/status_and_errors.rst b/contrib/python-legacy/docs/source/status_and_errors.rst new file mode 100644 index 00000000..68913f16 --- /dev/null +++ b/contrib/python-legacy/docs/source/status_and_errors.rst @@ -0,0 +1,57 @@ +.. currentmodule:: notmuch + +Status and Errors +================= + +Some methods return a status, indicating if an operation was successful and what the error was. Most of these status codes are expressed as a specific value, the :class:`notmuch.STATUS`. + +.. note:: + + Prior to version 0.12 the exception classes and the enumeration + :class:`notmuch.STATUS` were defined in `notmuch.globals`. They + have since then been moved into `notmuch.errors`. + +:class:`STATUS` -- Notmuch operation return value +-------------------------------------------------- + +.. autoclass:: notmuch.STATUS + :inherited-members: + +.. automethod:: notmuch.STATUS.status2str + +:exc:`NotmuchError` -- A Notmuch execution error +------------------------------------------------ +Whenever an error occurs, we throw a special Exception :exc:`NotmuchError`, or a more fine grained Exception which is derived from it. This means it is always safe to check for NotmuchErrors if you want to catch all errors. If you are interested in more fine grained exceptions, you can use those below. + +.. autoexception:: NotmuchError + +The following exceptions are all directly derived from NotmuchError. Each of them corresponds to a specific :class:`notmuch.STATUS` value. You can either check the :attr:`status` attribute of a NotmuchError to see if a specific error has occurred, or you can directly check for the following Exception types: + +.. autoexception:: OutOfMemoryError(message=None) + :members: +.. autoexception:: ReadOnlyDatabaseError(message=None) + :members: +.. autoexception:: XapianError(message=None) + :members: +.. autoexception:: FileError(message=None) + :members: +.. autoexception:: FileNotEmailError(message=None) + :members: +.. autoexception:: DuplicateMessageIdError(message=None) + :members: +.. autoexception:: NullPointerError(message=None) + :members: +.. autoexception:: TagTooLongError(message=None) + :members: +.. autoexception:: UnbalancedFreezeThawError(message=None) + :members: +.. autoexception:: UnbalancedAtomicError(message=None) + :members: +.. autoexception:: UnsupportedOperationError(message=None) + :members: +.. autoexception:: UpgradeRequiredError(message=None) + :members: +.. autoexception:: PathError(message=None) + :members: +.. autoexception:: NotInitializedError(message=None) + :members: diff --git a/contrib/python-legacy/docs/source/tags.rst b/contrib/python-legacy/docs/source/tags.rst new file mode 100644 index 00000000..31527d4b --- /dev/null +++ b/contrib/python-legacy/docs/source/tags.rst @@ -0,0 +1,17 @@ +:class:`Tags` -- Notmuch tags +----------------------------- + +.. currentmodule:: notmuch + +.. autoclass:: Tags + :members: + + .. method:: __len__ + + .. warning:: + + :meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke + list(Tags()). Use :meth:`len(list(msgs))` instead if you need to know the number of + tags. + + .. automethod:: __str__ diff --git a/contrib/python-legacy/docs/source/thread.rst b/contrib/python-legacy/docs/source/thread.rst new file mode 100644 index 00000000..40678725 --- /dev/null +++ b/contrib/python-legacy/docs/source/thread.rst @@ -0,0 +1,26 @@ +:class:`Thread` -- A single thread +================================== + +.. currentmodule:: notmuch + +.. autoclass:: Thread + + .. automethod:: get_thread_id + + .. automethod:: get_total_messages + + .. automethod:: get_toplevel_messages + + .. automethod:: get_matched_messages + + .. automethod:: get_authors + + .. automethod:: get_subject + + .. automethod:: get_oldest_date + + .. automethod:: get_newest_date + + .. automethod:: get_tags + + .. automethod:: __str__ diff --git a/contrib/python-legacy/docs/source/threads.rst b/contrib/python-legacy/docs/source/threads.rst new file mode 100644 index 00000000..46ce5be5 --- /dev/null +++ b/contrib/python-legacy/docs/source/threads.rst @@ -0,0 +1,14 @@ +:class:`Threads` -- Threads iterator +==================================== + +.. currentmodule:: notmuch + +.. autoclass:: Threads + + .. method:: __len__ + .. warning:: + :meth:`__len__` was removed in version 0.22 as it exhausted the + iterator and broke list(Threads()). Use `len(list(msgs))` + instead. + + .. automethod:: __str__ diff --git a/contrib/python-legacy/notmuch/__init__.py b/contrib/python-legacy/notmuch/__init__.py new file mode 100644 index 00000000..cf627ffa --- /dev/null +++ b/contrib/python-legacy/notmuch/__init__.py @@ -0,0 +1,84 @@ +"""The :mod:`notmuch` module provides most of the functionality that a user is +likely to need. + +.. note:: The underlying notmuch library is build on a hierarchical + memory allocator called talloc. All objects derive from a + top-level :class:`Database` object. + + This means that as soon as an object is deleted, all underlying + derived objects such as Queries, Messages, Message, and Tags will + be freed by the underlying library as well. Accessing these + objects will then lead to segfaults and other unexpected behavior. + + We implement reference counting, so that parent objects can be + automatically freed when they are not needed anymore. For + example:: + + db = Database('path',create=True) + msgs = Query(db,'from:myself').search_messages() + + This returns :class:`Messages` which internally contains a + reference to its parent :class:`Query` object. Otherwise the + Query() would be immediately freed, taking our *msgs* down with + it. + + In this case, the above Query() object will be automatically freed + whenever we delete all derived objects, ie in our case: + `del(msgs)` would also delete the parent Query. It would not + delete the parent Database() though, as that is still referenced + from the variable *db* in which it is stored. + + Pretty much the same is valid for all other objects in the + hierarchy, such as :class:`Query`, :class:`Messages`, + :class:`Message`, and :class:`Tags`. +""" + +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010-2011 Sebastian Spaeth +""" +from .database import Database +from .directory import Directory +from .filenames import Filenames +from .message import Message +from .messages import Messages +from .query import Query +from .tag import Tags +from .thread import Thread +from .threads import Threads +from .globals import nmlib +from .errors import ( + STATUS, + NotmuchError, + OutOfMemoryError, + ReadOnlyDatabaseError, + XapianError, + FileError, + FileNotEmailError, + DuplicateMessageIdError, + NullPointerError, + TagTooLongError, + UnbalancedFreezeThawError, + UnbalancedAtomicError, + NotInitializedError, + UnsupportedOperationError, + UpgradeRequiredError, + PathError, +) +from .version import __VERSION__ +__LICENSE__ = "GPL v3+" +__AUTHOR__ = 'Sebastian Spaeth ' diff --git a/contrib/python-legacy/notmuch/compat.py b/contrib/python-legacy/notmuch/compat.py new file mode 100644 index 00000000..4a94e05c --- /dev/null +++ b/contrib/python-legacy/notmuch/compat.py @@ -0,0 +1,74 @@ +''' +This file is part of notmuch. + +This module handles differences between python2.x and python3.x and +allows the notmuch bindings to support both version families with one +source tree. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +Copyright 2012 Justus Winter <4winter@informatik.uni-hamburg.de> +''' + +import sys + +if sys.version_info[0] == 2: + from ConfigParser import SafeConfigParser + + class Python3StringMixIn(object): + def __str__(self): + return unicode(self).encode('utf-8') + + def encode_utf8(value): + ''' + Ensure a nicely utf-8 encoded string to pass to wrapped + libnotmuch functions. + + C++ code expects strings to be well formatted and unicode + strings to have no null bytes. + ''' + if not isinstance(value, basestring): + raise TypeError('Expected str or unicode, got %s' % type(value)) + + if isinstance(value, unicode): + return value.encode('utf-8', 'replace') + + return value +else: + from configparser import ConfigParser as SafeConfigParser + + if not hasattr(SafeConfigParser, 'readfp'): # py >= 3.12 + SafeConfigParser.readfp = SafeConfigParser.read_file + + class Python3StringMixIn(object): + def __str__(self): + return self.__unicode__() + + def encode_utf8(value): + ''' + Ensure a nicely utf-8 encoded string to pass to wrapped + libnotmuch functions. + + C++ code expects strings to be well formatted and unicode + strings to have no null bytes. + ''' + if not isinstance(value, str): + raise TypeError('Expected str, got %s' % type(value)) + + return value.encode('utf-8', 'replace') + +# We import the SafeConfigParser class on behalf of other code to cope +# with the differences between Python 2 and 3. +SafeConfigParser # avoid warning about unused import diff --git a/contrib/python-legacy/notmuch/database.py b/contrib/python-legacy/notmuch/database.py new file mode 100644 index 00000000..8fb507fa --- /dev/null +++ b/contrib/python-legacy/notmuch/database.py @@ -0,0 +1,789 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +import os +import codecs +import warnings +from ctypes import c_char_p, c_void_p, c_uint, byref, POINTER +from .compat import SafeConfigParser +from .globals import ( + nmlib, + Enum, + _str, + NotmuchConfigListP, + NotmuchDatabaseP, + NotmuchDirectoryP, + NotmuchIndexoptsP, + NotmuchMessageP, + NotmuchTagsP, +) +from .errors import ( + STATUS, + FileError, + NotmuchError, + NullPointerError, + NotInitializedError, +) +from .message import Message +from .tag import Tags +from .query import Query +from .directory import Directory + +class Database(object): + """The :class:`Database` is the highest-level object that notmuch + provides. It references a notmuch database, and can be opened in + read-only or read-write mode. A :class:`Query` can be derived from + or be applied to a specific database to find messages. Also adding + and removing messages to the database happens via this + object. Modifications to the database are not atmic by default (see + :meth:`begin_atomic`) and once a database has been modified, all + other database objects pointing to the same data-base will throw an + :exc:`XapianError` as the underlying database has been + modified. Close and reopen the database to continue working with it. + + :class:`Database` objects implement the context manager protocol + so you can use the :keyword:`with` statement to ensure that the + database is properly closed. See :meth:`close` for more + information. + + .. note:: + + Any function in this class can and will throw an + :exc:`NotInitializedError` if the database was not initialized + properly. + """ + _std_db_path = None + """Class attribute to cache user's default database""" + + MODE = Enum(['READ_ONLY', 'READ_WRITE']) + """Constants: Mode in which to open the database""" + + DECRYPTION_POLICY = Enum(['FALSE', 'TRUE', 'AUTO', 'NOSTASH']) + """Constants: policies for decrypting messages during indexing""" + + """notmuch_database_get_directory""" + _get_directory = nmlib.notmuch_database_get_directory + _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)] + _get_directory.restype = c_uint + + """notmuch_database_get_path""" + _get_path = nmlib.notmuch_database_get_path + _get_path.argtypes = [NotmuchDatabaseP] + _get_path.restype = c_char_p + + """notmuch_database_get_version""" + _get_version = nmlib.notmuch_database_get_version + _get_version.argtypes = [NotmuchDatabaseP] + _get_version.restype = c_uint + + """notmuch_database_get_revision""" + _get_revision = nmlib.notmuch_database_get_revision + _get_revision.argtypes = [NotmuchDatabaseP, POINTER(c_char_p)] + _get_revision.restype = c_uint + + """notmuch_database_open""" + _open = nmlib.notmuch_database_open + _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)] + _open.restype = c_uint + + """notmuch_database_upgrade""" + _upgrade = nmlib.notmuch_database_upgrade + _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p] + _upgrade.restype = c_uint + + """ notmuch_database_find_message""" + _find_message = nmlib.notmuch_database_find_message + _find_message.argtypes = [NotmuchDatabaseP, c_char_p, + POINTER(NotmuchMessageP)] + _find_message.restype = c_uint + + """notmuch_database_find_message_by_filename""" + _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename + _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p, + POINTER(NotmuchMessageP)] + _find_message_by_filename.restype = c_uint + + """notmuch_database_get_all_tags""" + _get_all_tags = nmlib.notmuch_database_get_all_tags + _get_all_tags.argtypes = [NotmuchDatabaseP] + _get_all_tags.restype = NotmuchTagsP + + """notmuch_database_create""" + _create = nmlib.notmuch_database_create + _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)] + _create.restype = c_uint + + def __init__(self, path = None, create = False, + mode = MODE.READ_ONLY): + """If *path* is `None`, we will try to read a users notmuch + configuration and use his configured database. The location of the + configuration file can be specified through the environment variable + *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`. + + If *create* is `True`, the database will always be created in + :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY. + + :param path: Directory to open/create the database in (see + above for behavior if `None`) + :type path: `str` or `None` + :param create: Pass `False` to open an existing, `True` to create a new + database. + :type create: bool + :param mode: Mode to open a database in. Is always + :attr:`MODE`.READ_WRITE when creating a new one. + :type mode: :attr:`MODE` + :raises: :exc:`NotmuchError` or derived exception in case of + failure. + """ + self._db = None + self.mode = mode + if path is None: + # no path specified. use a user's default database + if Database._std_db_path is None: + #the following line throws a NotmuchError if it fails + Database._std_db_path = self._get_user_default_db() + path = Database._std_db_path + + if create == False: + self.open(path, mode) + else: + self.create(path) + + _destroy = nmlib.notmuch_database_destroy + _destroy.argtypes = [NotmuchDatabaseP] + _destroy.restype = c_uint + + def __del__(self): + if self._db: + status = self._destroy(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + def _assert_db_is_initialized(self): + """Raises :exc:`NotInitializedError` if self._db is `None`""" + if not self._db: + raise NotInitializedError() + + def create(self, path): + """Creates a new notmuch database + + This function is used by __init__() and usually does not need + to be called directly. It wraps the underlying + *notmuch_database_create* function and creates a new notmuch + database at *path*. It will always return a database in :attr:`MODE` + .READ_WRITE mode as creating an empty database for + reading only does not make a great deal of sense. + + :param path: A directory in which we should create the database. + :type path: str + :raises: :exc:`NotmuchError` in case of any failure + (possibly after printing an error message on stderr). + """ + if self._db: + raise NotmuchError(message="Cannot create db, this Database() " + "already has an open one.") + + db = NotmuchDatabaseP() + status = Database._create(_str(path), byref(db)) + + if status != STATUS.SUCCESS: + raise NotmuchError(status) + self._db = db + return status + + def open(self, path, mode=0): + """Opens an existing database + + This function is used by __init__() and usually does not need + to be called directly. It wraps the underlying + *notmuch_database_open* function. + + :param status: Open the database in read-only or read-write mode + :type status: :attr:`MODE` + :raises: Raises :exc:`NotmuchError` in case of any failure + (possibly after printing an error message on stderr). + """ + db = NotmuchDatabaseP() + status = Database._open(_str(path), mode, byref(db)) + + if status != STATUS.SUCCESS: + raise NotmuchError(status) + self._db = db + return status + + _close = nmlib.notmuch_database_close + _close.argtypes = [NotmuchDatabaseP] + _close.restype = c_uint + + def close(self): + ''' + Closes the notmuch database. + + .. warning:: + + This function closes the notmuch database. From that point + on every method invoked on any object ever derived from + the closed database may cease to function and raise a + NotmuchError. + ''' + if self._db: + status = self._close(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + def __enter__(self): + ''' + Implements the context manager protocol. + ''' + return self + + def __exit__(self, exc_type, exc_value, traceback): + ''' + Implements the context manager protocol. + ''' + self.close() + + def get_path(self): + """Returns the file path of an open database""" + self._assert_db_is_initialized() + return Database._get_path(self._db).decode('utf-8') + + def get_version(self): + """Returns the database format version + + :returns: The database version as positive integer + """ + self._assert_db_is_initialized() + return Database._get_version(self._db) + + def get_revision (self): + """Returns the committed database revision and UUID + + :returns: (revision, uuid) The database revision as a positive integer + and the UUID of the database. + """ + self._assert_db_is_initialized() + uuid = c_char_p () + revision = Database._get_revision(self._db, byref (uuid)) + return (revision, uuid.value.decode ('utf-8')) + + _needs_upgrade = nmlib.notmuch_database_needs_upgrade + _needs_upgrade.argtypes = [NotmuchDatabaseP] + _needs_upgrade.restype = bool + + def needs_upgrade(self): + """Does this database need to be upgraded before writing to it? + + If this function returns `True` then no functions that modify the + database (:meth:`index_file`, + :meth:`Message.add_tag`, :meth:`Directory.set_mtime`, + etc.) will work unless :meth:`upgrade` is called successfully first. + + :returns: `True` or `False` + """ + self._assert_db_is_initialized() + return self._needs_upgrade(self._db) + + def upgrade(self): + """Upgrades the current database + + After opening a database in read-write mode, the client should + check if an upgrade is needed (notmuch_database_needs_upgrade) and + if so, upgrade with this function before making any modifications. + + NOT IMPLEMENTED: The optional progress_notify callback can be + used by the caller to provide progress indication to the + user. If non-NULL it will be called periodically with + 'progress' as a floating-point value in the range of [0.0..1.0] + indicating the progress made so far in the upgrade process. + + :TODO: catch exceptions, document return values and etc... + """ + self._assert_db_is_initialized() + status = Database._upgrade(self._db, None, None) + # TODO: catch exceptions, document return values and etc + return status + + _begin_atomic = nmlib.notmuch_database_begin_atomic + _begin_atomic.argtypes = [NotmuchDatabaseP] + _begin_atomic.restype = c_uint + + def begin_atomic(self): + """Begin an atomic database operation + + Any modifications performed between a successful + :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to + the database atomically. Note that, unlike a typical database + transaction, this only ensures atomicity, not durability; + neither begin nor end necessarily flush modifications to disk. + + :returns: :attr:`STATUS`.SUCCESS or raises + :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION + Xapian exception occurred; atomic section not entered. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + status = self._begin_atomic(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return status + + _end_atomic = nmlib.notmuch_database_end_atomic + _end_atomic.argtypes = [NotmuchDatabaseP] + _end_atomic.restype = c_uint + + def end_atomic(self): + """Indicate the end of an atomic database operation + + See :meth:`begin_atomic` for details. + + :returns: :attr:`STATUS`.SUCCESS or raises + + :raises: + :exc:`NotmuchError`: + :attr:`STATUS`.XAPIAN_EXCEPTION + A Xapian exception occurred; atomic section not + ended. + :attr:`STATUS`.UNBALANCED_ATOMIC: + end_atomic has been called more times than begin_atomic. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + status = self._end_atomic(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return status + + def get_directory(self, path): + """Returns a :class:`Directory` of path, + + :param path: An unicode string containing the path relative to the path + of database (see :meth:`get_path`), or else should be an absolute + path with initial components that match the path of 'database'. + :returns: :class:`Directory` or raises an exception. + :raises: :exc:`FileError` if path is not relative database or absolute + with initial components same as database. + """ + self._assert_db_is_initialized() + + # sanity checking if path is valid, and make path absolute + if path and path[0] == os.sep: + # we got an absolute path + if not path.startswith(self.get_path()): + # but its initial components are not equal to the db path + raise FileError('Database().get_directory() called ' + 'with a wrong absolute path') + abs_dirpath = path + else: + #we got a relative path, make it absolute + abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path)) + + dir_p = NotmuchDirectoryP() + status = Database._get_directory(self._db, _str(path), byref(dir_p)) + + if status != STATUS.SUCCESS: + raise NotmuchError(status) + if not dir_p: + return None + + # return the Directory, init it with the absolute path + return Directory(abs_dirpath, dir_p, self) + + _get_default_indexopts = nmlib.notmuch_database_get_default_indexopts + _get_default_indexopts.argtypes = [NotmuchDatabaseP] + _get_default_indexopts.restype = NotmuchIndexoptsP + + _indexopts_set_decrypt_policy = nmlib.notmuch_indexopts_set_decrypt_policy + _indexopts_set_decrypt_policy.argtypes = [NotmuchIndexoptsP, c_uint] + _indexopts_set_decrypt_policy.restype = None + + _indexopts_destroy = nmlib.notmuch_indexopts_destroy + _indexopts_destroy.argtypes = [NotmuchIndexoptsP] + _indexopts_destroy.restype = None + + _index_file = nmlib.notmuch_database_index_file + _index_file.argtypes = [NotmuchDatabaseP, c_char_p, + c_void_p, + POINTER(NotmuchMessageP)] + _index_file.restype = c_uint + + def index_file(self, filename, sync_maildir_flags=False, decrypt_policy=None): + """Adds a new message to the database + + :param filename: should be a path relative to the path of the + open database (see :meth:`get_path`), or else should be an + absolute filename with initial components that match the + path of the database. + + The file should be a single mail message (not a + multi-message mbox) that is expected to remain at its + current location, since the notmuch database will reference + the filename, and will not copy the entire contents of the + file. + + :param sync_maildir_flags: If the message contains Maildir + flags, we will -depending on the notmuch configuration- sync + those tags to initial notmuch tags, if set to `True`. It is + `False` by default to remain consistent with the libnotmuch + API. You might want to look into the underlying method + :meth:`Message.maildir_flags_to_tags`. + + :param decrypt_policy: If the message contains any encrypted + parts, and decrypt_policy is set to + :attr:`DECRYPTION_POLICY`.TRUE, notmuch will try to + decrypt the message and index the cleartext, stashing any + discovered session keys. If it is set to + :attr:`DECRYPTION_POLICY`.FALSE, it will never try to + decrypt during indexing. If it is set to + :attr:`DECRYPTION_POLICY`.AUTO, then it will try to use + any stashed session keys it knows about, but will not try + to access the user's secret keys. + :attr:`DECRYPTION_POLICY`.NOSTASH behaves the same as + :attr:`DECRYPTION_POLICY`.TRUE except that no session keys + are stashed in the database. If decrypt_policy is set to + None (the default), then the database itself will decide + whether to decrypt, based on the `index.decrypt` + configuration setting (see notmuch-config(1)). + + :returns: On success, we return + + 1) a :class:`Message` object that can be used for things + such as adding tags to the just-added message. + 2) one of the following :attr:`STATUS` values: + + :attr:`STATUS`.SUCCESS + Message successfully added to database. + :attr:`STATUS`.DUPLICATE_MESSAGE_ID + Message has the same message ID as another message already + in the database. The new filename was successfully added + to the list of the filenames for the existing message. + + :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`) + + :raises: Raises a :exc:`NotmuchError` with the following meaning. + If such an exception occurs, nothing was added to the database. + + :attr:`STATUS`.FILE_ERROR + An error occurred trying to open the file, (such as + permission denied, or file not found, etc.). + :attr:`STATUS`.FILE_NOT_EMAIL + The contents of filename don't look like an email + message. + :attr:`STATUS`.READ_ONLY_DATABASE + Database was opened in read-only mode so no message can + be added. + """ + self._assert_db_is_initialized() + msg_p = NotmuchMessageP() + indexopts = c_void_p(None) + if decrypt_policy is not None: + indexopts = self._get_default_indexopts(self._db) + self._indexopts_set_decrypt_policy(indexopts, decrypt_policy) + + status = self._index_file(self._db, _str(filename), indexopts, byref(msg_p)) + + if indexopts: + self._indexopts_destroy(indexopts) + + if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]: + raise NotmuchError(status) + + #construct Message() and return + msg = Message(msg_p, self) + #automatic sync initial tags from Maildir flags + if sync_maildir_flags: + msg.maildir_flags_to_tags() + return (msg, status) + + def add_message(self, filename, sync_maildir_flags=False): + """Deprecated alias for :meth:`index_file` + """ + warnings.warn( + "This function is deprecated and will be removed in the future, use index_file.", DeprecationWarning) + + return self.index_file(filename, sync_maildir_flags=sync_maildir_flags) + + _remove_message = nmlib.notmuch_database_remove_message + _remove_message.argtypes = [NotmuchDatabaseP, c_char_p] + _remove_message.restype = c_uint + + def remove_message(self, filename): + """Removes a message (filename) from the given notmuch database + + Note that only this particular filename association is removed from + the database. If the same message (as determined by the message ID) + is still available via other filenames, then the message will + persist in the database for those filenames. When the last filename + is removed for a particular message, the database content for that + message will be entirely removed. + + :returns: A :attr:`STATUS` value with the following meaning: + + :attr:`STATUS`.SUCCESS + The last filename was removed and the message was removed + from the database. + :attr:`STATUS`.DUPLICATE_MESSAGE_ID + This filename was removed but the message persists in the + database with at least one other filename. + + :raises: Raises a :exc:`NotmuchError` with the following meaning. + If such an exception occurs, nothing was removed from the + database. + + :attr:`STATUS`.READ_ONLY_DATABASE + Database was opened in read-only mode so no message can be + removed. + """ + self._assert_db_is_initialized() + status = self._remove_message(self._db, _str(filename)) + if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]: + raise NotmuchError(status) + return status + + def find_message(self, msgid): + """Returns a :class:`Message` as identified by its message ID + + Wraps the underlying *notmuch_database_find_message* function. + + :param msgid: The message ID + :type msgid: unicode or str + :returns: :class:`Message` or `None` if no message is found. + :raises: + :exc:`OutOfMemoryError` + If an Out-of-memory occurred while constructing the message. + :exc:`XapianError` + In case of a Xapian Exception. These exceptions + include "Database modified" situations, e.g. when the + notmuch database has been modified by another program + in the meantime. In this case, you should close and + reopen the database and retry. + :exc:`NotInitializedError` if + the database was not initialized. + """ + self._assert_db_is_initialized() + msg_p = NotmuchMessageP() + status = Database._find_message(self._db, _str(msgid), byref(msg_p)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return msg_p and Message(msg_p, self) or None + + def find_message_by_filename(self, filename): + """Find a message with the given filename + + :returns: If the database contains a message with the given + filename, then a class:`Message:` is returned. This + function returns None if no message is found with the given + filename. + + :raises: :exc:`OutOfMemoryError` if an Out-of-memory occurred while + constructing the message. + :raises: :exc:`XapianError` in case of a Xapian Exception. + These exceptions include "Database modified" + situations, e.g. when the notmuch database has been + modified by another program in the meantime. In this + case, you should close and reopen the database and + retry. + :raises: :exc:`NotInitializedError` if the database was not + initialized. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + + msg_p = NotmuchMessageP() + status = Database._find_message_by_filename(self._db, _str(filename), + byref(msg_p)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return msg_p and Message(msg_p, self) or None + + def get_all_tags(self): + """Returns :class:`Tags` with a list of all tags found in the database + + :returns: :class:`Tags` + :exception: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER + on error + """ + self._assert_db_is_initialized() + tags_p = Database._get_all_tags(self._db) + if not tags_p: + raise NullPointerError() + return Tags(tags_p, self) + + def create_query(self, querystring): + """Returns a :class:`Query` derived from this database + + This is a shorthand method for doing:: + + # short version + # Automatically frees the Database() when 'q' is deleted + + q = Database(dbpath).create_query('from:"Biene Maja"') + + # long version, which is functionally equivalent but will keep the + # Database in the 'db' variable around after we delete 'q': + + db = Database(dbpath) + q = Query(db,'from:"Biene Maja"') + + This function is a python extension and not in the underlying C API. + """ + return Query(self, querystring) + + """notmuch_database_status_string""" + _status_string = nmlib.notmuch_database_status_string + _status_string.argtypes = [NotmuchDatabaseP] + _status_string.restype = c_char_p + + def status_string(self): + """Returns the status string of the database + + This is sometimes used for additional error reporting + """ + self._assert_db_is_initialized() + s = Database._status_string(self._db) + if s: + return s.decode('utf-8', 'ignore') + return s + + def __repr__(self): + return "'Notmuch DB " + self.get_path() + "'" + + def _get_user_default_db(self): + """ Reads a user's notmuch config and returns his db location + + Throws a NotmuchError if it cannot find it""" + config = SafeConfigParser() + conf_f = os.getenv('NOTMUCH_CONFIG', + os.path.expanduser('~/.notmuch-config')) + config.readfp(codecs.open(conf_f, 'r', 'utf-8')) + if not config.has_option('database', 'path'): + raise NotmuchError(message="No DB path specified" + " and no user default found") + db_path = config.get('database', 'path') + if not os.path.isabs(db_path): + db_path = os.path.expanduser(os.path.join("~", db_path)) + return db_path + + """notmuch_database_get_config""" + _get_config = nmlib.notmuch_database_get_config + _get_config.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(c_char_p)] + _get_config.restype = c_uint + + def get_config(self, key): + """Return the value of the given config key. + + Note that only config values that are stored in the database are + searched and returned. The config file is not read. + + :param key: the config key under which a value should be looked up, it + should probably be in the form "section.key" + :type key: str + :returns: the config value or the empty string if no value is present + for that key + :rtype: str + :raises: :exc:`NotmuchError` in case of failure. + + """ + self._assert_db_is_initialized() + return_string = c_char_p() + status = self._get_config(self._db, _str(key), byref(return_string)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return return_string.value.decode('utf-8') + + """notmuch_database_get_config_list""" + _get_config_list = nmlib.notmuch_database_get_config_list + _get_config_list.argtypes = [NotmuchDatabaseP, c_char_p, + POINTER(NotmuchConfigListP)] + _get_config_list.restype = c_uint + + _config_list_valid = nmlib.notmuch_config_list_valid + _config_list_valid.argtypes = [NotmuchConfigListP] + _config_list_valid.restype = bool + + _config_list_key = nmlib.notmuch_config_list_key + _config_list_key.argtypes = [NotmuchConfigListP] + _config_list_key.restype = c_char_p + + _config_list_value = nmlib.notmuch_config_list_value + _config_list_value.argtypes = [NotmuchConfigListP] + _config_list_value.restype = c_char_p + + _config_list_move_to_next = nmlib.notmuch_config_list_move_to_next + _config_list_move_to_next.argtypes = [NotmuchConfigListP] + _config_list_move_to_next.restype = None + + _config_list_destroy = nmlib.notmuch_config_list_destroy + _config_list_destroy.argtypes = [NotmuchConfigListP] + _config_list_destroy.restype = None + + def get_configs(self, prefix=''): + """Return a generator of key, value pairs where the start of key + matches the given prefix + + Note that only config values that are stored in the database are + searched and returned. The config file is not read. If no `prefix` is + given all config values are returned. + + This could be used to get all named queries into a dict for example:: + + queries = {k[6:]: v for k, v in db.get_configs('query.')} + + :param prefix: a string by which the keys should be selected + :type prefix: str + :yields: all key-value pairs where `prefix` matches the beginning + of the key + :ytype: pairs of str + :raises: :exc:`NotmuchError` in case of failure. + + """ + self._assert_db_is_initialized() + config_list_p = NotmuchConfigListP() + status = self._get_config_list(self._db, _str(prefix), + byref(config_list_p)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + while self._config_list_valid(config_list_p): + key = self._config_list_key(config_list_p).decode('utf-8') + value = self._config_list_value(config_list_p).decode('utf-8') + yield key, value + self._config_list_move_to_next(config_list_p) + + """notmuch_database_set_config""" + _set_config = nmlib.notmuch_database_set_config + _set_config.argtypes = [NotmuchDatabaseP, c_char_p, c_char_p] + _set_config.restype = c_uint + + def set_config(self, key, value): + """Set a config value in the notmuch database. + + If an empty string is provided as `value` the `key` is unset! + + :param key: the key to set + :type key: str + :param value: the value to store under `key` + :type value: str + :returns: None + :raises: :exc:`NotmuchError` in case of failure. + + """ + self._assert_db_is_initialized() + status = self._set_config(self._db, _str(key), _str(value)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) diff --git a/contrib/python-legacy/notmuch/directory.py b/contrib/python-legacy/notmuch/directory.py new file mode 100644 index 00000000..b30c9e35 --- /dev/null +++ b/contrib/python-legacy/notmuch/directory.py @@ -0,0 +1,185 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from ctypes import c_uint, c_long +from .globals import ( + nmlib, + NotmuchDirectoryP, + NotmuchFilenamesP +) +from .errors import ( + STATUS, + NotmuchError, + NotInitializedError, +) +from .filenames import Filenames + +class Directory(object): + """Represents a directory entry in the notmuch directory + + Modifying attributes of this object will modify the + database, not the real directory attributes. + + The Directory object is usually derived from another object + e.g. via :meth:`Database.get_directory`, and will automatically be + become invalid whenever that parent is deleted. You should + therefore initialized this object handing it a reference to the + parent, preventing the parent from automatically being garbage + collected. + """ + + """notmuch_directory_get_mtime""" + _get_mtime = nmlib.notmuch_directory_get_mtime + _get_mtime.argtypes = [NotmuchDirectoryP] + _get_mtime.restype = c_long + + """notmuch_directory_set_mtime""" + _set_mtime = nmlib.notmuch_directory_set_mtime + _set_mtime.argtypes = [NotmuchDirectoryP, c_long] + _set_mtime.restype = c_uint + + """notmuch_directory_get_child_files""" + _get_child_files = nmlib.notmuch_directory_get_child_files + _get_child_files.argtypes = [NotmuchDirectoryP] + _get_child_files.restype = NotmuchFilenamesP + + """notmuch_directory_get_child_directories""" + _get_child_directories = nmlib.notmuch_directory_get_child_directories + _get_child_directories.argtypes = [NotmuchDirectoryP] + _get_child_directories.restype = NotmuchFilenamesP + + def _assert_dir_is_initialized(self): + """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED) + if dir_p is None""" + if not self._dir_p: + raise NotInitializedError() + + def __init__(self, path, dir_p, parent): + """ + :param path: The absolute path of the directory object. + :param dir_p: The pointer to an internal notmuch_directory_t object. + :param parent: The object this Directory is derived from + (usually a :class:`Database`). We do not directly use + this, but store a reference to it as long as + this Directory object lives. This keeps the + parent object alive. + """ + self._path = path + self._dir_p = dir_p + self._parent = parent + + def set_mtime(self, mtime): + """Sets the mtime value of this directory in the database + + The intention is for the caller to use the mtime to allow efficient + identification of new messages to be added to the database. The + recommended usage is as follows: + + * Read the mtime of a directory from the filesystem + + * Call :meth:`Database.index_file` for all mail files in + the directory + + * Call notmuch_directory_set_mtime with the mtime read from the + filesystem. Then, when wanting to check for updates to the + directory in the future, the client can call :meth:`get_mtime` + and know that it only needs to add files if the mtime of the + directory and files are newer than the stored timestamp. + + .. note:: + + :meth:`get_mtime` function does not allow the caller to + distinguish a timestamp of 0 from a non-existent timestamp. So + don't store a timestamp of 0 unless you are comfortable with + that. + + :param mtime: A (time_t) timestamp + :raises: :exc:`XapianError` a Xapian exception occurred, mtime + not stored + :raises: :exc:`ReadOnlyDatabaseError` the database was opened + in read-only mode so directory mtime cannot be modified + :raises: :exc:`NotInitializedError` the directory object has not + been initialized + """ + self._assert_dir_is_initialized() + status = Directory._set_mtime(self._dir_p, mtime) + + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + def get_mtime(self): + """Gets the mtime value of this directory in the database + + Retrieves a previously stored mtime for this directory. + + :param mtime: A (time_t) timestamp + :raises: :exc:`NotmuchError`: + + :attr:`STATUS`.NOT_INITIALIZED + The directory has not been initialized + """ + self._assert_dir_is_initialized() + return Directory._get_mtime(self._dir_p) + + # Make mtime attribute a property of Directory() + mtime = property(get_mtime, set_mtime, doc="""Property that allows getting + and setting of the Directory *mtime* (read-write) + + See :meth:`get_mtime` and :meth:`set_mtime` for usage and + possible exceptions.""") + + def get_child_files(self): + """Gets a Filenames iterator listing all the filenames of + messages in the database within the given directory. + + The returned filenames will be the basename-entries only (not + complete paths. + """ + self._assert_dir_is_initialized() + files_p = Directory._get_child_files(self._dir_p) + return Filenames(files_p, self) + + def get_child_directories(self): + """Gets a :class:`Filenames` iterator listing all the filenames of + sub-directories in the database within the given directory + + The returned filenames will be the basename-entries only (not + complete paths. + """ + self._assert_dir_is_initialized() + files_p = Directory._get_child_directories(self._dir_p) + return Filenames(files_p, self) + + @property + def path(self): + """Returns the absolute path of this Directory (read-only)""" + return self._path + + def __repr__(self): + """Object representation""" + return "" % self._path + + _destroy = nmlib.notmuch_directory_destroy + _destroy.argtypes = [NotmuchDirectoryP] + _destroy.restype = None + + def __del__(self): + """Close and free the Directory""" + if self._dir_p: + self._destroy(self._dir_p) diff --git a/contrib/python-legacy/notmuch/errors.py b/contrib/python-legacy/notmuch/errors.py new file mode 100644 index 00000000..b7684ef6 --- /dev/null +++ b/contrib/python-legacy/notmuch/errors.py @@ -0,0 +1,204 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from ctypes import c_char_p, c_int + +from .globals import ( + nmlib, + Enum, + Python3StringMixIn, +) + +class Status(Enum): + """Enum with a string representation of a notmuch_status_t value.""" + _status2str = nmlib.notmuch_status_to_string + _status2str.restype = c_char_p + _status2str.argtypes = [c_int] + + def __init__(self, statuslist): + """It is initialized with a list of strings that are available as + Status().string1 - Status().stringn attributes. + """ + super(Status, self).__init__(statuslist) + + @classmethod + def status2str(self, status): + """Get a (unicode) string representation of a notmuch_status_t value.""" + # define strings for custom error messages + if status == STATUS.NOT_INITIALIZED: + return "Operation on uninitialized object impossible." + return unicode(Status._status2str(status)) + +STATUS = Status(['SUCCESS', + 'OUT_OF_MEMORY', + 'READ_ONLY_DATABASE', + 'XAPIAN_EXCEPTION', + 'FILE_ERROR', + 'FILE_NOT_EMAIL', + 'DUPLICATE_MESSAGE_ID', + 'NULL_POINTER', + 'TAG_TOO_LONG', + 'UNBALANCED_FREEZE_THAW', + 'UNBALANCED_ATOMIC', + 'UNSUPPORTED_OPERATION', + 'UPGRADE_REQUIRED', + 'PATH_ERROR', + 'NOT_INITIALIZED']) +"""STATUS is a class, whose attributes provide constants that serve as return +indicators for notmuch functions. Currently the following ones are defined. For +possible return values and specific meaning for each method, see the method +description. + + * SUCCESS + * OUT_OF_MEMORY + * READ_ONLY_DATABASE + * XAPIAN_EXCEPTION + * FILE_ERROR + * FILE_NOT_EMAIL + * DUPLICATE_MESSAGE_ID + * NULL_POINTER + * TAG_TOO_LONG + * UNBALANCED_FREEZE_THAW + * UNBALANCED_ATOMIC + * UNSUPPORTED_OPERATION + * UPGRADE_REQUIRED + * PATH_ERROR + * NOT_INITIALIZED + +Invoke the class method `notmuch.STATUS.status2str` with a status value as +argument to receive a human readable string""" +STATUS.__name__ = 'STATUS' + + +class NotmuchError(Exception, Python3StringMixIn): + """Is initiated with a (notmuch.STATUS[, message=None]). It will not + return an instance of the class NotmuchError, but a derived instance + of a more specific Error Message, e.g. OutOfMemoryError. Each status + but SUCCESS has a corresponding subclassed Exception.""" + + @classmethod + def get_exc_subclass(cls, status): + """Returns a fine grained Exception() type, + detailing the error status""" + subclasses = { + STATUS.OUT_OF_MEMORY: OutOfMemoryError, + STATUS.READ_ONLY_DATABASE: ReadOnlyDatabaseError, + STATUS.XAPIAN_EXCEPTION: XapianError, + STATUS.FILE_ERROR: FileError, + STATUS.FILE_NOT_EMAIL: FileNotEmailError, + STATUS.DUPLICATE_MESSAGE_ID: DuplicateMessageIdError, + STATUS.NULL_POINTER: NullPointerError, + STATUS.TAG_TOO_LONG: TagTooLongError, + STATUS.UNBALANCED_FREEZE_THAW: UnbalancedFreezeThawError, + STATUS.UNBALANCED_ATOMIC: UnbalancedAtomicError, + STATUS.UNSUPPORTED_OPERATION: UnsupportedOperationError, + STATUS.UPGRADE_REQUIRED: UpgradeRequiredError, + STATUS.PATH_ERROR: PathError, + STATUS.NOT_INITIALIZED: NotInitializedError, + } + assert 0 < status <= len(subclasses) + return subclasses[status] + + def __new__(cls, *args, **kwargs): + """Return a correct subclass of NotmuchError if needed + + We return a NotmuchError instance if status is None (or 0) and a + subclass that inherits from NotmuchError depending on the + 'status' parameter otherwise.""" + # get 'status'. Passed in as arg or kwarg? + status = args[0] if len(args) else kwargs.get('status', None) + # no 'status' or cls is subclass already, return 'cls' instance + if not status or cls != NotmuchError: + return super(NotmuchError, cls).__new__(cls) + subclass = cls.get_exc_subclass(status) # which class to use? + return subclass.__new__(subclass, *args, **kwargs) + + def __init__(self, status=None, message=None): + self.status = status + self.message = message + + def __unicode__(self): + if self.message is not None: + return self.message + elif self.status is not None: + return STATUS.status2str(self.status) + else: + return 'Unknown error' + + +# List of Subclassed exceptions that correspond to STATUS values and are +# subclasses of NotmuchError. +class OutOfMemoryError(NotmuchError): + status = STATUS.OUT_OF_MEMORY + + +class ReadOnlyDatabaseError(NotmuchError): + status = STATUS.READ_ONLY_DATABASE + + +class XapianError(NotmuchError): + status = STATUS.XAPIAN_EXCEPTION + + +class FileError(NotmuchError): + status = STATUS.FILE_ERROR + + +class FileNotEmailError(NotmuchError): + status = STATUS.FILE_NOT_EMAIL + + +class DuplicateMessageIdError(NotmuchError): + status = STATUS.DUPLICATE_MESSAGE_ID + + +class NullPointerError(NotmuchError): + status = STATUS.NULL_POINTER + + +class TagTooLongError(NotmuchError): + status = STATUS.TAG_TOO_LONG + + +class UnbalancedFreezeThawError(NotmuchError): + status = STATUS.UNBALANCED_FREEZE_THAW + + +class UnbalancedAtomicError(NotmuchError): + status = STATUS.UNBALANCED_ATOMIC + + +class UnsupportedOperationError(NotmuchError): + status = STATUS.UNSUPPORTED_OPERATION + + +class UpgradeRequiredError(NotmuchError): + status = STATUS.UPGRADE_REQUIRED + + +class PathError(NotmuchError): + status = STATUS.PATH_ERROR + + +class NotInitializedError(NotmuchError): + """Derived from NotmuchError, this occurs if the underlying data + structure (e.g. database is not initialized (yet) or an iterator has + been exhausted. You can test for NotmuchError with .status = + STATUS.NOT_INITIALIZED""" + status = STATUS.NOT_INITIALIZED diff --git a/contrib/python-legacy/notmuch/filenames.py b/contrib/python-legacy/notmuch/filenames.py new file mode 100644 index 00000000..3bbc22b9 --- /dev/null +++ b/contrib/python-legacy/notmuch/filenames.py @@ -0,0 +1,131 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" +from ctypes import c_char_p +from .globals import ( + nmlib, + NotmuchFilenamesP, + Python3StringMixIn, +) +from .errors import ( + NullPointerError, + NotInitializedError, +) + + +class Filenames(Python3StringMixIn): + """Represents a list of filenames as returned by notmuch + + Objects of this class implement the iterator protocol. + + .. note:: + + The underlying library only provides a one-time iterator (it + cannot reset the iterator to the start). Thus iterating over + the function will "exhaust" the list of tags, and a subsequent + iteration attempt will raise a + :exc:`NotInitializedError`. Also note, that any function that + uses iteration (nearly all) will also exhaust the tags. So + both:: + + for name in filenames: print name + + as well as:: + + list_of_names = list(names) + + and even a simple:: + + #str() iterates over all tags to construct a space separated list + print(str(filenames)) + + will "exhaust" the Filenames. However, you can use + :meth:`Message.get_filenames` repeatedly to get fresh + Filenames objects to perform various actions on filenames. + """ + + #notmuch_filenames_get + _get = nmlib.notmuch_filenames_get + _get.argtypes = [NotmuchFilenamesP] + _get.restype = c_char_p + + def __init__(self, files_p, parent): + """ + :param files_p: A pointer to an underlying *notmuch_tags_t* + structure. These are not publicly exposed, so a user + will almost never instantiate a :class:`Tags` object + herself. They are usually handed back as a result, + e.g. in :meth:`Database.get_all_tags`. *tags_p* must be + valid, we will raise an :exc:`NullPointerError` + if it is `None`. + :type files_p: :class:`ctypes.c_void_p` + :param parent: The parent object (ie :class:`Message` these + filenames are derived from, and saves a + reference to it, so we can automatically delete the db object + once all derived objects are dead. + """ + if not files_p: + raise NullPointerError() + + self._files_p = files_p + #save reference to parent object so we keep it alive + self._parent = parent + + def __iter__(self): + """ Make Filenames an iterator """ + return self + + _valid = nmlib.notmuch_filenames_valid + _valid.argtypes = [NotmuchFilenamesP] + _valid.restype = bool + + _move_to_next = nmlib.notmuch_filenames_move_to_next + _move_to_next.argtypes = [NotmuchFilenamesP] + _move_to_next.restype = None + + def __next__(self): + if not self._files_p: + raise NotInitializedError() + + if not self._valid(self._files_p): + self._files_p = None + raise StopIteration + + file_ = Filenames._get(self._files_p) + self._move_to_next(self._files_p) + return file_.decode('utf-8', 'ignore') + next = __next__ # python2.x iterator protocol compatibility + + def __unicode__(self): + """Represent Filenames() as newline-separated list of full paths + + .. note:: + + This method exhausts the iterator object, so you will not be able to + iterate over them again. + """ + return "\n".join(self) + + _destroy = nmlib.notmuch_filenames_destroy + _destroy.argtypes = [NotmuchFilenamesP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch filenames""" + if self._files_p: + self._destroy(self._files_p) diff --git a/contrib/python-legacy/notmuch/globals.py b/contrib/python-legacy/notmuch/globals.py new file mode 100644 index 00000000..11e328b7 --- /dev/null +++ b/contrib/python-legacy/notmuch/globals.py @@ -0,0 +1,105 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from ctypes import CDLL, Structure, POINTER +from notmuch.version import SOVERSION + +#----------------------------------------------------------------------------- +#package-global instance of the notmuch library +try: + from os import uname + if uname()[0] == 'Darwin': + nmlib = CDLL("libnotmuch.{0:s}.dylib".format(SOVERSION)) + else: + nmlib = CDLL("libnotmuch.so.{0:s}".format(SOVERSION)) +except: + raise ImportError("Could not find shared 'notmuch' library.") + +from .compat import Python3StringMixIn, encode_utf8 as _str + +# We import these on behalf of other modules. Silence warning about +# these symbols not being used. +Python3StringMixIn +_str + +class Enum(object): + """Provides ENUMS as "code=Enum(['a','b','c'])" where code.a=0 etc...""" + def __init__(self, names): + for number, name in enumerate(names): + setattr(self, name, number) + + +class NotmuchDatabaseS(Structure): + pass +NotmuchDatabaseP = POINTER(NotmuchDatabaseS) + + +class NotmuchQueryS(Structure): + pass +NotmuchQueryP = POINTER(NotmuchQueryS) + + +class NotmuchThreadsS(Structure): + pass +NotmuchThreadsP = POINTER(NotmuchThreadsS) + + +class NotmuchThreadS(Structure): + pass +NotmuchThreadP = POINTER(NotmuchThreadS) + + +class NotmuchMessagesS(Structure): + pass +NotmuchMessagesP = POINTER(NotmuchMessagesS) + + +class NotmuchMessageS(Structure): + pass +NotmuchMessageP = POINTER(NotmuchMessageS) + + +class NotmuchMessagePropertiesS(Structure): + pass +NotmuchMessagePropertiesP = POINTER(NotmuchMessagePropertiesS) + + +class NotmuchTagsS(Structure): + pass +NotmuchTagsP = POINTER(NotmuchTagsS) + + +class NotmuchDirectoryS(Structure): + pass +NotmuchDirectoryP = POINTER(NotmuchDirectoryS) + + +class NotmuchFilenamesS(Structure): + pass +NotmuchFilenamesP = POINTER(NotmuchFilenamesS) + + +class NotmuchConfigListS(Structure): + pass +NotmuchConfigListP = POINTER(NotmuchConfigListS) + + +class NotmuchIndexoptsS(Structure): + pass +NotmuchIndexoptsP = POINTER(NotmuchIndexoptsS) diff --git a/contrib/python-legacy/notmuch/message.py b/contrib/python-legacy/notmuch/message.py new file mode 100644 index 00000000..e71dbe3e --- /dev/null +++ b/contrib/python-legacy/notmuch/message.py @@ -0,0 +1,721 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth + Jesse Rosenthal +""" + + +from ctypes import c_char_p, c_long, c_uint, c_int, POINTER, byref +from datetime import date +from .globals import ( + nmlib, + Enum, + _str, + Python3StringMixIn, + NotmuchTagsP, + NotmuchMessageP, + NotmuchMessagesP, + NotmuchMessagePropertiesP, + NotmuchFilenamesP, +) +from .errors import ( + STATUS, + NotmuchError, + NullPointerError, + NotInitializedError, +) +from .tag import Tags +from .filenames import Filenames + +import email +import sys + + +class Message(Python3StringMixIn): + r"""Represents a single Email message + + Technically, this wraps the underlying *notmuch_message_t* + structure. A user will usually not create these objects themselves + but get them as search results. + + As it implements :meth:`__cmp__`, it is possible to compare two + :class:`Message`\s using `if msg1 == msg2: ...`. + """ + + """notmuch_message_get_filename (notmuch_message_t *message)""" + _get_filename = nmlib.notmuch_message_get_filename + _get_filename.argtypes = [NotmuchMessageP] + _get_filename.restype = c_char_p + + """return all filenames for a message""" + _get_filenames = nmlib.notmuch_message_get_filenames + _get_filenames.argtypes = [NotmuchMessageP] + _get_filenames.restype = NotmuchFilenamesP + + """notmuch_message_get_flag""" + _get_flag = nmlib.notmuch_message_get_flag + _get_flag.argtypes = [NotmuchMessageP, c_uint] + _get_flag.restype = bool + + """notmuch_message_set_flag""" + _set_flag = nmlib.notmuch_message_set_flag + _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int] + _set_flag.restype = None + + """notmuch_message_get_message_id (notmuch_message_t *message)""" + _get_message_id = nmlib.notmuch_message_get_message_id + _get_message_id.argtypes = [NotmuchMessageP] + _get_message_id.restype = c_char_p + + """notmuch_message_get_thread_id""" + _get_thread_id = nmlib.notmuch_message_get_thread_id + _get_thread_id.argtypes = [NotmuchMessageP] + _get_thread_id.restype = c_char_p + + """notmuch_message_get_replies""" + _get_replies = nmlib.notmuch_message_get_replies + _get_replies.argtypes = [NotmuchMessageP] + _get_replies.restype = NotmuchMessagesP + + """notmuch_message_get_tags (notmuch_message_t *message)""" + _get_tags = nmlib.notmuch_message_get_tags + _get_tags.argtypes = [NotmuchMessageP] + _get_tags.restype = NotmuchTagsP + + _get_date = nmlib.notmuch_message_get_date + _get_date.argtypes = [NotmuchMessageP] + _get_date.restype = c_long + + _get_header = nmlib.notmuch_message_get_header + _get_header.argtypes = [NotmuchMessageP, c_char_p] + _get_header.restype = c_char_p + + """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)""" + _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags + _tags_to_maildir_flags.argtypes = [NotmuchMessageP] + _tags_to_maildir_flags.restype = c_int + + """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)""" + _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags + _maildir_flags_to_tags.argtypes = [NotmuchMessageP] + _maildir_flags_to_tags.restype = c_int + + """notmuch_message_get_property""" + _get_property = nmlib.notmuch_message_get_property + _get_property.argtypes = [NotmuchMessageP, c_char_p, POINTER(c_char_p)] + _get_property.restype = c_int + + """notmuch_message_get_properties""" + _get_properties = nmlib.notmuch_message_get_properties + _get_properties.argtypes = [NotmuchMessageP, c_char_p, c_int] + _get_properties.restype = NotmuchMessagePropertiesP + + """notmuch_message_properties_valid""" + _properties_valid = nmlib.notmuch_message_properties_valid + _properties_valid.argtypes = [NotmuchMessagePropertiesP] + _properties_valid.restype = bool + + """notmuch_message_properties_value""" + _properties_value = nmlib.notmuch_message_properties_value + _properties_value.argtypes = [NotmuchMessagePropertiesP] + _properties_value.restype = c_char_p + + """notmuch_message_properties_key""" + _properties_key = nmlib.notmuch_message_properties_key + _properties_key.argtypes = [NotmuchMessagePropertiesP] + _properties_key.restype = c_char_p + + """notmuch_message_properties_move_to_next""" + _properties_move_to_next = nmlib.notmuch_message_properties_move_to_next + _properties_move_to_next.argtypes = [NotmuchMessagePropertiesP] + _properties_move_to_next.restype = None + + #Constants: Flags that can be set/get with set_flag + FLAG = Enum(['MATCH']) + + def __init__(self, msg_p, parent=None): + """ + :param msg_p: A pointer to an internal notmuch_message_t + Structure. If it is `None`, we will raise an + :exc:`NullPointerError`. + + :param parent: A 'parent' object is passed which this message is + derived from. We save a reference to it, so we can + automatically delete the parent object once all derived + objects are dead. + """ + if not msg_p: + raise NullPointerError() + self._msg = msg_p + #keep reference to parent, so we keep it alive + self._parent = parent + + def get_message_id(self): + """Returns the message ID + + :returns: String with a message ID + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + return Message._get_message_id(self._msg).decode('utf-8', 'ignore') + + def get_thread_id(self): + """Returns the thread ID + + The returned string belongs to 'message' will only be valid for as + long as the message is valid. + + This function will not return `None` since Notmuch ensures that every + message belongs to a single thread. + + :returns: String with a thread ID + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + + return Message._get_thread_id(self._msg).decode('utf-8', 'ignore') + + def get_replies(self): + """Gets all direct replies to this message as :class:`Messages` + iterator + + .. note:: + + This call only makes sense if 'message' was ultimately obtained from + a :class:`Thread` object, (such as by coming directly from the + result of calling :meth:`Thread.get_toplevel_messages` or by any + number of subsequent calls to :meth:`get_replies`). If this message + was obtained through some non-thread means, (such as by a call to + :meth:`Query.search_messages`), then this function will return + an empty Messages iterator. + + :returns: :class:`Messages`. + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + + msgs_p = Message._get_replies(self._msg) + + from .messages import Messages, EmptyMessagesResult + + if not msgs_p: + return EmptyMessagesResult(self) + + return Messages(msgs_p, self) + + def get_date(self): + """Returns time_t of the message date + + For the original textual representation of the Date header from the + message call notmuch_message_get_header() with a header value of + "date". + + :returns: A time_t timestamp. + :rtype: c_unit64 + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + return Message._get_date(self._msg) + + def get_header(self, header): + """Get the value of the specified header. + + The value will be read from the actual message file, not from + the notmuch database. The header name is case insensitive. + + Returns an empty string ("") if the message does not contain a + header line matching 'header'. + + :param header: The name of the header to be retrieved. + It is not case-sensitive. + :type header: str + :returns: The header value as string + :raises: :exc:`NotInitializedError` if the message is not + initialized + :raises: :exc:`NullPointerError` if any error occurred + """ + if not self._msg: + raise NotInitializedError() + + #Returns NULL if any error occurs. + header = Message._get_header(self._msg, _str(header)) + if header == None: + raise NullPointerError() + return header.decode('UTF-8', 'ignore') + + def get_filename(self): + """Returns the file path of the message file + + :returns: Absolute file path & name of the message file + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + return Message._get_filename(self._msg).decode('utf-8', 'ignore') + + def get_filenames(self): + """Get all filenames for the email corresponding to 'message' + + Returns a Filenames() generator with all absolute filepaths for + messages recorded to have the same Message-ID. These files must + not necessarily have identical content.""" + if not self._msg: + raise NotInitializedError() + + files_p = Message._get_filenames(self._msg) + + return Filenames(files_p, self) + + def get_flag(self, flag): + """Checks whether a specific flag is set for this message + + The method :meth:`Query.search_threads` sets + *Message.FLAG.MATCH* for those messages that match the + query. This method allows us to get the value of this flag. + + :param flag: One of the :attr:`Message.FLAG` values (currently only + *Message.FLAG.MATCH* + :returns: An unsigned int (0/1), indicating whether the flag is set. + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + return Message._get_flag(self._msg, flag) + + def set_flag(self, flag, value): + """Sets/Unsets a specific flag for this message + + :param flag: One of the :attr:`Message.FLAG` values (currently only + *Message.FLAG.MATCH* + :param value: A bool indicating whether to set or unset the flag. + + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + self._set_flag(self._msg, flag, value) + + def get_tags(self): + """Returns the message tags + + :returns: A :class:`Tags` iterator. + :raises: :exc:`NotInitializedError` if the message is not + initialized + :raises: :exc:`NullPointerError` if any error occurred + """ + if not self._msg: + raise NotInitializedError() + + tags_p = Message._get_tags(self._msg) + if not tags_p: + raise NullPointerError() + return Tags(tags_p, self) + + _add_tag = nmlib.notmuch_message_add_tag + _add_tag.argtypes = [NotmuchMessageP, c_char_p] + _add_tag.restype = c_uint + + def add_tag(self, tag, sync_maildir_flags=False): + """Adds a tag to the given message + + Adds a tag to the current message. The maximal tag length is defined in + the notmuch library and is currently 200 bytes. + + :param tag: String with a 'tag' to be added. + + :param sync_maildir_flags: If notmuch configuration is set to do + this, add maildir flags corresponding to notmuch tags. See + underlying method :meth:`tags_to_maildir_flags`. Use False + if you want to add/remove many tags on a message without + having to physically rename the file every time. Do note, + that this will do nothing when a message is frozen, as tag + changes will not be committed to the database yet. + + :returns: STATUS.SUCCESS if the tag was successfully added. + Raises an exception otherwise. + :raises: :exc:`NullPointerError` if the `tag` argument is NULL + :raises: :exc:`TagTooLongError` if the length of `tag` exceeds + Message.NOTMUCH_TAG_MAX) + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._add_tag(self._msg, _str(tag)) + + # bail out on failure + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + if sync_maildir_flags: + self.tags_to_maildir_flags() + return STATUS.SUCCESS + + _remove_tag = nmlib.notmuch_message_remove_tag + _remove_tag.argtypes = [NotmuchMessageP, c_char_p] + _remove_tag.restype = c_uint + + def remove_tag(self, tag, sync_maildir_flags=False): + """Removes a tag from the given message + + If the message has no such tag, this is a non-operation and + will report success anyway. + + :param tag: String with a 'tag' to be removed. + :param sync_maildir_flags: If notmuch configuration is set to do + this, add maildir flags corresponding to notmuch tags. See + underlying method :meth:`tags_to_maildir_flags`. Use False + if you want to add/remove many tags on a message without + having to physically rename the file every time. Do note, + that this will do nothing when a message is frozen, as tag + changes will not be committed to the database yet. + + :returns: STATUS.SUCCESS if the tag was successfully removed or if + the message had no such tag. + Raises an exception otherwise. + :raises: :exc:`NullPointerError` if the `tag` argument is NULL + :raises: :exc:`TagTooLongError` if the length of `tag` exceeds + Message.NOTMUCH_TAG_MAX) + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._remove_tag(self._msg, _str(tag)) + # bail out on error + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + if sync_maildir_flags: + self.tags_to_maildir_flags() + return STATUS.SUCCESS + + _remove_all_tags = nmlib.notmuch_message_remove_all_tags + _remove_all_tags.argtypes = [NotmuchMessageP] + _remove_all_tags.restype = c_uint + + def remove_all_tags(self, sync_maildir_flags=False): + """Removes all tags from the given message. + + See :meth:`freeze` for an example showing how to safely + replace tag values. + + + :param sync_maildir_flags: If notmuch configuration is set to do + this, add maildir flags corresponding to notmuch tags. See + :meth:`tags_to_maildir_flags`. Use False if you want to + add/remove many tags on a message without having to + physically rename the file every time. Do note, that this + will do nothing when a message is frozen, as tag changes + will not be committed to the database yet. + + :returns: STATUS.SUCCESS if the tags were successfully removed. + Raises an exception otherwise. + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._remove_all_tags(self._msg) + + # bail out on error + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + if sync_maildir_flags: + self.tags_to_maildir_flags() + return STATUS.SUCCESS + + _freeze = nmlib.notmuch_message_freeze + _freeze.argtypes = [NotmuchMessageP] + _freeze.restype = c_uint + + def get_property(self, prop): + """ Retrieve the value for a single property key + + :param prop: The name of the property to get. + :returns: String with the property value or None if there is no such + key. In the case of multiple values for the given key, the + first one is retrieved. + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + value = c_char_p() + status = Message._get_property(self._msg, _str(prop), byref(value)) + if status != 0: + raise NotmuchError(status) + + if value is None or value.value is None: + return None + return value.value.decode('utf-8') + + def get_properties(self, prop="", exact=False): + """ Get the properties of the message, returning a generator of + name, value pairs. + + The generator will yield once per value. There might be more than one + value on each name, so the generator might yield the same name several + times. + + :param prop: The name of the property to get. Otherwise it will return + the full list of properties of the message. + :param exact: if True, require exact match with key. Otherwise + treat as prefix. + :yields: Each property values as a pair of `name, value` + :ytype: pairs of str + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + properties = Message._get_properties(self._msg, _str(prop), exact) + while Message._properties_valid(properties): + key = Message._properties_key(properties) + value = Message._properties_value(properties) + yield key.decode("utf-8"), value.decode("utf-8") + Message._properties_move_to_next(properties) + + def freeze(self): + """Freezes the current state of 'message' within the database + + This means that changes to the message state, (via :meth:`add_tag`, + :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be + committed to the database until the message is :meth:`thaw` ed. + + Multiple calls to freeze/thaw are valid and these calls will + "stack". That is there must be as many calls to thaw as to freeze + before a message is actually thawed. + + The ability to do freeze/thaw allows for safe transactions to + change tag values. For example, explicitly setting a message to + have a given set of tags might look like this:: + + msg.freeze() + msg.remove_all_tags(False) + for tag in new_tags: + msg.add_tag(tag, False) + msg.thaw() + msg.tags_to_maildir_flags() + + With freeze/thaw used like this, the message in the database is + guaranteed to have either the full set of original tag values, or + the full set of new tag values, but nothing in between. + + Imagine the example above without freeze/thaw and the operation + somehow getting interrupted. This could result in the message being + left with no tags if the interruption happened after + :meth:`remove_all_tags` but before :meth:`add_tag`. + + :returns: STATUS.SUCCESS if the message was successfully frozen. + Raises an exception otherwise. + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._freeze(self._msg) + + if STATUS.SUCCESS == status: + # return on success + return status + + raise NotmuchError(status) + + _thaw = nmlib.notmuch_message_thaw + _thaw.argtypes = [NotmuchMessageP] + _thaw.restype = c_uint + + def thaw(self): + """Thaws the current 'message' + + Thaw the current 'message', synchronizing any changes that may have + occurred while 'message' was frozen into the notmuch database. + + See :meth:`freeze` for an example of how to use this + function to safely provide tag changes. + + Multiple calls to freeze/thaw are valid and these calls with + "stack". That is there must be as many calls to thaw as to freeze + before a message is actually thawed. + + :returns: STATUS.SUCCESS if the message was successfully frozen. + Raises an exception otherwise. + :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made + to thaw an unfrozen message. That is, there have been + an unbalanced number of calls to :meth:`freeze` and + :meth:`thaw`. + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._thaw(self._msg) + + if STATUS.SUCCESS == status: + # return on success + return status + + raise NotmuchError(status) + + def is_match(self): + """(Not implemented)""" + return self.get_flag(Message.FLAG.MATCH) + + def tags_to_maildir_flags(self): + """Synchronize notmuch tags to file Maildir flags + + 'D' if the message has the "draft" tag + 'F' if the message has the "flagged" tag + 'P' if the message has the "passed" tag + 'R' if the message has the "replied" tag + 'S' if the message does not have the "unread" tag + + Any existing flags unmentioned in the list above will be + preserved in the renaming. + + Also, if this filename is in a directory named "new", rename it + to be within the neighboring directory named "cur". + + Do note that calling this method while a message is frozen might + not work yet, as the modified tags have not been committed yet + to the database. + + :returns: a :class:`STATUS` value. In short, you want to see + notmuch.STATUS.SUCCESS here. See there for details.""" + if not self._msg: + raise NotInitializedError() + return Message._tags_to_maildir_flags(self._msg) + + def maildir_flags_to_tags(self): + """Synchronize file Maildir flags to notmuch tags + + Flag Action if present + ---- ----------------- + 'D' Adds the "draft" tag to the message + 'F' Adds the "flagged" tag to the message + 'P' Adds the "passed" tag to the message + 'R' Adds the "replied" tag to the message + 'S' Removes the "unread" tag from the message + + For each flag that is not present, the opposite action + (add/remove) is performed for the corresponding tags. If there + are multiple filenames associated with this message, the flag is + considered present if it appears in one or more filenames. (That + is, the flags from the multiple filenames are combined with the + logical OR operator.) + + As a convenience, you can set the sync_maildir_flags parameter in + :meth:`Database.index_file` to implicitly call this. + + :returns: a :class:`STATUS`. In short, you want to see + notmuch.STATUS.SUCCESS here. See there for details.""" + if not self._msg: + raise NotInitializedError() + return Message._maildir_flags_to_tags(self._msg) + + def __repr__(self): + """Represent a Message() object by str()""" + return self.__str__() + + def __unicode__(self): + format = "%s (%s) (%s)" + return format % (self.get_header('from'), + self.get_tags(), + date.fromtimestamp(self.get_date()), + ) + + def get_message_parts(self): + """Output like notmuch show""" + fp = open(self.get_filename(), 'rb') + if sys.version_info[0] < 3: + email_msg = email.message_from_file(fp) + else: + email_msg = email.message_from_binary_file(fp) + fp.close() + + out = [] + for msg in email_msg.walk(): + if not msg.is_multipart(): + out.append(msg) + return out + + def get_part(self, num): + """Returns the nth message body part""" + parts = self.get_message_parts() + if (num <= 0 or num > len(parts)): + return "" + else: + out_part = parts[(num - 1)] + return out_part.get_payload(decode=True) + + def __hash__(self): + """Implement hash(), so we can use Message() sets""" + file = self.get_filename() + if not file: + return None + return hash(file) + + def __cmp__(self, other): + """Implement cmp(), so we can compare Message()s + + 2 messages are considered equal if they point to the same + Message-Id and if they point to the same file names. If 2 + Messages derive from different queries where some files have + been added or removed, the same messages would not be considered + equal (as they do not point to the same set of files + any more).""" + res = cmp(self.get_message_id(), other.get_message_id()) + if res: + res = cmp(list(self.get_filenames()), list(other.get_filenames())) + return res + + _destroy = nmlib.notmuch_message_destroy + _destroy.argtypes = [NotmuchMessageP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch Message""" + if self._msg: + self._destroy(self._msg) diff --git a/contrib/python-legacy/notmuch/messages.py b/contrib/python-legacy/notmuch/messages.py new file mode 100644 index 00000000..3801c666 --- /dev/null +++ b/contrib/python-legacy/notmuch/messages.py @@ -0,0 +1,199 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth + Jesse Rosenthal +""" + +from .globals import ( + nmlib, + NotmuchTagsP, + NotmuchMessageP, + NotmuchMessagesP, +) +from .errors import ( + NullPointerError, + NotInitializedError, +) +from .tag import Tags +from .message import Message + +class Messages(object): + r"""Represents a list of notmuch messages + + This object provides an iterator over a list of notmuch messages + (Technically, it provides a wrapper for the underlying + *notmuch_messages_t* structure). Do note that the underlying library + only provides a one-time iterator (it cannot reset the iterator to + the start). Thus iterating over the function will "exhaust" the list + of messages, and a subsequent iteration attempt will raise a + :exc:`NotInitializedError`. If you need to + re-iterate over a list of messages you will need to retrieve a new + :class:`Messages` object or cache your :class:`Message`\s in a list + via:: + + msglist = list(msgs) + + You can store and reuse the single :class:`Message` objects as often + as you want as long as you keep the parent :class:`Messages` object + around. (Due to hierarchical memory allocation, all derived + :class:`Message` objects will be invalid when we delete the parent + :class:`Messages` object, even if it was already exhausted.) So + this works:: + + db = Database() + msgs = Query(db,'').search_messages() #get a Messages() object + msglist = list(msgs) + + # msgs is "exhausted" now and msgs.next() will raise an exception. + # However it will be kept alive until all retrieved Message() + # objects are also deleted. If you do e.g. an explicit del(msgs) + # here, the following lines would fail. + + # You can reiterate over *msglist* however as often as you want. + # It is simply a list with :class:`Message`s. + + print (msglist[0].get_filename()) + print (msglist[1].get_filename()) + print (msglist[0].get_message_id()) + + + As :class:`Message` implements both __hash__() and __cmp__(), it is + possible to make sets out of :class:`Messages` and use set + arithmetic (this happens in python and will of course be *much* + slower than redoing a proper query with the appropriate filters:: + + s1, s2 = set(msgs1), set(msgs2) + s.union(s2) + s1 -= s2 + ... + + Be careful when using set arithmetic between message sets derived + from different Databases (ie the same database reopened after + messages have changed). If messages have added or removed associated + files in the meantime, it is possible that the same message would be + considered as a different object (as it points to a different file). + """ + + #notmuch_messages_get + _get = nmlib.notmuch_messages_get + _get.argtypes = [NotmuchMessagesP] + _get.restype = NotmuchMessageP + + _collect_tags = nmlib.notmuch_messages_collect_tags + _collect_tags.argtypes = [NotmuchMessagesP] + _collect_tags.restype = NotmuchTagsP + + def __init__(self, msgs_p, parent=None): + """ + :param msgs_p: A pointer to an underlying *notmuch_messages_t* + structure. These are not publicly exposed, so a user + will almost never instantiate a :class:`Messages` object + herself. They are usually handed back as a result, + e.g. in :meth:`Query.search_messages`. *msgs_p* must be + valid, we will raise an :exc:`NullPointerError` if it is + `None`. + :type msgs_p: :class:`ctypes.c_void_p` + :param parent: The parent object + (ie :class:`Query`) these tags are derived from. It saves + a reference to it, so we can automatically delete the db + object once all derived objects are dead. + :TODO: Make the iterator work more than once and cache the tags in + the Python object.(?) + """ + if not msgs_p: + raise NullPointerError() + + self._msgs = msgs_p + #store parent, so we keep them alive as long as self is alive + self._parent = parent + + def collect_tags(self): + """Return the unique :class:`Tags` in the contained messages + + :returns: :class:`Tags` + :exceptions: :exc:`NotInitializedError` if not init'ed + + .. note:: + + :meth:`collect_tags` will iterate over the messages and therefore + will not allow further iterations. + """ + if not self._msgs: + raise NotInitializedError() + + # collect all tags (returns NULL on error) + tags_p = Messages._collect_tags(self._msgs) + #reset _msgs as we iterated over it and can do so only once + self._msgs = None + + if not tags_p: + raise NullPointerError() + return Tags(tags_p, self) + + def __iter__(self): + """ Make Messages an iterator """ + return self + + _valid = nmlib.notmuch_messages_valid + _valid.argtypes = [NotmuchMessagesP] + _valid.restype = bool + + _move_to_next = nmlib.notmuch_messages_move_to_next + _move_to_next.argtypes = [NotmuchMessagesP] + _move_to_next.restype = None + + def __next__(self): + if not self._msgs: + raise NotInitializedError() + + if not self._valid(self._msgs): + self._msgs = None + raise StopIteration + + msg = Message(Messages._get(self._msgs), self) + self._move_to_next(self._msgs) + return msg + next = __next__ # python2.x iterator protocol compatibility + + def __nonzero__(self): + ''' + Implement truth value testing. If __nonzero__ is not + implemented, the python runtime would fall back to `len(..) > + 0` thus exhausting the iterator. + + :returns: True if the wrapped iterator has at least one more object + left. + ''' + return self._msgs and self._valid(self._msgs) + + _destroy = nmlib.notmuch_messages_destroy + _destroy.argtypes = [NotmuchMessagesP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch Messages""" + if self._msgs: + self._destroy(self._msgs) + +class EmptyMessagesResult(Messages): + def __init__(self, parent): + self._msgs = None + self._parent = parent + + def __next__(self): + raise StopIteration() + next = __next__ diff --git a/contrib/python-legacy/notmuch/query.py b/contrib/python-legacy/notmuch/query.py new file mode 100644 index 00000000..ffb86df1 --- /dev/null +++ b/contrib/python-legacy/notmuch/query.py @@ -0,0 +1,237 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from ctypes import c_char_p, c_uint, POINTER, byref +from .globals import ( + nmlib, + Enum, + _str, + NotmuchQueryP, + NotmuchThreadsP, + NotmuchDatabaseP, + NotmuchMessagesP, +) +from .errors import ( + NotmuchError, + NullPointerError, + NotInitializedError, +) +from .threads import Threads +from .messages import Messages + + +class Query(object): + """Represents a search query on an opened :class:`Database`. + + A query selects and filters a subset of messages from the notmuch + database we derive from. + + :class:`Query` provides an instance attribute :attr:`sort`, which + contains the sort order (if specified via :meth:`set_sort`) or + `None`. + + Any function in this class may throw an :exc:`NotInitializedError` + in case the underlying query object was not set up correctly. + + .. note:: Do remember that as soon as we tear down this object, + all underlying derived objects such as threads, + messages, tags etc will be freed by the underlying library + as well. Accessing these objects will lead to segfaults and + other unexpected behavior. See above for more details. + """ + # constants + SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED']) + """Constants: Sort order in which to return results""" + + def __init__(self, db, querystr): + """ + :param db: An open database which we derive the Query from. + :type db: :class:`Database` + :param querystr: The query string for the message. + :type querystr: utf-8 encoded str or unicode + """ + self._db = None + self._query = None + self.sort = None + self.create(db, querystr) + + def _assert_query_is_initialized(self): + """Raises :exc:`NotInitializedError` if self._query is `None`""" + if not self._query: + raise NotInitializedError() + + """notmuch_query_create""" + _create = nmlib.notmuch_query_create + _create.argtypes = [NotmuchDatabaseP, c_char_p] + _create.restype = NotmuchQueryP + + def create(self, db, querystr): + """Creates a new query derived from a Database + + This function is utilized by __init__() and usually does not need to + be called directly. + + :param db: Database to create the query from. + :type db: :class:`Database` + :param querystr: The query string + :type querystr: utf-8 encoded str or unicode + :raises: + :exc:`NullPointerError` if the query creation failed + (e.g. too little memory). + :exc:`NotInitializedError` if the underlying db was not + initialized. + """ + db._assert_db_is_initialized() + # create reference to parent db to keep it alive + self._db = db + # create query, return None if too little mem available + query_p = Query._create(db._db, _str(querystr)) + if not query_p: + raise NullPointerError + self._query = query_p + + _set_sort = nmlib.notmuch_query_set_sort + _set_sort.argtypes = [NotmuchQueryP, c_uint] + _set_sort.restype = None + + def set_sort(self, sort): + """Set the sort order future results will be delivered in + + :param sort: Sort order (see :attr:`Query.SORT`) + """ + self._assert_query_is_initialized() + self.sort = sort + self._set_sort(self._query, sort) + + _exclude_tag = nmlib.notmuch_query_add_tag_exclude + _exclude_tag.argtypes = [NotmuchQueryP, c_char_p] + _exclude_tag.restype = None + + def exclude_tag(self, tagname): + """Add a tag that will be excluded from the query results by default. + + This exclusion will be overridden if this tag appears explicitly in the + query. + + :param tagname: Name of the tag to be excluded + """ + self._assert_query_is_initialized() + self._exclude_tag(self._query, _str(tagname)) + + """notmuch_query_search_threads""" + _search_threads = nmlib.notmuch_query_search_threads + _search_threads.argtypes = [NotmuchQueryP, POINTER(NotmuchThreadsP)] + _search_threads.restype = c_uint + + def search_threads(self): + r"""Execute a query for threads + + Execute a query for threads, returning a :class:`Threads` iterator. + The returned threads are owned by the query and as such, will only be + valid until the Query is deleted. + + The method sets :attr:`Message.FLAG`\.MATCH for those messages that + match the query. The method :meth:`Message.get_flag` allows us + to get the value of this flag. + + :returns: :class:`Threads` + :raises: :exc:`NullPointerError` if search_threads failed + """ + self._assert_query_is_initialized() + threads_p = NotmuchThreadsP() # == NULL + status = Query._search_threads(self._query, byref(threads_p)) + if status != 0: + raise NotmuchError(status) + + if not threads_p: + raise NullPointerError + return Threads(threads_p, self) + + """notmuch_query_search_messages_st""" + _search_messages = nmlib.notmuch_query_search_messages + _search_messages.argtypes = [NotmuchQueryP, POINTER(NotmuchMessagesP)] + _search_messages.restype = c_uint + + def search_messages(self): + """Filter messages according to the query and return + :class:`Messages` in the defined sort order + + :returns: :class:`Messages` + :raises: :exc:`NullPointerError` if search_messages failed + """ + self._assert_query_is_initialized() + msgs_p = NotmuchMessagesP() # == NULL + status = Query._search_messages(self._query, byref(msgs_p)) + if status != 0: + raise NotmuchError(status) + + if not msgs_p: + raise NullPointerError + return Messages(msgs_p, self) + + _count_messages = nmlib.notmuch_query_count_messages + _count_messages.argtypes = [NotmuchQueryP, POINTER(c_uint)] + _count_messages.restype = c_uint + + def count_messages(self): + ''' + This function performs a search and returns Xapian's best + guess as to the number of matching messages. + + :returns: the estimated number of messages matching this query + :rtype: int + ''' + self._assert_query_is_initialized() + count = c_uint(0) + status = Query._count_messages(self._query, byref(count)) + if status != 0: + raise NotmuchError(status) + return count.value + + _count_threads = nmlib.notmuch_query_count_threads + _count_threads.argtypes = [NotmuchQueryP, POINTER(c_uint)] + _count_threads.restype = c_uint + + def count_threads(self): + ''' + This function performs a search and returns the number of + unique thread IDs in the matching messages. This is the same + as number of threads matching a search. + + Note that this is a significantly heavier operation than + meth:`Query.count_messages`. + + :returns: the number of threads returned by this query + :rtype: int + ''' + self._assert_query_is_initialized() + count = c_uint(0) + status = Query._count_threads(self._query, byref(count)) + if status != 0: + raise NotmuchError(status) + return count.value + + _destroy = nmlib.notmuch_query_destroy + _destroy.argtypes = [NotmuchQueryP] + _destroy.restype = None + + def __del__(self): + """Close and free the Query""" + if self._query: + self._destroy(self._query) diff --git a/contrib/python-legacy/notmuch/tag.py b/contrib/python-legacy/notmuch/tag.py new file mode 100644 index 00000000..fbb18ce4 --- /dev/null +++ b/contrib/python-legacy/notmuch/tag.py @@ -0,0 +1,141 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" +from ctypes import c_char_p +from .globals import ( + nmlib, + Python3StringMixIn, + NotmuchTagsP, +) +from .errors import ( + NullPointerError, + NotInitializedError, +) + + +class Tags(Python3StringMixIn): + """Represents a list of notmuch tags + + This object provides an iterator over a list of notmuch tags (which + are unicode instances). + + Do note that the underlying library only provides a one-time + iterator (it cannot reset the iterator to the start). Thus iterating + over the function will "exhaust" the list of tags, and a subsequent + iteration attempt will raise a :exc:`NotInitializedError`. + Also note, that any function that uses iteration (nearly all) will + also exhaust the tags. So both:: + + for tag in tags: print tag + + as well as:: + + number_of_tags = len(tags) + + and even a simple:: + + #str() iterates over all tags to construct a space separated list + print(str(tags)) + + will "exhaust" the Tags. If you need to re-iterate over a list of + tags you will need to retrieve a new :class:`Tags` object. + """ + + #notmuch_tags_get + _get = nmlib.notmuch_tags_get + _get.argtypes = [NotmuchTagsP] + _get.restype = c_char_p + + def __init__(self, tags_p, parent=None): + """ + :param tags_p: A pointer to an underlying *notmuch_tags_t* + structure. These are not publicly exposed, so a user + will almost never instantiate a :class:`Tags` object + herself. They are usually handed back as a result, + e.g. in :meth:`Database.get_all_tags`. *tags_p* must be + valid, we will raise an :exc:`NullPointerError` if it is + `None`. + :type tags_p: :class:`ctypes.c_void_p` + :param parent: The parent object (ie :class:`Database` or + :class:`Message` these tags are derived from, and saves a + reference to it, so we can automatically delete the db object + once all derived objects are dead. + :TODO: Make the iterator optionally work more than once by + cache the tags in the Python object(?) + """ + if not tags_p: + raise NullPointerError() + + self._tags = tags_p + #save reference to parent object so we keep it alive + self._parent = parent + + def __iter__(self): + """ Make Tags an iterator """ + return self + + _valid = nmlib.notmuch_tags_valid + _valid.argtypes = [NotmuchTagsP] + _valid.restype = bool + + _move_to_next = nmlib.notmuch_tags_move_to_next + _move_to_next.argtypes = [NotmuchTagsP] + _move_to_next.restype = None + + def __next__(self): + if not self._tags: + raise NotInitializedError() + if not self._valid(self._tags): + self._tags = None + raise StopIteration + tag = Tags._get(self._tags).decode('UTF-8') + self._move_to_next(self._tags) + return tag + next = __next__ # python2.x iterator protocol compatibility + + def __nonzero__(self): + ''' + Implement truth value testing. If __nonzero__ is not + implemented, the python runtime would fall back to `len(..) > + 0` thus exhausting the iterator. + + :returns: True if the wrapped iterator has at least one more object + left. + ''' + return self._tags and self._valid(self._tags) + + def __unicode__(self): + """string representation of :class:`Tags`: a space separated list of tags + + .. note:: + + As this iterates over the tags, we will not be able to iterate over + them again (as in retrieve them)! If the tags have been exhausted + already, this will raise a :exc:`NotInitializedError`on subsequent + attempts. + """ + return " ".join(self) + + _destroy = nmlib.notmuch_tags_destroy + _destroy.argtypes = [NotmuchTagsP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch tags""" + if self._tags: + self._destroy(self._tags) diff --git a/contrib/python-legacy/notmuch/thread.py b/contrib/python-legacy/notmuch/thread.py new file mode 100644 index 00000000..9aa0e08c --- /dev/null +++ b/contrib/python-legacy/notmuch/thread.py @@ -0,0 +1,281 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from ctypes import c_char_p, c_long, c_int +from .globals import ( + nmlib, + NotmuchThreadP, + NotmuchMessagesP, + NotmuchTagsP, +) +from .errors import ( + NullPointerError, + NotInitializedError, +) +from .messages import Messages +from .tag import Tags +from datetime import date + +class Thread(object): + """Represents a single message thread.""" + + """notmuch_thread_get_thread_id""" + _get_thread_id = nmlib.notmuch_thread_get_thread_id + _get_thread_id.argtypes = [NotmuchThreadP] + _get_thread_id.restype = c_char_p + + """notmuch_thread_get_authors""" + _get_authors = nmlib.notmuch_thread_get_authors + _get_authors.argtypes = [NotmuchThreadP] + _get_authors.restype = c_char_p + + """notmuch_thread_get_subject""" + _get_subject = nmlib.notmuch_thread_get_subject + _get_subject.argtypes = [NotmuchThreadP] + _get_subject.restype = c_char_p + + """notmuch_thread_get_toplevel_messages""" + _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages + _get_toplevel_messages.argtypes = [NotmuchThreadP] + _get_toplevel_messages.restype = NotmuchMessagesP + + _get_newest_date = nmlib.notmuch_thread_get_newest_date + _get_newest_date.argtypes = [NotmuchThreadP] + _get_newest_date.restype = c_long + + _get_oldest_date = nmlib.notmuch_thread_get_oldest_date + _get_oldest_date.argtypes = [NotmuchThreadP] + _get_oldest_date.restype = c_long + + """notmuch_thread_get_tags""" + _get_tags = nmlib.notmuch_thread_get_tags + _get_tags.argtypes = [NotmuchThreadP] + _get_tags.restype = NotmuchTagsP + + def __init__(self, thread_p, parent=None): + """ + :param thread_p: A pointer to an internal notmuch_thread_t + Structure. These are not publicly exposed, so a user + will almost never instantiate a :class:`Thread` object + herself. They are usually handed back as a result, + e.g. when iterating through :class:`Threads`. *thread_p* + must be valid, we will raise an :exc:`NullPointerError` + if it is `None`. + + :param parent: A 'parent' object is passed which this message is + derived from. We save a reference to it, so we can + automatically delete the parent object once all derived + objects are dead. + """ + if not thread_p: + raise NullPointerError() + self._thread = thread_p + #keep reference to parent, so we keep it alive + self._parent = parent + + def get_thread_id(self): + """Get the thread ID of 'thread' + + The returned string belongs to 'thread' and will only be valid + for as long as the thread is valid. + + :returns: String with a message ID + :raises: :exc:`NotInitializedError` if the thread + is not initialized. + """ + if not self._thread: + raise NotInitializedError() + return Thread._get_thread_id(self._thread).decode('utf-8', 'ignore') + + _get_total_messages = nmlib.notmuch_thread_get_total_messages + _get_total_messages.argtypes = [NotmuchThreadP] + _get_total_messages.restype = c_int + + def get_total_messages(self): + """Get the total number of messages in 'thread' + + :returns: The number of all messages in the database + belonging to this thread. Contrast with + :meth:`get_matched_messages`. + :raises: :exc:`NotInitializedError` if the thread + is not initialized. + """ + if not self._thread: + raise NotInitializedError() + return self._get_total_messages(self._thread) + + def get_toplevel_messages(self): + """Returns a :class:`Messages` iterator for the top-level messages in + 'thread' + + This iterator will not necessarily iterate over all of the messages + in the thread. It will only iterate over the messages in the thread + which are not replies to other messages in the thread. + + :returns: :class:`Messages` + :raises: :exc:`NotInitializedError` if query is not initialized + :raises: :exc:`NullPointerError` if search_messages failed + """ + if not self._thread: + raise NotInitializedError() + + msgs_p = Thread._get_toplevel_messages(self._thread) + + if not msgs_p: + raise NullPointerError() + + return Messages(msgs_p, self) + + """notmuch_thread_get_messages""" + _get_messages = nmlib.notmuch_thread_get_messages + _get_messages.argtypes = [NotmuchThreadP] + _get_messages.restype = NotmuchMessagesP + + def get_messages(self): + """Returns a :class:`Messages` iterator for all messages in 'thread' + + :returns: :class:`Messages` + :raises: :exc:`NotInitializedError` if query is not initialized + :raises: :exc:`NullPointerError` if get_messages failed + """ + if not self._thread: + raise NotInitializedError() + + msgs_p = Thread._get_messages(self._thread) + + if not msgs_p: + raise NullPointerError() + + return Messages(msgs_p, self) + + _get_matched_messages = nmlib.notmuch_thread_get_matched_messages + _get_matched_messages.argtypes = [NotmuchThreadP] + _get_matched_messages.restype = c_int + + def get_matched_messages(self): + """Returns the number of messages in 'thread' that matched the query + + :returns: The number of all messages belonging to this thread that + matched the :class:`Query`from which this thread was created. + Contrast with :meth:`get_total_messages`. + :raises: :exc:`NotInitializedError` if the thread + is not initialized. + """ + if not self._thread: + raise NotInitializedError() + return self._get_matched_messages(self._thread) + + def get_authors(self): + """Returns the authors of 'thread' + + The returned string is a comma-separated list of the names of the + authors of mail messages in the query results that belong to this + thread. + + The returned string belongs to 'thread' and will only be valid for + as long as this Thread() is not deleted. + """ + if not self._thread: + raise NotInitializedError() + authors = Thread._get_authors(self._thread) + if not authors: + return None + return authors.decode('UTF-8', 'ignore') + + def get_subject(self): + """Returns the Subject of 'thread' + + The returned string belongs to 'thread' and will only be valid for + as long as this Thread() is not deleted. + """ + if not self._thread: + raise NotInitializedError() + subject = Thread._get_subject(self._thread) + if not subject: + return None + return subject.decode('UTF-8', 'ignore') + + def get_newest_date(self): + """Returns time_t of the newest message date + + :returns: A time_t timestamp. + :rtype: c_unit64 + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._thread: + raise NotInitializedError() + return Thread._get_newest_date(self._thread) + + def get_oldest_date(self): + """Returns time_t of the oldest message date + + :returns: A time_t timestamp. + :rtype: c_unit64 + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._thread: + raise NotInitializedError() + return Thread._get_oldest_date(self._thread) + + def get_tags(self): + """ Returns the message tags + + In the Notmuch database, tags are stored on individual + messages, not on threads. So the tags returned here will be all + tags of the messages which matched the search and which belong to + this thread. + + The :class:`Tags` object is owned by the thread and as such, will only + be valid for as long as this :class:`Thread` is valid (e.g. until the + query from which it derived is explicitly deleted). + + :returns: A :class:`Tags` iterator. + :raises: :exc:`NotInitializedError` if query is not initialized + :raises: :exc:`NullPointerError` if search_messages failed + """ + if not self._thread: + raise NotInitializedError() + + tags_p = Thread._get_tags(self._thread) + if not tags_p: + raise NullPointerError() + return Tags(tags_p, self) + + def __unicode__(self): + frm = "thread:%s %12s [%d/%d] %s; %s (%s)" + + return frm % (self.get_thread_id(), + date.fromtimestamp(self.get_newest_date()), + self.get_matched_messages(), + self.get_total_messages(), + self.get_authors(), + self.get_subject(), + self.get_tags(), + ) + + _destroy = nmlib.notmuch_thread_destroy + _destroy.argtypes = [NotmuchThreadP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch Thread""" + if self._thread: + self._destroy(self._thread) diff --git a/contrib/python-legacy/notmuch/threads.py b/contrib/python-legacy/notmuch/threads.py new file mode 100644 index 00000000..0c382d5b --- /dev/null +++ b/contrib/python-legacy/notmuch/threads.py @@ -0,0 +1,152 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from .globals import ( + nmlib, + Python3StringMixIn, + NotmuchThreadP, + NotmuchThreadsP, +) +from .errors import ( + NullPointerError, + NotInitializedError, +) +from .thread import Thread + +class Threads(Python3StringMixIn): + """Represents a list of notmuch threads + + This object provides an iterator over a list of notmuch threads + (Technically, it provides a wrapper for the underlying + *notmuch_threads_t* structure). Do note that the underlying + library only provides a one-time iterator (it cannot reset the + iterator to the start). Thus iterating over the function will + "exhaust" the list of threads, and a subsequent iteration attempt + will raise a :exc:`NotInitializedError`. Also + note, that any function that uses iteration will also + exhaust the messages. So both:: + + for thread in threads: print thread + + as well as:: + + list_of_threads = list(threads) + + will "exhaust" the threads. If you need to re-iterate over a list of + messages you will need to retrieve a new :class:`Threads` object. + + Things are not as bad as it seems though, you can store and reuse + the single Thread objects as often as you want as long as you + keep the parent Threads object around. (Recall that due to + hierarchical memory allocation, all derived Threads objects will + be invalid when we delete the parent Threads() object, even if it + was already "exhausted".) So this works:: + + db = Database() + threads = Query(db,'').search_threads() #get a Threads() object + threadlist = [] + for thread in threads: + threadlist.append(thread) + + # threads is "exhausted" now. + # However it will be kept around until all retrieved Thread() objects are + # also deleted. If you did e.g. an explicit del(threads) here, the + # following lines would fail. + + # You can reiterate over *threadlist* however as often as you want. + # It is simply a list with Thread objects. + + print (threadlist[0].get_thread_id()) + print (threadlist[1].get_thread_id()) + print (threadlist[0].get_total_messages()) + """ + + #notmuch_threads_get + _get = nmlib.notmuch_threads_get + _get.argtypes = [NotmuchThreadsP] + _get.restype = NotmuchThreadP + + def __init__(self, threads_p, parent=None): + """ + :param threads_p: A pointer to an underlying *notmuch_threads_t* + structure. These are not publicly exposed, so a user + will almost never instantiate a :class:`Threads` object + herself. They are usually handed back as a result, + e.g. in :meth:`Query.search_threads`. *threads_p* must be + valid, we will raise an :exc:`NullPointerError` if it is + `None`. + :type threads_p: :class:`ctypes.c_void_p` + :param parent: The parent object + (ie :class:`Query`) these tags are derived from. It saves + a reference to it, so we can automatically delete the db + object once all derived objects are dead. + :TODO: Make the iterator work more than once and cache the tags in + the Python object.(?) + """ + if not threads_p: + raise NullPointerError() + + self._threads = threads_p + #store parent, so we keep them alive as long as self is alive + self._parent = parent + + def __iter__(self): + """ Make Threads an iterator """ + return self + + _valid = nmlib.notmuch_threads_valid + _valid.argtypes = [NotmuchThreadsP] + _valid.restype = bool + + _move_to_next = nmlib.notmuch_threads_move_to_next + _move_to_next.argtypes = [NotmuchThreadsP] + _move_to_next.restype = None + + def __next__(self): + if not self._threads: + raise NotInitializedError() + + if not self._valid(self._threads): + self._threads = None + raise StopIteration + + thread = Thread(Threads._get(self._threads), self) + self._move_to_next(self._threads) + return thread + next = __next__ # python2.x iterator protocol compatibility + + def __nonzero__(self): + ''' + Implement truth value testing. If __nonzero__ is not + implemented, the python runtime would fall back to `len(..) > + 0` thus exhausting the iterator. + + :returns: True if the wrapped iterator has at least one more object + left. + ''' + return self._threads and self._valid(self._threads) + + _destroy = nmlib.notmuch_threads_destroy + _destroy.argtypes = [NotmuchThreadsP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch Threads""" + if self._threads: + self._destroy(self._threads) diff --git a/contrib/python-legacy/notmuch/version.py b/contrib/python-legacy/notmuch/version.py new file mode 100644 index 00000000..fd4152ee --- /dev/null +++ b/contrib/python-legacy/notmuch/version.py @@ -0,0 +1,3 @@ +# this file should be kept in sync with ../../../version +__VERSION__ = '0.38.3' +SOVERSION = '5' diff --git a/contrib/python-legacy/setup.py b/contrib/python-legacy/setup.py new file mode 100644 index 00000000..6308b9f9 --- /dev/null +++ b/contrib/python-legacy/setup.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +import os +from distutils.core import setup + +# get the notmuch version number without importing the notmuch module +version_file = os.path.join(os.path.dirname(__file__), + 'notmuch', 'version.py') +exec(compile(open(version_file).read(), version_file, 'exec')) +assert '__VERSION__' in globals(), \ + 'Failed to read the notmuch binding version number' + +setup(name='notmuch', + version=__VERSION__, + description='Python binding of the notmuch mail search and indexing library.', + author='Sebastian Spaeth', + author_email='Sebastian@SSpaeth.de', + url='https://notmuchmail.org/', + download_url='https://notmuchmail.org/releases/notmuch-%s.tar.gz' % __VERSION__, + packages=['notmuch'], + keywords=['library', 'email'], + long_description='''Overview +======== + +The notmuch module provides an interface to the `notmuch +`_ functionality, directly interfacing with a +shared notmuch library. Notmuch provides a maildatabase that allows +for extremely quick searching and filtering of your email according to +various criteria. + +The documentation for the latest notmuch release can be `viewed +online `_. + +Requirements +------------ + +You need to have notmuch installed (or rather libnotmuch.so.1). Also, +notmuch makes use of the ctypes library, and has only been tested with +python >= 2.5. It will not work on earlier python versions. +''', + classifiers=['Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + 'Topic :: Communications :: Email', + 'Topic :: Software Development :: Libraries' + ], + platforms='', + license='https://www.gnu.org/licenses/gpl-3.0.txt', + ) diff --git a/debian/changelog b/debian/changelog index ae65ab6e..ef835166 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,262 @@ +notmuch (0.39-1) unstable; urgency=medium + + * New upstream release. + * Legacy python bindings (package python3-notmuch) are no longer + built. Consider switching to python3-notmuch2. + + -- David Bremner Sat, 15 Mar 2025 07:03:50 -0300 + +notmuch (0.39~rc3-1) experimental; urgency=medium + + * New upstream pre-release + + -- David Bremner Fri, 14 Mar 2025 19:07:04 -0300 + +notmuch (0.39~rc2-2) experimental; urgency=medium + + * Increase test timeout to 10m (per test script). This is currently + only needed for riscv64, but should be harmless elsewhere. + + -- David Bremner Tue, 11 Mar 2025 19:39:27 -0300 + +notmuch (0.39~rc2-1) experimental; urgency=medium + + * New upstream pre-release + * Bug fix: "FTBFS: 1 test failed.", thanks to Santiago Vila (Closes: + #1099252). + + -- David Bremner Mon, 10 Mar 2025 21:00:09 -0300 + +notmuch (0.39~rc1-1) experimental; urgency=medium + + * New upstream pre-release + * Bug fix: "FTBFS: 3 tests fail against Emacs 30.1", thanks to Sean + Whitton (Closes: #1098852). Force emacs 30 to use the older + pretty-printing style. + + -- David Bremner Tue, 25 Feb 2025 16:35:06 -0400 + +notmuch (0.39~rc0-1) experimental; urgency=medium + + * New upstream pre-release + * Remove binary package python3-notmuch (Closes: #893057, #970834, #749890). + * Bug fix: "saving draft causing missing header fields", thanks to + Xiyue Deng (Closes: #1092047). + + -- David Bremner Sat, 22 Feb 2025 08:12:33 -0400 + +notmuch (0.38.3-5) unstable; urgency=medium + + * Bug fix: "test suite regressions with fixed GnuPG", thanks to + Daniel Kahn Gillmor (Closes: #1098995). + + -- David Bremner Fri, 28 Feb 2025 16:24:35 -0500 + +notmuch (0.38.3-4) unstable; urgency=medium + + * Bug fix: "FTBFS: 3 tests fail against Emacs 30.1", thanks to Sean + Whitton (Closes: #1098852). Cherry-pick upstream commit e3d4721b1. + + -- David Bremner Tue, 25 Feb 2025 16:51:36 -0400 + +notmuch (0.38.3-3) unstable; urgency=medium + + [ James McCoy ] + * Convert notmuch-vim to Vim addon policy 2.0 (Closes: #1015936). + + [ Nicholas D Steeves ] + * Allow neomutt to fulfill the "mutt" requirement of notmuch-mutt, and add + related Enhances (Closes: #1029190). + * Add Astroid as an alternative Recommends; this one is a GUI client that + may be less intimidating for new users. + + [ David Bremner ] + * remove "Build-Conflicts: gdb-minimal", thanks to Jeremy Bícha (Closes: + #1077911). + * skip test T810-tsan everywhere, as it seems broken by the latest glib + (Closes: #1077910). Thanks to Jeremy Bícha for the report + * Replace build-dependency on pkg-config with pkgconf + + -- David Bremner Mon, 05 Aug 2024 10:13:01 -0300 + +notmuch (0.38.3-2) unstable; urgency=medium + + * No change upload to rebuild with dh-elpa 2.1.5 + + -- David Bremner Fri, 26 Jul 2024 11:59:41 +0900 + +notmuch (0.38.3-1) unstable; urgency=medium + + * New upstream bugfix release + * Bug fix: "Recommends transitional package gnupg-agent instead of + gpg-agent", thanks to Andreas Metzler (Closes: #1064114). + + -- David Bremner Sat, 09 Mar 2024 23:13:07 -0400 + +notmuch (0.38.2-1.1) unstable; urgency=medium + + * Non-maintainer upload. + * Rename libraries for 64-bit time_t transition. Closes: #1063205 + + -- Benjamin Drung Wed, 28 Feb 2024 23:56:48 +0000 + +notmuch (0.38.2-1) unstable; urgency=medium + + * New upstream bugfix release + + -- David Bremner Fri, 01 Dec 2023 07:51:09 -0400 + +notmuch (0.38.1-1) unstable; urgency=medium + + * New upstream bugfix release + + -- David Bremner Thu, 26 Oct 2023 19:58:42 -0300 + +notmuch (0.38.1~rc1-1) experimental; urgency=medium + + * New upstream release candidate + + -- David Bremner Thu, 12 Oct 2023 19:53:10 -0300 + +notmuch (0.38.1~pre0-1) experimental; urgency=medium + + * New upstream release candidate + + -- David Bremner Sun, 01 Oct 2023 08:14:17 -0300 + +notmuch (0.38-2) unstable; urgency=medium + + * Restrict autopkgtests to amd64 and aarch64. There are failures in + remaining architectures, but the same tests pass at build time, so any + bugs are probably related to either the autopkgtest environment, or + the (new) upstream test runner for installed notmuch. + + -- David Bremner Wed, 13 Sep 2023 19:55:00 -0300 + +notmuch (0.38-1) unstable; urgency=medium + + * New upstream release + * Bug fix: "FTBFS: 6 tests failed.", thanks to Aurelien Jarno (Closes: + #1051111). + * Run most of upstream test suite as autopkgtests + + -- David Bremner Tue, 12 Sep 2023 08:33:24 -0300 + +notmuch (0.38~rc2-1) experimental; urgency=medium + + * New upstream release candidate + + -- David Bremner Sun, 03 Sep 2023 09:10:24 -0300 + +notmuch (0.38~rc1-1) experimental; urgency=medium + + * New upstream release candidate + * Hopefully reduce/eliminate intermittent failures of T460 by + controlling Emacs native compilation. + * Disable T810-tsan on ppc64el + + -- David Bremner Sat, 26 Aug 2023 08:31:21 -0300 + +notmuch (0.38~rc0-1) experimental; urgency=medium + + * New upstream release candidate + + -- David Bremner Thu, 24 Aug 2023 10:56:06 -0300 + +notmuch (0.37-1) unstable; urgency=medium + + * New upstream release. + * Build-depend on emacs-el to work around #1017698 + + -- David Bremner Wed, 24 Aug 2022 09:12:19 -0700 + +notmuch (0.37~rc0-3) experimental; urgency=medium + + * Another no-change re-upload with binaries. + + -- David Bremner Sun, 14 Aug 2022 11:49:21 -0300 + +notmuch (0.37~rc0-2) experimental; urgency=medium + + * Binary upload for NEW (notmuch-git is a new binary package) + + -- David Bremner Sun, 14 Aug 2022 10:55:24 -0300 + +notmuch (0.37~rc0-1) experimental; urgency=medium + + * New upstream release candidate + + -- David Bremner Sun, 14 Aug 2022 07:28:22 -0300 + +notmuch (0.36-1) unstable; urgency=medium + + * New upstream release + + -- David Bremner Mon, 25 Apr 2022 08:47:41 -0300 + +notmuch (0.36~rc1-1) experimental; urgency=medium + + * New upstream release candidate + * Fix for build in environments where libsexp is not available + (i.e. outside Debian). + + -- David Bremner Sat, 16 Apr 2022 08:37:12 -0300 + +notmuch (0.36~rc0-1) experimental; urgency=medium + + * New upstream release candidate + * Re-enable test smime.4, allegedly fixed upstream. + + -- David Bremner Fri, 15 Apr 2022 08:45:10 -0300 + +notmuch (0.35-2) unstable; urgency=medium + + * Disable test smime.4, which is broken by gmime 3.2.9 thanks to Lucas + Nussbaum for the report (Closes: #1008462). + + -- David Bremner Mon, 28 Mar 2022 11:45:11 -0600 + +notmuch (0.35-1) unstable; urgency=medium + + * New upstream release + + -- David Bremner Sun, 06 Feb 2022 12:15:19 -0400 + +notmuch (0.35~rc0-2) experimental; urgency=medium + + * Reupload with binaries + + -- David Bremner Sat, 29 Jan 2022 21:53:29 -0400 + +notmuch (0.35~rc0-1) experimental; urgency=medium + + * New upstream release candidate + + -- David Bremner Sat, 29 Jan 2022 18:14:57 -0400 + +notmuch (0.34.3-1) unstable; urgency=medium + + * New upstream bugfix release, with several fixes for the notmuch2 + python module. + + -- David Bremner Sun, 09 Jan 2022 15:30:38 -0400 + +notmuch (0.34.2-1) unstable; urgency=medium + + * New upstream bugfix with release, with fixes database location in + library and notmuch2 python module. + * Build only against the default version of python, to avoid including + multiple .abi3.so files in python3-notmuch2 + + -- David Bremner Fri, 10 Dec 2021 09:35:43 -0400 + +notmuch (0.34.1-1) unstable; urgency=medium + + * New upstream bugfix release. Fixes a memory deallocation error in + libnotmuch. + + -- David Bremner Wed, 03 Nov 2021 10:20:33 -0300 + notmuch (0.34-1) unstable; urgency=medium * New upstream release diff --git a/debian/control b/debian/control index 8a2e29dd..8f59911e 100644 --- a/debian/control +++ b/debian/control @@ -7,29 +7,31 @@ Uploaders: David Bremner , Build-Conflicts: gdb [ia64 mips mips64el hppa], - gdb-minimal, ruby1.8, Build-Depends: bash-completion (>=1.9.0~), debhelper-compat (= 13), dh-elpa (>= 1.3), dh-python, + dh-sequence-vim-addon, desktop-file-utils, doxygen, - dpkg-dev (>= 1.17.14), + dpkg-dev (>= 1.22.5), dtach (>= 0.8) , - emacs-nox | emacs-gtk | emacs-lucid | emacs25-nox | emacs25 (>=25~) | emacs25-lucid (>=25~) | emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~), + emacs-nox | emacs-gtk | emacs-lucid | emacs-pgtk | emacs25-nox | emacs25 (>=25~) | emacs25-lucid (>=25~) | emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~), + emacs-el, gdb [!ia64 !mips !mips64el !kfreebsd-any !alpha !hppa] , + git , gnupg , gpgsm , libgmime-3.0-dev (>= 3.0.3~), - libpython3-all-dev, + libpython3-dev, libsexp-dev, libtalloc-dev, libxapian-dev, libz-dev, - pkg-config, - python3-all (>= 3.1.2-7~), + pkgconf, + python3, python3-cffi, python3-pytest, python3-pytest-cov, @@ -48,15 +50,16 @@ Rules-Requires-Root: no Package: notmuch Architecture: any Depends: - libnotmuch5 (= ${binary:Version}), + libnotmuch5t64 (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}, Recommends: - elpa-notmuch | notmuch-vim | notmuch-mutt | alot, - gnupg-agent, + elpa-notmuch | notmuch-vim | notmuch-mutt | alot | astroid, + gpg-agent, gpgsm, Suggests: - mailscripts + mailscripts, + notmuch-doc, Description: thread-based email index, search and tagging Notmuch is a system for indexing, searching, reading, and tagging large collections of email messages in maildir or mh format. It uses @@ -65,7 +68,41 @@ Description: thread-based email index, search and tagging . This package contains the notmuch command-line interface -Package: libnotmuch5 +Package: notmuch-git +Architecture: all +Depends: + git, + notmuch, + python3, + ${misc:Depends} +Description: thread-based email index, search and tagging + Notmuch is a system for indexing, searching, reading, and tagging + large collections of email messages in maildir or mh format. It uses + the Xapian library to provide fast, full-text search with a very + convenient search syntax. + . + This package contains a simple tool to save, restore, and synchronize + notmuch tags via git repositories. + +Package: notmuch-doc +Architecture: all +Depends: + ${misc:Depends}, + ${sphinxdoc:Depends}, +Suggests: + notmuch +Description: thread-based email index, search and tagging + Notmuch is a system for indexing, searching, reading, and tagging + large collections of email messages in maildir or mh format. It uses + the Xapian library to provide fast, full-text search with a very + convenient search syntax. + . + This package contains the HTML documentation + +Package: libnotmuch5t64 +Provides: ${t64:Provides} +Replaces: libnotmuch5 +Breaks: libnotmuch5 (<< ${source:Version}) Section: libs Architecture: any Depends: @@ -86,7 +123,7 @@ Package: libnotmuch-dev Section: libdevel Architecture: any Depends: - libnotmuch5 (= ${binary:Version}), + libnotmuch5t64 (= ${binary:Version}), ${misc:Depends}, Description: thread-based email index, search and tagging (development) Notmuch is a system for indexing, searching, reading, and tagging @@ -97,29 +134,11 @@ Description: thread-based email index, search and tagging (development) This package provides the necessary development libraries and header files to allow you to develop new software using libnotmuch. -Package: python3-notmuch -Architecture: all -Section: python -Depends: - libnotmuch5 (>= ${source:Version}), - ${misc:Depends}, - ${python3:Depends}, -Description: Python 3 legacy interface to the notmuch mail search and index library - Notmuch is a system for indexing, searching, reading, and tagging - large collections of email messages in maildir or mh format. It uses - the Xapian library to provide fast, full-text search with a very - convenient search syntax. - . - This package provides a legacy Python 3 interface to the notmuch - functionality, directly interfacing with a shared notmuch library. - . - New projects are encouraged to use python3-notmuch2 instead. - Package: python3-notmuch2 Architecture: any Section: python Depends: - libnotmuch5 (>= ${source:Version}), + libnotmuch5t64 (>= ${source:Version}), ${misc:Depends}, ${python3:Depends}, ${shlibs:Depends}, @@ -133,7 +152,7 @@ Description: Python 3 interface to the notmuch mail search and index library functionality using CFFI bindings, which interface with a shared notmuch library. . - This is the preferred way to use notmuch via Python. + This is the current supported way to use notmuch via Python. Package: ruby-notmuch Architecture: any @@ -174,9 +193,9 @@ Replaces: Depends: notmuch, ruby-notmuch, - vim-addon-manager, vim-ruby, ${misc:Depends}, + ${vim-addon:Depends}, Recommends: ruby-mail, Description: thread-based email index, search and tagging (vim interface) @@ -193,15 +212,15 @@ Architecture: all Depends: libmail-box-perl, libmailtools-perl, - libstring-shellquote-perl, libterm-readline-gnu-perl, notmuch (>= 0.4), ${misc:Depends}, ${perl:Depends}, Recommends: - mutt, + mutt | neomutt, Enhances: mutt, + neomutt, notmuch, Description: thread-based email index, search and tagging (Mutt interface) notmuch-mutt provides integration among the Mutt mail user agent and diff --git a/debian/elpa-notmuch.elpa b/debian/elpa-notmuch.elpa index 4712b73f..7b3ce0fa 100644 --- a/debian/elpa-notmuch.elpa +++ b/debian/elpa-notmuch.elpa @@ -1,3 +1,3 @@ debian/tmp/usr/share/emacs/site-lisp/*.el -debian/tmp/usr/share/emacs/site-lisp/notmuch-logo.png +debian/tmp/usr/share/emacs/site-lisp/notmuch-logo.svg emacs/notmuch-pkg.el diff --git a/debian/libnotmuch5.install b/debian/libnotmuch5.install deleted file mode 100644 index a513b475..00000000 --- a/debian/libnotmuch5.install +++ /dev/null @@ -1 +0,0 @@ -usr/lib/*/libnotmuch.so.* diff --git a/debian/libnotmuch5.symbols b/debian/libnotmuch5.symbols deleted file mode 100644 index e6d2b7c9..00000000 --- a/debian/libnotmuch5.symbols +++ /dev/null @@ -1,166 +0,0 @@ -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 - notmuch_database_find_message_by_filename@Base 0.9~rc2 - notmuch_database_get_all_tags@Base 0.3 - notmuch_database_get_config@Base 0.23~rc0 - notmuch_database_get_config_list@Base 0.23~rc0 - notmuch_database_get_default_indexopts@Base 0.26~rc0 - notmuch_database_get_directory@Base 0.3 - notmuch_database_get_path@Base 0.3 - 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 - notmuch_directory_delete@Base 0.21~rc1 - notmuch_directory_destroy@Base 0.3 - notmuch_directory_get_child_directories@Base 0.3 - notmuch_directory_get_child_files@Base 0.3 - notmuch_directory_get_mtime@Base 0.3 - notmuch_directory_set_mtime@Base 0.3 - notmuch_filenames_destroy@Base 0.3 - notmuch_filenames_get@Base 0.3 - notmuch_filenames_move_to_next@Base 0.3 - notmuch_filenames_valid@Base 0.3 - notmuch_indexopts_destroy@Base 0.26~rc0 - notmuch_indexopts_get_decrypt_policy@Base 0.26~rc0 - notmuch_indexopts_set_decrypt_policy@Base 0.26~rc0 - notmuch_message_add_property@Base 0.23~rc0 - notmuch_message_add_tag@Base 0.3 - notmuch_message_count_files@Base 0.26~rc0 - notmuch_message_count_properties@Base 0.27~rc0 - notmuch_message_destroy@Base 0.3 - notmuch_message_freeze@Base 0.3 - notmuch_message_get_database@Base 0.27~rc0 - notmuch_message_get_date@Base 0.3 - notmuch_message_get_filename@Base 0.3 - notmuch_message_get_filenames@Base 0.5 - notmuch_message_get_flag@Base 0.3 - notmuch_message_get_flag_st@Base 0.31~rc0 - notmuch_message_get_header@Base 0.3 - notmuch_message_get_message_id@Base 0.3 - notmuch_message_get_properties@Base 0.23~rc0 - notmuch_message_get_property@Base 0.23~rc0 - notmuch_message_get_replies@Base 0.3 - notmuch_message_get_tags@Base 0.3 - notmuch_message_get_thread_id@Base 0.3 - notmuch_message_has_maildir_flag@Base 0.26~rc0 - notmuch_message_has_maildir_flag_st@Base 0.31~rc0 - notmuch_message_maildir_flags_to_tags@Base 0.5 - notmuch_message_properties_destroy@Base 0.23~rc0 - notmuch_message_properties_key@Base 0.23~rc0 - notmuch_message_properties_move_to_next@Base 0.23~rc0 - notmuch_message_properties_valid@Base 0.23~rc0 - notmuch_message_properties_value@Base 0.23~rc0 - notmuch_message_reindex@Base 0.26~rc0 - notmuch_message_remove_all_properties@Base 0.23~rc0 - notmuch_message_remove_all_properties_with_prefix@Base 0.26~rc0 - notmuch_message_remove_all_tags@Base 0.3 - notmuch_message_remove_property@Base 0.23~rc0 - notmuch_message_remove_tag@Base 0.3 - notmuch_message_set_flag@Base 0.3 - notmuch_message_tags_to_maildir_flags@Base 0.5 - notmuch_message_thaw@Base 0.3 - notmuch_messages_collect_tags@Base 0.3 - notmuch_messages_destroy@Base 0.3 - notmuch_messages_get@Base 0.3 - notmuch_messages_move_to_next@Base 0.3 - notmuch_messages_valid@Base 0.3 - notmuch_query_add_tag_exclude@Base 0.12~rc1 - notmuch_query_count_messages@Base 0.3 - notmuch_query_count_messages_st@Base 0.21~rc1 - notmuch_query_count_threads@Base 0.10~rc1 - notmuch_query_count_threads_st@Base 0.21~rc1 - notmuch_query_create@Base 0.3 - notmuch_query_create_with_syntax@Base 0.34~rc0 - notmuch_query_destroy@Base 0.3 - notmuch_query_get_database@Base 0.21~rc1 - notmuch_query_get_query_string@Base 0.4 - notmuch_query_get_sort@Base 0.4 - notmuch_query_search_messages@Base 0.3 - notmuch_query_search_messages_st@Base 0.20~rc1 - notmuch_query_search_threads@Base 0.3 - notmuch_query_search_threads_st@Base 0.20~rc1 - notmuch_query_set_omit_excluded@Base 0.13~rc1 - notmuch_query_set_sort@Base 0.3 - notmuch_status_to_string@Base 0.3 - notmuch_tags_destroy@Base 0.3 - notmuch_tags_get@Base 0.3 - notmuch_tags_move_to_next@Base 0.3 - notmuch_tags_valid@Base 0.3 - notmuch_thread_destroy@Base 0.3 - notmuch_thread_get_authors@Base 0.3 - notmuch_thread_get_matched_messages@Base 0.3 - notmuch_thread_get_messages@Base 0.16 - notmuch_thread_get_newest_date@Base 0.3 - notmuch_thread_get_oldest_date@Base 0.3 - notmuch_thread_get_subject@Base 0.3 - notmuch_thread_get_tags@Base 0.3 - notmuch_thread_get_thread_id@Base 0.3 - notmuch_thread_get_toplevel_messages@Base 0.3 - notmuch_thread_get_total_files@Base 0.26~rc0 - notmuch_thread_get_total_messages@Base 0.3 - notmuch_threads_destroy@Base 0.3 - notmuch_threads_get@Base 0.3 - notmuch_threads_move_to_next@Base 0.3 - notmuch_threads_valid@Base 0.3 - (c++)"typeinfo for Xapian::LogicError@Base" 0.6.1 - (c++)"typeinfo for Xapian::RuntimeError@Base" 0.6.1 - (c++)"typeinfo for Xapian::DocNotFoundError@Base" 0.6.1 - (c++)"typeinfo for Xapian::InvalidArgumentError@Base" 0.6.1 - (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 - (c++)"typeinfo name for Xapian::DocNotFoundError@Base" 0.6.1 - (c++)"typeinfo name for Xapian::InvalidArgumentError@Base" 0.6.1 - (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/debian/libnotmuch5t64.install b/debian/libnotmuch5t64.install new file mode 100644 index 00000000..a513b475 --- /dev/null +++ b/debian/libnotmuch5t64.install @@ -0,0 +1 @@ +usr/lib/*/libnotmuch.so.* diff --git a/debian/libnotmuch5t64.lintian-overrides b/debian/libnotmuch5t64.lintian-overrides new file mode 100644 index 00000000..affb63b4 --- /dev/null +++ b/debian/libnotmuch5t64.lintian-overrides @@ -0,0 +1 @@ +libnotmuch5t64: package-name-doesnt-match-sonames libnotmuch5 diff --git a/debian/libnotmuch5t64.symbols b/debian/libnotmuch5t64.symbols new file mode 100644 index 00000000..5715dec0 --- /dev/null +++ b/debian/libnotmuch5t64.symbols @@ -0,0 +1,166 @@ +libnotmuch.so.5 libnotmuch5t64 #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 + notmuch_database_find_message_by_filename@Base 0.9~rc2 + notmuch_database_get_all_tags@Base 0.3 + notmuch_database_get_config@Base 0.23~rc0 + notmuch_database_get_config_list@Base 0.23~rc0 + notmuch_database_get_default_indexopts@Base 0.26~rc0 + notmuch_database_get_directory@Base 0.3 + notmuch_database_get_path@Base 0.3 + 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 + notmuch_directory_delete@Base 0.21~rc1 + notmuch_directory_destroy@Base 0.3 + notmuch_directory_get_child_directories@Base 0.3 + notmuch_directory_get_child_files@Base 0.3 + notmuch_directory_get_mtime@Base 0.3 + notmuch_directory_set_mtime@Base 0.3 + notmuch_filenames_destroy@Base 0.3 + notmuch_filenames_get@Base 0.3 + notmuch_filenames_move_to_next@Base 0.3 + notmuch_filenames_valid@Base 0.3 + notmuch_indexopts_destroy@Base 0.26~rc0 + notmuch_indexopts_get_decrypt_policy@Base 0.26~rc0 + notmuch_indexopts_set_decrypt_policy@Base 0.26~rc0 + notmuch_message_add_property@Base 0.23~rc0 + notmuch_message_add_tag@Base 0.3 + notmuch_message_count_files@Base 0.26~rc0 + notmuch_message_count_properties@Base 0.27~rc0 + notmuch_message_destroy@Base 0.3 + notmuch_message_freeze@Base 0.3 + notmuch_message_get_database@Base 0.27~rc0 + notmuch_message_get_date@Base 0.3 + notmuch_message_get_filename@Base 0.3 + notmuch_message_get_filenames@Base 0.5 + notmuch_message_get_flag@Base 0.3 + notmuch_message_get_flag_st@Base 0.31~rc0 + notmuch_message_get_header@Base 0.3 + notmuch_message_get_message_id@Base 0.3 + notmuch_message_get_properties@Base 0.23~rc0 + notmuch_message_get_property@Base 0.23~rc0 + notmuch_message_get_replies@Base 0.3 + notmuch_message_get_tags@Base 0.3 + notmuch_message_get_thread_id@Base 0.3 + notmuch_message_has_maildir_flag@Base 0.26~rc0 + notmuch_message_has_maildir_flag_st@Base 0.31~rc0 + notmuch_message_maildir_flags_to_tags@Base 0.5 + notmuch_message_properties_destroy@Base 0.23~rc0 + notmuch_message_properties_key@Base 0.23~rc0 + notmuch_message_properties_move_to_next@Base 0.23~rc0 + notmuch_message_properties_valid@Base 0.23~rc0 + notmuch_message_properties_value@Base 0.23~rc0 + notmuch_message_reindex@Base 0.26~rc0 + notmuch_message_remove_all_properties@Base 0.23~rc0 + notmuch_message_remove_all_properties_with_prefix@Base 0.26~rc0 + notmuch_message_remove_all_tags@Base 0.3 + notmuch_message_remove_property@Base 0.23~rc0 + notmuch_message_remove_tag@Base 0.3 + notmuch_message_set_flag@Base 0.3 + notmuch_message_tags_to_maildir_flags@Base 0.5 + notmuch_message_thaw@Base 0.3 + notmuch_messages_collect_tags@Base 0.3 + notmuch_messages_destroy@Base 0.3 + notmuch_messages_get@Base 0.3 + notmuch_messages_move_to_next@Base 0.3 + notmuch_messages_valid@Base 0.3 + notmuch_query_add_tag_exclude@Base 0.12~rc1 + notmuch_query_count_messages@Base 0.3 + notmuch_query_count_messages_st@Base 0.21~rc1 + notmuch_query_count_threads@Base 0.10~rc1 + notmuch_query_count_threads_st@Base 0.21~rc1 + notmuch_query_create@Base 0.3 + notmuch_query_create_with_syntax@Base 0.34~rc0 + notmuch_query_destroy@Base 0.3 + notmuch_query_get_database@Base 0.21~rc1 + notmuch_query_get_query_string@Base 0.4 + notmuch_query_get_sort@Base 0.4 + notmuch_query_search_messages@Base 0.3 + notmuch_query_search_messages_st@Base 0.20~rc1 + notmuch_query_search_threads@Base 0.3 + notmuch_query_search_threads_st@Base 0.20~rc1 + notmuch_query_set_omit_excluded@Base 0.13~rc1 + notmuch_query_set_sort@Base 0.3 + notmuch_status_to_string@Base 0.3 + notmuch_tags_destroy@Base 0.3 + notmuch_tags_get@Base 0.3 + notmuch_tags_move_to_next@Base 0.3 + notmuch_tags_valid@Base 0.3 + notmuch_thread_destroy@Base 0.3 + notmuch_thread_get_authors@Base 0.3 + notmuch_thread_get_matched_messages@Base 0.3 + notmuch_thread_get_messages@Base 0.16 + notmuch_thread_get_newest_date@Base 0.3 + notmuch_thread_get_oldest_date@Base 0.3 + notmuch_thread_get_subject@Base 0.3 + notmuch_thread_get_tags@Base 0.3 + notmuch_thread_get_thread_id@Base 0.3 + notmuch_thread_get_toplevel_messages@Base 0.3 + notmuch_thread_get_total_files@Base 0.26~rc0 + notmuch_thread_get_total_messages@Base 0.3 + notmuch_threads_destroy@Base 0.3 + notmuch_threads_get@Base 0.3 + notmuch_threads_move_to_next@Base 0.3 + notmuch_threads_valid@Base 0.3 + (c++)"typeinfo for Xapian::LogicError@Base" 0.6.1 + (c++)"typeinfo for Xapian::RuntimeError@Base" 0.6.1 + (c++)"typeinfo for Xapian::DocNotFoundError@Base" 0.6.1 + (c++)"typeinfo for Xapian::InvalidArgumentError@Base" 0.6.1 + (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 + (c++)"typeinfo name for Xapian::DocNotFoundError@Base" 0.6.1 + (c++)"typeinfo name for Xapian::InvalidArgumentError@Base" 0.6.1 + (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/debian/notmuch-doc.install b/debian/notmuch-doc.install new file mode 100644 index 00000000..fa902fe1 --- /dev/null +++ b/debian/notmuch-doc.install @@ -0,0 +1 @@ +doc/_build/html usr/share/doc/notmuch diff --git a/debian/notmuch-git.install b/debian/notmuch-git.install new file mode 100644 index 00000000..2be08276 --- /dev/null +++ b/debian/notmuch-git.install @@ -0,0 +1,2 @@ +notmuch-git /usr/bin +nmbug /usr/bin diff --git a/debian/notmuch-git.manpages b/debian/notmuch-git.manpages new file mode 100644 index 00000000..e0895c86 --- /dev/null +++ b/debian/notmuch-git.manpages @@ -0,0 +1,2 @@ +usr/share/man/man1/notmuch-git.1.gz +usr/share/man/man1/nmbug.1.gz diff --git a/debian/notmuch-vim.dirs b/debian/notmuch-vim.dirs deleted file mode 100644 index 2b531314..00000000 --- a/debian/notmuch-vim.dirs +++ /dev/null @@ -1,4 +0,0 @@ -usr/share/vim/addons/doc -usr/share/vim/addons/plugin -usr/share/vim/addons/syntax -usr/share/vim/registry diff --git a/debian/notmuch-vim.install b/debian/notmuch-vim.install index cf898738..08096d37 100644 --- a/debian/notmuch-vim.install +++ b/debian/notmuch-vim.install @@ -1,4 +1 @@ -vim/notmuch.txt usr/share/vim/addons/doc -vim/notmuch.vim usr/share/vim/addons/plugin -vim/notmuch.yaml usr/share/vim/registry -vim/syntax/notmuch-*.vim usr/share/vim/addons/syntax +/usr/share/notmuch-vim diff --git a/debian/notmuch-vim.neovim-addon b/debian/notmuch-vim.neovim-addon new file mode 120000 index 00000000..a576db7c --- /dev/null +++ b/debian/notmuch-vim.neovim-addon @@ -0,0 +1 @@ +notmuch-vim.vim-addon \ No newline at end of file diff --git a/debian/notmuch-vim.vim-addon b/debian/notmuch-vim.vim-addon new file mode 100644 index 00000000..9c23ba18 --- /dev/null +++ b/debian/notmuch-vim.vim-addon @@ -0,0 +1 @@ +/usr/share/notmuch-vim notmuch diff --git a/debian/rules b/debian/rules index fa0551a9..4e0836e7 100755 --- a/debian/rules +++ b/debian/rules @@ -1,13 +1,16 @@ #!/usr/bin/make -f +include /usr/share/dpkg/architecture.mk +export NOTMUCH_SKIP_TESTS = T810-tsan +export NOTMUCH_TEST_TIMEOUT = 10m export DEB_BUILD_MAINT_OPTIONS = hardening=+all %: - dh $@ --with python3,elpa + dh $@ --with python3,elpa,sphinxdoc override_dh_auto_configure: BASHCMD=/bin/bash ./configure --prefix=/usr \ - --libdir=/usr/lib/$$(dpkg-architecture -q DEB_TARGET_MULTIARCH) \ + --libdir=/usr/lib/${DEB_TARGET_MULTIARCH} \ --includedir=/usr/include \ --mandir=/usr/share/man \ --infodir=/usr/share/info \ @@ -15,25 +18,19 @@ override_dh_auto_configure: --zshcompletiondir=/usr/share/zsh/vendor-completions \ --localstatedir=/var -override_dh_auto_test: - dh_auto_test -- V=1 - override_dh_auto_build: - dh_auto_build -- V=1 - PYBUILD_NAME=notmuch dh_auto_build --buildsystem=pybuild --sourcedirectory bindings/python + dh_auto_build -- V=1 all sphinx-html PYBUILD_NAME=notmuch2 dh_auto_build --buildsystem=pybuild --sourcedirectory bindings/python-cffi $(MAKE) -C contrib/notmuch-mutt override_dh_auto_clean: dh_auto_clean - PYBUILD_NAME=notmuch dh_auto_clean --buildsystem=pybuild --sourcedirectory bindings/python - PYBUILD_NAME=notmuch2 dh_auto_clean --buildsystem=pybuild --sourcedirectory bindings/python-cffi dh_auto_clean --sourcedirectory bindings/ruby $(MAKE) -C contrib/notmuch-mutt clean override_dh_auto_install: dh_auto_install - PYBUILD_NAME=notmuch dh_auto_install --buildsystem=pybuild --sourcedirectory bindings/python PYBUILD_NAME=notmuch2 dh_auto_install --buildsystem=pybuild --sourcedirectory bindings/python-cffi $(MAKE) -C contrib/notmuch-mutt DESTDIR=$(CURDIR)/debian/tmp install + $(MAKE) -C vim prefix=/usr/share/notmuch-vim DESTDIR=$(CURDIR)/debian/tmp install dh_auto_install --sourcedirectory bindings/ruby diff --git a/debian/tests/control b/debian/tests/control new file mode 100644 index 00000000..11aebad9 --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,18 @@ +Test-command: env NOTMUCH_TEST_INSTALLED=1 TERM=dumb + NOTMUCH_HAVE_MAN=1 NOTMUCH_HAVE_SFSEXP=1 NOTMUCH_HAVE_XAPIAN_DB_RETRY_LOCK=1 + NOTMUCH_HAVE_PYTHON3_CFFI=1 NOTMUCH_HAVE_PYTHON3_PYTEST=1 + NOTMUCH_HAVE_ASAN=1 NOTMUCH_HAVE_TSAN=1 NOTMUCH_SKIP_TESTS=T810-tsan + ./test/notmuch-test +Restrictions: allow-stderr +Architecture: amd64, arm64 +Depends: @, + build-essential, + dtach, + emacs-nox, + gdb, + git, + gnupg, + gpgsm, + libtalloc-dev, + man, + xapian-tools diff --git a/devel/emacs-keybindings.org b/devel/emacs-keybindings.org index 2f73a198..ac98cb66 100644 --- a/devel/emacs-keybindings.org +++ b/devel/emacs-keybindings.org @@ -1,58 +1,61 @@ -|-----------+----------------------------------------+-------------------------------------------------------+-----------------------------------------| -| Key | Search Mode | Show Mode | Tree Mode | -|-----------+----------------------------------------+-------------------------------------------------------+-----------------------------------------| -| a | notmuch-search-archive-thread | notmuch-show-archive-message-then-next-or-next-thread | notmuch-tree-archive-message-then-next | -| b | notmuch-search-scroll-down | notmuch-show-resend-message | notmuch-show-resend-message | -| c | notmuch-search-stash-map | notmuch-show-stash-map | notmuch-show-stash-map | -| d | | | | -| e | | | (notmuch-tree-button-activate) | -| f | | notmuch-show-forward-message | notmuch-show-forward-message | -| g | | | | -| h | | notmuch-show-toggle-visibility-headers | | -| i | | | | -| j | notmuch-jump-search | notmuch-jump-search | notmuch-jump-search | -| k | notmuch-tag-jump | notmuch-tag-jump | notmuch-tag-jump | -| l | notmuch-search-filter | notmuch-show-filter-thread | notmuch-tree-filter | -| m | notmuch-mua-new-mail | notmuch-mua-new-mail | notmuch-mua-new-mail | -| n | notmuch-search-next-thread | notmuch-show-next-open-message | notmuch-tree-next-matching-message | -| o | notmuch-search-toggle-order | | notmuch-tree-toggle-order | -| p | notmuch-search-previous-thread | notmuch-show-previous-open-message | notmuch-tree-prev-matching-message | -| q | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | -| r | notmuch-search-reply-to-thread-sender | notmuch-show-reply-sender | notmuch-show-reply-sender | -| s | notmuch-search | notmuch-search | notmuch-search | -| t | notmuch-search-filter-by-tag | toggle-truncate-lines | notmuch-tree-filter-by-tag | -| u | | | | -| v | | | notmuch-show-view-all-mime-parts | -| w | | notmuch-show-save-attachments | notmuch-show-save-attachments | -| x | notmuch-bury-or-kill-this-buffer | notmuch-show-archive-message-then-next-or-exit | notmuch-tree-quit | -| y | | | | -| z | notmuch-tree | notmuch-tree | notmuch-tree-to-tree | -| A | | notmuch-show-archive-thread-then-next | notmuch-tree-archive-thread | -| F | | notmuch-show-forward-open-messages | | -| G | notmuch-poll-and-refresh-this-buffer | notmuch-poll-and-refresh-this-buffer | notmuch-poll-and-refresh-this-buffer | -| N | | notmuch-show-next-message | notmuch-tree-next-message | -| O | | | | -| P | | notmuch-show-previous-message | notmuch-tree-prev-message | -| R | notmuch-search-reply-to-thread | notmuch-show-reply | notmuch-show-reply | -| S | | | notmuch-search-from-tree-current-query | -| V | | notmuch-show-view-raw-message | notmuch-show-view-raw-message | -| X | | notmuch-show-archive-thread-then-exit | | -| Z | notmuch-tree-from-search-current-query | notmuch-tree-from-show-current-query | | -| =!= | | notmuch-show-toggle-elide-non-matching | | -| =#= | | notmuch-show-print-message | | -| =$= | | notmuch-show-toggle-process-crypto | | -| =*= | notmuch-search-tag-all | notmuch-show-tag-all | notmuch-tree-tag-thread | -| + | notmuch-search-add-tag | notmuch-show-add-tag | notmuch-tree-add-tag | -| - | notmuch-search-remove-tag | notmuch-show-remove-tag | notmuch-tree-remove-tag | -| . | | notmuch-show-part-map | | -| < | notmuch-search-first-thread | notmuch-show-toggle-thread-indentation | | -| | notmuch-search-scroll-down | notmuch-show-rewind | notmuch-tree-scroll-message-window-back | -| | notmuch-search-show-thread | notmuch-show-toggle-message | notmuch-tree-show-message | -| | notmuch-search-scroll-up | notmuch-show-advance | notmuch-tree-scroll-or-next | -| | | notmuch-show-next-button | notmuch-show-next-button | -| | | notmuch-show-previous-button | notmuch-show-previous-button | -| = | notmuch-refresh-this-buffer | notmuch-refresh-this-buffer | notmuch-tree-refresh-view | -| > | notmuch-search-last-thread | | | -| ? | notmuch-help | notmuch-help | notmuch-help | -| \vert | | notmuch-show-pipe-message | notmuch-show-pipe-message | -|-----------+----------------------------------------+-------------------------------------------------------+-----------------------------------------| +|--------------+----------------------------------------------+-------------------------------------------------------+--------------------------------------------| +| Key | Search Mode | Show Mode | Tree Mode | +|--------------+----------------------------------------------+-------------------------------------------------------+--------------------------------------------| +| a | notmuch-search-archive-thread | notmuch-show-archive-message-then-next-or-next-thread | notmuch-tree-archive-message-then-next | +| b | notmuch-search-scroll-down | notmuch-show-resend-message | notmuch-show-resend-message | +| c | notmuch-search-stash-map | notmuch-show-stash-map | notmuch-show-stash-map | +| d | | | | +| e | | notmuch-show-resume-message | notmuch-tree-resume-message | +| f | | notmuch-show-forward-message | notmuch-show-forward-message | +| g | | | | +| h | | notmuch-show-toggle-visibility-headers | | +| i | notmuch-search-toggle-hide-excluded | | notmuch-tree-toggle-hide-excluded | +| j | notmuch-jump-search | notmuch-jump-search | notmuch-jump-search | +| k | notmuch-tag-jump | notmuch-tag-jump | notmuch-tag-jump | +| l | notmuch-search-filter | notmuch-show-filter-thread | notmuch-tree-filter | +| m | notmuch-mua-new-mail | notmuch-mua-new-mail | notmuch-mua-new-mail | +| n | notmuch-search-next-thread | notmuch-show-next-open-message | notmuch-tree-next-matching-message | +| o | notmuch-search-toggle-order | | notmuch-tree-toggle-order | +| p | notmuch-search-previous-thread | notmuch-show-previous-open-message | notmuch-tree-prev-matching-message | +| q | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | +| r | notmuch-search-reply-to-thread-sender | notmuch-show-reply-sender | notmuch-show-reply-sender | +| s | notmuch-search | notmuch-search | notmuch-search | +| t | notmuch-search-filter-by-tag | toggle-truncate-lines | notmuch-tree-filter-by-tag | +| u | notmuch-unthreaded | notmuch-unthreaded | notmuch-unthreaded | +| v | | | notmuch-show-view-all-mime-parts | +| w | | notmuch-show-save-attachments | notmuch-show-save-attachments | +| x | notmuch-bury-or-kill-this-buffer | notmuch-show-archive-message-then-next-or-exit | notmuch-tree-quit | +| y | | | | +| z | notmuch-tree | notmuch-tree | notmuch-tree-to-tree | +| A | | notmuch-show-archive-thread-then-next | notmuch-tree-archive-thread | +| F | | notmuch-show-forward-open-messages | | +| G | notmuch-poll-and-refresh-this-buffer | notmuch-poll-and-refresh-this-buffer | notmuch-poll-and-refresh-this-buffer | +| N | | notmuch-show-next-message | notmuch-tree-next-message | +| O | | | | +| P | | notmuch-show-previous-message | notmuch-tree-prev-message | +| R | notmuch-search-reply-to-thread | notmuch-show-reply | notmuch-show-reply | +| S | | | notmuch-search-from-tree-current-query | +| U | notmuch-unthreaded-from-search-current-query | notmuch-unthreaded-from-show-current-query | notmuch-unthreaded-from-tree-current-query | +| V | | notmuch-show-view-raw-message | notmuch-show-view-raw-message | +| X | | notmuch-show-archive-thread-then-exit | | +| Z | notmuch-tree-from-search-current-query | notmuch-tree-from-show-current-query | | +| =!= | | notmuch-show-toggle-elide-non-matching | | +| =#= | | notmuch-show-print-message | | +| =%= | | notmuch-show-replace-msg | | +| =$= | | notmuch-show-toggle-process-crypto | | +| =*= | notmuch-search-tag-all | notmuch-show-tag-all | notmuch-tree-tag-thread | +| + | notmuch-search-add-tag | notmuch-show-add-tag | notmuch-tree-add-tag | +| - | notmuch-search-remove-tag | notmuch-show-remove-tag | notmuch-tree-remove-tag | +| . | | notmuch-show-part-map | | +| < | notmuch-search-first-thread | notmuch-show-toggle-thread-indentation | | +| | notmuch-search-scroll-down | notmuch-show-rewind | notmuch-tree-scroll-message-window-back | +| | notmuch-search-show-thread | notmuch-show-toggle-message | notmuch-tree-show-message | +| | notmuch-search-scroll-up | notmuch-show-advance | notmuch-tree-scroll-or-next | +| | | notmuch-show-next-button | notmuch-show-next-button | +| | | notmuch-show-previous-button | notmuch-show-previous-button | +| = | notmuch-refresh-this-buffer | notmuch-refresh-this-buffer | notmuch-tree-refresh-view | +| > | notmuch-search-last-thread | | | +| ? | notmuch-help | notmuch-help | notmuch-help | +| \vert | | notmuch-show-pipe-message | notmuch-show-pipe-message | +| [remap undo] | notmuch-tag-undo | notmuch-tag-undo | notmuch-tag-undo | +|--------------+----------------------------------------------+-------------------------------------------------------+--------------------------------------------| diff --git a/devel/nmbug/nmbug b/devel/nmbug/nmbug deleted file mode 100755 index 043c1863..00000000 --- a/devel/nmbug/nmbug +++ /dev/null @@ -1,852 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2011-2014 David Bremner -# W. Trevor King -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see https://www.gnu.org/licenses/ . - -""" -Manage notmuch tags with Git - -Environment variables: - -* NMBGIT specifies the location of the git repository used by nmbug. - If not specified $HOME/.nmbug is used. -* NMBPREFIX specifies the prefix in the notmuch database for tags of - interest to nmbug. If not specified 'notmuch::' is used. -""" - -from __future__ import print_function -from __future__ import unicode_literals - -import codecs as _codecs -import collections as _collections -import functools as _functools -import inspect as _inspect -import locale as _locale -import logging as _logging -import os as _os -import re as _re -import shutil as _shutil -import subprocess as _subprocess -import sys as _sys -import tempfile as _tempfile -import textwrap as _textwrap -try: # Python 3 - from urllib.parse import quote as _quote - from urllib.parse import unquote as _unquote -except ImportError: # Python 2 - from urllib import quote as _quote - from urllib import unquote as _unquote - - -__version__ = '0.3' - -_LOG = _logging.getLogger('nmbug') -_LOG.setLevel(_logging.WARNING) -_LOG.addHandler(_logging.StreamHandler()) - -NMBGIT = _os.path.expanduser( - _os.getenv('NMBGIT', _os.path.join('~', '.nmbug'))) -_NMBGIT = _os.path.join(NMBGIT, '.git') -if _os.path.isdir(_NMBGIT): - NMBGIT = _NMBGIT - -TAG_PREFIX = _os.getenv('NMBPREFIX', 'notmuch::') -_HEX_ESCAPE_REGEX = _re.compile('%[0-9A-F]{2}') -_TAG_DIRECTORY = 'tags/' -_TAG_FILE_REGEX = _re.compile(_TAG_DIRECTORY + '(?P[^/]*)/(?P[^/]*)') - -# magic hash for Git (git hash-object -t blob /dev/null) -_EMPTYBLOB = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391' - - -try: - getattr(_tempfile, 'TemporaryDirectory') -except AttributeError: # Python < 3.2 - class _TemporaryDirectory(object): - """ - Fallback context manager for Python < 3.2 - - See PEP 343 for details on context managers [1]. - - [1]: https://www.python.org/dev/peps/pep-0343/ - """ - def __init__(self, **kwargs): - self.name = _tempfile.mkdtemp(**kwargs) - - def __enter__(self): - return self.name - - def __exit__(self, type, value, traceback): - _shutil.rmtree(self.name) - - - _tempfile.TemporaryDirectory = _TemporaryDirectory - - -def _hex_quote(string, safe='+@=:,'): - """ - quote('abc def') -> 'abc%20def'. - - Wrap urllib.parse.quote with additional safe characters (in - addition to letters, digits, and '_.-') and lowercase hex digits - (e.g. '%3a' instead of '%3A'). - """ - uppercase_escapes = _quote(string, safe) - return _HEX_ESCAPE_REGEX.sub( - lambda match: match.group(0).lower(), - uppercase_escapes) - - -_ENCODED_TAG_PREFIX = _hex_quote(TAG_PREFIX, safe='+@=,') # quote ':' - - -def _xapian_quote(string): - """ - Quote a string for Xapian's QueryParser. - - Xapian uses double-quotes for quoting strings. You can escape - internal quotes by repeating them [1,2,3]. - - [1]: https://trac.xapian.org/ticket/128#comment:2 - [2]: https://trac.xapian.org/ticket/128#comment:17 - [3]: https://trac.xapian.org/changeset/13823/svn - """ - return '"{0}"'.format(string.replace('"', '""')) - - -def _xapian_unquote(string): - """ - Unquote a Xapian-quoted string. - """ - if string.startswith('"') and string.endswith('"'): - return string[1:-1].replace('""', '"') - return string - - -class SubprocessError(RuntimeError): - "A subprocess exited with a nonzero status" - def __init__(self, args, status, stdout=None, stderr=None): - self.status = status - self.stdout = stdout - self.stderr = stderr - msg = '{args} exited with {status}'.format(args=args, status=status) - if stderr: - msg = '{msg}: {stderr}'.format(msg=msg, stderr=stderr) - super(SubprocessError, self).__init__(msg) - - -class _SubprocessContextManager(object): - """ - PEP 343 context manager for subprocesses. - - 'expect' holds a tuple of acceptable exit codes, otherwise we'll - raise a SubprocessError in __exit__. - """ - def __init__(self, process, args, expect=(0,)): - self._process = process - self._args = args - self._expect = expect - - def __enter__(self): - return self._process - - def __exit__(self, type, value, traceback): - for name in ['stdin', 'stdout', 'stderr']: - stream = getattr(self._process, name) - if stream: - stream.close() - setattr(self._process, name, None) - status = self._process.wait() - _LOG.debug( - 'collect {args} with status {status} (expected {expect})'.format( - args=self._args, status=status, expect=self._expect)) - if status not in self._expect: - raise SubprocessError(args=self._args, status=status) - - def wait(self): - return self._process.wait() - - -def _spawn(args, input=None, additional_env=None, wait=False, stdin=None, - stdout=None, stderr=None, encoding=_locale.getpreferredencoding(), - expect=(0,), **kwargs): - """Spawn a subprocess, and optionally wait for it to finish. - - This wrapper around subprocess.Popen has two modes, depending on - the truthiness of 'wait'. If 'wait' is true, we use p.communicate - internally to write 'input' to the subprocess's stdin and read - from it's stdout/stderr. If 'wait' is False, we return a - _SubprocessContextManager instance for fancier handling - (e.g. piping between processes). - - For 'wait' calls when you want to write to the subprocess's stdin, - you only need to set 'input' to your content. When 'input' is not - None but 'stdin' is, we'll automatically set 'stdin' to PIPE - before calling Popen. This avoids having the subprocess - accidentally inherit the launching process's stdin. - """ - _LOG.debug('spawn {args} (additional env. var.: {env})'.format( - args=args, env=additional_env)) - if not stdin and input is not None: - stdin = _subprocess.PIPE - if additional_env: - if not kwargs.get('env'): - kwargs['env'] = dict(_os.environ) - kwargs['env'].update(additional_env) - p = _subprocess.Popen( - args, stdin=stdin, stdout=stdout, stderr=stderr, **kwargs) - if wait: - if hasattr(input, 'encode'): - input = input.encode(encoding) - (stdout, stderr) = p.communicate(input=input) - status = p.wait() - _LOG.debug( - 'collect {args} with status {status} (expected {expect})'.format( - args=args, status=status, expect=expect)) - if stdout is not None: - stdout = stdout.decode(encoding) - if stderr is not None: - stderr = stderr.decode(encoding) - if status not in expect: - raise SubprocessError( - args=args, status=status, stdout=stdout, stderr=stderr) - return (status, stdout, stderr) - if p.stdin and not stdin: - p.stdin.close() - p.stdin = None - if p.stdin: - p.stdin = _codecs.getwriter(encoding=encoding)(stream=p.stdin) - stream_reader = _codecs.getreader(encoding=encoding) - if p.stdout: - p.stdout = stream_reader(stream=p.stdout) - if p.stderr: - p.stderr = stream_reader(stream=p.stderr) - return _SubprocessContextManager(args=args, process=p, expect=expect) - - -def _git(args, **kwargs): - args = ['git', '--git-dir', NMBGIT] + list(args) - return _spawn(args=args, **kwargs) - - -def _get_current_branch(): - """Get the name of the current branch. - - Return 'None' if we're not on a branch. - """ - try: - (status, branch, stderr) = _git( - args=['symbolic-ref', '--short', 'HEAD'], - stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, wait=True) - except SubprocessError as e: - if 'not a symbolic ref' in e: - return None - raise - return branch.strip() - - -def _get_remote(): - "Get the default remote for the current branch." - local_branch = _get_current_branch() - (status, remote, stderr) = _git( - args=['config', 'branch.{0}.remote'.format(local_branch)], - stdout=_subprocess.PIPE, wait=True) - return remote.strip() - - -def get_tags(prefix=None): - "Get a list of tags with a given prefix." - if prefix is None: - prefix = TAG_PREFIX - (status, stdout, stderr) = _spawn( - args=['notmuch', 'search', '--output=tags', '*'], - stdout=_subprocess.PIPE, wait=True) - return [tag for tag in stdout.splitlines() if tag.startswith(prefix)] - - -def archive(treeish='HEAD', args=()): - """ - Dump a tar archive of the current nmbug tag set. - - Using 'git archive'. - - Each tag $tag for message with Message-Id $id is written to - an empty file - - tags/encode($id)/encode($tag) - - The encoding preserves alphanumerics, and the characters - "+-_@=.:," (not the quotes). All other octets are replaced with - '%' followed by a two digit hex number. - """ - _git(args=['archive', treeish] + list(args), wait=True) - - -def clone(repository): - """ - Create a local nmbug repository from a remote source. - - This wraps 'git clone', adding some options to avoid creating a - working tree while preserving remote-tracking branches and - upstreams. - """ - with _tempfile.TemporaryDirectory(prefix='nmbug-clone.') as workdir: - _spawn( - args=[ - 'git', 'clone', '--no-checkout', '--separate-git-dir', NMBGIT, - repository, workdir], - wait=True) - _git(args=['config', '--unset', 'core.worktree'], wait=True, expect=(0, 5)) - _git(args=['config', 'core.bare', 'true'], wait=True) - _git(args=['branch', 'config', 'origin/config'], wait=True) - existing_tags = get_tags() - if existing_tags: - _LOG.warning( - 'Not checking out to avoid clobbering existing tags: {}'.format( - ', '.join(existing_tags))) - else: - checkout() - - -def _is_committed(status): - return len(status['added']) + len(status['deleted']) == 0 - - -def commit(treeish='HEAD', message=None): - """ - Commit prefix-matching tags from the notmuch database to Git. - """ - status = get_status() - - if _is_committed(status=status): - _LOG.warning('Nothing to commit') - return - - _git(args=['read-tree', '--empty'], wait=True) - _git(args=['read-tree', treeish], wait=True) - try: - _update_index(status=status) - (_, tree, _) = _git( - args=['write-tree'], - stdout=_subprocess.PIPE, - wait=True) - (_, parent, _) = _git( - args=['rev-parse', treeish], - stdout=_subprocess.PIPE, - wait=True) - (_, commit, _) = _git( - args=['commit-tree', tree.strip(), '-p', parent.strip()], - input=message, - stdout=_subprocess.PIPE, - wait=True) - _git( - args=['update-ref', treeish, commit.strip()], - stdout=_subprocess.PIPE, - wait=True) - except Exception as e: - _git(args=['read-tree', '--empty'], wait=True) - _git(args=['read-tree', treeish], wait=True) - raise - -def _update_index(status): - with _git( - args=['update-index', '--index-info'], - stdin=_subprocess.PIPE) as p: - for id, tags in status['deleted'].items(): - for line in _index_tags_for_message(id=id, status='D', tags=tags): - p.stdin.write(line) - for id, tags in status['added'].items(): - for line in _index_tags_for_message(id=id, status='A', tags=tags): - p.stdin.write(line) - - -def fetch(remote=None): - """ - Fetch changes from the remote repository. - - See 'merge' to bring those changes into notmuch. - """ - args = ['fetch'] - if remote: - args.append(remote) - _git(args=args, wait=True) - - -def checkout(): - """ - Update the notmuch database from Git. - - This is mainly useful to discard your changes in notmuch relative - to Git. - """ - status = get_status() - with _spawn( - args=['notmuch', 'tag', '--batch'], stdin=_subprocess.PIPE) as p: - for id, tags in status['added'].items(): - p.stdin.write(_batch_line(action='-', id=id, tags=tags)) - for id, tags in status['deleted'].items(): - p.stdin.write(_batch_line(action='+', id=id, tags=tags)) - - -def _batch_line(action, id, tags): - """ - 'notmuch tag --batch' line for adding/removing tags. - - Set 'action' to '-' to remove a tag or '+' to add the tags to a - given message id. - """ - tag_string = ' '.join( - '{action}{prefix}{tag}'.format( - action=action, prefix=_ENCODED_TAG_PREFIX, tag=_hex_quote(tag)) - for tag in tags) - line = '{tags} -- id:{id}\n'.format( - tags=tag_string, id=_xapian_quote(string=id)) - return line - - -def _insist_committed(): - "Die if the the notmuch tags don't match the current HEAD." - status = get_status() - if not _is_committed(status=status): - _LOG.error('\n'.join([ - 'Uncommitted changes to {prefix}* tags in notmuch', - '', - "For a summary of changes, run 'nmbug status'", - "To save your changes, run 'nmbug commit' before merging/pull", - "To discard your changes, run 'nmbug checkout'", - ]).format(prefix=TAG_PREFIX)) - _sys.exit(1) - - -def pull(repository=None, refspecs=None): - """ - Pull (merge) remote repository changes to notmuch. - - 'pull' is equivalent to 'fetch' followed by 'merge'. We use the - Git-configured repository for your current branch - (branch..repository, likely 'origin', and - branch..merge, likely 'master'). - """ - _insist_committed() - if refspecs and not repository: - repository = _get_remote() - args = ['pull'] - if repository: - args.append(repository) - if refspecs: - args.extend(refspecs) - with _tempfile.TemporaryDirectory(prefix='nmbug-pull.') as workdir: - for command in [ - ['reset', '--hard'], - args]: - _git( - args=command, - additional_env={'GIT_WORK_TREE': workdir}, - wait=True) - checkout() - - -def merge(reference='@{upstream}'): - """ - Merge changes from 'reference' into HEAD and load the result into notmuch. - - The default reference is '@{upstream}'. - """ - _insist_committed() - with _tempfile.TemporaryDirectory(prefix='nmbug-merge.') as workdir: - for command in [ - ['reset', '--hard'], - ['merge', reference]]: - _git( - args=command, - additional_env={'GIT_WORK_TREE': workdir}, - wait=True) - checkout() - - -def log(args=()): - """ - A simple wrapper for 'git log'. - - After running 'nmbug fetch', you can inspect the changes with - 'nmbug log HEAD..@{upstream}'. - """ - # we don't want output trapping here, because we want the pager. - args = ['log', '--name-status', '--no-renames'] + list(args) - with _git(args=args, expect=(0, 1, -13)) as p: - p.wait() - - -def push(repository=None, refspecs=None): - "Push the local nmbug Git state to a remote repository." - if refspecs and not repository: - repository = _get_remote() - args = ['push'] - if repository: - args.append(repository) - if refspecs: - args.extend(refspecs) - _git(args=args, wait=True) - - -def status(): - """ - Show pending updates in notmuch or git repo. - - Prints lines of the form - - ng Message-Id tag - - where n is a single character representing notmuch database status - - * A - - Tag is present in notmuch database, but not committed to nmbug - (equivalently, tag has been deleted in nmbug repo, e.g. by a - pull, but not restored to notmuch database). - - * D - - Tag is present in nmbug repo, but not restored to notmuch - database (equivalently, tag has been deleted in notmuch). - - * U - - Message is unknown (missing from local notmuch database). - - The second character (if present) represents a difference between - local and upstream branches. Typically 'nmbug fetch' needs to be - run to update this. - - * a - - Tag is present in upstream, but not in the local Git branch. - - * d - - Tag is present in local Git branch, but not upstream. - """ - status = get_status() - # 'output' is a nested defaultdict for message status: - # * The outer dict is keyed by message id. - # * The inner dict is keyed by tag name. - # * The inner dict values are status strings (' a', 'Dd', ...). - output = _collections.defaultdict( - lambda : _collections.defaultdict(lambda : ' ')) - for id, tags in status['added'].items(): - for tag in tags: - output[id][tag] = 'A' - for id, tags in status['deleted'].items(): - for tag in tags: - output[id][tag] = 'D' - for id, tags in status['missing'].items(): - for tag in tags: - output[id][tag] = 'U' - if _is_unmerged(): - for id, tag in _diff_refs(filter='A'): - output[id][tag] += 'a' - for id, tag in _diff_refs(filter='D'): - output[id][tag] += 'd' - for id, tag_status in sorted(output.items()): - for tag, status in sorted(tag_status.items()): - print('{status}\t{id}\t{tag}'.format( - status=status, id=id, tag=tag)) - - -def _is_unmerged(ref='@{upstream}'): - try: - (status, fetch_head, stderr) = _git( - args=['rev-parse', ref], - stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, wait=True) - except SubprocessError as e: - if 'No upstream configured' in e.stderr: - return - raise - (status, base, stderr) = _git( - args=['merge-base', 'HEAD', ref], - stdout=_subprocess.PIPE, wait=True) - return base != fetch_head - - -def get_status(): - status = { - 'deleted': {}, - 'missing': {}, - } - index = _index_tags() - maybe_deleted = _diff_index(index=index, filter='D') - for id, tags in maybe_deleted.items(): - (_, stdout, stderr) = _spawn( - args=['notmuch', 'search', '--output=files', 'id:{0}'.format(id)], - stdout=_subprocess.PIPE, - wait=True) - if stdout: - status['deleted'][id] = tags - else: - status['missing'][id] = tags - status['added'] = _diff_index(index=index, filter='A') - _os.remove(index) - return status - - -def _index_tags(): - "Write notmuch tags to the nmbug.index." - path = _os.path.join(NMBGIT, 'nmbug.index') - query = ' '.join('tag:"{tag}"'.format(tag=tag) for tag in get_tags()) - prefix = '+{0}'.format(_ENCODED_TAG_PREFIX) - _git( - args=['read-tree', '--empty'], - additional_env={'GIT_INDEX_FILE': path}, wait=True) - with _spawn( - args=['notmuch', 'dump', '--format=batch-tag', '--', query], - stdout=_subprocess.PIPE) as notmuch: - with _git( - args=['update-index', '--index-info'], - stdin=_subprocess.PIPE, - additional_env={'GIT_INDEX_FILE': path}) as git: - for line in notmuch.stdout: - if line.strip().startswith('#'): - continue - (tags_string, id) = [_.strip() for _ in line.split(' -- id:')] - tags = [ - _unquote(tag[len(prefix):]) - for tag in tags_string.split() - if tag.startswith(prefix)] - id = _xapian_unquote(string=id) - for line in _index_tags_for_message( - id=id, status='A', tags=tags): - git.stdin.write(line) - return path - - -def _index_tags_for_message(id, status, tags): - """ - Update the Git index to either create or delete an empty file. - - Neither 'id' nor the tags in 'tags' should be encoded/escaped. - """ - mode = '100644' - hash = _EMPTYBLOB - - if status == 'D': - mode = '0' - hash = '0000000000000000000000000000000000000000' - - for tag in tags: - path = 'tags/{id}/{tag}'.format( - id=_hex_quote(string=id), tag=_hex_quote(string=tag)) - yield '{mode} {hash}\t{path}\n'.format(mode=mode, hash=hash, path=path) - - -def _diff_index(index, filter): - """ - Get an {id: {tag, ...}} dict for a given filter. - - For example, use 'A' to find added tags, and 'D' to find deleted tags. - """ - s = _collections.defaultdict(set) - with _git( - args=[ - 'diff-index', '--cached', '--diff-filter', filter, - '--name-only', 'HEAD'], - additional_env={'GIT_INDEX_FILE': index}, - stdout=_subprocess.PIPE) as p: - # Once we drop Python < 3.3, we can use 'yield from' here - for id, tag in _unpack_diff_lines(stream=p.stdout): - s[id].add(tag) - return s - - -def _diff_refs(filter, a='HEAD', b='@{upstream}'): - with _git( - args=['diff', '--diff-filter', filter, '--name-only', a, b], - stdout=_subprocess.PIPE) as p: - # Once we drop Python < 3.3, we can use 'yield from' here - for id, tag in _unpack_diff_lines(stream=p.stdout): - yield id, tag - - -def _unpack_diff_lines(stream): - "Iterate through (id, tag) tuples in a diff stream." - for line in stream: - match = _TAG_FILE_REGEX.match(line.strip()) - if not match: - message = 'non-tag line in diff: {!r}'.format(line.strip()) - if line.startswith(_TAG_DIRECTORY): - raise ValueError(message) - _LOG.info(message) - continue - id = _unquote(match.group('id')) - tag = _unquote(match.group('tag')) - yield (id, tag) - - -def _help(parser, command=None): - """ - Show help for an nmbug command. - - Because some folks prefer: - - $ nmbug help COMMAND - - to - - $ nmbug COMMAND --help - """ - if command: - parser.parse_args([command, '--help']) - else: - parser.parse_args(['--help']) - - -if __name__ == '__main__': - import argparse - - parser = argparse.ArgumentParser( - description=__doc__.strip(), - formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument( - '-v', '--version', action='version', - version='%(prog)s {}'.format(__version__)) - parser.add_argument( - '-l', '--log-level', - choices=['critical', 'error', 'warning', 'info', 'debug'], - help='Log verbosity. Defaults to {!r}.'.format( - _logging.getLevelName(_LOG.level).lower())) - - help = _functools.partial(_help, parser=parser) - help.__doc__ = _help.__doc__ - subparsers = parser.add_subparsers( - title='commands', - description=( - 'For help on a particular command, run: ' - "'%(prog)s ... --help'.")) - for command in [ - 'archive', - 'checkout', - 'clone', - 'commit', - 'fetch', - 'help', - 'log', - 'merge', - 'pull', - 'push', - 'status', - ]: - func = locals()[command] - doc = _textwrap.dedent(func.__doc__).strip().replace('%', '%%') - subparser = subparsers.add_parser( - command, - help=doc.splitlines()[0], - description=doc, - formatter_class=argparse.RawDescriptionHelpFormatter) - subparser.set_defaults(func=func) - if command == 'archive': - subparser.add_argument( - 'treeish', metavar='TREE-ISH', nargs='?', default='HEAD', - help=( - 'The tree or commit to produce an archive for. Defaults ' - "to 'HEAD'.")) - subparser.add_argument( - 'args', metavar='ARG', nargs='*', - help=( - "Argument passed through to 'git archive'. Set anything " - 'before , see git-archive(1) for details.')) - elif command == 'clone': - subparser.add_argument( - 'repository', - help=( - 'The (possibly remote) repository to clone from. See the ' - 'URLS section of git-clone(1) for more information on ' - 'specifying repositories.')) - elif command == 'commit': - subparser.add_argument( - 'message', metavar='MESSAGE', default='', nargs='?', - help='Text for the commit message.') - elif command == 'fetch': - subparser.add_argument( - 'remote', metavar='REMOTE', nargs='?', - help=( - 'Override the default configured in branch..remote ' - 'to fetch from a particular remote repository (e.g. ' - "'origin').")) - elif command == 'help': - subparser.add_argument( - 'command', metavar='COMMAND', nargs='?', - help='The command to show help for.') - elif command == 'log': - subparser.add_argument( - 'args', metavar='ARG', nargs='*', - help="Additional argument passed through to 'git log'.") - elif command == 'merge': - subparser.add_argument( - 'reference', metavar='REFERENCE', default='@{upstream}', - nargs='?', - help=( - 'Reference, usually other branch heads, to merge into ' - "our branch. Defaults to '@{upstream}'.")) - elif command == 'pull': - subparser.add_argument( - 'repository', metavar='REPOSITORY', default=None, nargs='?', - help=( - 'The "remote" repository that is the source of the pull. ' - 'This parameter can be either a URL (see the section GIT ' - 'URLS in git-pull(1)) or the name of a remote (see the ' - 'section REMOTES in git-pull(1)).')) - subparser.add_argument( - 'refspecs', metavar='REFSPEC', default=None, nargs='*', - help=( - 'Refspec (usually a branch name) to fetch and merge. See ' - 'the entry in the OPTIONS section of ' - 'git-pull(1) for other possibilities.')) - elif command == 'push': - subparser.add_argument( - 'repository', metavar='REPOSITORY', default=None, nargs='?', - help=( - 'The "remote" repository that is the destination of the ' - 'push. This parameter can be either a URL (see the ' - 'section GIT URLS in git-push(1)) or the name of a remote ' - '(see the section REMOTES in git-push(1)).')) - subparser.add_argument( - 'refspecs', metavar='REFSPEC', default=None, nargs='*', - help=( - 'Refspec (usually a branch name) to push. See ' - 'the entry in the OPTIONS section of ' - 'git-push(1) for other possibilities.')) - - args = parser.parse_args() - - if args.log_level: - level = getattr(_logging, args.log_level.upper()) - _LOG.setLevel(level) - - if not getattr(args, 'func', None): - parser.print_usage() - _sys.exit(1) - - if args.func == help: - arg_names = ['command'] - else: - (arg_names, varargs, varkw) = _inspect.getargs(args.func.__code__) - kwargs = {key: getattr(args, key) for key in arg_names if key in args} - try: - args.func(**kwargs) - except SubprocessError as e: - if _LOG.level == _logging.DEBUG: - raise # don't mask the traceback - _LOG.error(str(e)) - _sys.exit(1) diff --git a/devel/notmuch-web/nmgunicorn.py b/devel/notmuch-web/nmgunicorn.py new file mode 100644 index 00000000..e71ba12a --- /dev/null +++ b/devel/notmuch-web/nmgunicorn.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +# to launch nmweb from gunicorn. + +from nmweb import urls, index, search, show +import web + +app = web.application(urls, globals()) + +# get the wsgi app from web.py application object +wsgiapp = app.wsgifunc() diff --git a/devel/notmuch-web/nmweb.py b/devel/notmuch-web/nmweb.py new file mode 100755 index 00000000..e0e87b49 --- /dev/null +++ b/devel/notmuch-web/nmweb.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python + +from __future__ import absolute_import + +try: + from urllib.parse import quote_plus + from urllib.parse import unquote_plus +except ImportError: + from urllib import quote_plus + from urllib import unquote_plus + +from datetime import datetime +from mailbox import MaildirMessage +import mimetypes +import email +import re +import html +import os + +import bleach +import web +from notmuch2 import Database +from jinja2 import Environment, FileSystemLoader # FIXME to PackageLoader +from jinja2 import Markup +try: + import bjoern # from https://github.com/jonashaag/bjoern/ + use_bjoern = True +except: + use_bjoern = False + +# Configuration options +safe_tags = bleach.sanitizer.ALLOWED_TAGS + \ + [u'div', u'span', u'p', u'br', u'table', u'tr', u'td', u'th'] +linkify_plaintext = True # delays page load by about 0.02s of 0.20s budget +show_thread_nav = True # delays page load by about 0.04s of 0.20s budget + +prefix = os.environ.get('NMWEB_PREFIX', "http://localhost:8080") +webprefix = os.environ.get('NMWEB_STATIC', prefix + "/static") +cachedir = os.environ.get('NMWEB_CACHE', "static/cache") # special for webpy server; changeable if using your own +cachepath = os.environ.get('NMWEB_CACHE_PATH', cachedir) # location of static cache in the local filesystem + +if 'NMWEB_DEBUG' in os.environ: + web.config.debug = True +else: + web.config.debug = False + +# End of config options + +env = Environment(autoescape=True, + loader=FileSystemLoader('templates')) + +urls = ( + '/', 'index', + '/search/(.*)', 'search', + '/show/(.*)', 'show', +) + +def urlencode_filter(s): + if type(s) == 'Markup': + s = s.unescape() + s = s.encode('utf8') + s = quote_plus(s) + return Markup(s) +env.filters['url'] = urlencode_filter + +class index: + def GET(self): + web.header('Content-type', 'text/html') + base = env.get_template('base.html') + template = env.get_template('index.html') + db = Database() + tags = db.tags + return template.render(tags=tags, + title="Notmuch webmail", + prefix=prefix, + sprefix=webprefix) + +class search: + def GET(self, terms): + redir = False + if web.input(terms=None).terms: + redir = True + terms = web.input().terms + terms = unquote_plus (terms) + if web.input(afters=None).afters: + afters = web.input(afters=None).afters[:-3] + else: + afters = '0' + if web.input(befores=None).befores: + befores = web.input(befores=None).befores + else: + befores = '4294967296' # 2^32 + try: + if int(afters) > 0 or int(befores) < 4294967296: + redir = True + terms += ' date:@%s..@%s' % (int(afters), int(befores)) + except ValueError: + pass + if redir: + raise web.seeother('/search/%s' % quote_plus(terms.encode('utf8'))) + web.header('Content-type', 'text/html') + db = Database() + ts = db.threads(query=terms, sort=Database.SORT.NEWEST_FIRST) + template = env.get_template('search.html') + return template.generate(terms=terms, + ts=ts, + title=terms, + prefix=prefix, + sprefix=webprefix) + +def format_time_range(start, end): + if end-start < (60*60*24): + time = datetime.fromtimestamp(start).strftime('%Y %b %d %H:%M') + else: + start = datetime.fromtimestamp(start).strftime("%Y %b %d") + end = datetime.fromtimestamp(end).strftime("%Y %b %d") + time = "%s through %s" % (start, end) + return time +env.globals['format_time_range'] = format_time_range + +def mailto_addrs(msg,header_name): + try: + hdr = msg.header(header_name) + except LookupError: + return '' + + frm = email.utils.getaddresses([hdr]) + return ', '.join(['%s' % ((l, p) if p else (l, l)) for (p, l) in frm]) +env.globals['mailto_addrs'] = mailto_addrs + +def link_msg(msg): + lnk = quote_plus(msg.messageid.encode('utf8')) + try: + subj = html.escape(msg.header('Subject')) + except LookupError: + subj = "" + out = '%s' % (prefix, lnk, subj) + return out +env.globals['link_msg'] = link_msg + +def show_msgs(msgs): + r = '
    ' + for msg in msgs: + red = 'color:black; font-style:normal' + if msg.matched: + red = 'color:red; font-style:italic' + frm = mailto_addrs(msg,'From') + lnk = link_msg(msg) + tags = ", ".join(msg.tags) + rs = show_msgs(msg.replies()) + r += '
  • %s—%s [%s] %s
  • ' % (red, frm, lnk, tags, rs) + r += '
' + return r +env.globals['show_msgs'] = show_msgs + +# As email.message.walk, but showing close tags as well +def mywalk(self): + yield self + if self.is_multipart(): + for subpart in self.get_payload(): + for subsubpart in mywalk(subpart): + yield subsubpart + yield 'close-div' + +class show: + def GET(self, mid): + web.header('Content-type', 'text/html') + db = Database() + try: + m = db.find(mid) + except: + raise web.notfound("No such message id.") + template = env.get_template('show.html') + # FIXME add reply-all link with email.urils.getaddresses + # FIXME add forward link using mailto with body parameter? + return template.render(m=m, + mid=mid, + title=m.header('Subject'), + prefix=prefix, + sprefix=webprefix) + +def thread_nav(m): + if not show_thread_nav: return + db = Database() + thread = next(db.threads('thread:'+m.threadid)) + prv = None + found = False + nxt = None + for msg in thread: + if m == msg: + found = True + elif not found: + prv = msg + else: # found message, but not on this loop + nxt = msg + break + yield "
    " + if prv: yield "
  • Previous message (by thread): %s
  • " % link_msg(prv) + if nxt: yield "
  • Next message (by thread): %s
  • " % link_msg(nxt) + yield "

Thread:

" + # FIXME show now takes three queries instead of 1; + # can we yield the message body while computing the thread shape? + thread = next(db.threads('thread:'+m.threadid)) + yield show_msgs(thread.toplevel()) + return +env.globals['thread_nav'] = thread_nav + +def format_message(nm_msg, mid): + fn = list(nm_msg.filenames())[0] + msg = MaildirMessage(open(fn, 'rb')) + return format_message_walk(msg, mid) + +def decodeAnyway(txt, charset='ascii'): + try: + out = txt.decode(charset) + except: + try: + out = txt.decode('utf-8') + except UnicodeDecodeError: + out = txt.decode('latin1') + return out + +def require_protocol_prefix(attrs, new=False): + if not new: + return attrs + link_text = attrs[u'_text'] + if link_text.startswith(('http:', 'https:', 'mailto:', 'git:', 'id:')): + return attrs + return None + +# Bleach doesn't even try to linkify id:... text, so no point invoking this yet +def modify_id_links(attrs, new=False): + if attrs[(None, u'href')].startswith(u'id:'): + attrs[(None, u'href')] = prefix + "/show/" + attrs[(None, u'href')][3:] + return attrs + +def css_part_id(content_type, parts=[]): + c = content_type.replace('/', '-') + out = "-".join(parts + [c]) + return out + +def format_message_walk(msg, mid): + counter = 0 + cid_refd = [] + parts = ['main'] + for part in mywalk(msg): + if part == 'close-div': + parts.pop() + yield '' + elif part.get_content_maintype() == 'multipart': + yield '
' % \ + (part.get_content_subtype(), css_part_id(part.get_content_type(), parts)) + parts.append(part.get_content_subtype()) + if part.get_content_subtype() == 'alternative': + yield '
    ' + for subpart in part.get_payload(): + yield ('
  • %s
  • ' % + (css_part_id(subpart.get_content_type(), parts), + subpart.get_content_type())) + yield '
' + elif part.get_content_type() == 'message/rfc822': + # FIXME extract subject, date, to/cc/from into a separate template and use it here + yield '
' + elif part.get_content_maintype() == 'text': + if part.get_content_subtype() == 'plain': + yield '
' % css_part_id(part.get_content_type(), parts) + yield '
'
+        out = part.get_payload(decode=True)
+        out = decodeAnyway(out, part.get_content_charset('ascii'))
+        out = html.escape(out)
+        out = out.encode('ascii', 'xmlcharrefreplace').decode('ascii')
+        if linkify_plaintext: out = bleach.linkify(out, callbacks=[require_protocol_prefix])
+        yield out
+        yield '
' + elif part.get_content_subtype() == 'html': + yield '
' % css_part_id(part.get_content_type(), parts) + unb64 = part.get_payload(decode=True) + decoded = decodeAnyway(unb64, part.get_content_charset('ascii')) + cid_refd += find_cids(decoded) + part.set_payload(bleach.clean(replace_cids(decoded, mid), tags=safe_tags). + encode(part.get_content_charset('ascii'), 'xmlcharrefreplace')) + (filename, cid) = link_to_cached_file(part, mid, counter) + counter += 1 + yield '' % \ + os.path.join(prefix, cachedir, mid, filename) + yield '
' + else: + yield '
' % css_part_id(part.get_content_type(), parts) + (filename, cid) = link_to_cached_file(part, mid, counter) + counter += 1 + yield '%s (%s)' % (os.path.join(prefix, + cachedir, + mid, + filename), + filename, + part.get_content_type()) + yield '
' + elif part.get_content_maintype() == 'image': + (filename, cid) = link_to_cached_file(part, mid, counter) + if cid not in cid_refd: + counter += 1 + yield '%s' % (os.path.join(prefix, + cachedir, + mid, + filename), + filename) + else: + (filename, cid) = link_to_cached_file(part, mid, counter) + counter += 1 + yield '%s (%s)' % (os.path.join(prefix, + cachedir, + mid, + filename), + filename, + part.get_content_type()) +env.globals['format_message'] = format_message + +def replace_cids(body, mid): + return body.replace('cid:', os.path.join(prefix, cachedir, mid)+'/') + +def find_cids(body): + return re.findall(r'cid:([^ "\'>]*)', body) + +def link_to_cached_file(part, mid, counter): + filename = part.get_filename() + if not filename: + ext = mimetypes.guess_extension(part.get_content_type()) + if not ext: + ext = '.bin' + filename = 'part-%03d%s' % (counter, ext) + try: + os.makedirs(os.path.join(cachepath, mid)) + except OSError: + pass + fn = os.path.join(cachepath, mid, filename) # FIXME escape mid, filename + fp = open(fn, 'wb') + if part.get_content_maintype() == 'text': + data = part.get_payload(decode=True) + data = decodeAnyway(data, part.get_content_charset('ascii')).encode('utf-8') + else: + try: + data = part.get_payload(decode=True) + except: + data = part.get_payload(decode=False) + if data: + fp.write(data) + fp.close() + if 'Content-ID' in part: + cid = part['Content-ID'] + if cid[0] == '<' and cid[-1] == '>': cid = cid[1:-1] + cid_fn = os.path.join(cachepath, mid, cid) # FIXME escape mid, cid + try: + os.unlink(cid_fn) + except OSError: + pass + os.link(fn, cid_fn) + return (filename, cid) + else: + return (filename, None) + +if __name__ == '__main__': + app = web.application(urls, globals()) + if use_bjoern: + bjoern.run(app.wsgifunc(), "127.0.0.1", 8080) + else: + app.run() diff --git a/devel/notmuch-web/static/css/jquery-ui.css b/devel/notmuch-web/static/css/jquery-ui.css new file mode 120000 index 00000000..eba7c769 --- /dev/null +++ b/devel/notmuch-web/static/css/jquery-ui.css @@ -0,0 +1 @@ +/usr/share/javascript/jquery-ui/themes/base/jquery-ui.min.css \ No newline at end of file diff --git a/devel/notmuch-web/static/css/notmuch-0.1.css b/devel/notmuch-web/static/css/notmuch-0.1.css new file mode 100644 index 00000000..0f085644 --- /dev/null +++ b/devel/notmuch-web/static/css/notmuch-0.1.css @@ -0,0 +1,15 @@ +pre { + white-space: pre-wrap; +} + +.message-rfc822 { + border: 1px solid; + border-radius: 25px; +} + +.embedded-html { + frameborder: 0; + border: 0; + scrolling: no; + width: 100%; +} diff --git a/devel/notmuch-web/static/js/jquery-ui.js b/devel/notmuch-web/static/js/jquery-ui.js new file mode 120000 index 00000000..5c053bab --- /dev/null +++ b/devel/notmuch-web/static/js/jquery-ui.js @@ -0,0 +1 @@ +/usr/share/javascript/jquery-ui/jquery-ui.min.js \ No newline at end of file diff --git a/devel/notmuch-web/static/js/jquery.js b/devel/notmuch-web/static/js/jquery.js new file mode 120000 index 00000000..7fff8870 --- /dev/null +++ b/devel/notmuch-web/static/js/jquery.js @@ -0,0 +1 @@ +/usr/share/javascript/jquery/jquery.min.js \ No newline at end of file diff --git a/devel/notmuch-web/static/js/notmuch-0.1.js b/devel/notmuch-web/static/js/notmuch-0.1.js new file mode 100644 index 00000000..ed6e9f4a --- /dev/null +++ b/devel/notmuch-web/static/js/notmuch-0.1.js @@ -0,0 +1,35 @@ +$(function(){ + $("#after").datepicker({ + altField: "#afters", + altFormat: "@", + changeMonth: true, + changeYear: true, + defaultDate: "-7d", + minDate: "01/01/1970", + yearRange: "2000:+0", + onSelect: function(selectedDate) { + $("#before").datepicker("option","minDate",selectedDate); + } + }); + $("#before").datepicker({ + altField: "#befores", + altFormat: "@", + changeMonth: true, + changeYear: true, + defaultDate: "+1d", + maxDate: "+1d", + yearRange: "2000:+0", + onSelect: function(selectedDate) { + $("#after").datepicker("option","maxDate",selectedDate); + } + }); + $(function(){ + $('.multipart-alternative').tabs() + }); + $(function(){ + $('.embedded-html').on('load',function(){ + this.style.height = this.contentWindow.document.body.offsetHeight + 'px'; + }); + }); +}); + diff --git a/devel/notmuch-web/templates/base.html b/devel/notmuch-web/templates/base.html new file mode 100644 index 00000000..90d92931 --- /dev/null +++ b/devel/notmuch-web/templates/base.html @@ -0,0 +1,39 @@ + + + + + + + + + + +{{title}} + +
+
+{% block searchform %} +
+ + + + +
+{% endblock searchform %} +

{{title}}

+
+
+{% block content %} +

Common tags

+ +
+{% endblock content %} +
+ diff --git a/devel/notmuch-web/templates/index.html b/devel/notmuch-web/templates/index.html new file mode 100644 index 00000000..0eb3fd3c --- /dev/null +++ b/devel/notmuch-web/templates/index.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block content %} +

Common tags

+ +{% endblock content %} diff --git a/devel/notmuch-web/templates/search.html b/devel/notmuch-web/templates/search.html new file mode 100644 index 00000000..6719c356 --- /dev/null +++ b/devel/notmuch-web/templates/search.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +

{{ terms|e }}

+{% block content %} +{% for t in ts %} +

{{ t.subject|e }}

+

{{ t.authors|e }}

+

{{ format_time_range(t.first,t.last)|e }}

+ {{ show_msgs(t.toplevel())|safe }} +{% endfor %} +{% endblock content %} diff --git a/devel/notmuch-web/templates/show.html b/devel/notmuch-web/templates/show.html new file mode 100644 index 00000000..690f5464 --- /dev/null +++ b/devel/notmuch-web/templates/show.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block content %} +{% set headers = ['Subject', 'Date'] %} +{% set addr_headers = ['To', 'Cc', 'From'] %} +{% for header in headers: %} +

{{header}}: {{m.header(header)|e}}

+{% endfor %} +{% for header in addr_headers: %} +

{{header}}: {{mailto_addrs(m,header)|safe}}

+{% endfor %} +
+{% for part in format_message(m,mid): %}{{ part|safe }}{% endfor %} +{% for b in thread_nav(m): %}{{b|safe}}{% endfor %} +
+{% endblock content %} diff --git a/devel/notmuch-web/todo b/devel/notmuch-web/todo new file mode 100644 index 00000000..3c885bd9 --- /dev/null +++ b/devel/notmuch-web/todo @@ -0,0 +1,14 @@ +review escaping and safety handling mail from Bad People + +revise template loader---can we make this faster? + +add reply-all link with email.urils.getaddresses + +change current reply links to quote body + +add forward link using mailto with body parameter? + +unescape the current search term, including translating back dates + + +later: json support, iOS app? diff --git a/devel/release-checks.sh b/devel/release-checks.sh index 23c29eaa..4f81722b 100755 --- a/devel/release-checks.sh +++ b/devel/release-checks.sh @@ -18,8 +18,6 @@ readonly DEFAULT_IFS="$IFS" # Note: In this particular case quotes are needed. # Avoid locale-specific differences in output of executed commands LANG=C LC_ALL=C; export LANG LC_ALL -readonly PV_FILE='bindings/python/notmuch/version.py' - # Using array here turned out to be unnecessarily complicated emsgs='' emsg_count=0 @@ -29,7 +27,7 @@ append_emsg () emsgs="${emsgs:+$emsgs\n} $1" } -for f in ./version.txt debian/changelog NEWS "$PV_FILE" +for f in ./version.txt debian/changelog NEWS do if [ ! -f "$f" ]; then append_emsg "File '$f' is missing" elif [ ! -r "$f" ]; then append_emsg "File '$f' is unreadable" @@ -59,7 +57,7 @@ readonly VERSION # In the rest of this file, tests collect list of errors to be fixed -echo -n "Checking that git working directory is clean... " +printf %s "Checking that git working directory is clean... " git_status=`git status --porcelain` if [ "$git_status" = '' ] then @@ -77,7 +75,7 @@ verfail () append_emsg " Please follow the instructions in RELEASING to choose a version" } -echo -n "Checking that '$VERSION' is good with digits and periods... " +printf %s "Checking that '$VERSION' is good with digits and periods... " case $VERSION in *[!0-9.]*) verfail "'$VERSION' contains other characters than digits and periods" ;; @@ -88,7 +86,7 @@ case $VERSION in *) verfail "'$VERSION' is a single number" ;; esac -echo -n "Checking that this is Debian package for notmuch... " +printf %s "Checking that this is Debian package for notmuch... " read deb_notmuch deb_version rest < debian/changelog if [ "$deb_notmuch" = 'notmuch' ] then @@ -98,7 +96,7 @@ else append_emsg "Package name '$deb_notmuch' is not 'notmuch' in debian/changelog" fi -echo -n "Checking that Debian package version is $VERSION-1... " +printf %s "Checking that Debian package version is $VERSION-1... " if [ "$deb_version" = "($VERSION-1)" ] then @@ -108,17 +106,7 @@ else append_emsg "Version '$deb_version' is not '($VERSION-1)' in debian/changelog" fi -echo -n "Checking that python bindings version is $VERSION... " -py_version=`python3 -c "with open('$PV_FILE') as vf: exec(vf.read()); print(__VERSION__)"` -if [ "$py_version" = "$VERSION" ] -then - echo Yes. -else - echo No. - append_emsg "Version '$py_version' is not '$VERSION' in $PV_FILE" -fi - -echo -n "Checking that NEWS header is tidy... " +printf %s "Checking that NEWS header is tidy... " if [ "`exec sed 's/./=/g; 1q' NEWS`" = "`exec sed '1d; 2q' NEWS`" ] then echo Yes. @@ -132,7 +120,7 @@ else fi fi -echo -n "Checking that this is Notmuch NEWS... " +printf %s "Checking that this is Notmuch NEWS... " read news_notmuch news_version news_date < NEWS if [ "$news_notmuch" = "Notmuch" ] then @@ -142,7 +130,7 @@ else append_emsg "First word '$news_notmuch' is not 'Notmuch' in NEWS file" fi -echo -n "Checking that NEWS version is $VERSION... " +printf %s "Checking that NEWS version is $VERSION... " if [ "$news_version" = "$VERSION" ] then echo Yes. @@ -154,7 +142,7 @@ fi #eval `date '+year=%Y mon=%m day=%d'` today0utc=`date --date=0Z +%s` # gnu date feature -echo -n "Checking that NEWS date is right... " +printf %s "Checking that NEWS date is right... " case $news_date in '('[2-9][0-9][0-9][0-9]-[01][0-9]-[0123][0-9]')') newsdate0utc=`nd=${news_date#\\(}; date --date="${nd%)} 0Z" +%s` @@ -176,7 +164,7 @@ case $news_date in esac year=`exec date +%Y` -echo -n "Checking that copyright in documentation contains 2009-$year... " +printf %s "Checking that copyright in documentation contains 2009-$year... " # Read the value of variable `copyright' defined in 'doc/conf.py'. copyrightline=$(grep ^copyright doc/conf.py) case $copyrightline in diff --git a/devel/schemata b/devel/schemata index 01e3a3df..4e05cdac 100644 --- a/devel/schemata +++ b/devel/schemata @@ -76,6 +76,7 @@ message = { # (format_message_sprinter) id: messageid, match: bool, + excluded: bool, filename: [string*], timestamp: unix_time, # date header as unix time date_relative: string, # user-friendly timestamp @@ -83,6 +84,7 @@ message = { headers: headers, crypto: crypto, + duplicate: integer, body?: [part] # omitted if --body=false } @@ -145,9 +147,11 @@ headers = { Cc?: string, Bcc?: string, Reply-To?: string, - Date: string + Date: string, + extra_header_pair* } +extra_header_pair= (header_name: string) # Encryption status (format_part_sprinter) encstatus = [{status: "good"|"bad"}] diff --git a/doc/Makefile.local b/doc/Makefile.local index f476d1da..aafa77a0 100644 --- a/doc/Makefile.local +++ b/doc/Makefile.local @@ -4,7 +4,7 @@ dir := doc # You can set these variables from the command line. SPHINXOPTS := -q -SPHINXBUILD = sphinx-build +SPHINXBUILD = env LD_LIBRARY_PATH=${NOTMUCH_BUILDDIR}/lib sphinx-build DOCBUILDDIR := $(dir)/_build # Internal variables. @@ -18,7 +18,9 @@ MAN7_RST := $(wildcard $(srcdir)/doc/man7/*.rst) MAN_RST_FILES := $(MAN1_RST) $(MAN5_RST) $(MAN7_RST) ALL_RST_FILES := $(MAN_RST_FILES) $(srcdir)/doc/notmuch-emacs.rst +COPY_ROFF1 := $(patsubst %,$(DOCBUILDDIR)/man/man1/%.1,nmbug notmuch-setup) MAN1_ROFF := $(patsubst $(srcdir)/doc/%,$(DOCBUILDDIR)/man/%,$(MAN1_RST:.rst=.1)) +MAN1_ROFF := $(MAN1_ROFF) $(COPY_ROFF1) MAN5_ROFF := $(patsubst $(srcdir)/doc/%,$(DOCBUILDDIR)/man/%,$(MAN5_RST:.rst=.5)) MAN7_ROFF := $(patsubst $(srcdir)/doc/%,$(DOCBUILDDIR)/man/%,$(MAN7_RST:.rst=.7)) MAN_ROFF_FILES := $(MAN1_ROFF) $(MAN5_ROFF) $(MAN7_ROFF) @@ -33,7 +35,8 @@ ifeq ($(WITH_EMACS),1) INFO_TEXI_FILES += $(DOCBUILDDIR)/texinfo/notmuch-emacs.texi endif -INFO_INFO_FILES := $(INFO_TEXI_FILES:.texi=.info) +COPY_INFO1 := $(patsubst $(DOCBUILDDIR)/man/man1/%.1,$(DOCBUILDDIR)/texinfo/%.info,$(COPY_ROFF1)) +INFO_INFO_FILES := $(INFO_TEXI_FILES:.texi=.info) $(COPY_INFO1) .PHONY: sphinx-html sphinx-texinfo sphinx-info @@ -46,20 +49,29 @@ ifeq ($(WITH_EMACS),1) $(DOCBUILDDIR)/.roff.stamp $(DOCBUILDDIR)/.html.stamp $(DOCBUILDDIR)/.texi.stamp : docstring.stamp endif +ifeq ($(HAVE_PYTHON3_CFFI),1) +DOC_PREREQS=bindings/python-cffi.stamp +else +DOC_PREREQS= +endif + sphinx-html: $(DOCBUILDDIR)/.html.stamp -$(DOCBUILDDIR)/.html.stamp: $(ALL_RST_FILES) +$(DOCBUILDDIR)/.html.stamp: $(ALL_RST_FILES) $(DOC_PREREQS) $(SPHINXBUILD) -b html -d $(DOCBUILDDIR)/html_doctrees $(ALLSPHINXOPTS) $(DOCBUILDDIR)/html touch $@ sphinx-texinfo: $(DOCBUILDDIR)/.texi.stamp -$(DOCBUILDDIR)/.texi.stamp: $(ALL_RST_FILES) +$(DOCBUILDDIR)/.texi.stamp: $(ALL_RST_FILES) $(DOC_PREREQS) $(SPHINXBUILD) -b texinfo -d $(DOCBUILDDIR)/texinfo_doctrees $(ALLSPHINXOPTS) $(DOCBUILDDIR)/texinfo touch $@ -sphinx-info: sphinx-texinfo +sphinx-info: $(DOCBUILDDIR)/.info.stamp + +$(DOCBUILDDIR)/.info.stamp: $(DOCBUILDDIR)/.texi.stamp $(DOC_PREREQS) $(MAKE) -C $(DOCBUILDDIR)/texinfo info + touch $@ # Use the man page converter that is available. We should never depend # on MAN_ROFF_FILES if a converter is not available. @@ -108,6 +120,11 @@ build-man: install-man: @echo "No sphinx, will not install man pages." else + +# it should be safe to depend on the stamp file, because it is created +# after all roff files are moved into place. +${MAN_GZIP_FILES}: ${DOCBUILDDIR}/.roff.stamp + build-man: ${MAN_GZIP_FILES} install-man: ${MAN_GZIP_FILES} mkdir -m0755 -p "$(DESTDIR)$(mandir)/man1" @@ -116,14 +133,13 @@ install-man: ${MAN_GZIP_FILES} install -m0644 $(filter %.1.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man1 install -m0644 $(filter %.5.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man5 install -m0644 $(filter %.7.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man7 - cd $(DESTDIR)/$(mandir)/man1 && ln -sf notmuch.1.gz notmuch-setup.1.gz endif ifneq ($(HAVE_SPHINX)$(HAVE_MAKEINFO),11) build-info: @echo "Missing sphinx or makeinfo, not building info pages" else -build-info: sphinx-info +build-info: $(DOCBUILDDIR)/.info.stamp endif ifneq ($(HAVE_SPHINX)$(HAVE_MAKEINFO)$(HAVE_INSTALL_INFO),111) @@ -141,5 +157,7 @@ $(dir)/config.dox: version.stamp echo "INPUT=${srcdir}/lib/notmuch.h" >> $@ CLEAN := $(CLEAN) $(DOCBUILDDIR) $(DOCBUILDDIR)/.roff.stamp $(DOCBUILDDIR)/.texi.stamp -CLEAN := $(CLEAN) $(DOCBUILDDIR)/.html.stamp +CLEAN := $(CLEAN) $(DOCBUILDDIR)/.html.stamp $(DOCBUILDDIR)/.info.stamp CLEAN := $(CLEAN) $(MAN_GZIP_FILES) $(MAN_ROFF_FILES) $(dir)/conf.pyc $(dir)/config.dox + +CLEAN := $(CLEAN) $(dir)/__pycache__ diff --git a/doc/command-line.rst b/doc/command-line.rst new file mode 100644 index 00000000..543a5f9e --- /dev/null +++ b/doc/command-line.rst @@ -0,0 +1,36 @@ +Notmuch Command Line Interface +============================== + +Main commands +------------- + +.. toctree:: + :titlesonly: + + man1/notmuch + man1/notmuch-address + man1/notmuch-compact + man1/notmuch-config + man1/notmuch-count + man1/notmuch-dump + man1/notmuch-emacs-mua + man1/notmuch-git + man1/notmuch-insert + man1/notmuch-new + man1/notmuch-reindex + man1/notmuch-reply + man1/notmuch-restore + man1/notmuch-search + man1/notmuch-show + man1/notmuch-tag + man5/notmuch-hooks + +Aliases +------- + +.. toctree:: + :titlesonly: + + nmbug + notmuch-setup + diff --git a/doc/conf.py b/doc/conf.py index 1fbd102b..1cc9961e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -3,8 +3,10 @@ import sys import os +from pathlib import Path +sys.path.append(str(Path(__file__).parent)) -extensions = [ 'sphinx.ext.autodoc' ] +extensions = [ 'sphinx.ext.autodoc', 'elisp' ] # The suffix of source filenames. source_suffix = '.rst' @@ -14,7 +16,7 @@ master_doc = 'index' # General information about the project. project = u'notmuch' -copyright = u'2009-2021, Carl Worth and many others' +copyright = u'2009-2025, Carl Worth and many others' location = os.path.dirname(__file__) @@ -25,7 +27,7 @@ for pathdir in ['.', '..']: version=infile.read().replace('\n','') # for autodoc -sys.path.insert(0, os.path.join(location, '..', 'bindings', 'python-cffi', 'notmuch2')) +sys.path.insert(0, os.path.join(location, '..', 'bindings', 'python-cffi', 'build', 'stage')) # read generated config for pathdir in ['.', '..']: @@ -45,7 +47,7 @@ if tags.has('WITH_EMACS'): # Hacky reimplementation of include to workaround limitations of # sphinx-doc lines = ['.. include:: /../emacs/rstdoc.rsti\n\n'] # in the source tree - for file in ('notmuch.rsti', 'notmuch-lib.rsti', 'notmuch-show.rsti', 'notmuch-tag.rsti', 'notmuch-tree.rsti'): + for file in ('notmuch.rsti', 'notmuch-lib.rsti', 'notmuch-hello.rsti', 'notmuch-show.rsti', 'notmuch-tag.rsti', 'notmuch-tree.rsti', 'notmuch-maildir-fcc.rsti'): lines.extend(open(rsti_dir+'/'+file)) rst_epilog = ''.join(lines) del lines @@ -67,6 +69,8 @@ pygments_style = 'sphinx' # a list of builtin themes. html_theme = 'default' +# prevent generation of python module index +html_domain_indices=[] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -123,6 +127,14 @@ man_pages = [ u'send mail with notmuch and emacs', [notmuch_authors], 1), + ('man1/notmuch-git', 'notmuch-git', + u'manage notmuch tags with git', + [notmuch_authors], 1), + + ('man1/notmuch-git', 'nmbug', + u'manage notmuch bugs with git', + [notmuch_authors], 1), + ('man5/notmuch-hooks', 'notmuch-hooks', u'hooks for notmuch', [notmuch_authors], 5), @@ -159,6 +171,10 @@ man_pages = [ u'syntax for notmuch queries', [notmuch_authors], 7), + ('man1/notmuch', 'notmuch-setup', + u'getting started with notmuch', + [notmuch_authors], 1), + ('man7/notmuch-sexp-queries', 'notmuch-sexp-queries', u's-expression syntax for notmuch queries', [notmuch_authors], 7), @@ -200,3 +216,11 @@ texinfo_documents += [ x[2], # description 'Miscellaneous' # category ) for x in man_pages] + +def setup(app): + import docutils.nodes + # define nmconfig role and directive for config items. + app.add_object_type('nmconfig','nmconfig', + indextemplate='pair: configuration item; %s', + ref_nodeclass=docutils.nodes.generated, + objname='config item' ) diff --git a/doc/elisp.py b/doc/elisp.py new file mode 100644 index 00000000..1b0392e6 --- /dev/null +++ b/doc/elisp.py @@ -0,0 +1,445 @@ +# Copyright (C) 2016 Sebastian Wiesner and Flycheck contributors + +# This file is not part of GNU Emacs. + +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. + +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. + +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + + +from collections import namedtuple +from sphinx import addnodes +from sphinx.util import ws_re +from sphinx.roles import XRefRole +from sphinx.domains import Domain, ObjType +from sphinx.util.nodes import make_refnode +from sphinx.directives import ObjectDescription + + +def make_target(cell, name): + """Create a target name from ``cell`` and ``name``. + + ``cell`` is the name of a symbol cell, and ``name`` is a symbol name, both + as strings. + + The target names are used as cross-reference targets for Sphinx. + + """ + return '{cell}-{name}'.format(cell=cell, name=name) + + +def to_mode_name(symbol_name): + """Convert ``symbol_name`` to a mode name. + + Split at ``-`` and titlecase each part. + + """ + return ' '.join(p.title() for p in symbol_name.split('-')) + + +class Cell(namedtuple('Cell', 'objtype docname')): + """A cell in a symbol. + + A cell holds the object type and the document name of the description for + the cell. + + Cell objects are used within symbol entries in the domain data. + + """ + + pass + + +class KeySequence(namedtuple('KeySequence', 'keys')): + """A key sequence.""" + + PREFIX_KEYS = {'C-u'} + PREFIX_KEYS.update('M-{}'.format(n) for n in range(10)) + + @classmethod + def fromstring(cls, s): + return cls(s.split()) + + @property + def command_name(self): + """The command name in this key sequence. + + Return ``None`` for key sequences that are no command invocations with + ``M-x``. + + """ + try: + return self.keys[self.keys.index('M-x') + 1] + except ValueError: + return None + + @property + def has_prefix(self): + """Whether this key sequence has a prefix.""" + return self.keys[0] in self.PREFIX_KEYS + + def __str__(self): + return ' '.join(self.keys) + + +class EmacsLispSymbol(ObjectDescription): + """An abstract base class for directives documenting symbols. + + Provide target and index generation and registration of documented symbols + within the domain data. + + Deriving classes must have a ``cell`` attribute which refers to the cell + the documentation goes in, and a ``label`` attribute which provides a + human-readable name for what is documented, used in the index entry. + + """ + + cell_for_objtype = { + 'defcustom': 'variable', + 'defconst': 'variable', + 'defvar': 'variable', + 'defface': 'face' + } + + category_for_objtype = { + 'defcustom': 'Emacs variable (customizable)', + 'defconst': 'Emacs constant', + 'defvar': 'Emacs variable', + 'defface': 'Emacs face' + } + + @property + def cell(self): + """The cell in which to store symbol metadata.""" + return self.cell_for_objtype[self.objtype] + + @property + def label(self): + """The label for the documented object type.""" + return self.objtype + + @property + def category(self): + """Index category""" + return self.category_for_objtype[self.objtype] + + def handle_signature(self, signature, signode): + """Create nodes in ``signode`` for the ``signature``. + + ``signode`` is a docutils node to which to add the nodes, and + ``signature`` is the symbol name. + + Add the object type label before the symbol name and return + ``signature``. + + """ + label = self.label + ' ' + signode += addnodes.desc_annotation(label, label) + signode += addnodes.desc_name(signature, signature) + return signature + + def _add_index(self, name, target): + index_text = '{name}; {label}'.format( + name=name, label=self.category) + self.indexnode['entries'].append( + ('pair', index_text, target, '', None)) + + def _add_target(self, name, sig, signode): + target = make_target(self.cell, name) + if target not in self.state.document.ids: + signode['names'].append(name) + signode['ids'].append(target) + signode['first'] = (not self.names) + self.state.document.note_explicit_target(signode) + + obarray = self.env.domaindata['el']['obarray'] + symbol = obarray.setdefault(name, {}) + if self.cell in symbol: + self.state_machine.reporter.warning( + 'duplicate description of %s %s, ' % (self.objtype, name) + + 'other instance in ' + + self.env.doc2path(symbol[self.cell].docname), + line=self.lineno) + symbol[self.cell] = Cell(self.objtype, self.env.docname) + + return target + + def add_target_and_index(self, name, sig, signode): + target = self._add_target(name, sig, signode) + self._add_index(name, target) + + +class EmacsLispMinorMode(EmacsLispSymbol): + cell = 'function' + label = 'Minor Mode' + + def handle_signature(self, signature, signode): + """Create nodes in ``signode`` for the ``signature``. + + ``signode`` is a docutils node to which to add the nodes, and + ``signature`` is the symbol name. + + Add the object type label before the symbol name and return + ``signature``. + + """ + label = self.label + ' ' + signode += addnodes.desc_annotation(label, label) + signode += addnodes.desc_name(signature, to_mode_name(signature)) + return signature + + def _add_index(self, name, target): + return super()._add_index(to_mode_name(name), target) + + +class EmacsLispFunction(EmacsLispSymbol): + """A directive to document Emacs Lisp functions.""" + + cell_for_objtype = { + 'defun': 'function', + 'defmacro': 'function' + } + + def handle_signature(self, signature, signode): + function_name, *args = ws_re.split(signature) + label = self.label + ' ' + signode += addnodes.desc_annotation(label, label) + signode += addnodes.desc_name(function_name, function_name) + for arg in args: + is_keyword = arg.startswith('&') + node = (addnodes.desc_annotation + if is_keyword + else addnodes.desc_addname) + signode += node(' ' + arg, ' ' + arg) + + return function_name + + +class EmacsLispKey(ObjectDescription): + """A directive to document interactive commands via their bindings.""" + + label = 'Emacs command' + + def handle_signature(self, signature, signode): + """Create nodes to ``signode`` for ``signature``. + + ``signode`` is a docutils node to which to add the nodes, and + ``signature`` is the symbol name. + """ + key_sequence = KeySequence.fromstring(signature) + signode += addnodes.desc_name(signature, str(key_sequence)) + return str(key_sequence) + + def _add_command_target_and_index(self, name, sig, signode): + target_name = make_target('function', name) + if target_name not in self.state.document.ids: + signode['names'].append(name) + signode['ids'].append(target_name) + self.state.document.note_explicit_target(signode) + + obarray = self.env.domaindata['el']['obarray'] + symbol = obarray.setdefault(name, {}) + if 'function' in symbol: + self.state_machine.reporter.warning( + 'duplicate description of %s %s, ' % (self.objtype, name) + + 'other instance in ' + + self.env.doc2path(symbol['function'].docname), + line=self.lineno) + symbol['function'] = Cell(self.objtype, self.env.docname) + + index_text = '{name}; {label}'.format(name=name, label=self.label) + self.indexnode['entries'].append( + ('pair', index_text, target_name, '', None)) + + def _add_binding_target_and_index(self, binding, sig, signode): + reftarget = make_target('key', binding) + + if reftarget not in self.state.document.ids: + signode['names'].append(reftarget) + signode['ids'].append(reftarget) + signode['first'] = (not self.names) + self.state.document.note_explicit_target(signode) + + keymap = self.env.domaindata['el']['keymap'] + if binding in keymap: + self.state_machine.reporter.warning( + 'duplicate description of binding %s, ' % binding + + 'other instance in ' + + self.env.doc2path(keymap[binding]), + line=self.lineno) + keymap[binding] = self.env.docname + + index_text = '{name}; Emacs key binding'.format(name=binding) + self.indexnode['entries'].append( + ('pair', index_text, reftarget, '', None)) + + def add_target_and_index(self, name, sig, signode): + # If unprefixed M-x command index as function and not as key binding + sequence = KeySequence.fromstring(name) + if sequence.command_name and not sequence.has_prefix: + self._add_command_target_and_index(sequence.command_name, + sig, signode) + else: + self._add_binding_target_and_index(name, sig, signode) + + +class XRefModeRole(XRefRole): + """A role to cross-reference a minor mode. + + Like a normal cross-reference role but appends ``-mode`` to the reference + target and title-cases the symbol name like Emacs does when referring to + modes. + + """ + + fix_parens = False + lowercase = False + + def process_link(self, env, refnode, has_explicit_title, title, target): + refnode['reftype'] = 'minor-mode' + target = target + '-mode' + return (title if has_explicit_title else to_mode_name(target), target) + + +class EmacsLispDomain(Domain): + """A domain to document Emacs Lisp code.""" + + name = 'el' + label = '' + + object_types = { + # TODO: Set search prio for object types + # Types for user-facing options and commands + 'minor-mode': ObjType('minor-mode', 'function', 'mode', + cell='function'), + 'define-key': ObjType('key binding', cell='interactive'), + 'defcustom': ObjType('defcustom', 'defcustom', cell='variable'), + 'defface': ObjType('defface', 'defface', cell='face'), + # Object types for code + 'defun': ObjType('defun', 'defun', cell='function'), + 'defmacro': ObjType('defmacro', 'defmacro', cell='function'), + 'defvar': ObjType('defvar', 'defvar', cell='variable'), + 'defconst': ObjType('defconst', 'defconst', cell='variable') + } + directives = { + 'minor-mode': EmacsLispMinorMode, + 'define-key': EmacsLispKey, + 'defcustom': EmacsLispSymbol, + 'defvar': EmacsLispSymbol, + 'defconst': EmacsLispSymbol, + 'defface': EmacsLispSymbol, + 'defun': EmacsLispFunction, + 'defmacro': EmacsLispFunction + } + roles = { + 'mode': XRefModeRole(), + 'defvar': XRefRole(), + 'defconst': XRefRole(), + 'defcustom': XRefRole(), + 'defface': XRefRole(), + 'defun': XRefRole(), + 'defmacro': XRefRole() + } + + data_version = 1 + initial_data = { + # Our domain data attempts to somewhat mirror the semantics of Emacs + # Lisp, so we have an obarray which holds symbols which in turn have + # function, variable, face, etc. cells, and a keymap which holds the + # documentation for key bindings. + 'obarray': {}, + 'keymap': {} + } + + def clear_doc(self, docname): + """Clear all cells documented ``docname``.""" + for symbol in self.data['obarray'].values(): + for cell in list(symbol.keys()): + if docname == symbol[cell].docname: + del symbol[cell] + for binding in list(self.data['keymap']): + if self.data['keymap'][binding] == docname: + del self.data['keymap'][binding] + + def resolve_xref(self, env, fromdocname, builder, + objtype, target, node, contnode): + """Resolve a cross reference to ``target``.""" + if objtype == 'key': + todocname = self.data['keymap'].get(target) + if not todocname: + return None + reftarget = make_target('key', target) + else: + cell = self.object_types[objtype].attrs['cell'] + symbol = self.data['obarray'].get(target, {}) + if cell not in symbol: + return None + reftarget = make_target(cell, target) + todocname = symbol[cell].docname + + return make_refnode(builder, fromdocname, todocname, + reftarget, contnode, target) + + def resolve_any_xref(self, env, fromdocname, builder, + target, node, contnode): + """Return all possible cross references for ``target``.""" + nodes = ((objtype, self.resolve_xref(env, fromdocname, builder, + objtype, target, node, contnode)) + for objtype in ['key', 'defun', 'defvar', 'defface']) + return [('el:{}'.format(objtype), node) for (objtype, node) in nodes + if node is not None] + + def merge_warn_duplicate(self, objname, our_docname, their_docname): + self.env.warn( + their_docname, + "Duplicate declaration: '{}' also defined in '{}'.\n".format( + objname, their_docname)) + + def merge_keymapdata(self, docnames, our_keymap, their_keymap): + for key, docname in their_keymap.items(): + if docname in docnames: + if key in our_keymap: + our_docname = our_keymap[key] + self.merge_warn_duplicate(key, our_docname, docname) + else: + our_keymap[key] = docname + + def merge_obarraydata(self, docnames, our_obarray, their_obarray): + for objname, their_cells in their_obarray.items(): + our_cells = our_obarray.setdefault(objname, dict()) + for cellname, their_cell in their_cells.items(): + if their_cell.docname in docnames: + our_cell = our_cells.get(cellname) + if our_cell: + self.merge_warn_duplicate(objname, our_cell.docname, + their_cell.docname) + else: + our_cells[cellname] = their_cell + + def merge_domaindata(self, docnames, otherdata): + self.merge_keymapdata(docnames, self.data['keymap'], + otherdata['keymap']) + self.merge_obarraydata(docnames, self.data['obarray'], + otherdata['obarray']) + + def get_objects(self): + """Get all documented symbols for use in the search index.""" + for name, symbol in self.data['obarray'].items(): + for cellname, cell in symbol.items(): + yield (name, name, cell.objtype, cell.docname, + make_target(cellname, name), + self.object_types[cell.objtype].attrs['searchprio']) + + +def setup(app): + app.add_domain(EmacsLispDomain) + return {'version': '0.1', 'parallel_read_safe': True} diff --git a/doc/index.rst b/doc/index.rst index fbdcf779..fec3e6c2 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -2,36 +2,19 @@ Welcome to notmuch's documentation! =================================== -Contents: +Content +------- .. toctree:: :titlesonly: - man1/notmuch - man1/notmuch-address - man1/notmuch-compact - man1/notmuch-config - man1/notmuch-count - man1/notmuch-dump + command-line + queries notmuch-emacs - man1/notmuch-emacs-mua - man5/notmuch-hooks - man1/notmuch-insert - man1/notmuch-new - man7/notmuch-properties - man1/notmuch-reindex - man1/notmuch-reply - man1/notmuch-restore - man1/notmuch-search - man7/notmuch-search-terms - man7/notmuch-sexp-queries - man1/notmuch-show - man1/notmuch-tag python-bindings - -Indices and tables -================== + +Index and search +----------------- * :ref:`genindex` -* :ref:`modindex` * :ref:`search` diff --git a/doc/man1/notmuch-address.rst b/doc/man1/notmuch-address.rst index 7423b629..c70dde35 100644 --- a/doc/man1/notmuch-address.rst +++ b/doc/man1/notmuch-address.rst @@ -42,7 +42,7 @@ Supported options for **address** include neither ``--output=sender`` nor ``--output=recipients`` is given, ``--output=sender`` is implied. - **sender** + sender Output all addresses from the *From* header. Note: Searching for **sender** should be much faster than @@ -50,17 +50,17 @@ Supported options for **address** include cached directly in the database whereas other addresses need to be fetched from message files. - **recipients** + recipients Output all addresses from the *To*, *Cc* and *Bcc* headers. - **count** + count Print the count of how many times was the address encountered during search. Note: With this option, addresses are printed only after the whole search is finished. This may take long time. - **address** + address Output only the email addresses instead of the full mailboxes with names and email addresses. This option has no effect on the JSON or S-Expression output formats. @@ -69,17 +69,17 @@ Supported options for **address** include Control the deduplication of results. - **no** + no Output all occurrences of addresses in the matching messages. This is not applicable with ``--output=count``. - **mailbox** + mailbox Deduplicate addresses based on the full, case sensitive name and email address, or mailbox. This is effectively the same as piping the ``--deduplicate=no`` output to **sort | uniq**, except for the order of results. This is the default. - **address** + address Deduplicate addresses based on the case insensitive address part of the mailbox. Of all the variants (with different name or case), print the one occurring most frequently among the diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst index 7d901758..5106655f 100644 --- a/doc/man1/notmuch-config.rst +++ b/doc/man1/notmuch-config.rst @@ -55,24 +55,23 @@ 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). +.. nmconfig:: built_with. - Default: see :ref:`database` + Compile time feature . Current possibilities include + "retry_lock" (configure option, included by default). + (since notmuch 0.30, "compact" and "field_processor" are + always included.) -**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. +.. nmconfig:: database.autocommit - History: this configuration value was introduced in notmuch 0.32. + How often to commit transactions to disk. `0` means wait until + command completes, otherwise an integer `n` specifies to commit to + disk after every `n` completed transactions. - Default: For compatibility with older configurations, the value of - database.path is used if **database.mail\_root** is unset. + History: this configuration value was introduced in notmuch 0.33. + +.. nmconfig:: database.backup_dir -**database.backup_dir** Directory to store tag dumps when upgrading database. History: this configuration value was introduced in notmuch 0.32. @@ -80,7 +79,8 @@ paths are presumed relative to `$HOME` for items in section Default: A sibling directory of the Xapian database called `backups`. -**database.hook_dir** +.. nmconfig:: database.hook_dir + Directory containing hooks run by notmuch commands. See :any:`notmuch-hooks(5)`. @@ -88,97 +88,52 @@ paths are presumed relative to `$HOME` for items in section Default: See HOOKS, below. -**database.autocommit** - - How often to commit transactions to disk. `0` means wait until - command completes, otherwise an integer `n` specifies to commit to - disk after every `n` completed transactions. - - History: this configuration value was introduced in notmuch 0.33. - -**user.name** - Your full name. - - Default: ``$NAME`` variable if set, otherwise read from - ``/etc/passwd``. - -**user.primary\_email** - Your primary email address. - - Default: ``$EMAIL`` variable if set, otherwise constructed from - the username and hostname of the current machine. +.. nmconfig:: database.mail_root -**user.other\_email** - A list of other email addresses at which you receive email. + 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. - Default: not set. + History: this configuration value was introduced in notmuch 0.32. -**new.tags** - A list of tags that will be added to all messages incorporated by - **notmuch new**. + Default: For compatibility with older configurations, the value of + database.path is used if :nmconfig:`database.mail_root` is unset. - Default: ``unread;inbox``. +.. nmconfig:: database.path -**new.ignore** - A list to specify files and directories that will not be searched - for messages by :any:`notmuch-new(1)`. Each entry in the list is either: + Notmuch will store its database here, (in + sub-directory named ``.notmuch`` if :nmconfig:`database.mail_root` + is unset). - A file or a directory name, without path, that will be ignored, - regardless of the location in the mail store directory hierarchy. + Default: see :ref:`database` - Or: +.. nmconfig:: git.path - A regular expression delimited with // that will be matched - against the path of the file or directory relative to the database - path. Matching files and directories will be ignored. The - beginning and end of string must be explicitly anchored. For - example, /.*/foo$/ would match "bar/foo" and "bar/baz/foo", but - not "foo" or "bar/foobar". + Default location for git repository for :any:`notmuch-git`. - Default: empty list. +.. nmconfig:: git.safe_fraction -**search.exclude\_tags** - A list of tags that will be excluded from search results by - default. Using an excluded tag in a query will override that - exclusion. + Some :any:`notmuch-git` operations check that the fraction of + messages changed (in the database or in git, as appropriate) is not + too large. This item controls what fraction of total messages is + considered "not too large". - Default: empty list. Note that :any:`notmuch-setup(1)` puts - ``deleted;spam`` here when creating new configuration file. +.. nmconfig:: git.tag_prefix -**maildir.synchronize\_flags** - If true, then the following maildir flags (in message filenames) - will be synchronized with the corresponding notmuch tags: + Default tag prefix (filter) for :any:`notmuch-git`. - +--------+-----------------------------------------------+ - | Flag | Tag | - +========+===============================================+ - | D | draft | - +--------+-----------------------------------------------+ - | F | flagged | - +--------+-----------------------------------------------+ - | P | passed | - +--------+-----------------------------------------------+ - | R | replied | - +--------+-----------------------------------------------+ - | S | unread (added when 'S' flag is not present) | - +--------+-----------------------------------------------+ +.. nmconfig:: index.as_text - The :any:`notmuch-new(1)` command will notice flag changes in - filenames and update tags, while the :any:`notmuch-tag(1)` and - :any:`notmuch-restore(1)` commands will notice tag changes and - update flags in filenames. + List of regular expressions (without delimiters) for MIME types to + be indexed as text. Currently this applies only to attachments. By + default the regex matches anywhere in the content type; if the + user wants an anchored match, they should include anchors in their + regexes. - If there have been any changes in the maildir (new messages added, - old ones removed or renamed, maildir flags changed, etc.), it is - advisable to run :any:`notmuch-new(1)` before - :any:`notmuch-tag(1)` or :any:`notmuch-restore(1)` commands to - ensure the tag changes are properly synchronized to the maildir - flags, as the commands expect the database and maildir to be in - sync. + History: This configuration value was introduced in notmuch 0.38. - Default: ``true``. +.. nmconfig:: index.decrypt -**index.decrypt** Policy for decrypting encrypted messages during indexing. Must be one of: ``false``, ``auto``, ``nostash``, or ``true``. @@ -231,7 +186,10 @@ paths are presumed relative to `$HOME` for items in section Default: ``auto``. -**index.header.** +.. _index.header: + +.. nmconfig:: 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 @@ -240,25 +198,138 @@ paths are presumed relative to `$HOME` for items in section supported. See :any:`notmuch-search-terms(7)` for a list of existing prefixes, and an explanation of probabilistic prefixes. -**built_with.** - Compile time feature . Current possibilities include - "retry_lock" (configure option, included by default). - (since notmuch 0.30, "compact" and "field_processor" are - always included.) +.. nmconfig:: maildir.synchronize_flags + + 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 :any:`notmuch-new(1)` command will notice flag changes in + filenames and update tags, while the :any:`notmuch-tag(1)` and + :any:`notmuch-restore(1)` commands will notice tag changes and + update flags in filenames. + + If there have been any changes in the maildir (new messages added, + old ones removed or renamed, maildir flags changed, etc.), it is + advisable to run :any:`notmuch-new(1)` before + :any:`notmuch-tag(1)` or :any:`notmuch-restore(1)` commands to + ensure the tag changes are properly synchronized to the maildir + flags, as the commands expect the database and maildir to be in + sync. + + Default: ``true``. + +.. nmconfig:: new.ignore + + A list to specify files and directories that will not be searched + for messages by :any:`notmuch-new(1)`. Each entry in the list is either: + + A file or a directory name, without path, that will be ignored, + regardless of the location in the mail store directory hierarchy. + + Or: + + A regular expression delimited with // that will be matched + against the path of the file or directory relative to the database + path. Matching files and directories will be ignored. The + beginning and end of string must be explicitly anchored. For + example, /.*/foo$/ would match "bar/foo" and "bar/baz/foo", but + not "foo" or "bar/foobar". + + Default: empty list. + +.. nmconfig:: new.tags + + A list of tags that will be added to all messages incorporated by + **notmuch new**. + + Default: ``unread;inbox``. + +.. nmconfig:: query. -**query.** Expansion for named query called . See :any:`notmuch-search-terms(7)` for more information about named queries. -**squery.** +.. nmconfig:: search.exclude_tags + + A list of tags that will be excluded from search results by + default. Using an excluded tag in a query will override that + exclusion. + + Default: empty list. Note that :any:`notmuch-setup(1)` puts + ``deleted;spam`` here when creating new configuration file. + +.. nmconfig:: search.authors_separator + + The string to separate authors when showing a thread. + + Default: , + +.. nmconfig:: search.authors_matched_separator + + The string to separate matched from non-matched authors when showing a + thread. + + Default: | + +.. nmconfig:: show.extra_headers + + By default :any:`notmuch-show(1)` includes the following headers + in structured output if they are present in the message: + `Subject`, `From`, `To`, `Cc`, `Bcc`, `Reply-To`, `Date`. This + option allows the specification of a list of further + headers to output. + + History: This configuration value was introduced in notmuch 0.35. + + Default: empty list. + +.. nmconfig:: squery. + Expansion for named query called , using s-expression syntax. See :any:`notmuch-sexp-queries(7)` for more information about s-expression queries. +.. nmconfig:: user.name + + Your full name. + + Default: ``$NAME`` variable if set, otherwise read from + ``/etc/passwd``. + +.. nmconfig:: user.other_email + + A list of other email addresses at which you receive email + (see also, :nmconfig:`user.primary_email`) + + Default: not set. + +.. nmconfig:: user.primary_email + + Your primary email address. + + Default: ``$EMAIL`` variable if set, otherwise constructed from + the username and hostname of the current machine. + FILES ===== +.. _config_search: + CONFIGURATION ------------- diff --git a/doc/man1/notmuch-count.rst b/doc/man1/notmuch-count.rst index 9a7e4bac..4c9c9a1c 100644 --- a/doc/man1/notmuch-count.rst +++ b/doc/man1/notmuch-count.rst @@ -28,13 +28,13 @@ Supported options for **count** include .. option:: --output=(messages|threads|files) - **messages** + messages Output the number of matching messages. This is the default. - **threads** + threads Output the number of matching threads. - **files** + files Output the number of files associated with matching messages. This may be bigger than the number of matching messages due to duplicates (i.e. multiple files having the diff --git a/doc/man1/notmuch-dump.rst b/doc/man1/notmuch-dump.rst index 4319c5c3..a7ca39d0 100644 --- a/doc/man1/notmuch-dump.rst +++ b/doc/man1/notmuch-dump.rst @@ -39,7 +39,7 @@ Supported options for **dump** include Notmuch restore supports two plain text dump formats, both with one message-id per line, followed by a list of tags. - **batch-tag** + batch-tag The default **batch-tag** dump format is intended to more robust against malformed message-ids and tags containing whitespace or non-\ :manpage:`ascii(7)` characters. Each line @@ -58,7 +58,7 @@ Supported options for **dump** include :any:`notmuch-tag(1)`; note that the single message-id query is mandatory for :any:`notmuch-restore(1)`. - **sup** + sup The **sup** dump file format is specifically chosen to be compatible with the format of files produced by :manpage:`sup-dump(1)`. So if you've previously been using sup @@ -77,18 +77,18 @@ Supported options for **dump** include Control what kind of metadata is included in the output. - **config** + config Output configuration data stored in the database. Each line starts with "#@ ", followed by a space separated key-value pair. Both key and value are hex encoded if needed. - **properties** + properties Output per-message (key,value) metadata. Each line starts with "#= ", followed by a message id, and a space separated list of key=value pairs. Ids, keys and values are hex encoded if needed. See :any:`notmuch-properties(7)` for more details. - **tags** + tags Output per-message boolean metadata, namely tags. See *format* above for description of the output. diff --git a/doc/man1/notmuch-git.rst b/doc/man1/notmuch-git.rst new file mode 100644 index 00000000..ae393ca1 --- /dev/null +++ b/doc/man1/notmuch-git.rst @@ -0,0 +1,340 @@ +.. _notmuch-git(1): + +=========== +notmuch-git +=========== + +SYNOPSIS +======== + +**notmuch** **git** [-h] [-N] [-C *repo*] [-p *prefix*] [-v] [-l *log level*] *subcommand* + +**nmbug** [-h] [-C *repo*] [-p *prefix*] [-v] [-l *log level*] *subcommand* + +DESCRIPTION +=========== + +Manage notmuch tags with Git. + +OPTIONS +------- + +Supported options for `notmuch git` include + +.. program:: notmuch-git + +.. option:: -h, --help + + show help message and exit + +.. option:: -N, --nmbug + + Set defaults for :option:`--tag-prefix` and :option:`--git-dir` suitable for the + :any:`notmuch` bug tracker + +.. option:: -C , --git-dir + + Operate on git repository *repo*. See :ref:`repo_location` for + defaults. + +.. option:: -p , --tag-prefix + + Operate only on tags with prefix *prefix*. See :ref:`prefix_val` for + defaults. + +.. option:: -v, --version + + show notmuch-git's version number and exit + +.. option:: -l , --log-level + + Log verbosity, one of: `critical`, `error`, `warning`, `info`, + `debug`. Defaults to `warning`. + +SUBCOMMANDS +----------- + +For help on a particular subcommand, run: 'notmuch-git ... --help'. + +.. program:: notmuch-git + +.. option:: archive [tree-ish] [arg ...] + +Dump a tar archive of a committed tag set using 'git archive'. See +:any:`format` for details of the archive contents. + + .. describe:: tree-ish + + The tree or commit to produce an archive for. Defaults to 'HEAD'. + + .. describe:: arg + + If present, any optional arguments are passed through to + :manpage:`git-archive(1)`. Arguments to `git-archive` are reordered + so that *tree-ish* comes last. + +.. option:: checkout [-f|--force] + +Update the notmuch database from git. + +This is mainly useful to discard your changes in notmuch relative +to git. + + .. describe:: [-f|--force] + + Override checks that prevent modifying tags for large fractions of + messages in the database. See also :nmconfig:`git.safe_fraction`. + +.. option:: clone + +Create a local `notmuch git` repository from a remote source. + +This wraps 'git clone', adding some options to avoid creating a +working tree while preserving remote-tracking branches and +upstreams. + + .. describe:: repository + + The (possibly remote) repository to clone from. See the URLS + section of :manpage:`git-clone(1)` for more information on + specifying repositories. + +.. option:: commit [-f|--force] [message] + +Commit prefix-matching tags from the notmuch database to git. + + .. describe:: message + + Optional text for the commit message. + + .. describe:: -f|--force + + Override checks that prevent modifying tags for large fractions of + messages in the database. See also :nmconfig:`git.safe_fraction`. + +.. option:: fetch [remote] + +Fetch changes from the remote repository. + + .. describe:: remote + + Override the default configured in `branch..remote` to fetch + from a particular remote repository (e.g. `origin`). + +.. option:: help + +Show brief help for an `notmuch git` command. + +.. option:: init [--format-version=N] + +Create an empty `notmuch git` repository. + +This wraps 'git init' with a few extra steps to support subsequent +status and commit commands. + + .. describe:: --format-version=N + + Create a repo in format version N. By default :any:`notmuch-git` + uses the highest supported version, which is the best choice for + most use-cases. + +.. option:: log [arg ...] + +A wrapper for 'git log'. + + .. describe:: arg + + Additional arguments are passed through to 'git log'. + +After running `notmuch git fetch`, you can inspect the changes with + +:: + + $ notmuch git log HEAD..@{upstream} + +.. option:: merge [reference] + +Merge changes from 'reference' into HEAD and load the result into notmuch. + + .. describe:: reference + + Reference, usually other branch heads, to merge into our + branch. Defaults to `@{upstream}`. + +.. option:: pull [repository] [refspec ...] + +Pull (merge) remote repository changes to notmuch. + +**pull** is equivalent to **fetch** followed by **merge**. We use the +git-configured repository for your current branch +(`branch..repository`, likely `origin`, and `branch..merge`, +likely `master` or `main`). + + .. describe:: repository + + The "remote" repository that is the source of the pull. This parameter + can be either a URL (see the section GIT URLS in :manpage:`git-pull(1)`) or the + name of a remote (see the section REMOTES in :manpage:`git-pull(1)`). + + .. describe:: refspec + + Refspec (usually a branch name) to fetch and merge. See the + *refspec* entry in the OPTIONS section of :manpage:`git-pull(1`) for + other possibilities. + +.. option:: push [repository] [refspec] + +Push the local `notmuch git` git state to a remote repository. + + .. describe:: repository + + The "remote" repository that is the destination of the push. This + parameter can be either a URL (see the section GIT URLS in + :manpage:`git-push(1)`) or the name of a remote (see the section + REMOTES in :manpage:`git-push(1)`). + + .. describe:: refspec + + Refspec (usually a branch name) to push. See the *refspec* entry in the OPTIONS section of + :manpage:`git-push(1)` for other possibilities. + +.. option:: status + +Show pending updates in notmuch or git repo. + +Prints lines of the form + +| ng Message-Id tag + +where n is a single character representing notmuch database status + + .. describe:: A + + Tag is present in notmuch database, but not committed to git + (equivalently, tag has been deleted in git repo, e.g. by a + pull, but not restored to notmuch database). + + .. describe:: D + + Tag is present in git repo, but not restored to notmuch + database (equivalently, tag has been deleted in notmuch). + + .. describe:: U + + Message is unknown (missing from local notmuch database). + +The second character *g* (if present) represents a difference between +local and upstream branches. Typically `notmuch git fetch` needs to be +run to update this. + + .. describe:: a + + Tag is present in upstream, but not in the local git branch. + + .. describe:: d + + Tag is present in local git branch, but not upstream. + +.. _format: + +REPOSITORY CONTENTS +=================== + +The tags are stored in the git repo (and exported) as a set of empty +files. These empty files are contained within a directory named after +the message-id. + +In what follows `encode()` represents a POSIX filesystem safe +encoding. The encoding preserves alphanumerics, and the characters +`+-_@=.,:`. All other octets are replaced with `%` followed by a two +digit hex number. + +Currently :any:`notmuch-git` can read any format version, but can only +create (via :any:`init`) :ref:`version 1 ` repositories. + +.. _format_version_0: + +Version 0 +--------- + +This is the legacy format created by the `nmbug` tool prior to release +0.37. For a message with Message-Id *id*, for each tag *tag*, there +is an empty file with path + + tags/ `encode` (*id*) / `encode` (*tag*) + +.. _format_version_1: + +Version 1 +--------- + +In format version 1 and later, the format version is contained in a +top level file called FORMAT. + +For a message with Message-Id *id*, for each tag *tag*, there +is an empty file with path + + tags/ `hash1` (*id*) / `hash2` (*id*) `encode` (*id*) / `encode` (*tag*) + +The hash functions each represent one byte of the `blake2b` hex +digest. + +Compared to :ref:`version 0 `, this reduces the +number of subdirectories within each directory. + +.. _repo_location: + +REPOSITORY LOCATION +=================== + +:any:`notmuch-git` uses the first of the following with a non-empty +value to locate the git repository. + +- Option :option:`--git-dir`. + +- Environment variable :envvar:`NOTMUCH_GIT_DIR`. + +- Configuration item :nmconfig:`git.path` + +- If invoked as `nmbug` or with the :option:`--nmbug` option, + :code:`$HOME/.nmbug`; otherwise + :code:`$XDG_DATA_HOME/notmuch/$NOTMUCH_PROFILE/git`. + +.. _prefix_val: + +PREFIX VALUE +============ + +:any:`notmuch-git` uses the first of the following with a non-null +value to define the tag prefix. + +- Option :option:`--tag-prefix`. + +- Environment variable :envvar:`NOTMUCH_GIT_PREFIX`. + +- Configuration item :nmconfig:`git.tag_prefix`. + +- If invoked as `nmbug` or with the :option:`--nmbug` option, + :code:`notmuch::`, otherwise the empty string. + +ENVIRONMENT +=========== + +Variable :envvar:`NOTMUCH_PROFILE` influences :ref:`repo_location`. +If it is unset, 'default' is assumed. + +.. envvar:: NOTMUCH_GIT_DIR + + Default location of git repository. Overridden by :option:`--git-dir`. + +.. envvar:: NOTMUCH_GIT_PREFIX + + Default tag prefix (filter). Overridden by :option:`--tag-prefix`. + +SEE ALSO +======== + +:any:`notmuch(1)`, +:any:`notmuch-dump(1)`, +:any:`notmuch-restore(1)`, +:any:`notmuch-tag(1)` diff --git a/doc/man1/notmuch-insert.rst b/doc/man1/notmuch-insert.rst index 82c4a7a0..e05bd0b5 100644 --- a/doc/man1/notmuch-insert.rst +++ b/doc/man1/notmuch-insert.rst @@ -14,12 +14,12 @@ DESCRIPTION **notmuch insert** reads a message from standard input and delivers it into the maildir directory given by configuration option -**database.path**, then incorporates the message into the notmuch +:nmconfig:`database.mail_root`, then incorporates the message into the notmuch database. It is an alternative to using a separate tool to deliver the message then running :any:`notmuch-new(1)` afterwards. The new message will be tagged with the tags specified by the -**new.tags** configuration option, then by operations specified on the +:nmconfig:`new.tags` configuration option, then by operations specified on the command-line: tags prefixed by '+' are added while those prefixed by '-' are removed. @@ -38,7 +38,7 @@ Supported options for **insert** include .. option:: --folder= Deliver the message to the specified folder, relative to the - top-level directory given by the value of **database.path**. The + top-level directory given by the value of :nmconfig:`database.mail_root`. The default is the empty string, which means delivering to the top-level directory. @@ -86,7 +86,16 @@ Supported options for **insert** include ``--decrypt=nostash`` without considering the security of your index. - See also ``index.decrypt`` in :any:`notmuch-config(1)`. + See also :nmconfig:`index.decrypt` in :any:`notmuch-config(1)`. + +CONFIGURATION +============= + +Indexing is influenced by the configuration options +:nmconfig:`index.decrypt` and :nmconfig:`index.header.\`. Tagging +is controlled by options :nmconfig:`new.tags` and +:nmconfig:`maildir.synchronize_flags`. See +:any:`notmuch-config(1)` for details. EXIT STATUS =========== diff --git a/doc/man1/notmuch-new.rst b/doc/man1/notmuch-new.rst index 9cb4a54e..f0ed8eb8 100644 --- a/doc/man1/notmuch-new.rst +++ b/doc/man1/notmuch-new.rst @@ -78,6 +78,16 @@ Supported options for **new** include to optimize the scanning of directories for new mail. This option turns that optimization off. +CONFIGURATION +============= + +Indexing is influenced by the configuration options +:nmconfig:`index.decrypt`, :nmconfig:`index.header.\` +and :nmconfig:`new.ignore`. Tagging +is controlled by :nmconfig:`new.tags` and +:nmconfig:`maildir.synchronize_flags`. See +:any:`notmuch-config(1)` for details. + EXIT STATUS =========== diff --git a/doc/man1/notmuch-reply.rst b/doc/man1/notmuch-reply.rst index 4a78a90b..4f39a959 100644 --- a/doc/man1/notmuch-reply.rst +++ b/doc/man1/notmuch-reply.rst @@ -38,24 +38,30 @@ Supported options for **reply** include .. program:: reply +.. option:: --duplicate=N + + Reply to duplicate number N. The numbering starts from 1, and + matches the order used by :option:`show --duplicate` and + :option:`search --output=files `. + .. option:: --format=(default|json|sexp|headers-only) - **default** + default Includes subject and quoted message body as an RFC 2822 message. - **json** + json Produces JSON output containing headers for a reply message and the contents of the original message. This output can be used by a client to create a reply message intelligently. - **sexp** + sexp Produces S-Expression output containing headers for a reply message and the contents of the original message. This output can be used by a client to create a reply message intelligently. - **headers-only** + headers-only Only produces In-Reply-To, References, To, Cc, and Bcc headers. @@ -67,10 +73,10 @@ Supported options for **reply** include .. option:: --reply-to=(all|sender) - **all** (default) + all (default) Replies to all addresses. - **sender** + sender Replies only to the sender. If replying to user's own message (Reply-to: or From: header is one of the user's configured email addresses), try To:, Cc:, and Bcc: headers in this diff --git a/doc/man1/notmuch-restore.rst b/doc/man1/notmuch-restore.rst index bd452475..ac6b4245 100644 --- a/doc/man1/notmuch-restore.rst +++ b/doc/man1/notmuch-restore.rst @@ -32,14 +32,14 @@ Supported options for **restore** include line specifying a message-id and a set of tags. For details of the actual formats, see :any:`notmuch-dump(1)`. - **sup** + sup The **sup** dump file format is specifically chosen to be compatible with the format of files produced by sup-dump. So if you've previously been using sup for mail, then the **notmuch restore** command provides you a way to import all of your tags (or labels as sup calls them). - **batch-tag** + batch-tag The **batch-tag** dump format is intended to more robust against malformed message-ids and tags containing whitespace or non-\ **ascii(7)** characters. See :any:`notmuch-dump(1)` for @@ -49,7 +49,7 @@ Supported options for **restore** include changes if the **maildir.synchronize\_flags** configuration option is enabled. See :any:`notmuch-config(1)` for details. - **auto** + auto This option (the default) tries to guess the format from the input. For correctly formed input in either supported format, this heuristic, based the fact that batch-tag format contains @@ -59,18 +59,18 @@ Supported options for **restore** include Control what kind of metadata is restored. - **config** + config Restore configuration data to the database. Each configuration line starts with "#@ ", followed by a space separated key-value pair. Both key and value are hex encoded if needed. - **properties** + properties Restore per-message (key,value) metadata. Each line starts with "#= ", followed by a message id, and a space separated list of key=value pairs. Ids, keys and values are hex encoded if needed. See :any:`notmuch-properties(7)` for more details. - **tags** + tags Restore per-message metadata, namely tags. See *format* above for more details. diff --git a/doc/man1/notmuch-search.rst b/doc/man1/notmuch-search.rst index 2d9ca2d5..b87737ea 100644 --- a/doc/man1/notmuch-search.rst +++ b/doc/man1/notmuch-search.rst @@ -43,7 +43,7 @@ Supported options for **search** include .. option:: --output=(summary|threads|messages|files|tags) - **summary** + summary (default) Output a summary of each thread with any message matching the search terms. The summary includes the thread ID, date, the number of messages in the thread (both the number matched and @@ -52,19 +52,19 @@ Supported options for **search** include for some messages, the total number of files is printed in parentheses (see below for an example). - **threads** + threads Output the thread IDs of all threads with any message matching the search terms, either one per line (``--format=text``), separated by null characters (``--format=text0``), as a JSON array (``--format=json``), or an S-Expression list (``--format=sexp``). - **messages** + messages Output the message IDs of all messages matching the search terms, either one per line (``--format=text``), separated by null characters (``--format=text0``), as a JSON array (``--format=json``), or as an S-Expression list (``--format=sexp``). - **files** + files Output the filenames of all messages matching the search terms, either one per line (``--format=text``), separated by null characters (``--format=text0``), as a JSON array (``--format=json``), @@ -78,7 +78,7 @@ Supported options for **search** include in other directories that are included in the output, although these files alone would not match the search. - **tags** + tags Output all tags that appear on any message matching the search terms, either one per line (``--format=text``), separated by null characters (``--format=text0``), as a JSON array (``--format=json``), @@ -115,20 +115,20 @@ Supported options for **search** include terms. This option specifies whether to omit excluded messages in the search process. - **true** (default) + true (default) Prevent excluded messages from matching the search terms. - **all** + all Additionally prevent excluded messages from appearing in displayed results, in effect behaving as though the excluded messages do not exist. - **false** + false Allow excluded messages to match search terms and appear in displayed results. Excluded messages are still marked in the relevant outputs. - **flag** + flag Only has an effect when ``--output=summary``. The output is almost identical to **false**, but the "match count" is the number of matching non-excluded messages in the thread, rather diff --git a/doc/man1/notmuch-show.rst b/doc/man1/notmuch-show.rst index 64639174..c13d94de 100644 --- a/doc/man1/notmuch-show.rst +++ b/doc/man1/notmuch-show.rst @@ -27,6 +27,12 @@ Supported options for **show** include .. program:: show +.. option:: --duplicate=N + + Output duplicate number N. The numbering starts from 1, and matches + the order used by :option:`search --duplicate` and + :option:`search --output=files ` + .. option:: --entire-thread=(true|false) If true, **notmuch show** outputs all messages in the thread of @@ -36,7 +42,7 @@ Supported options for **show** include .. option:: --format=(text|json|sexp|mbox|raw) - **text** (default for messages) + text (default for messages) The default plain-text format has all text-content MIME parts decoded. Various components in the output, (**message**, **header**, **body**, **attachment**, and MIME **part**), will @@ -46,7 +52,7 @@ Supported options for **show** include '}'), to either open or close the component. For a multipart MIME message, these parts will be nested. - **json** + json The output is formatted with Javascript Object Notation (JSON). This format is more robust than the text format for automated processing. The nested structure of multipart MIME @@ -58,7 +64,7 @@ Supported options for **show** include as UTF-8 and any message content included in the output will be charset-converted to UTF-8. - **sexp** + sexp The output is formatted as the Lisp s-expression (sexp) equivalent of the JSON format above. Objects are formatted as property lists whose keys are keywords (symbols preceded by a @@ -66,7 +72,7 @@ Supported options for **show** include formatted as ``nil``. As for JSON, the s-expression output is always encoded as UTF-8. - **mbox** + mbox All matching messages are output in the traditional, Unix mbox format with each message being prefixed by a line beginning with "From " and a blank line separating each message. Lines @@ -77,7 +83,7 @@ Supported options for **show** include http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/mail-mbox-formats.html - **raw** (default if ``--part`` is given) + raw (default if ``--part`` is given) Write the raw bytes of the given MIME part of a message to standard out. For this format, it is an error to specify a query that matches more than one message. @@ -124,6 +130,15 @@ Supported options for **show** include By default, results will be displayed in reverse chronological order, (that is, the newest results will be displayed first). +.. option:: --offset=[-]N + + Skip displaying the first N results. With the leading '-', start + at the Nth result from the end. + +.. option:: --limit=N + + Limit the number of displayed results to N. + .. option:: --verify Compute and report the validity of any MIME cryptographic @@ -221,6 +236,13 @@ email messages. For this, use a search term of "thread:" as can be seen in the first column of output from the :any:`notmuch-search(1)` command. +CONFIGURATION +============= + +Structured output (json / sexp) is influenced by the configuration +option :nmconfig:`show.extra_headers`. See +:any:`notmuch-config(1)` for details. + EXIT STATUS =========== diff --git a/doc/man1/notmuch.rst b/doc/man1/notmuch.rst index c6ee3036..c488f12a 100644 --- a/doc/man1/notmuch.rst +++ b/doc/man1/notmuch.rst @@ -130,14 +130,16 @@ and to restore from that dump. The :any:`notmuch-config(1)` command can be used to get or set settings in the notmuch configuration file. -CUSTOM COMMANDS ---------------- +EXTERNAL COMMANDS +----------------- If the given command is not known to notmuch, notmuch tries to execute the external **notmuch-** in :envvar:`PATH` instead. This allows users to have their own notmuch related tools to be run via the notmuch command. By design, this does not allow notmuch's own commands -to be overridden using external commands. +to be overridden using external commands. The environment variable +:envvar:`NOTMUCH_CONFIG` will be set according to :option:`--config`, +if the latter is present. OPTION SYNTAX ------------- @@ -153,6 +155,23 @@ equivalent: notmuch --config:alt-config config get user.name notmuch --config alt-config config get user.name +.. _duplicate-files: + +DUPLICATE MESSAGE FILES +======================= + +Notmuch considers the :mailheader:`Message-ID` to be the primary +identifier of message. Per :rfc:`5322` the :mailheader:`Message-ID` is +supposed to be globally unique, but this fails in two distinct +ways. When you receive copies of a message via a mechanism like +:mailheader:`Cc` or via a mailing list, the copies are typically +interchangeable. In the case of some broken mail sending software, the +same :mailheader:`Message-ID` is used for completely unrelated +messages. The options :option:`search --duplicate` and :option:`show +--duplicate` options provide the user with control over which message +file is displayed. Front ends will need to provide their own +interface, see e.g. the Emacs front-end :any:`emacs-show-duplicates`. + ENVIRONMENT =========== diff --git a/doc/man5/notmuch-hooks.rst b/doc/man5/notmuch-hooks.rst index 268917cd..d778bdb8 100644 --- a/doc/man5/notmuch-hooks.rst +++ b/doc/man5/notmuch-hooks.rst @@ -19,7 +19,7 @@ must have executable permissions. The currently available hooks are described below. -**pre-new** +pre-new This hook is invoked by the :any:`notmuch-new(1)` command before scanning or importing new messages into the database. If this hook exits with a non-zero status, notmuch will abort further @@ -28,16 +28,16 @@ The currently available hooks are described below. Typically this hook is used for fetching or delivering new mail to be imported into the database. -**post-new** +post-new This hook is invoked by the :any:`notmuch-new(1)` command after - new messages have been imported into the database and initial tags - have been applied. The hook will not be run if there have been any - errors during the scan or import. + any new messages have been imported into the database and initial + tags have been applied. The hook will not be run if there have + been any errors during the scan or import. Typically this hook is used to perform additional query-based tagging on the imported messages. -**post-insert** +post-insert This hook is invoked by the :any:`notmuch-insert(1)` command after the message has been delivered, added to the database, and initial tags have been applied. The hook will not be run if there have diff --git a/doc/man7/notmuch-properties.rst b/doc/man7/notmuch-properties.rst index 7f128b5c..ff79f4c2 100644 --- a/doc/man7/notmuch-properties.rst +++ b/doc/man7/notmuch-properties.rst @@ -55,7 +55,7 @@ MESSAGE PROPERTIES The following properties are set by notmuch internally in the course of its normal activity. -**index.decryption** +index.decryption If a message contains encrypted content, and notmuch tries to decrypt that content during indexing, it will add the property ``index.decryption=success`` when the cleartext was successfully @@ -75,8 +75,7 @@ of its normal activity. :any:`notmuch-config(1)`), then this property will not be set on that message. -**session-key** - +session-key When :any:`notmuch-show(1)` or :any:`notmuch-reply(1)` encounters a message with an encrypted part, if notmuch finds a ``session-key`` property associated with the message, it will try @@ -111,8 +110,7 @@ of its normal activity. example, an AES-128 key might be stashed in a notmuch property as: ``session-key=7:14B16AF65536C28AF209828DFE34C9E0``. -**index.repaired** - +index.repaired Some messages arrive in forms that are confusing to view; they can be mangled by mail transport agents, or the sending mail user agent may structure them in a way that is confusing. If notmuch diff --git a/doc/man7/notmuch-search-terms.rst b/doc/man7/notmuch-search-terms.rst index e80cc7d0..acc1c967 100644 --- a/doc/man7/notmuch-search-terms.rst +++ b/doc/man7/notmuch-search-terms.rst @@ -153,10 +153,11 @@ date:.. or date: lastmod:.. The **lastmod:** prefix can be used to restrict the result by the database revision number of when messages were last modified (tags - were added/removed or filenames changed). This is usually used in - conjunction with the ``--uuid`` argument to - :any:`notmuch-search(1)` to find messages that have changed since - an earlier query. + were added/removed or filenames changed). Negative revisions are + interpreted relative to the most recent database revision (see + :option:`count --lastmod`). This is usually used in conjunction + with the ``--uuid`` argument to :any:`notmuch-search(1)` to find + messages that have changed since an earlier query. query: The **query:** prefix allows queries to refer to previously saved @@ -169,6 +170,12 @@ property:= can be present on a given message with several different values. See :any:`notmuch-properties(7)` for more details. +sexp: + The **sexp:** prefix allows subqueries in the format + documented in :any:`notmuch-sexp-queries(7)`. Note that subqueries containing + spaces must be quoted, and any embedded double quotes must be escaped + (see :any:`quoting`). + User defined prefixes are also supported, see :any:`notmuch-config(1)` for details. @@ -257,7 +264,7 @@ Boolean Probabilistic **body:**, **to:**, **attachment:**, **mimetype:** Special - **from:**, **query:**, **subject:** + **from:**, **query:**, **subject:**, **sexp:** Terms and phrases ----------------- @@ -275,11 +282,13 @@ the same phrase. - a.list.of.words Both parenthesised lists of terms and quoted phrases are ok with -probabilistic prefixes such as **to:**, **from:**, and **subject:**. In particular +probabilistic prefixes such as **to:**, **from:**, and **subject:**. +For prefixes supporting regex search, the parenthesised list should be +quoted. In particular :: - subject:(pizza free) + subject:"(pizza free)" is equivalent to @@ -295,6 +304,8 @@ Both of these will match a subject "Free Delicious Pizza" while will not. +.. _quoting: + Quoting ------- @@ -322,6 +333,13 @@ e.g. % notmuch search 'folder:"/^.*/(Junk|Spam)$/"' % notmuch search 'thread:"{from:mallory and date:2009}" and thread:{to:mallory}' +Double quotes within query strings need to be doubled to escape them. + +:: + + % notmuch search 'tag:"""quoted tag"""' + % notmuch search 'sexp:"(or ""wizard"" ""php"")"' + DATE AND TIME SEARCH ==================== diff --git a/doc/man7/notmuch-sexp-queries.rst b/doc/man7/notmuch-sexp-queries.rst index 019d15f0..858ff685 100644 --- a/doc/man7/notmuch-sexp-queries.rst +++ b/doc/man7/notmuch-sexp-queries.rst @@ -21,7 +21,7 @@ build of notmuch supports it with :: - $ notmuch config get built_with.sexpr_query + $ notmuch config get built_with.sexp_queries S-EXPRESSIONS @@ -31,10 +31,12 @@ An *s-expression* is either an atom, or list of whitespace delimited s-expressions inside parentheses. Atoms are either *basic value* + A basic value is an unquoted string containing no whitespace, double quotes, or parentheses. *quoted string* + Double quotes (") delimit strings possibly containing whitespace or parentheses. These can contain double quote characters by escaping with backslash. E.g. ``"this is a quote \""``. @@ -48,9 +50,11 @@ a *field*, *logical operation*, or *modifier*, and 0 or more subqueries. ``*`` + "*" matches any non-empty string in the current field. ``()`` + The empty list matches all messages *term* @@ -62,19 +66,23 @@ subqueries. phrase splitting see :any:`fields`. ``(`` *field* |q1| |q2| ... |qn| ``)`` + Restrict the queries |q1| to |qn| to *field*, and combine with *and* (for most fields) or *or*. See :any:`fields` for more information. ``(`` *operator* |q1| |q2| ... |qn| ``)`` + Combine queries |q1| to |qn|. Currently supported operators are ``and``, ``or``, and ``not``. ``(not`` |q1| ... |qn| ``)`` is equivalent to ``(and (not`` |q1| ``) ... (not`` |qn| ``))``. ``(`` *modifier* |q1| |q2| ... |qn| ``)`` + Combine queries |q1| to |qn|, and reinterpret the result (e.g. as a regular expression). See :any:`modifiers` for more information. ``(macro (`` |p1| ... |pn| ``) body)`` + Define saved query with parameter substitution. The syntax is recognized only in saved s-expression queries (see ``squery.*`` in :any:`notmuch-config(1)`). Parameter names in ``body`` must be @@ -111,6 +119,15 @@ a message has one such attribute, and ``and`` otherwise. Term or phrase fields can contain arbitrarily complex queries made up from terms, operators, and modifiers, but not other fields. +Range fields take one or two arguments specifying lower and upper +bounds. One argument is interpreted as identical upper and lower +bounds. Either upper or lower bound may be specified as ``""`` or +``*`` to specify the lowest possible lower bound or highest possible +upper bound. + +``lastmod`` ranges support negative arguments, interpreted relative to +the most recent database revision (see :option:`count --lastmod`). + .. _field-table: .. table:: Fields with supported modifiers @@ -164,26 +181,31 @@ MODIFIERS that are neither operators nor fields. ``(infix`` *atom* ``)`` + Interpret *atom* as an infix notmuch query (see :any:`notmuch-search-terms(7)`). Not supported inside fields. ``(matching`` |q1| |q2| ... |qn| ``)`` ``(of`` |q1| |q2| ... |qn| ``)`` + Match all messages have the same values of the current field as those matching all of |q1| ... |qn|. Supported in most term [#not-path]_ or phrase fields. Most commonly used in the ``thread`` field. ``(query`` *atom* ``)`` + Expand to the saved query named by *atom*. See :any:`notmuch-config(1)` for more. Note that the saved query must be in infix syntax (:any:`notmuch-search-terms(7)`). Not supported inside fields. ``(regex`` *atom* ``)`` ``(rx`` *atom* ``)`` + Interpret *atom* as a POSIX.2 regular expression (see :manpage:`regex(7)`). This applies in term fields and a subset [#not-phrase]_ of phrase fields (see :any:`field-table`). ``(starts-with`` *subword* ``)`` + Matches any term starting with *subword*. This applies in either phrase or term :any:`fields `, or outside of fields [#not-body]_. Note that a ``starts-with`` query cannot be part of a phrase. The @@ -193,65 +215,94 @@ EXAMPLES ======== ``Wizard`` + Match all messages containing the word "wizard", ignoring case. ``added`` + Match all messages containing "added", but also those containing "add", "additional", "Additional", "adds", etc... via stemming. ``(and Bob Marley)`` + Match messages containing words "Bob" and "Marley", or their stems The words need not be adjacent. ``(not Bob Marley)`` - Match messages containing neither "Bob" nor "Marley", nor their stems, + + Match messages containing neither "Bob" nor "Marley", nor their stems. ``"quick fox"`` ``quick-fox`` ``quick@fox`` + Match the *phrase* "quick" followed by "fox" in phrase fields (or outside a field). Match the literal string in a term field. ``(folder (of (id 1234@invalid)))`` - Match any message in the same folder as the one with Message-Id "1234@invalid" + + Match any message in the same folder as the one with Message-Id "1234\@invalid". ``(id 1234@invalid blah@test)`` - Matches Message-Id "1234@invalid" *or* Message-Id "blah@test" + + Matches Message-Id "1234\@invalid" *or* Message-Id "blah\@test". ``(and (infix "date:2009-11-18..2009-11-18") (tag unread))`` + + Match messages in the given date range with tag unread. + +``(and (date 2009-11-18 2009-11-18) (tag unread))`` + Match messages in the given date range with tag unread. +``(and (date 2009-11-18 *) (tag unread))`` + + Match messages from 2009-11-18 or later with tag unread. + +``(and (date * 2009-11-18) (tag unread))`` + + Match messages from 2009-11-18 or earlier with tag unread. + ``(starts-with prelim)`` + Match any words starting with "prelim". ``(subject quick "brown fox")`` + Match messages whose subject contains "quick" (anywhere, stemmed) and the phrase "brown fox". ``(subject (starts-with prelim))`` + Matches any word starting with "prelim", inside a message subject. -``(subject (starts-wih quick) "brown fox")`` +``(subject (starts-with quick) "brown fox")`` + Match messages whose subject contains "quick brown fox", but also "brown fox quicksand". ``(thread (of (id 1234@invalid)))`` - Match any message in the same thread as the one with Message-Id "1234@invalid" + + Match any message in the same thread as the one with Message-Id "1234\@invalid". ``(thread (matching (from bob@example.com) (to bob@example.com)))`` + Match any (messages in) a thread containing a message from - "bob@example.com" and a (possibly distinct) message to "bob at - example.com") + "bob\@example.com" and a (possibly distinct) message to + "bob\@example.com". ``(to (or bob@example.com mallory@example.org))`` ``(or (to bob@example.com) (to mallory@example.org))`` - Match in the "To" or "Cc" headers, "bob@example.com", - "mallory@example.org", and also "bob@example.com.au" since it + + Match in the "To" or "Cc" headers, "bob\@example.com", + "mallory\@example.org", and also "bob\@example.com.au" since it contains the adjacent triple "bob", "example", "com". ``(not (to *))`` + Match messages with an empty or invalid 'To' and 'Cc' field. ``(List *)`` + Match messages with a non-empty List-Id header, assuming - configuration ``index.header.List=List-Id`` + configuration ``index.header.List=List-Id``. .. _macro_examples: @@ -285,7 +336,7 @@ user defined fields is permitted within a macro. NOTES ===== -.. [#macro-details] Technically macros impliment lazy evaluation and +.. [#macro-details] Technically macros implement lazy evaluation and lexical scope. There is one top level scope containing all macro definitions, but all parameter definitions are local to a given macro. @@ -296,20 +347,20 @@ NOTES .. [#aka-bool] a.k.a. boolean prefixes -.. [#not-phrase] Due to the implemention of phrase fields in Xapian, +.. [#not-phrase] Due to the implementation of phrase fields in Xapian, regex queries could only match individual words. -.. [#not-body] Due the the way ``body`` is implemented in notmuch, +.. [#not-body] Due to the way ``body`` is implemented in notmuch, this modifier is not supported in the ``body`` field. .. [#not-path] Due to the way recursive ``path`` queries are implemented in notmuch, this modifier is not supported in the ``path`` field. -.. |q1| replace:: :math:`q_1` -.. |q2| replace:: :math:`q_2` -.. |qn| replace:: :math:`q_n` +.. |q1| replace:: `q`\ :sub:`1` +.. |q2| replace:: `q`\ :sub:`2` +.. |qn| replace:: `q`\ :sub:`n` -.. |p1| replace:: :math:`p_1` -.. |p2| replace:: :math:`p_2` -.. |pn| replace:: :math:`p_n` +.. |p1| replace:: `p`\ :sub:`1` +.. |p2| replace:: `p`\ :sub:`2` +.. |pn| replace:: `p`\ :sub:`n` diff --git a/doc/notmuch-emacs.rst b/doc/notmuch-emacs.rst index 12ee25e5..9c2dab47 100644 --- a/doc/notmuch-emacs.rst +++ b/doc/notmuch-emacs.rst @@ -1,6 +1,8 @@ -============= -notmuch-emacs -============= +.. _notmuch-emacs: + +========================== +Emacs Frontend for Notmuch +========================== About this Manual ================= @@ -12,7 +14,7 @@ manual to refer to the Emacs interface to Notmuch. When this distinction is important, we’ll refer to the Emacs interface as *notmuch-emacs*. -Notmuch-emacs is highly customizable via the the Emacs customization +Notmuch-emacs is highly customizable via the Emacs customization framework (or just by setting the appropriate variables). We try to point out relevant variables in this manual, but in order to avoid duplication of information, you can usually find the most detailed @@ -46,39 +48,95 @@ a mouse or by positioning the cursor and pressing ```` | Customize Notmuch or this page. You can change the overall appearance of the notmuch-hello screen by -customizing the variable :index:`notmuch-hello-sections`. +customizing the variables + +.. el:defcustom:: notmuch-hello-sections + + |docstring::notmuch-hello-sections| + +.. el:defcustom:: notmuch-hello-thousands-separator + + |docstring::notmuch-hello-thousands-separator| + +.. el:defcustom:: notmuch-show-logo + + |docstring::notmuch-show-logo| + +.. el:defcustom:: notmuch-column-control + + Controls the number of columns for saved searches/tags in notmuch view. + + This variable has three potential types of values: + + .. describe:: t + Automatically calculate the number of columns possible based + on the tags to be shown and the window width. + .. describe:: integer + + A lower bound on the number of characters that will + be used to display each column. + + .. describe:: float + + A fraction of the window width that is the lower bound on the + number of characters that should be used for each column. + + So: + + - if you would like two columns of tags, set this to 0.5. + + - if you would like a single column of tags, set this to 1.0. + + - if you would like tags to be 30 characters wide, set this to 30. + + - if you don't want to worry about all of this nonsense, leave + this set to `t`. + +.. el:defcustom:: notmuch-show-empty-saved-searches + + |docstring::notmuch-show-empty-saved-searches| notmuch-hello key bindings -------------------------- -```` +.. el:define-key:: + Move to the next widget (button or text entry field) -```` +.. el:define-key:: + Move to the previous widget. -```` +.. el:define-key:: + Activate the current widget. -``g`` ``=`` +.. el:define-key:: g + = + Refresh the buffer; mainly update the counts of messages for various saved searches. -``G`` +.. el:define-key:: G + Import mail, See :ref:`importing` -``m`` +.. el:define-key:: m + Compose a message -``s`` +.. el:define-key:: s + Search the notmuch database using :ref:`notmuch-search` -``v`` +.. el:define-key:: v + Print notmuch version -``q`` +.. el:define-key:: q + Quit .. _saved-searches: @@ -96,25 +154,28 @@ The saved searches default to various common searches such as ``tag:inbox`` to access the inbox and ``tag:unread`` to access all unread mail, but there are several options for customization: -:index:`notmuch-saved-searches` +.. el:defcustom:: notmuch-saved-searches + The list of saved searches, including names, queries, and additional per-query options. -:index:`notmuch-saved-search-sort-function` +.. el:defcustom:: 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’. -:index:`notmuch-column-control` - Controls the number of columns for displaying saved-searches/tags - Search Box ---------- The search box lets the user enter a Notmuch query. See section “Description” in Notmuch Query Syntax, for more info on Notmuch query syntax. A history of recent searches is also displayed by default. The -latter is controlled by the variable :index:`notmuch-hello-recent-searches-max`. +latter is controlled by the variable `notmuch-hello-recent-searches-max`. + +.. el:defcustom:: notmuch-hello-recent-searches-max + + |docstring::notmuch-hello-recent-searches-max| Known Tags ---------- @@ -123,15 +184,14 @@ One special kind of saved search provided by default is for each individual tag defined in the database. This can be controlled via the following variables. -:index:`notmuch-hello-tag-list-make-query` +.. el:defcustom:: notmuch-hello-tag-list-make-query + Control how to construct a search (“virtual folder”) from a given tag. -:index:`notmuch-hello-hide-tags` - Which tags not to display at all. +.. el:defcustom:: notmuch-hello-hide-tags -:index:`notmuch-column-control` - Controls the number of columns for displaying saved-searches/tags + Which tags not to display at all. .. _notmuch-search: @@ -150,31 +210,93 @@ The main purpose of the ``notmuch-search-mode`` buffer is to act as a menu of results that the user can explore further by pressing ```` on the appropriate line. -``n,C-n,`` +.. el:define-key:: n + C-n + + Move to next line -``p,C-p,`` +.. el:define-key:: + p + C-p + + Move to previous line -```` +.. el:define-key:: + Open thread on current line in :ref:`notmuch-show` mode -``g`` ``=`` +.. el:define-key:: g + = + Refresh the buffer -``?`` +.. el:define-key:: i + + Toggle whether to show messages with excluded tags in search results. + +.. el:define-key:: ? + Display full set of key bindings The presentation of results can be controlled by the following variables. -:index:`notmuch-search-result-format` - Control how each thread of messages is presented in the - ``notmuch-show-mode`` buffer +.. el:defcustom:: notmuch-search-result-format + + |docstring::notmuch-search-result-format| + + If the car of an element in notmuch-search-result-format is a + function, insert the result of calling the function into the buffer. + + This allows a user to generate custom fields in the output of a + search result. For example, with the following settings, the first + few characters on each line of the search result are used to show + information about some significant tags associated with the thread. + + .. code:: lisp + + (defun -notmuch-result-flags (format-string result) + (let ((tags-to-letters '(("flagged" . "!") + ("unread" . "u") + ("mine" . "m") + ("sent" . "s") + ("replied" . "r"))) + (tags (plist-get result :tags))) + (format format-string + (mapconcat (lambda (t2l) + (if (member (car t2l) tags) + (cdr t2l) + " ")) + tags-to-letters "")))) + + (setq notmuch-search-result-format '((-notmuch-result-flags . "%s ") + ("date" . "%12s ") + ("count" . "%9s ") + ("authors" . "%-30s ") + ("subject" . "%s ") + ("tags" . "(%s)"))) + + See also :el:defcustom:`notmuch-tree-result-format` and + :el:defcustom:`notmuch-unthreaded-result-format`. + +.. el:defcustom:: notmuch-search-oldest-first -:index:`notmuch-search-oldest-first` Display the oldest threads at the top of the buffer +It is also possible to customize how the name of buffers containing +search results is formatted using the following variables: + +.. el:defcustom:: notmuch-search-buffer-name-format + + |docstring::notmuch-search-buffer-name-format| + +.. el:defcustom:: notmuch-saved-search-buffer-name-format + + |docstring::notmuch-saved-search-buffer-name-format| + + .. _notmuch-show: notmuch-show @@ -188,40 +310,161 @@ signatures, already-read messages), are hidden. You can make these parts visible by clicking with the mouse button or by pressing RET after positioning the cursor on a hidden part. -```` +.. el:define-key:: + Scroll the current message (if necessary), advance to the next message, or advance to the next thread (if already on the last message of a thread). -``c`` +.. el:define-key:: c + :ref:`show-copy` -``N`` +.. el:define-key:: + + - + + Add or remove arbitrary tags from the current message. + +.. el:define-key:: ! + + |docstring::notmuch-show-toggle-elide-non-matching| + +.. el:define-key:: ? + + Display full set of key bindings + +.. _show-navigation: + +Navigation +---------- + +The following motion/navigation bindings are also available in +:ref:`notmuch-tree` and :ref:`notmuch-unthreaded`. + +.. el:define-key:: N + Move to next message -``P`` +.. el:define-key:: P + Move to previous message (or start of current message) -``n`` +.. el:define-key:: n + Move to next matching message -``p`` +.. el:define-key:: p + Move to previous matching message -``+,-`` - Add or remove arbitrary tags from the current message. +Actions +------- -``?`` - Display full set of key bindings +The following actions on messages are also available in +:ref:`notmuch-tree` and :ref:`notmuch-unthreaded`. + +.. el:define-key:: b + M-x notmuch-show-resend-message + + Resend (*bounce*) message. + +.. el:define-key:: e + + Resume *editing* a message in :ref:`notmuch-message`. This is + particularly useful for :ref:`notmuch-emacs-drafts`, but can be + used for other messages as well. + +.. el:define-key:: f + M-x notmuch-show-forward-message + + Forward (as a MIME attachment) message + +.. el:define-key:: r + M-x notmuch-show-reply-to-sender + + Reply to sender (only) using :ref:`notmuch-message` + +.. el:define-key:: w + M-x notmuch-show-save-attachments + + |docstring::notmuch-show-save-attachments| -Display of messages can be controlled by the following variables +.. el:define-key:: R + M-x notmuch-show-reply + + |docstring::notmuch-show-reply| + +Configuration +------------- + +Display of messages can be controlled by the following variables; see also :ref:`show-large`. + +.. el:defcustom:: notmuch-message-headers -:index:`notmuch-message-headers` |docstring::notmuch-message-headers| -:index:`notmuch-message-headers-visible` +.. el:defcustom:: notmuch-message-headers-visible + |docstring::notmuch-message-headers-visible| +.. el:defcustom:: notmuch-show-header-line + + |docstring::notmuch-show-header-line| + +.. el:defcustom:: notmuch-multipart/alternative-discouraged + + Which mime types to hide by default for multipart messages. + + Can either be a list of mime types (as strings) or a function + mapping a plist representing the current message to such a list. + The following example function would discourage `text/html` and + `multipart/related` generally, but discourage `text/plain` should + the message be sent from `whatever@example.com`. + + .. code:: lisp + + (defun my--determine-discouraged (msg) + (let* ((headers (plist-get msg :headers)) + (from (or (plist-get headers :From) ""))) + (cond + ((string-match "whatever@example.com" from) + (list "text/plain")) + (t + (list "text/html" "multipart/related"))))) + +.. _show-large: + +Dealing with large messages and threads +--------------------------------------- + +If you are finding :ref:`notmuch-show` is annoyingly slow displaying +large messages, you can customize +:el:defcustom:`notmuch-show-max-text-part-size`. If you want to speed up the +display of large threads (with or without large messages), there are +several options. First, you can display the same query in one of the +other modes. :ref:`notmuch-unthreaded` is the most robust for +extremely large queries, but :ref:`notmuch-tree` is also be faster +than :ref:`notmuch-show` in general, since it only renders a single +message a time. If you prefer to stay with the rendered thread +("conversation") view of :ref:`notmuch-show`, you can customize the +variables :el:defcustom:`notmuch-show-depth-limit`, +:el:defcustom:`notmuch-show-height-limit` and +:el:defcustom:`notmuch-show-max-text-part-size` to limit the amount of +rendering done initially. Note that these limits are implicitly +*OR*-ed together, and combinations might have surprising effects. + +.. el:defcustom:: notmuch-show-depth-limit + + |docstring::notmuch-show-depth-limit| + +.. el:defcustom:: notmuch-show-height-limit + + |docstring::notmuch-show-height-limit| + +.. el:defcustom:: notmuch-show-max-text-part-size + + |docstring::notmuch-show-max-text-part-size| + .. _show-copy: Copy to kill-ring @@ -232,44 +475,92 @@ but notmuch also provides some shortcuts. These keys are available in :ref:`notmuch-show`, and :ref:`notmuch-tree`. A subset are available in :ref:`notmuch-search`. -``c F`` ``notmuch-show-stash-filename`` +.. el:define-key:: c F + M-x notmuch-show-stash-filename + |docstring::notmuch-show-stash-filename| -``c G`` ``notmuch-show-stash-git-send-email`` +.. el:define-key:: c G + M-x notmuch-show-stash-git-send-email + |docstring::notmuch-show-stash-git-send-email| -``c I`` ``notmuch-show-stash-message-id-stripped`` +.. el:define-key:: c I + M-x notmuch-show-stash-message-id-stripped + |docstring::notmuch-show-stash-message-id-stripped| -``c L`` ``notmuch-show-stash-mlarchive-link-and-go`` +.. el:define-key:: c L + M-x notmuch-show-stash-mlarchive-link-and-go + |docstring::notmuch-show-stash-mlarchive-link-and-go| -``c T`` ``notmuch-show-stash-tags`` +.. el:define-key:: c T + M-x notmuch-show-stash-tags + |docstring::notmuch-show-stash-tags| -``c c`` ``notmuch-show-stash-cc`` +.. el:define-key:: c c + M-x notmuch-show-stash-cc + |docstring::notmuch-show-stash-cc| -``c d`` ``notmuch-show-stash-date`` +.. el:define-key:: c d + M-x notmuch-show-stash-date + |docstring::notmuch-show-stash-date| -``c f`` ``notmuch-show-stash-from`` +.. el:define-key:: c f + M-x notmuch-show-stash-from + |docstring::notmuch-show-stash-from| -``c i`` ``notmuch-show-stash-message-id`` +.. el:define-key:: c i + M-x notmuch-show-stash-message-id + |docstring::notmuch-show-stash-message-id| -``c l`` ``notmuch-show-stash-mlarchive-link`` +.. el:define-key:: c l + M-x notmuch-show-stash-mlarchive-link + |docstring::notmuch-show-stash-mlarchive-link| -``c s`` ``notmuch-show-stash-subject`` +.. el:define-key:: c s + M-x notmuch-show-stash-subject + |docstring::notmuch-show-stash-subject| -``c t`` ``notmuch-show-stash-to`` +.. el:define-key:: c t + M-x notmuch-show-stash-to + |docstring::notmuch-show-stash-to| -``c ?`` - Show all available copying commands +.. el:define-key:: c ? + M-x notmuch-subkeymap-help + + Show all available copying commands + +.. _emacs-show-duplicates: + +Dealing with duplicates +----------------------- + +If there are multiple files with the same :mailheader:`Message-ID` +(see :any:`duplicate-files`), then :any:`notmuch-show` displays the +number of duplicates and identifies the current duplicate. In the +following example duplicate 3 of 5 is displayed. + +.. code-block:: + :emphasize-lines: 1 + + M. Mustermann (Sat, 30 Jul 2022 10:33:10 -0300) (inbox signed) 3/5 + Subject: Re: Multiple files per message in emacs + To: notmuch@notmuchmail.org + +.. el:define-key:: % + M-x notmuch-show-choose-duplicate + + |docstring::notmuch-show-choose-duplicate| .. _notmuch-tree: @@ -279,58 +570,209 @@ notmuch-tree ``notmuch-tree-mode`` displays the results of a "notmuch tree" of your email archives. Each line in the buffer represents a single message giving the relative date, the author, subject, and any -tags. +tags. For common navigation commands see :ref:`show-navigation`. -``c`` - :ref:`show-copy` +.. el:define-key:: c -```` - Displays that message. - -``N`` - Move to next message + :ref:`show-copy` -``P`` - Move to previous message +.. el:define-key:: -``n`` - Move to next matching message + Displays that message. -``p`` - Move to previous matching message +.. el:define-key:: o + M-x notmuch-tree-toggle-order -``o`` ``notmuch-tree-toggle-order`` |docstring::notmuch-tree-toggle-order| -``l`` ``notmuch-tree-filter`` +.. el:define-key:: l + M-x notmuch-tree-filter + Filter or LIMIT the current search results based on an additional query string -``t`` ``notmuch-tree-filter-by-tag`` +.. el:define-key:: t + M-x notmuch-tree-filter-by-tag + Filter the current search results based on an additional tag +.. el:define-key:: i + + Toggle whether to show messages with excluded tags in search results. + +.. el:define-key:: g + = -``g`` ``=`` Refresh the buffer -``?`` +.. el:define-key:: ? + Display full set of key bindings As is the case with :ref:`notmuch-search`, the presentation of results can be controlled by the variable ``notmuch-search-oldest-first``. +.. el:defcustom:: notmuch-tree-result-format + + |docstring::notmuch-tree-result-format| + + The following example shows how to optionally display recipients instead + of authors for sent mail (assuming the user is named Mustermann). + + .. code:: lisp + + (defun -notmuch-authors-or-to (format-string result) + (let* ((headers (plist-get result :headers)) + (to (plist-get headers :To)) + (author (plist-get headers :From)) + (face (if (plist-get result :match) + 'notmuch-tree-match-author-face + 'notmuch-tree-no-match-author-face))) + (propertize + (format format-string + (if (string-match "Mustermann" author) + (concat "To:" (notmuch-tree-clean-address to)) + author)) + 'face face))) + + (setq notmuch-tree-result-format + '(("date" . "%12s ") + (-notmuch-authors-or-to . "%-20.20s") + ((("tree" . "%s") + ("subject" . "%s")) + . " %-54s ") + ("tags" . "(%s)"))) + + See also :el:defcustom:`notmuch-search-result-format` and + :el:defcustom:`notmuch-unthreaded-result-format`. + +.. _notmuch-tree-outline: + +notmuch-tree-outline +-------------------- + +When this mode is set, each thread and subthread in the results +list is treated as a foldable section, with its first message as +its header. + +The mode just makes available in the tree buffer all the +keybindings in info:emacs#Outline_Mode, and binds the following +additional keys: + +.. el:define-key:: + + Cycle visibility state of the current message's tree. + +.. el:define-key:: + + Cycle visibility state of all trees in the buffer. + +The behaviour of this minor mode is affected by the following +customizable variables: + +.. el:defcustom:: notmuch-tree-outline-enabled + + |docstring::notmuch-tree-outline-enabled| + +.. el:defcustom:: notmuch-tree-outline-visibility + + |docstring::notmuch-tree-outline-visibility| + +.. el:defcustom:: notmuch-tree-outline-auto-close + + |docstring::notmuch-tree-outline-auto-close| + +.. el:defcustom:: notmuch-tree-outline-open-on-next + + |docstring::notmuch-tree-outline-open-on-next| + +.. _notmuch-unthreaded: + +notmuch-unthreaded +------------------ + +``notmuch-unthreaded-mode`` is similar to :any:`notmuch-tree` in that +each line corresponds to a single message, but no thread information +is presented. + +Keybindings are the same as :any:`notmuch-tree`. + +.. el:defcustom:: notmuch-unthreaded-result-format + + |docstring::notmuch-unthreaded-result-format| + + See also :el:defcustom:`notmuch-search-result-format` and + :el:defcustom:`notmuch-tree-result-format`. + +.. _notmuch-message: + +notmuch-message +=============== + +Notmuch uses a slighly customized version of the standard emacs email +composition mode info:message. + +.. _notmuch-emacs-drafts: + +Drafts +------ + +.. el:define-key:: C-x C-p + M-x notmuch-draft-postpone + + Postpone (save and bury) a draft. + +.. el:define-key:: C-x C-s + M-x notmuch-draft-save + + Save a draft and continue editing. + +.. _notmuch-emacs-replies: + +.. _notmuch-emacs-fcc: + +Fcc +--- + +.. el:defcustom:: notmuch-fcc-dirs + + |docstring::notmuch-fcc-dirs| + +.. el:defcustom:: notmuch-maildir-use-notmuch-insert + + |docstring::notmuch-maildir-use-notmuch-insert| + +Replies +------- + +.. el:defcustom:: message-dont-reply-to-names + + When composing mail replies, Emacs's message mode uses the + variable :code:`message-dont-reply-to-names` to exclude + recipients matching a given collection of regular expressions + or satisfying an arbitrary predicate. Notmuch's MUA inherits + this standard mechanism and will honour your customization of + this variable. Global key bindings =================== -Several features are accessible from anywhere in notmuch through the +Several features are accessible from most places in notmuch through the following key bindings: -``j`` +.. el:define-key:: j + Jump to saved searches using :ref:`notmuch-jump`. -``k`` +.. el:define-key:: k + Tagging operations using :ref:`notmuch-tag-jump` +.. el:define-key:: C-_ + C-/ + C-x u + + Undo previous tagging operation using :any:`notmuch-tag-undo` + .. _notmuch-jump: notmuch-jump @@ -356,14 +798,32 @@ prefix (:kbd:`C-u k`), notmuch displays a menu of the reverses of the operations specified in ``notmuch-tagging-keys``; i.e. each ``+tag`` is replaced by ``-tag`` and vice versa. -:index:`notmuch-tagging-keys` +.. el:defcustom:: notmuch-tagging-keys |docstring::notmuch-tagging-keys| + +notmuch-tag-undo +---------------- + +Each notmuch buffer supporting tagging operations (i.e. buffers in +:any:`notmuch-show`, :any:`notmuch-search`, :any:`notmuch-tree`, and +:any:`notmuch-unthreaded` mode) keeps a local stack of tagging +operations. These can be undone via :any:`notmuch-tag-undo`. By default +this is bound to the usual Emacs keys for undo. + +.. el:define-key:: C-_ + C-/ + C-x u + M-x notmuch-tag-undo + + |docstring::notmuch-tag-undo| + Buffer navigation ================= -:index:`notmuch-cycle-notmuch-buffers` +.. el:define-key:: M-x notmuch-cycle-notmuch-buffers + |docstring::notmuch-cycle-notmuch-buffers| Configuration @@ -374,16 +834,18 @@ Configuration Importing Mail -------------- -:index:`notmuch-poll` +.. el:define-key:: M-x notmuch-poll + |docstring::notmuch-poll| -:index:`notmuch-poll-script` +.. el:defcustom:: notmuch-poll-script + |docstring::notmuch-poll-script| Sending Mail ------------ -:index:`mail-user-agent` +.. el:defcustom:: mail-user-agent Emacs consults the variable :code:`mail-user-agent` to choose a mail sending package for commands like :code:`report-emacs-bug` and diff --git a/doc/queries.rst b/doc/queries.rst new file mode 100644 index 00000000..b76e71e0 --- /dev/null +++ b/doc/queries.rst @@ -0,0 +1,9 @@ +Notmuch Queries +=============== + +.. toctree:: + :titlesonly: + + man7/notmuch-search-terms + man7/notmuch-sexp-queries + man7/notmuch-properties diff --git a/emacs/Makefile.local b/emacs/Makefile.local index d1b320c3..0f1f0eb2 100644 --- a/emacs/Makefile.local +++ b/emacs/Makefile.local @@ -42,7 +42,7 @@ emacs_mua := $(dir)/notmuch-emacs-mua emacs_mua_desktop := $(dir)/notmuch-emacs-mua.desktop emacs_images := \ - $(srcdir)/$(dir)/notmuch-logo.png + $(srcdir)/$(dir)/notmuch-logo.svg emacs_bytecode = $(emacs_sources:.el=.elc) emacs_docstrings = $(emacs_sources:.el=.rsti) diff --git a/emacs/coolj.el b/emacs/coolj.el index d820525b..79d2a1b7 100644 --- a/emacs/coolj.el +++ b/emacs/coolj.el @@ -45,7 +45,7 @@ Otherwise respect `fill-column'." :group 'coolj :type 'boolean) -(defcustom coolj-line-prefix-regexp "^\\(>+ \\)*" +(defcustom coolj-line-prefix-regexp "^\\(>+ ?\\)*" "Regular expression that matches line prefixes." :group 'coolj :type 'regexp) diff --git a/emacs/notmuch-address.el b/emacs/notmuch-address.el index 1a4cdda2..f756254c 100644 --- a/emacs/notmuch-address.el +++ b/emacs/notmuch-address.el @@ -153,8 +153,7 @@ matching `notmuch-address-completion-headers-regexp'." ;;; Setup (defun notmuch-address-selection-function (prompt collection initial-input) - "Call (`completing-read' - PROMPT COLLECTION nil nil INITIAL-INPUT 'notmuch-address-history)" + "Default address selection function: delegate to completing read." (completing-read prompt collection nil nil initial-input 'notmuch-address-history)) diff --git a/emacs/notmuch-emacs-mua b/emacs/notmuch-emacs-mua index a5214977..254e6407 100755 --- a/emacs/notmuch-emacs-mua +++ b/emacs/notmuch-emacs-mua @@ -41,6 +41,9 @@ CREATE_FRAME= ELISP= MAILTO= HELLO= +TO_SEP= +CC_SEP= +BCC_SEP= # Short options compatible with mutt(1). while getopts :s:c:b:i:h opt; do @@ -86,13 +89,16 @@ while getopts :s:c:b:i:h opt; do ELISP="${ELISP} (message-goto-subject) (insert \"${OPTARG}\")" ;; --to) - ELISP="${ELISP} (message-goto-to) (insert \"${OPTARG}, \")" + ELISP="${ELISP} (message-goto-to) (insert \"${TO_SEP}${OPTARG}\")" + TO_SEP=", " ;; --cc|c) - ELISP="${ELISP} (message-goto-cc) (insert \"${OPTARG}, \")" + ELISP="${ELISP} (message-goto-cc) (insert \"${CC_SEP}${OPTARG}\")" + CC_SEP=", " ;; --bcc|b) - ELISP="${ELISP} (message-goto-bcc) (insert \"${OPTARG}, \")" + ELISP="${ELISP} (message-goto-bcc) (insert \"${BCC_SEP}${OPTARG}\")" + BCC_SEP=", " ;; --body|i) ELISP="${ELISP} (message-goto-body) (insert-file \"${OPTARG}\")" diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index 71487bd9..67c805a8 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -33,10 +33,11 @@ (declare-function notmuch-poll "notmuch-lib" ()) (declare-function notmuch-tree "notmuch-tree" (&optional query query-context target buffer-name - open-target unthreaded parent-buffer oldest-first)) + open-target unthreaded parent-buffer + oldest-first hide-excluded)) (declare-function notmuch-unthreaded "notmuch-tree" (&optional query query-context target buffer-name - open-target)) + open-target oldest-first hide-excluded)) ;;; Options @@ -115,6 +116,12 @@ searches so they still work in customize." (const :tag "Default" nil) (const :tag "Oldest-first" oldest-first) (const :tag "Newest-first" newest-first))) + (group :format "%v" :inline t + (const :format "" :excluded) + (choice :tag " Hide Excluded" + (const :tag "Default" nil) + (const :tag "Hide" hide) + (const :tag "Show" show))) (group :format "%v" :inline t (const :format "" :search-type) (choice :tag " Search Type" @@ -143,9 +150,13 @@ a plist. Supported properties are :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. + :excluded Whether to show mail with excluded tags in the + search. Possible values are `hide', `show', + or nil. Nil means use the default value of + `notmuch-search-hide-excluded'. :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 + specify tree mode, \\='unthreaded to specify unthreaded mode, and set to nil (or anything except tree and unthreaded) to specify search mode. @@ -198,7 +209,7 @@ 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"))) +(defimage notmuch-hello-logo ((:type svg :file "notmuch-logo.svg"))) (defcustom notmuch-show-logo t "Should the notmuch logo be shown?" @@ -484,16 +495,19 @@ diagonal." append (notmuch-hello-reflect-generate-row ncols nrows row list)))) (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) - nil nil nil nil nil nil - (widget-get widget :notmuch-search-oldest-first))) - (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))))) + (let ((search-terms (widget-get widget :notmuch-search-terms)) + (oldest-first (widget-get widget :notmuch-search-oldest-first)) + (exclude (widget-get widget :notmuch-search-hide-excluded))) + (cl-case (widget-get widget :notmuch-search-type) + (tree + (let ((n (notmuch-search-format-buffer-name (widget-value widget) "tree" t))) + (notmuch-tree search-terms nil nil n nil nil nil oldest-first exclude))) + (unthreaded + (let ((n (notmuch-search-format-buffer-name (widget-value widget) + "unthreaded" t))) + (notmuch-unthreaded search-terms nil nil n nil oldest-first exclude))) + (t + (notmuch-search search-terms oldest-first exclude))))) (defun notmuch-saved-search-count (search) (car (notmuch--process-lines notmuch-command "count" search))) @@ -557,7 +571,8 @@ with any properties in the original saved-search. The values :show-empty-searches, :filter and :filter-count from options will be handled as specified for -`notmuch-hello-insert-searches'." +`notmuch-hello-insert-searches'. :disable-includes can be used to +turn off the default exclude processing in `notmuch-count(1)'" (with-temp-buffer (dolist (elem query-list nil) (let ((count-query (or (notmuch-saved-search-get elem :count-query) @@ -570,7 +585,11 @@ options will be handled as specified for (plist-get options :filter)))) "\n"))) (unless (= (notmuch--call-process-region (point-min) (point-max) notmuch-command - t t nil "count" "--batch") 0) + t t nil "count" + (if (plist-get options :disable-excludes) + "--exclude=false" + "--exclude=true") + "--batch") 0) (notmuch-logged-error "notmuch count --batch failed" "Please check that the notmuch CLI is new enough to support `count @@ -635,6 +654,10 @@ with `notmuch-hello-query-counts'." (newest-first nil) (oldest-first t) (otherwise notmuch-search-oldest-first))) + (exclude (cl-case (plist-get elem :excluded) + (hide t) + (show nil) + (otherwise notmuch-search-hide-excluded))) (search-type (plist-get elem :search-type)) (msg-count (plist-get elem :count))) (widget-insert (format "%8s " @@ -644,6 +667,7 @@ with `notmuch-hello-query-counts'." :notmuch-search-terms query :notmuch-search-oldest-first oldest-first :notmuch-search-type search-type + :notmuch-search-hide-excluded exclude name) (setq column-indent (1+ (max 0 (- column-width (length name))))))) @@ -686,11 +710,7 @@ with `notmuch-hello-query-counts'." ;; configuration change, and this is not a new window) (setq do-refresh t))))) (when (and do-refresh notmuch-hello-auto-refresh) - ;; 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)) + (notmuch-hello t)) (unless hello-buf ;; Clean up hook (remove-hook 'window-configuration-change-hook @@ -702,12 +722,15 @@ with `notmuch-hello-query-counts'." ;; 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 (kbd "") 'widget-backward) + ;; Currently notmuch-hello-mode supports free text entry, but not + ;; tagging operations, so provide standard undo. + (define-key map [remap notmuch-tag-undo] #'undo) map) "Keymap for \"notmuch hello\" buffers.") (define-derived-mode notmuch-hello-mode fundamental-mode "notmuch-hello" - "Major mode for convenient notmuch navigation. This is your entry portal into notmuch. + "Major mode for convenient notmuch navigation. This is your entry +portal into notmuch. Saved searches are \"bookmarks\" for arbitrary queries. Hit RET or click on a saved search to view matching threads. Edit saved @@ -737,7 +760,9 @@ 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 notmuch-buffer-refresh-function #'notmuch-hello-update) + (when (boundp 'untrusted-content) + (setq untrusted-content t))) ;;; Inserters @@ -786,7 +811,7 @@ Complete list of currently available key bindings: :help-echo "Refresh" (notmuch-hello-nice-number (string-to-number - (car (notmuch--process-lines notmuch-command "count"))))) + (car (notmuch--process-lines notmuch-command "count" "--exclude=false"))))) (widget-insert " messages.\n"))) (defun notmuch-hello-insert-saved-searches () @@ -843,7 +868,8 @@ Complete list of currently available key bindings: (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. + "Insert a section with TITLE showing a list of buttons made from +QUERY-LIST. QUERY-LIST should ideally be a plist but for backwards compatibility other forms are also accepted (see @@ -857,13 +883,16 @@ Supports the following entries in OPTIONS as a plist: :show-empty-searches - show buttons with no matching messages :hide-if-empty - hide if no buttons would be shown (only makes sense without :show-empty-searches) -:filter - This can be a function that takes the search query as its argument and - returns a filter to be used in conjunction with the query for that search or nil - to hide the element. This can also be a string that is used as a combined with - each query using \"and\". -:filter-count - Separate filter to generate the count displayed each search. Accepts - the same values as :filter. If :filter and :filter-count are specified, this - will be used instead of :filter, not in conjunction with it." +:filter - This can be a function that takes the search query as + its argument and returns a filter to be used in conjunction + with the query for that search or nil to hide the + element. This can also be a string that is used as a combined + with each query using \"and\". +:filter-count - Separate filter to generate the count displayed + each search. Accepts the same values as :filter. If :filter + and :filter-count are specified, this will be used instead of + :filter, not in conjunction with it." + (widget-insert title ": ") (when (and notmuch-hello-first-run (plist-get options :initially-hidden)) (add-to-list 'notmuch-hello-hidden-sections title)) @@ -918,7 +947,8 @@ following: nil :initially-hidden (not notmuch-show-all-tags-list) :hide-tags notmuch-hello-hide-tags - :filter notmuch-hello-tag-list-make-query)) + :filter notmuch-hello-tag-list-make-query + :disable-excludes t)) (defun notmuch-hello-insert-footer () "Insert the notmuch-hello footer." diff --git a/emacs/notmuch-jump.el b/emacs/notmuch-jump.el index 6a276928..3161ed95 100644 --- a/emacs/notmuch-jump.el +++ b/emacs/notmuch-jump.el @@ -50,15 +50,21 @@ fast way to jump to a saved search from anywhere in Notmuch." (cl-case (plist-get saved-search :sort-order) (newest-first nil) (oldest-first t) - (otherwise (default-value 'notmuch-search-oldest-first))))) + (otherwise (default-value 'notmuch-search-oldest-first)))) + (exclude (cl-case (plist-get saved-search :excluded) + (hide t) + (show nil) + (otherwise notmuch-search-hide-excluded)))) (push (list key name (cond ((eq (plist-get saved-search :search-type) 'tree) - (lambda () (notmuch-tree query))) + (lambda () (notmuch-tree query nil nil nil nil nil nil + oldest-first exclude))) ((eq (plist-get saved-search :search-type) 'unthreaded) - (lambda () (notmuch-unthreaded query))) + (lambda () (notmuch-unthreaded query nil nil nil nil + oldest-first exclude))) (t - (lambda () (notmuch-search query oldest-first))))) + (lambda () (notmuch-search query oldest-first exclude))))) action-map))))) (setq action-map (nreverse action-map)) (if action-map diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index 45817e13..bf9c4a53 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -105,6 +105,16 @@ search." :group 'notmuch-search) (make-variable-buffer-local 'notmuch-search-oldest-first) +(defcustom notmuch-search-hide-excluded t + "Hide mail tagged with a excluded tag. + +Excluded tags are defined in the users configuration file under +the search section. When this variable is true, any mail with +such a tag will not be shown in the search output." + :type 'boolean + :group 'notmuch-search) +(make-variable-buffer-local 'notmuch-search-hide-excluded) + (defcustom notmuch-poll-script nil "[Deprecated] Command to run to incorporate new mail into the notmuch database. @@ -166,6 +176,7 @@ For example, if you wanted to remove an \"inbox\" tag and add an (define-key map (kbd "M-=") 'notmuch-refresh-all-buffers) (define-key map "G" 'notmuch-poll-and-refresh-this-buffer) (define-key map "j" 'notmuch-jump-search) + (define-key map [remap undo] 'notmuch-tag-undo) map) "Keymap shared by all notmuch modes.") @@ -413,9 +424,9 @@ This is similar to `describe-function' for the current major mode, but bindings tables are shown with documentation strings rather than command names. By default, this uses the first line of each command's documentation string. A command can override -this by setting the 'notmuch-doc property of its command symbol. +this by setting the \\='notmuch-doc property of its command symbol. A command that supports a prefix argument can explicitly document -its prefixed behavior by setting the 'notmuch-prefix-doc property +its prefixed behavior by setting the \\='notmuch-prefix-doc property of its command symbol." (interactive) (let ((doc (substitute-command-keys @@ -566,12 +577,20 @@ Take wildcards into account." (string= (downcase t1) (downcase t2)))))) -(defvar notmuch-multipart/alternative-discouraged +(defcustom notmuch-multipart/alternative-discouraged '(;; Avoid HTML parts. "text/html" ;; multipart/related usually contain a text/html part and some ;; associated graphics. - "multipart/related")) + "multipart/related") + "Which mime types to hide by default for multipart messages. + +Can either be a list of mime types (as strings) or a function +mapping a plist representing the current message to such a list. +See Info node `(notmuch-emacs) notmuch-show' for a sample function." + :group 'notmuch-show + :type '(radio (repeat :tag "MIME Types" string) + (function :tag "Function"))) (defun notmuch-multipart/alternative-determine-discouraged (msg) "Return the discouraged alternatives for the specified message." @@ -694,6 +713,7 @@ current buffer, if possible." (when (mm-inlinable-p handle) (set-buffer display-buffer) (mm-display-part handle) + (plist-put part :undisplayer (mm-handle-undisplayer handle)) t)))))) ;;; Generic Utilities @@ -716,7 +736,7 @@ single element face list." (list face))) (defun notmuch-apply-face (object face &optional below start end) - "Combine FACE into the 'face text property of OBJECT between START and END. + "Combine FACE into the \\='face text property of OBJECT between START and END. This function combines FACE with any existing faces between START and END in OBJECT. Attributes specified by FACE take precedence @@ -1020,6 +1040,20 @@ status." (defvar-local notmuch-show-process-crypto nil) +(defun notmuch--run-show (search-terms &optional duplicate) + "Return a list of threads of messages matching SEARCH-TERMS. + +A thread is a forest or list of trees. A tree is a two element +list where the first element is a message, and the second element +is a possibly empty forest of replies." + (let ((args '("show" "--format=sexp" "--format-version=5"))) + (when notmuch-show-process-crypto + (setq args (append args '("--decrypt=true")))) + (when duplicate + (setq args (append args (list (format "--duplicate=%d" duplicate))))) + (setq args (append args search-terms)) + (apply #'notmuch-call-notmuch-sexp args))) + ;;; Generic Utilities (defun notmuch-interactive-region () @@ -1036,6 +1070,14 @@ region if the region is active, or both `point' otherwise." 'notmuch-interactive-region "notmuch 0.29") +(defun notmuch--inline-override-types () + "Override mm-inline-override-types to stop application/* +parts from being displayed unless the user has customized +it themselves." + (if (equal mm-inline-override-types + (eval (car (get 'mm-inline-override-types 'standard-value)))) + (cons "application/.*" mm-inline-override-types) + mm-inline-override-types)) ;;; _ (provide 'notmuch-lib) diff --git a/emacs/notmuch-logo.png b/emacs/notmuch-logo.png deleted file mode 100644 index 53b5e6a4..00000000 Binary files a/emacs/notmuch-logo.png and /dev/null differ diff --git a/emacs/notmuch-logo.svg b/emacs/notmuch-logo.svg new file mode 100644 index 00000000..2c65a73b --- /dev/null +++ b/emacs/notmuch-logo.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/emacs/notmuch-maildir-fcc.el b/emacs/notmuch-maildir-fcc.el index 7e177bf7..c7b403cf 100644 --- a/emacs/notmuch-maildir-fcc.el +++ b/emacs/notmuch-maildir-fcc.el @@ -145,20 +145,26 @@ Otherwise set it according to `notmuch-fcc-dirs'." (defmacro with-temporary-notmuch-message-buffer (&rest body) "Set-up a temporary copy of the current message-mode buffer." - `(let ((case-fold-search t) - (buf (current-buffer)) - (mml-externalize-attachments message-fcc-externalize-attachments)) - (with-current-buffer (get-buffer-create " *message temp*") - (erase-buffer) - (insert-buffer-substring buf) - ,@body))) + `(save-restriction + (widen) + (let ((case-fold-search t) + (buf (current-buffer)) + (mml-externalize-attachments message-fcc-externalize-attachments)) + (with-current-buffer (get-buffer-create " *message temp*") + (message-clone-locals buf) ;; for message-encoded-mail-cache + (erase-buffer) + (insert-buffer-substring buf) + ,@body)))) (defun notmuch-maildir-setup-message-for-saving () "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) + (if (not message-encoded-mail-cache) + (message-encode-message-body) + (erase-buffer) + (insert message-encoded-mail-cache)) (save-restriction (message-narrow-to-headers) (mail-encode-encoded-word-buffer)) @@ -179,12 +185,12 @@ This is a rearranged version of message mode's message-do-fcc." (setq file (message-fetch-field "fcc" t))) (when file (with-temporary-notmuch-message-buffer + (notmuch-maildir-setup-message-for-saving) (save-restriction (message-narrow-to-headers) (while (setq file (message-fetch-field "fcc" t)) (push file files) (message-remove-header "fcc" nil t))) - (notmuch-maildir-setup-message-for-saving) ;; Process FCC operations. (mapc #'notmuch-fcc-handler files) (kill-buffer (current-buffer))))))) diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el index c679373b..74c62aaf 100644 --- a/emacs/notmuch-mua.el +++ b/emacs/notmuch-mua.el @@ -21,7 +21,10 @@ ;;; Code: +(eval-when-compile (require 'subr-x)) + (require 'message) +(require 'gmm-utils) (require 'mm-view) (require 'format-spec) @@ -139,6 +142,16 @@ to `notmuch-mua-send-hook'." :type 'regexp :group 'notmuch-send) +(defcustom notmuch-mua-subject-regexp + "[[:blank:]]*$" + "Message subject indicating that something may be amiss. +By default, this checks for empty subject lines. + +This is not used unless `notmuch-mua-subject-check' is added to +`notmuch-mua-send-hook'." + :type 'regexp + :group 'notmuch-send) + ;;; Various functions (defun notmuch-mua-attachment-check () @@ -176,6 +189,19 @@ Typically this is added to `notmuch-mua-send-hook'." ;; ...signal an error. (error "Missing attachment"))) +(defun notmuch-mua-subject-check () + "Signal an error if the subject seems amiss. +More precisely, if the subject conforms to +`notmuch-mua-subject-regexp'. + +Typically this is added to `notmuch-mua-send-hook'." + (or (save-excursion + (message-goto-subject) + (message-beginning-of-header t) + (not (looking-at-p notmuch-mua-subject-regexp))) + (y-or-n-p "Subject may be erroneous – is that okay?") + (error "Erroneous subject"))) + (defun notmuch-mua-get-switch-function () "Get a switch function according to `notmuch-mua-compose-in'." (pcase notmuch-mua-compose-in @@ -234,11 +260,12 @@ Typically this is added to `notmuch-mua-send-hook'." ;;; Mua reply -(defun notmuch-mua-reply (query-string &optional sender reply-all) - (let ((args '("reply" "--format=sexp" "--format-version=5")) - (process-crypto notmuch-show-process-crypto) - reply - original) +(defun notmuch-mua-reply (query-string &optional sender reply-all duplicate) + (let* ((duparg (and duplicate (list (format "--duplicate=%d" duplicate)))) + (args `("reply" "--format=sexp" "--format-version=5" ,@duparg)) + (process-crypto notmuch-show-process-crypto) + reply + original) (when process-crypto (setq args (append args '("--decrypt=true")))) (if reply-all @@ -316,7 +343,9 @@ Typically this is added to `notmuch-mua-send-hook'." ;; text. (notmuch-show-process-crypto process-crypto) ;; Don't indent multipart sub-parts. - (notmuch-show-indent-multipart nil)) + (notmuch-show-indent-multipart nil) + ;; Stop certain mime types from being inlined + (mm-inline-override-types (notmuch--inline-override-types))) ;; We don't want sigstatus buttons (an information leak and usually wrong anyway). (cl-letf (((symbol-function 'notmuch-crypto-insert-sigstatus-button) #'ignore) ((symbol-function 'notmuch-crypto-insert-encstatus-button) #'ignore)) @@ -350,7 +379,9 @@ Typically this is added to `notmuch-mua-send-hook'." (define-derived-mode notmuch-message-mode message-mode "Message[Notmuch]" "Notmuch message composition mode. Mostly like `message-mode'." - (notmuch-address-setup)) + (notmuch-address-setup) + (when (boundp 'untrusted-content) + (setq untrusted-content t))) (put 'notmuch-message-mode 'flyspell-mode-predicate 'mail-mode-flyspell-verify) @@ -380,20 +411,37 @@ instead of `message-mode' and SWITCH-FUNCTION is mandatory." (erase-buffer) (notmuch-message-mode))) +(defun notmuch-mua--remove-dont-reply-to-names () + (when-let* ((nr (if (functionp message-dont-reply-to-names) + message-dont-reply-to-names + (gmm-regexp-concat message-dont-reply-to-names))) + (nr-filter + (if (functionp nr) + (lambda (mail) (and (not (funcall nr mail)) mail)) + (lambda (mail) (and (not (string-match-p nr mail)) mail))))) + (dolist (header '("To" "Cc")) + (when-let ((v (message-fetch-field header))) + (let* ((tokens (mapcar #'string-trim (message-tokenize-header v))) + (good-tokens (delq nil (mapcar nr-filter tokens))) + (addr (and good-tokens (mapconcat #'identity good-tokens ", ")))) + (message-replace-header header addr)))))) + +;;;###autoload (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." + return-action &rest _ignored) + "Invoke the notmuch mail composition window. + +The position of point when the function returns differs depending +on the values of TO and SUBJECT. If both are non-nil, point is +moved to the message's body. If SUBJECT is nil but TO isn't, +point is moved to the \"Subject:\" header. Otherwise, point is +moved to the \"To:\" header." (interactive) (when notmuch-mua-user-agent-function (let ((user-agent (funcall notmuch-mua-user-agent-function))) (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 - (notmuch-user-name) - (notmuch-user-primary-email))) - other-headers)) (notmuch-mua-pop-to-buffer (message-buffer-name "mail" to) (or switch-function (notmuch-mua-get-switch-function))) @@ -412,13 +460,22 @@ instead of `message-mode' and SWITCH-FUNCTION is mandatory." ;; Cause `message-setup-1' to do things relevant for mail, ;; such as observe `message-default-mail-headers'. (message-this-is-mail t)) + (unless (assq 'From headers) + (push (cons 'From (message-make-from + (notmuch-user-name) + (notmuch-user-primary-email))) + headers)) (message-setup-1 headers yank-action send-actions return-action)) (notmuch-fcc-header-setup) + (notmuch-mua--remove-dont-reply-to-names) (message-sort-headers) (message-hide-headers) (set-buffer-modified-p nil) (notmuch-mua-maybe-set-window-dedicated) - (message-goto-to)) + (cond + ((and to subject) (message-goto-body)) + (to (message-goto-subject)) + (t (message-goto-to)))) (defvar notmuch-mua-sender-history nil) @@ -510,12 +567,13 @@ the From: address." (message-hide-headers) (set-buffer-modified-p nil)))) -(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender reply-all) +(defun notmuch-mua-new-reply (query-string &optional prompt-for-sender reply-all duplicate) "Compose a reply to the message identified by QUERY-STRING. 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." +will be addressed to all recipients of the source message. If +DUPLICATE is non-nil, based the reply on that duplicate file" ;; `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 @@ -530,7 +588,7 @@ will be addressed to all recipients of the source message." (let ((sender (and prompt-for-sender (notmuch-mua-prompt-for-sender))) (select-active-regions nil)) - (notmuch-mua-reply query-string sender reply-all) + (notmuch-mua-reply query-string sender reply-all duplicate) (deactivate-mark))) ;;; Checks @@ -590,20 +648,24 @@ unencrypted. Really send? ")))) (message-send-and-exit arg) (message-send arg))))) +;;;###autoload (defun notmuch-mua-send-and-exit (&optional arg) (interactive "P") (notmuch-mua-send-common arg t)) +;;;###autoload (defun notmuch-mua-send (&optional arg) (interactive "P") (notmuch-mua-send-common arg)) +;;;###autoload (defun notmuch-mua-kill-buffer () (interactive) (message-kill-buffer)) ;;; _ +;;;###autoload (define-mail-user-agent 'notmuch-user-agent 'notmuch-mua-mail 'notmuch-mua-send-and-exit diff --git a/emacs/notmuch-parser.el b/emacs/notmuch-parser.el index f04b07c2..710c60e1 100644 --- a/emacs/notmuch-parser.el +++ b/emacs/notmuch-parser.el @@ -35,7 +35,7 @@ complete S-expression from the input. However, it extends this with an additional function that requires the next value in the input to be a list and descends into it, allowing its elements to be read one at a time or further descended into. Both functions -can return 'retry to indicate that not enough input is available. +can return \\='retry to indicate that not enough input is available. The parser always consumes input from point in the current buffer. Hence, the caller is allowed to delete any data before @@ -52,10 +52,10 @@ point and may resynchronize after an error by moving point." (defun notmuch-sexp-read (sp) "Consume and return the value at point in the current buffer. -Returns 'retry if there is insufficient input to parse a complete +Returns \\='retry if there is insufficient input to parse a complete value (though it may still move point over whitespace). If the parser is currently inside a list and the next token ends the -list, this moves point just past the terminator and returns 'end. +list, this moves point just past the terminator and returns \\='end. Otherwise, this moves point to just past the end of the value and returns the value." (skip-chars-forward " \n\r\t") @@ -125,7 +125,7 @@ returns the value." (defun notmuch-sexp-begin-list (sp) "Parse the beginning of a list value and enter the list. -Returns 'retry if there is insufficient input to parse the +Returns \\='retry if there is insufficient input to parse the beginning of the list. If this is able to parse the beginning of a list, it moves point past the token that opens the list and returns t. Later calls to `notmuch-sexp-read' will return the diff --git a/emacs/notmuch-print.el b/emacs/notmuch-print.el index 85fa1f21..8d9f1b08 100644 --- a/emacs/notmuch-print.el +++ b/emacs/notmuch-print.el @@ -41,11 +41,11 @@ ;;; Utility functions (defun notmuch-print-run-evince (file) - "View FILE using 'evince'." + "View FILE using `evince'." (start-process "evince" nil "evince" file)) (defun notmuch-print-run-muttprint (&optional output) - "Pass the contents of the current buffer to 'muttprint'. + "Pass the contents of the current buffer to `muttprint'. Optional OUTPUT allows passing a list of flags to muttprint." (apply #'notmuch--call-process-region (point-min) (point-max) diff --git a/emacs/notmuch-query.el b/emacs/notmuch-query.el index 5c7f4f8d..2a46144c 100644 --- a/emacs/notmuch-query.el +++ b/emacs/notmuch-query.el @@ -25,17 +25,10 @@ ;;; Basic query function -(defun notmuch-query-get-threads (search-terms) - "Return a list of threads of messages matching SEARCH-TERMS. - -A thread is a forest or list of trees. A tree is a two element -list where the first element is a message, and the second element -is a possibly empty forest of replies." - (let ((args '("show" "--format=sexp" "--format-version=5"))) - (when notmuch-show-process-crypto - (setq args (append args '("--decrypt=true")))) - (setq args (append args search-terms)) - (apply #'notmuch-call-notmuch-sexp args))) +(define-obsolete-function-alias + 'notmuch-query-get-threads + #'notmuch--run-show + "notmuch 0.37") ;;; Mapping functions across collections of messages @@ -60,7 +53,7 @@ Flatten results to a list. See the function (defun notmuch-query-map-tree (fn tree) "Apply function FN to every message in TREE. Flatten results to a list. See the function -`notmuch-query-get-threads' for more information." +`notmuch--run-show' for more information." (cons (funcall fn (car tree)) (notmuch-query-map-forest fn (cadr tree)))) @@ -70,7 +63,11 @@ Flatten results to a list. See the function "Return a list of message-ids of messages that match SEARCH-TERMS." (notmuch-query-map-threads (lambda (msg) (plist-get msg :id)) - (notmuch-query-get-threads search-terms))) + (notmuch--run-show search-terms))) + +;;; Everything in this library is obsolete +(dolist (fun '(map-aux map-threads map-forest map-tree get-message-ids)) + (make-obsolete (intern (format "notmuch-query-%s" fun)) nil "notmuch 0.37")) (provide 'notmuch-query) diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index 4de3e423..19b51753 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -32,7 +32,6 @@ (require 'notmuch-lib) (require 'notmuch-tag) -(require 'notmuch-query) (require 'notmuch-wash) (require 'notmuch-mua) (require 'notmuch-crypto) @@ -84,6 +83,59 @@ visible for any given message." :type 'boolean :group 'notmuch-show) +(defcustom notmuch-show-header-line t + "Show a header line in notmuch show buffers. + +If t (the default), the header line will contain the current +message's subject. + +If a string, this value is interpreted as a format string to be +passed to `format-spec` with `%s` as the substitution variable +for the message's subject. E.g., to display the subject trimmed +to a maximum of 80 columns, you could use \"%>-80s\" as format. + +If you assign to this variable a function, it will be called with +the subject as argument, and the return value will be used as the +header line format. Since the function is called with the +message buffer as the current buffer, it is also possible to +access any other properties of the message, using for instance +notmuch-show functions such as +`notmuch-show-get-message-properties'. + +Finally, if this variable is set to nil, no header is +displayed." + :type '(choice (const :tag "No header" nil) + (const :tag "Subject" t) + (string :tag "Format") + (function :tag "Function")) + :group 'notmuch-show) + +(defcustom notmuch-show-depth-limit nil + "Depth beyond which message bodies are displayed lazily. + +If bound to an integer, any message with tree depth greater than +this will have its body display lazily, initially +inserting only a button. + +If this variable is set to nil (the default) no such lazy +insertion is done." + :type '(choice (const :tag "No limit" nil) + (number :tag "Limit" 10)) + :group 'notmuch-show) + +(defcustom notmuch-show-height-limit nil + "Height (from leaves) beyond which message bodies are displayed lazily. + +If bound to an integer, any message with height in the message +tree greater than this will have its body displayed lazily, +initially only a button. + +If this variable is set to nil (the default) no such lazy +display is done." + :type '(choice (const :tag "No limit" nil) + (number :tag "Limit" 10)) + :group 'notmuch-show) + (defcustom notmuch-show-relative-dates t "Display relative dates in the message summary line." :type 'boolean @@ -400,14 +452,19 @@ operation on the contents of the current buffer." (defun notmuch-show-update-tags (tags) "Update the displayed tags of the current message." (save-excursion - (goto-char (notmuch-show-message-top)) - (when (re-search-forward "(\\([^()]*\\))$" (line-end-position) t) - (let ((inhibit-read-only t)) - (replace-match (concat "(" - (notmuch-tag-format-tags - tags - (notmuch-show-get-prop :orig-tags)) - ")")))))) + (let ((inhibit-read-only t) + (start (notmuch-show-message-top)) + (depth (notmuch-show-get-prop :depth)) + (orig-tags (notmuch-show-get-prop :orig-tags)) + (props (notmuch-show-get-message-properties)) + (extent (notmuch-show-message-extent))) + (goto-char start) + (notmuch-show-insert-headerline props depth tags orig-tags) + (put-text-property start (1+ start) + :notmuch-message-properties props) + (put-text-property (car extent) (cdr extent) :notmuch-message-extent extent) + ;; delete original headerline, but do not save to kill ring + (delete-region (point) (1+ (line-end-position)))))) (defun notmuch-clean-address (address) "Try to clean a single email ADDRESS for display. Return a cons @@ -466,11 +523,29 @@ Return unchanged ADDRESS if parsing fails." ;; Otherwise format the name and address together. (concat p-name " <" p-address ">")))) -(defun notmuch-show-insert-headerline (headers date tags depth) +(defun notmuch-show--mark-height (tree) + "Calculate and cache height (distance from deepest descendent)" + (let* ((msg (car tree)) + (children (cadr tree)) + (cached-height (plist-get msg :height))) + (or cached-height + (let ((height + (if (null children) 0 + (1+ (apply #'max (mapcar #'notmuch-show--mark-height children)))))) + (plist-put msg :height height) + height)))) + +(defun notmuch-show-insert-headerline (msg-plist depth tags &optional orig-tags) "Insert a notmuch style headerline based on HEADERS for a message at DEPTH in the current thread." - (let ((start (point)) - (from (notmuch-sanitize + (let* ((start (point)) + (headers (plist-get msg-plist :headers)) + (duplicate (or (plist-get msg-plist :duplicate) 0)) + (file-count (length (plist-get msg-plist :filename))) + (date (or (and notmuch-show-relative-dates + (plist-get msg-plist :date_relative)) + (plist-get headers :Date))) + (from (notmuch-sanitize (notmuch-show-clean-address (plist-get headers :From))))) (when (string-match "\\cR" from) ;; If the From header has a right-to-left character add @@ -485,8 +560,15 @@ message at DEPTH in the current thread." " (" date ") (" - (notmuch-tag-format-tags tags tags) - ")\n") + (notmuch-tag-format-tags tags (or orig-tags tags)) + ")") + (insert + (if (> file-count 1) + (let ((txt (format "%d/%d\n" duplicate file-count))) + (concat + (notmuch-show-spaces-n (max 0 (- (window-width) (+ (current-column) (length txt))))) + txt)) + "\n")) (overlay-put (make-overlay start (point)) 'face 'notmuch-message-summary-face))) @@ -562,8 +644,24 @@ message at DEPTH in the current thread." (when show (button-put button :notmuch-lazy-part nil) (notmuch-show-lazy-part lazy-part button)) - ;; else there must be an overlay. - (overlay-put overlay 'invisible (not show)) + (let* ((part (plist-get properties :notmuch-part)) + (undisplayer (plist-get part :undisplayer)) + (mime-type (plist-get part :computed-type)) + (redisplay-data (button-get button + :notmuch-redisplay-data)) + (imagep (string-match "^image/" mime-type))) + (cond + ((and imagep (not show) undisplayer) + ;; call undisplayer thunk created by gnus. + (funcall undisplayer) + ;; there is an extra newline left + (delete-region + (+ 1 (button-end button)) + (+ 2 (button-end button)))) + ((and imagep show redisplay-data) + (notmuch-show-lazy-part redisplay-data button)) + (t + (overlay-put overlay 'invisible (not show))))) t))))))) ;;; Part content ID handling @@ -766,7 +864,7 @@ will return nil if the CID is unknown or cannot be retrieved." (unless (icalendar-import-buffer file t) (error "Icalendar import error. %s" "See *icalendar-errors* for more information")) - (set-buffer (get-file-buffer file)) + (set-buffer (find-buffer-visiting file)) (setq result (buffer-substring (point-min) (point-max))) (set-buffer-modified-p nil) (kill-buffer (current-buffer))) @@ -937,10 +1035,13 @@ will return nil if the CID is unknown or cannot be retrieved." (part-end (copy-marker (point) t)) ;; We have to save the depth as we can't find the depth ;; when narrowed. - (depth (notmuch-show-get-depth))) + (depth (notmuch-show-get-depth)) + (mime-type (plist-get (cadr part-args) :computed-type))) (save-restriction (narrow-to-region part-beg part-end) (delete-region part-beg part-end) + (when (and mime-type (string-match "^image/" mime-type)) + (button-put button :notmuch-redisplay-data part-args)) (apply #'notmuch-show-insert-bodypart-internal part-args) (indent-rigidly part-beg part-end @@ -1000,30 +1101,42 @@ is t, hide the part initially and show the button." (let* ((content-type (plist-get part :content-type)) (mime-type (notmuch-show-mime-type part)) (nth (plist-get part :id)) + (height (plist-get msg :height)) (long (and (notmuch-match-content-type mime-type "text/*") (> notmuch-show-max-text-part-size 0) (> (length (plist-get part :content)) notmuch-show-max-text-part-size))) + (deep (and notmuch-show-depth-limit + (> depth notmuch-show-depth-limit))) + (high (and notmuch-show-height-limit + (> height notmuch-show-height-limit))) (beg (point)) ;; This default header-p function omits the part button for ;; the first (or only) part if this is text/plain. - (button (and (funcall notmuch-show-insert-header-p-function part hide) + (button (and (or deep long high + (funcall notmuch-show-insert-header-p-function part hide)) (notmuch-show-insert-part-header 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 + ;; Hide the part initially if HIDE is t, or if it is too long/deep ;; and we have a button to allow toggling. (show-part (not (or (equal hide t) + (and deep button) + (and high button) (and long button)))) - (content-beg (point))) + (content-beg (point)) + (part-data (list msg part mime-type nth depth button))) ;; Store the computed mime-type for later use (e.g. by attachment handlers). (plist-put part :computed-type mime-type) - (if show-part - (notmuch-show-insert-bodypart-internal msg part mime-type nth depth button) + (cond + (show-part + (apply #'notmuch-show-insert-bodypart-internal part-data) + (when (and button (string-match "^image/" mime-type)) + (button-put button :notmuch-redisplay-data part-data))) + (t (when button - (button-put button :notmuch-lazy-part - (list msg part mime-type nth depth button)))) + (button-put button :notmuch-lazy-part part-data)))) ;; Some of the body part handlers leave point somewhere up in the ;; part, so we make sure that we're down at the end. (goto-char (point-max)) @@ -1055,6 +1168,40 @@ is t, hide the part initially and show the button." (defvar notmuch-show-previous-subject "") (make-variable-buffer-local 'notmuch-show-previous-subject) +(defun notmuch-show-choose-duplicate (duplicate) + "Display message file with index DUPLICATE in place of the current one. + +Message file indices are based on the order the files are +discovered by `notmuch new' (and hence are somewhat arbitrary), +and correspond to those passed to the \"\\-\\-duplicate\" arguments +to the CLI. + +When called interactively, the function will prompt for the index +of the file to display. An error will be signaled if the index +is out of range." + (interactive "Nduplicate: ") + (let ((count (length (notmuch-show-get-prop :filename)))) + (when (or (> duplicate count) + (< duplicate 1)) + (error "Duplicate %d out of range [1,%d]" duplicate count))) + (notmuch-show-move-to-message-top) + (save-excursion + (let* ((extent (notmuch-show-message-extent)) + (id (notmuch-show-get-message-id)) + (depth (notmuch-show-get-depth)) + (inhibit-read-only t) + (new-msg (notmuch--run-show (list id) duplicate))) + ;; clean up existing overlays to avoid extending them. + (dolist (o (overlays-in (car extent) (cdr extent))) + (delete-overlay o)) + ;; pretend insertion is happening at end of buffer + (narrow-to-region (point-min) (car extent)) + ;; Insert first, then delete, to avoid marker for start of next + ;; message being in same place as the start of this one. + (notmuch-show-insert-msg new-msg depth) + (widen) + (delete-region (point) (cdr extent))))) + (defun notmuch-show-insert-msg (msg depth) "Insert the message MSG at depth DEPTH in the current thread." (let* ((headers (plist-get msg :headers)) @@ -1065,11 +1212,7 @@ is t, hide the part initially and show the button." headers-start headers-end (bare-subject (notmuch-show-strip-re (plist-get headers :Subject)))) (setq message-start (point-marker)) - (notmuch-show-insert-headerline headers - (or (and notmuch-show-relative-dates - (plist-get msg :date_relative)) - (plist-get headers :Date)) - (plist-get msg :tags) depth) + (notmuch-show-insert-headerline msg depth (plist-get msg :tags)) (setq content-start (point-marker)) ;; Set `headers-start' to point after the 'Subject:' header to be ;; compatible with the existing implementation. This just sets it @@ -1158,6 +1301,7 @@ is t, hide the part initially and show the button." (replies (cadr tree))) ;; We test whether there is a message or just some replies. (when msg + (notmuch-show--mark-height tree) (notmuch-show-insert-msg msg depth)) (notmuch-show-insert-thread replies (1+ depth)))) @@ -1259,14 +1403,8 @@ matched." (let ((buffer-name (generate-new-buffer-name (or buffer-name (concat "*notmuch-" thread-id "*")))) - ;; We override mm-inline-override-types to stop application/* - ;; parts from being displayed unless the user has customized - ;; it themselves. - (mm-inline-override-types - (if (equal mm-inline-override-types - (eval (car (get 'mm-inline-override-types 'standard-value)))) - (cons "application/*" mm-inline-override-types) - mm-inline-override-types))) + (mm-inline-override-types (notmuch--inline-override-types))) + (pop-to-buffer-same-window (get-buffer-create buffer-name)) ;; No need to track undo information for this buffer. (setq buffer-undo-list t) @@ -1314,6 +1452,18 @@ fallback if the prior matches no messages." (push (list thread "and (" context ")") queries)) queries)) +(defun notmuch-show--header-line-format () + "Compute the header line format of a notmuch-show buffer." + (when notmuch-show-header-line + (let* ((s (notmuch-sanitize + (notmuch-show-strip-re (notmuch-show-get-subject)))) + (subject (replace-regexp-in-string "%" "%%" s))) + (cond ((stringp notmuch-show-header-line) + (format-spec notmuch-show-header-line `((?s . ,subject)))) + ((functionp notmuch-show-header-line) + (funcall notmuch-show-header-line subject)) + (notmuch-show-header-line subject))))) + (defun notmuch-show--build-buffer (&optional state) "Display messages matching the current buffer context. @@ -1333,7 +1483,7 @@ If no messages match the query return NIL." (notmuch-show-previous-subject "")) ;; Use results from the first query that returns some. (while (and (not forest) queries) - (setq forest (notmuch-query-get-threads + (setq forest (notmuch--run-show (append cli-args (list "'") (car queries) (list "'")))) (when (and forest notmuch-show-single-message) (setq forest (list (list (list forest))))) @@ -1344,12 +1494,7 @@ If no messages match the query return NIL." ;; display changes. (notmuch-show-mapc (lambda () (notmuch-show-set-prop :orig-tags (notmuch-show-get-tags)))) - ;; Set the header line to the subject of the first message. - (setq header-line-format - (replace-regexp-in-string "%" "%%" - (notmuch-sanitize - (notmuch-show-strip-re - (notmuch-show-get-subject))))) + (setq header-line-format (notmuch-show--header-line-format)) (run-hooks 'notmuch-show-hook) (if state (notmuch-show-apply-state state) @@ -1421,6 +1566,7 @@ non-nil) then the state of the buffer (open/closed messages) is reset based on the original query." (interactive "P") (let ((inhibit-read-only t) + (mm-inline-override-types (notmuch--inline-override-types)) (state (unless reset-state (notmuch-show-capture-state)))) ;; `erase-buffer' does not seem to remove overlays, which can lead @@ -1509,6 +1655,7 @@ reset based on the original query." (define-key map "#" 'notmuch-show-print-message) (define-key map "!" 'notmuch-show-toggle-elide-non-matching) (define-key map "$" 'notmuch-show-toggle-process-crypto) + (define-key map "%" 'notmuch-show-choose-duplicate) (define-key map "<" 'notmuch-show-toggle-thread-indentation) (define-key map "t" 'toggle-truncate-lines) (define-key map "." 'notmuch-show-part-map) @@ -1549,6 +1696,8 @@ All currently available key bindings: (setq notmuch-buffer-refresh-function #'notmuch-show-refresh-view) (setq buffer-read-only t) (setq truncate-lines t) + (when (boundp 'untrusted-content) + (setq untrusted-content t)) (setq imenu-prev-index-position-function #'notmuch-show-imenu-prev-index-position-function) (setq imenu-extract-index-name-function @@ -1703,10 +1852,10 @@ current thread." ;; dme: Would it make sense to use a macro for many of these? -;; XXX TODO figure out what to do about multiple filenames (defun notmuch-show-get-filename () "Return the filename of the current message." - (car (notmuch-show-get-prop :filename))) + (let ((duplicate (notmuch-show-get-duplicate))) + (nth (1- duplicate) (notmuch-show-get-prop :filename)))) (defun notmuch-show-get-header (header &optional props) "Return the named header of the current message, if any." @@ -1718,6 +1867,10 @@ current thread." (defun notmuch-show-get-date () (notmuch-show-get-header :Date)) +(defun notmuch-show-get-duplicate () + ;; if no duplicate property exists, assume first file + (or (notmuch-show-get-prop :duplicate) 1)) + (defun notmuch-show-get-timestamp () (notmuch-show-get-prop :timestamp)) @@ -1912,13 +2065,15 @@ any effects from previous calls to (defun notmuch-show-reply (&optional prompt-for-sender) "Reply to the sender and all recipients of the current message." (interactive "P") - (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender t)) + (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender t + (notmuch-show-get-prop :duplicate))) (put 'notmuch-show-reply-sender 'notmuch-prefix-doc "... and prompt for sender") (defun notmuch-show-reply-sender (&optional prompt-for-sender) "Reply to the sender of the current message." (interactive "P") - (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender nil)) + (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender nil + (notmuch-show-get-prop :duplicate))) (put 'notmuch-show-forward-message 'notmuch-prefix-doc "... and prompt for sender") @@ -2029,12 +2184,16 @@ to show, nil otherwise." "View the original source of the current message." (interactive) (let* ((id (notmuch-show-get-message-id)) - (buf (get-buffer-create (concat "*notmuch-raw-" id "*"))) + (duplicate (notmuch-show-get-duplicate)) + (args (if (> duplicate 1) + (list (format "--duplicate=%d" duplicate) id) + (list id))) + (buf (get-buffer-create (format "*notmuch-raw-%s-%d*" id duplicate))) (inhibit-read-only t)) (pop-to-buffer-same-window buf) (erase-buffer) (let ((coding-system-for-read 'no-conversion)) - (notmuch--call-process notmuch-command nil t nil "show" "--format=raw" id)) + (apply #'notmuch--call-process notmuch-command nil t nil "show" "--format=raw" args)) (goto-char (point-min)) (set-buffer-modified-p nil) (setq buffer-read-only t) @@ -2063,7 +2222,7 @@ message." (interactive (let ((query-string (if current-prefix-arg "Pipe all open messages to command: " "Pipe message to command: "))) - (list current-prefix-arg (read-string query-string)))) + (list current-prefix-arg (read-shell-command query-string)))) (let (shell-command) (if entire-thread (setq shell-command @@ -2199,7 +2358,9 @@ argument, hide all of the messages." If SHOW is non-nil, open the next item in a show buffer. Otherwise just highlight the next item in the search buffer. If PREVIOUS is non-nil, move to the previous item in the -search results instead." +search results instead. + +Return non-nil on success." (interactive "P") (let ((parent-buffer notmuch-show-parent-buffer)) (notmuch-bury-or-kill-this-buffer) @@ -2348,10 +2509,12 @@ kill-ring." (defun notmuch-show-stash-mlarchive-link (&optional mla) "Copy an ML Archive URI for the current message to the kill-ring. -This presumes that the message is available at the selected Mailing List Archive. +This presumes that the message is available at the selected +Mailing List Archive. -If optional argument MLA is non-nil, use the provided key instead of prompting -the user (see `notmuch-show-stash-mlarchive-link-alist')." +If optional argument MLA is non-nil, use the provided key instead +of prompting the user (see +`notmuch-show-stash-mlarchive-link-alist')." (interactive) (let ((url (cdr (assoc (or mla @@ -2368,12 +2531,15 @@ the user (see `notmuch-show-stash-mlarchive-link-alist')." (concat url (notmuch-show-get-message-id t)))))) (defun notmuch-show-stash-mlarchive-link-and-go (&optional mla) - "Copy an ML Archive URI for the current message to the kill-ring and visit it. + "Copy an ML Archive URI for the current message to the + kill-ring and visit it. -This presumes that the message is available at the selected Mailing List Archive. +This presumes that the message is available at the selected +Mailing List Archive. -If optional argument MLA is non-nil, use the provided key instead of prompting -the user (see `notmuch-show-stash-mlarchive-link-alist')." +If optional argument MLA is non-nil, use the provided key instead +of prompting the user (see +`notmuch-show-stash-mlarchive-link-alist')." (interactive) (notmuch-show-stash-mlarchive-link mla) (browse-url (current-kill 0 t))) @@ -2522,7 +2688,9 @@ This function is used as a value for `imenu-prev-index-position-function'." (if (bobp) nil - (notmuch-show-previous-message) + (if (eobp) + (notmuch-show-move-to-message-top) + (notmuch-show-goto-message-previous)) t)) (defun notmuch-show-imenu-extract-index-name-function () diff --git a/emacs/notmuch-tag.el b/emacs/notmuch-tag.el index 536315e9..81101828 100644 --- a/emacs/notmuch-tag.el +++ b/emacs/notmuch-tag.el @@ -159,7 +159,7 @@ place of the tag. For example, to replace a tag with another string, simply use that string as a formatting expression. To change the foreground of a tag to red, use the expression - (propertize tag 'face '(:foreground \"red\")) + (propertize tag \\='face \\='(:foreground \"red\")) See also `notmuch-tag-format-image', which can help replace tags with images." @@ -191,7 +191,7 @@ By default this shows deleted tags with strike-through in red, unless strike-through is not available (e.g., emacs is running in a terminal) in which case it uses inverse video. To hide deleted tags completely set this to - '((\".*\" nil)) + \\='((\".*\" nil)) See `notmuch-tag-formats' for full documentation." :group 'notmuch-show @@ -241,7 +241,7 @@ DATA is the content of an SVG picture (e.g., as returned by "Return SVG data representing a star icon. This can be used with `notmuch-tag-format-image-data'." " - + - + - + ") +;;; track history of tag operations +(defvar-local notmuch-tag-history nil + "Buffer local history of `notmuch-tag' function.") +(put 'notmuch-tag-history 'permanent-local t) + ;;; Format Handling (defvar notmuch-tag--format-cache (make-hash-table :test 'equal) @@ -361,9 +366,9 @@ changed (the normal case) are shown using formats from (defcustom notmuch-before-tag-hook nil "Hooks that are run before tags of a message are modified. -'tag-changes' will contain the tags that are about to be added or removed as +`tag-changes' will contain the tags that are about to be added or removed as a list of strings of the form \"+TAG\" or \"-TAG\". -'query' will be a string containing the search query that determines +`query' will be a string containing the search query that determines the messages that are about to be tagged." :type 'hook :options '(notmuch-hl-line-mode) @@ -372,9 +377,9 @@ the messages that are about to be tagged." (defcustom notmuch-after-tag-hook nil "Hooks that are run after tags of a message are modified. -'tag-changes' will contain the tags that were added or removed as +`tag-changes' will contain the tags that were added or removed as a list of strings of the form \"+TAG\" or \"-TAG\". -'query' will be a string containing the search query that determines +`query' will be a string containing the search query that determines the messages that were tagged." :type 'hook :options '(notmuch-hl-line-mode) @@ -429,17 +434,9 @@ initial input in the minibuffer." (set-keymap-parent map crm-local-completion-map) (define-key map " " 'self-insert-command) map))) - (delete "" (completing-read-multiple - prompt - ;; Append the separator to each completion so when the - ;; user completes a tag they can immediately begin - ;; entering another. `completing-read-multiple' - ;; ultimately splits the input on crm-separator, so we - ;; don't need to strip this back off (we just need to - ;; delete "empty" entries caused by trailing spaces). - (mapcar (lambda (tag-op) (concat tag-op crm-separator)) tag-list) - nil nil initial-input - 'notmuch-read-tag-changes-history)))) + (completing-read-multiple prompt tag-list + nil nil initial-input + 'notmuch-read-tag-changes-history))) ;;; Tagging @@ -466,14 +463,19 @@ from TAGS if present." "Use batch tagging if the tagging query is longer than this. This limits the length of arguments passed to the notmuch CLI to -avoid system argument length limits and performance problems.") +avoid system argument length limits and performance problems. + +NOTE: this variable is no longer used.") + +(make-obsolete-variable 'notmuch-tag-argument-limit nil "notmuch 0.36") -(defun notmuch-tag (query tag-changes) +(defun notmuch-tag (query tag-changes &optional omit-hist) "Add/remove tags in TAG-CHANGES to messages matching QUERY. QUERY should be a string containing the search-terms. -TAG-CHANGES is a list of strings of the form \"+tag\" or -\"-tag\" to add or remove tags, respectively. +TAG-CHANGES is a list of strings of the form \"+tag\" or \"-tag\" +to add or remove tags, respectively. OMIT-HIST disables history +tracking if non-nil. Note: Other code should always use this function to alter tags of messages instead of running (notmuch-call-notmuch-process \"tag\" ..) @@ -489,16 +491,30 @@ notmuch-after-tag-hook will be run." (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))) - ;; Use batch tag mode to avoid argument length limitations - (let ((batch-op (concat (mapconcat #'notmuch-hex-encode tag-changes " ") - " -- " query))) - (notmuch-call-notmuch-process :stdin-string batch-op "tag" "--batch"))) - (notmuch-dlet ((tag-changes tag-changes) - (query query)) - (run-hooks 'notmuch-after-tag-hook)))) + (with-temp-buffer + (insert (concat (mapconcat #'notmuch-hex-encode tag-changes " ") " -- " query)) + (unless (= 0 + (notmuch--call-process-region + (point-min) (point-max) notmuch-command t t nil "tag" "--batch")) + (notmuch-logged-error "notmuch tag failed" (buffer-string)))) + (unless omit-hist + (push (list :query query :tag-changes tag-changes) notmuch-tag-history))) + (notmuch-dlet ((tag-changes tag-changes) + (query query)) + (run-hooks 'notmuch-after-tag-hook))) + +(defun notmuch-tag-undo () + "Undo the previous tagging operation in the current buffer. Uses +buffer local variable `notmuch-tag-history' to determine what +that operation was." + (interactive) + (when (null notmuch-tag-history) + (error "no further notmuch undo information")) + (let* ((action (pop notmuch-tag-history)) + (query (plist-get action :query)) + (changes (notmuch-tag-change-list (plist-get action :tag-changes) t))) + (notmuch-tag query changes t)) + (notmuch-refresh-this-buffer)) (defun notmuch-tag-change-list (tags &optional reverse) "Convert TAGS into a list of tag changes. diff --git a/emacs/notmuch-tree.el b/emacs/notmuch-tree.el index 001a367d..aa0d92a7 100644 --- a/emacs/notmuch-tree.el +++ b/emacs/notmuch-tree.el @@ -27,7 +27,6 @@ (require 'mail-parse) (require 'notmuch-lib) -(require 'notmuch-query) (require 'notmuch-show) (require 'notmuch-tag) (require 'notmuch-parser) @@ -46,9 +45,6 @@ (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-local notmuch-tree-unthreaded nil "A buffer local copy of argument unthreaded to the function notmuch-tree.") @@ -98,6 +94,15 @@ different kind of arrow point." :type '(alist :key-type symbol :value-type string) :group 'notmuch-tree) +(defconst notmuch-tree--field-names + '(choice :tag "Field" + (const :tag "Date" "date") + (const :tag "Authors" "authors") + (const :tag "Subject" "subject") + (const :tag "Tree" "tree") + (const :tag "Tags" "tags") + (function))) + (defcustom notmuch-tree-result-format `(("date" . "%12s ") ("authors" . "%-20s") @@ -107,7 +112,11 @@ different kind of arrow point." ("tags" . "(%s)")) "Result formatting for tree view. -Supported fields are: date, authors, subject, tree, tags. +List of pairs of (field . format-string). Supported field +strings are: \"date\", \"authors\", \"subject\", \"tree\", +\"tags\". It is also supported to pass a function in place of a +field-name. In this case the function is passed the thread +object (plist) and format string. Tree means the thread tree box graphics. The field may also be a list in which case the formatting rules are @@ -115,14 +124,12 @@ applied recursively and then the output of all the fields in the list is inserted according to format-string. Note that the author string should not contain whitespace -\(put it in the neighbouring fields instead). For example: - (setq notmuch-tree-result-format - '((\"authors\" . \"%-40s\") - (\"subject\" . \"%s\")))" - :type '(alist :key-type (choice string - (alist :key-type string - :value-type string)) - :value-type string) +\(put it in the neighbouring fields instead)." + + :type `(alist :key-type (choice ,notmuch-tree--field-names + (alist :key-type ,notmuch-tree--field-names + :value-type (string :tag "Format"))) + :value-type (string :tag "Format")) :group 'notmuch-tree) (defcustom notmuch-unthreaded-result-format @@ -132,7 +139,11 @@ Note that the author string should not contain whitespace ("tags" . "(%s)")) "Result formatting for unthreaded tree view. -Supported fields are: date, authors, subject, tree, tags. +List of pairs of (field . format-string). Supported field +strings are: \"date\", \"authors\", \"subject\", \"tree\", +\"tags\". It is also supported to pass a function in place of a +field-name. In this case the function is passed the thread +object (plist) and format string. Tree means the thread tree box graphics. The field may also be a list in which case the formatting rules are @@ -140,14 +151,12 @@ applied recursively and then the output of all the fields in the list is inserted according to format-string. Note that the author string should not contain whitespace -\(put it in the neighbouring fields instead). For example: - (setq notmuch-unthreaded-result-format - '((\"authors\" . \"%-40s\") - (\"subject\" . \"%s\")))" - :type '(alist :key-type (choice string - (alist :key-type string - :value-type string)) - :value-type string) +\(put it in the neighbouring fields instead)." + + :type `(alist :key-type (choice ,notmuch-tree--field-names + (alist :key-type ,notmuch-tree--field-names + :value-type (string :tag "Format"))) + :value-type (string :tag "Format")) :group 'notmuch-tree) (defun notmuch-tree-result-format () @@ -179,7 +188,7 @@ Note that the author string should not contain whitespace (:foreground "dark blue")) (t (:bold t))) - "Face used in tree mode for the date in messages matching the query." + "Face used in tree mode for the author in messages matching the query." :group 'notmuch-tree :group 'notmuch-faces) @@ -191,7 +200,8 @@ Note that the author string should not contain whitespace (defface notmuch-tree-match-tree-face nil - "Face used in tree mode for the thread tree block graphics in messages matching the query." + "Face used in tree mode for the thread tree block graphics in +messages matching the query." :group 'notmuch-tree :group 'notmuch-faces) @@ -230,13 +240,14 @@ Note that the author string should not contain whitespace (defface notmuch-tree-no-match-tree-face nil - "Face used in tree mode for the thread tree block graphics in messages matching the query." + "Face used in tree mode for the thread tree block graphics in +messages matching the query." :group 'notmuch-tree :group 'notmuch-faces) (defface notmuch-tree-no-match-author-face nil - "Face used in tree mode for the date in messages matching the query." + "Face used in tree mode for non-matching authors." :group 'notmuch-tree :group 'notmuch-faces) @@ -363,6 +374,7 @@ then NAME behaves like CMD." (define-key map [remap notmuch-jump-search] 'notmuch-tree-jump-search) (define-key map "o" 'notmuch-tree-toggle-order) + (define-key map "i" 'notmuch-tree-toggle-hide-excluded) (define-key map "S" 'notmuch-search-from-tree-current-query) (define-key map "U" 'notmuch-unthreaded-from-tree-current-query) (define-key map "Z" 'notmuch-tree-from-unthreaded-current-query) @@ -387,6 +399,7 @@ then NAME behaves like CMD." (define-key map "V" 'notmuch-tree-view-raw-message) (define-key map "l" 'notmuch-tree-filter) (define-key map "t" 'notmuch-tree-filter-by-tag) + (define-key map "E" 'notmuch-tree-edit-search) ;; The main tree view bindings (define-key map (kbd "RET") 'notmuch-tree-show-message) @@ -578,7 +591,9 @@ NOT change the database." "Call notmuch search with the current query." (interactive) (notmuch-tree-close-message-window) - (notmuch-search (notmuch-tree-get-query))) + (notmuch-search (notmuch-tree-get-query) + notmuch-search-oldest-first + notmuch-search-hide-excluded)) (defun notmuch-tree-message-window-kill-hook () "Close the message pane when exiting the show buffer." @@ -791,7 +806,8 @@ nil otherwise." target nil unthreaded - notmuch-search-oldest-first))) + notmuch-search-oldest-first + notmuch-search-hide-excluded))) (defun notmuch-tree-thread-top () (when (notmuch-tree-get-message-properties) @@ -930,7 +946,9 @@ unchanged ADDRESS if parsing fails." 'face face))) ((string-equal field "subject") - (let ((bare-subject (notmuch-show-strip-re (plist-get headers :Subject))) + (let ((bare-subject + (notmuch-sanitize + (notmuch-show-strip-re (plist-get headers :Subject)))) (previous-subject notmuch-tree-previous-subject) (face (if match 'notmuch-tree-match-subject-face @@ -1004,7 +1022,10 @@ unchanged ADDRESS if parsing fails." A message tree is another name for a single sub-thread: i.e., a message together with all its descendents." (let ((msg (car tree)) - (replies (cadr tree))) + (replies (cadr tree)) + ;; outline level, computed from the message's depth and + ;; whether or not it's the first message in the tree. + (level (1+ (if (and (eq 0 depth) (not first)) 1 depth)))) (cond ((and (< 0 depth) (not last)) (push (alist-get 'vertical-tee notmuch-tree-thread-symbols) tree-status)) @@ -1024,6 +1045,7 @@ message together with all its descendents." (setq msg (plist-put msg :first (and first (eq 0 depth)))) (setq msg (plist-put msg :tree-status tree-status)) (setq msg (plist-put msg :orig-tags (plist-get msg :tags))) + (setq msg (plist-put msg :level level)) (notmuch-tree-goto-and-insert-msg msg) (pop tree-status) (pop tree-status) @@ -1033,7 +1055,8 @@ message together with all its descendents." (notmuch-tree-insert-thread replies (1+ depth) tree-status))) (defun notmuch-tree-insert-thread (thread depth tree-status) - "Insert the collection of sibling sub-threads THREAD at depth DEPTH in the current forest." + "Insert the collection of sibling sub-threads THREAD at depth +DEPTH in the current forest." (let ((n (length thread))) (cl-loop for tree in thread for count from 1 to n @@ -1068,9 +1091,19 @@ Complete list of currently available key bindings: \\{notmuch-tree-mode-map}" (setq notmuch-buffer-refresh-function #'notmuch-tree-refresh-view) - (hl-line-mode 1) + (when notmuch-hl-line + (hl-line-mode 1)) (setq buffer-read-only t) - (setq truncate-lines t)) + (setq truncate-lines t) + (when (boundp 'untrusted-content) + (setq untrusted-content t)) + (when notmuch-tree-outline-enabled (notmuch-tree-outline-mode 1))) + +(defvar notmuch-tree-process-exit-functions nil + "Functions called when the process inserting a tree of results finishes. + +Functions in this list are called with one argument, the process +object, and with the tree results buffer as the current buffer.") (defun notmuch-tree-process-sentinel (proc _msg) "Add a message to let user know when \"notmuch tree\" exits." @@ -1090,7 +1123,10 @@ Complete list of currently available key bindings: (insert "End of search results.") (unless (= exit-status 0) (insert (format " (process returned %d)" exit-status))) - (insert "\n"))))))))) + (insert "\n")))) + (when (and notmuch-hl-line (= exit-status 0)) + (notmuch-hl-line-mode)) + (run-hook-with-args 'notmuch-tree-process-exit-functions proc)))))) (defun notmuch-tree-process-filter (proc string) "Process and filter the output of \"notmuch show\" for tree view." @@ -1105,10 +1141,14 @@ Complete list of currently available key bindings: (goto-char (point-max)) (insert string)) (notmuch-sexp-parse-partial-list 'notmuch-tree-insert-forest-thread - results-buf))))) + results-buf)) + (with-current-buffer results-buf + (when notmuch-hl-line + (notmuch-hl-line-mode)))))) (defun notmuch-tree-worker (basic-query &optional query-context target - open-target unthreaded oldest-first) + open-target unthreaded oldest-first + exclude) "Insert the tree view of the search in the current buffer. This is is a helper function for notmuch-tree. The arguments are @@ -1117,6 +1157,7 @@ the same as for the function notmuch-tree." (notmuch-tree-mode) (add-hook 'post-command-hook #'notmuch-tree-command-hook t t) (setq notmuch-search-oldest-first oldest-first) + (setq notmuch-search-hide-excluded exclude) (setq notmuch-tree-unthreaded unthreaded) (setq notmuch-tree-basic-query basic-query) (setq notmuch-tree-query-context (if (or (string= query-context "") @@ -1136,14 +1177,15 @@ the same as for the function notmuch-tree." (and query-context (concat " and (" query-context ")")))) (sort-arg (if oldest-first "--sort=oldest-first" "--sort=newest-first")) - (message-arg (if unthreaded "--unthreaded" "--entire-thread"))) + (message-arg (if unthreaded "--unthreaded" "--entire-thread")) + (exclude-arg (if exclude "--exclude=true" "--exclude=false"))) (when (equal (car (notmuch--process-lines notmuch-command "count" search-args)) "0") (setq search-args basic-query)) (notmuch-tag-clear-cache) (let ((proc (notmuch-start-notmuch "notmuch-tree" (current-buffer) #'notmuch-tree-process-sentinel "show" "--body=false" "--format=sexp" "--format-version=5" - sort-arg message-arg search-args)) + sort-arg message-arg exclude-arg search-args)) ;; 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. @@ -1170,8 +1212,19 @@ default sort order is defined by `notmuch-search-oldest-first'." (setq notmuch-search-oldest-first (not notmuch-search-oldest-first)) (notmuch-tree-refresh-view)) +(defun notmuch-tree-toggle-hide-excluded () + "Toggle whether to hide excluded messages. + +This command toggles whether to hide excluded messages for the current +search. The default value for this is defined by `notmuch-search-hide-excluded'." + (interactive) + (setq notmuch-search-hide-excluded (not notmuch-search-hide-excluded)) + (notmuch-tree-refresh-view)) + +;;;###autoload (defun notmuch-tree (&optional query query-context target buffer-name - open-target unthreaded parent-buffer oldest-first) + open-target unthreaded parent-buffer + oldest-first hide-excluded) "Display threads matching QUERY in tree view. The arguments are: @@ -1186,28 +1239,49 @@ The arguments are: it is nil \"*notmuch-tree\" followed by QUERY is used. OPEN-TARGET: If TRUE open the target message in the message pane. UNTHREADED: If TRUE only show matching messages in an unthreaded view." - (interactive) + (interactive + (list + ;; Prompt for a query + nil + ;; Fill other args with nil. + nil nil nil nil nil nil + ;; Populate these from the default value of these options. + (default-value 'notmuch-search-oldest-first) + (default-value 'notmuch-search-hide-excluded))) (unless query (setq query (notmuch-read-query (concat "Notmuch " (if unthreaded "unthreaded " "tree ") "view search: ")))) - (let ((buffer (get-buffer-create (generate-new-buffer-name - (or buffer-name - (concat "*notmuch-" - (if unthreaded "unthreaded-" "tree-") - query "*"))))) + (let* ((name + (or buffer-name + (notmuch-search-buffer-title query + (if unthreaded "unthreaded" "tree")))) + (buffer (get-buffer-create (generate-new-buffer-name name))) (inhibit-read-only t)) (pop-to-buffer-same-window buffer)) ;; Don't track undo information for this buffer (setq buffer-undo-list t) - (notmuch-tree-worker query query-context target open-target unthreaded oldest-first) + (notmuch-tree-worker query query-context target open-target + unthreaded oldest-first hide-excluded) (setq notmuch-tree-parent-buffer parent-buffer) (setq truncate-lines t)) (defun notmuch-unthreaded (&optional query query-context target buffer-name - open-target) - (interactive) - (notmuch-tree query query-context target buffer-name open-target t)) + open-target oldest-first hide-excluded) + "Display threads matching QUERY in unthreaded view. + +See function NOTMUCH-TREE for documentation of the arguments" + (interactive + (list + ;; Prompt for a query + nil + ;; Fill other args with nil. + nil nil nil nil + ;; Populate these from the default value of these options. + (default-value 'notmuch-search-oldest-first) + (default-value 'notmuch-search-hide-excluded))) + (notmuch-tree query query-context target buffer-name open-target + t nil oldest-first hide-excluded)) (defun notmuch-tree-filter (query) "Filter or LIMIT the current search results based on an additional query string. @@ -1235,6 +1309,22 @@ search results and that are also tagged with the given TAG." (let ((notmuch-show-process-crypto (notmuch-tree--message-process-crypto))) (notmuch-tree-close-message-window) (notmuch-tree (concat notmuch-tree-basic-query " and tag:" tag) + notmuch-tree-query-context + nil + nil + nil + notmuch-tree-unthreaded + nil + notmuch-search-oldest-first + notmuch-search-hide-excluded))) + +(defun notmuch-tree-edit-search (query) + "Edit the current search" + (interactive (list (read-from-minibuffer "Edit search: " + notmuch-tree-basic-query))) + (let ((notmuch-show-process-crypto (notmuch-tree--message-process-crypto))) + (notmuch-tree-close-message-window) + (notmuch-tree query notmuch-tree-query-context nil nil @@ -1243,6 +1333,180 @@ search results and that are also tagged with the given TAG." nil notmuch-search-oldest-first))) +;;; Tree outline mode +;;;; Custom variables +(defcustom notmuch-tree-outline-enabled nil + "Whether to automatically activate `notmuch-tree-outline-mode' in tree views." + :type 'boolean) + +(defcustom notmuch-tree-outline-visibility 'hide-others + "Default state of the forest outline for `notmuch-tree-outline-mode'. + +This variable controls the state of a forest initially and after +a movement command. If set to nil, all trees are displayed while +the symbol hide-all indicates that all trees in the forest should +be folded and hide-other that only the first one should be +unfolded." + :type '(choice (const :tag "Show all" nil) + (const :tag "Hide others" hide-others) + (const :tag "Hide all" hide-all))) + +(defcustom notmuch-tree-outline-auto-close nil + "Close message and tree windows when moving past the last message." + :type 'boolean) + +(defcustom notmuch-tree-outline-open-on-next nil + "Open new messages under point if they are closed when moving to next one. + +When this flag is set, using the command +`notmuch-tree-outline-next' with point on a header for a new +message that is not shown will open its `notmuch-show' buffer +instead of moving point to next matching message." + :type 'boolean) + +;;;; Helper functions +(defsubst notmuch-tree-outline--pop-at-end (pop-at-end) + (if notmuch-tree-outline-auto-close (not pop-at-end) pop-at-end)) + +(defun notmuch-tree-outline--set-visibility () + (when (and notmuch-tree-outline-mode (> (point-max) (point-min))) + (cl-case notmuch-tree-outline-visibility + (hide-others (notmuch-tree-outline-hide-others)) + (hide-all (outline-hide-body))))) + +(defun notmuch-tree-outline--on-exit (proc) + (when (eq (process-status proc) 'exit) + (notmuch-tree-outline--set-visibility))) + +(add-hook 'notmuch-tree-process-exit-functions #'notmuch-tree-outline--on-exit) + +(defsubst notmuch-tree-outline--level (&optional props) + (or (plist-get (or props (notmuch-tree-get-message-properties)) :level) 0)) + +(defsubst notmuch-tree-outline--message-open-p () + (and (buffer-live-p notmuch-tree-message-buffer) + (get-buffer-window notmuch-tree-message-buffer) + (let ((id (notmuch-tree-get-message-id))) + (and id + (with-current-buffer notmuch-tree-message-buffer + (string= (notmuch-show-get-message-id) id)))))) + +(defsubst notmuch-tree-outline--at-original-match-p () + (and (notmuch-tree-get-prop :match) + (equal (notmuch-tree-get-prop :orig-tags) + (notmuch-tree-get-prop :tags)))) + +(defun notmuch-tree-outline--next (prev thread pop-at-end &optional open-new) + (cond (thread + (notmuch-tree-thread-top) + (if prev + (outline-backward-same-level 1) + (outline-forward-same-level 1)) + (when (> (notmuch-tree-outline--level) 0) (outline-show-branches)) + (notmuch-tree-outline--next nil nil pop-at-end t)) + ((and (or open-new notmuch-tree-outline-open-on-next) + (notmuch-tree-outline--at-original-match-p) + (not (notmuch-tree-outline--message-open-p))) + (notmuch-tree-outline-hide-others t)) + (t (outline-next-visible-heading (if prev -1 1)) + (unless (notmuch-tree-get-prop :match) + (notmuch-tree-matching-message prev pop-at-end)) + (notmuch-tree-outline-hide-others t)))) + +;;;; User commands +(defun notmuch-tree-outline-hide-others (&optional and-show) + "Fold all threads except the one around point. +If AND-SHOW is t, make the current message visible if it's not." + (interactive) + (save-excursion + (while (and (not (bobp)) (> (notmuch-tree-outline--level) 1)) + (outline-previous-heading)) + (outline-hide-sublevels 1)) + (when (> (notmuch-tree-outline--level) 0) + (outline-show-subtree) + (when and-show (notmuch-tree-show-message nil)))) + +(defun notmuch-tree-outline-next (&optional pop-at-end) + "Next matching message in a forest, taking care of thread visibility. +A prefix argument reverses the meaning of `notmuch-tree-outline-auto-close'." + (interactive "P") + (let ((pop (notmuch-tree-outline--pop-at-end pop-at-end))) + (if (null notmuch-tree-outline-visibility) + (notmuch-tree-matching-message nil pop) + (notmuch-tree-outline--next nil nil pop)))) + +(defun notmuch-tree-outline-previous (&optional pop-at-end) + "Previous matching message in forest, taking care of thread visibility. +With prefix, quit the tree view if there is no previous message." + (interactive "P") + (if (null notmuch-tree-outline-visibility) + (notmuch-tree-prev-matching-message pop-at-end) + (notmuch-tree-outline--next t nil pop-at-end))) + +(defun notmuch-tree-outline-next-thread () + "Next matching thread in forest, taking care of thread visibility." + (interactive) + (if (null notmuch-tree-outline-visibility) + (notmuch-tree-next-thread) + (notmuch-tree-outline--next nil t nil))) + +(defun notmuch-tree-outline-previous-thread () + "Previous matching thread in forest, taking care of thread visibility." + (interactive) + (if (null notmuch-tree-outline-visibility) + (notmuch-tree-prev-thread) + (notmuch-tree-outline--next t t nil))) + +;;;; Mode definition +(defvar notmuch-tree-outline-mode-lighter nil + "The lighter mark for notmuch-tree-outline mode. +Usually empty since outline-minor-mode's lighter will be active.") + +(define-minor-mode notmuch-tree-outline-mode + "Minor mode allowing message trees to be folded as outlines. + +When this mode is set, each thread and subthread in the results +list is treated as a foldable section, with its first message as +its header. + +The mode just makes available in the tree buffer all the +keybindings in `outline-minor-mode', and binds the following +additional keys: + +\\{notmuch-tree-outline-mode-map} + +The customizable variable `notmuch-tree-outline-visibility' +controls how navigation in the buffer is affected by this mode: + + - If it is set to nil, `notmuch-tree-outline-previous', + `notmuch-tree-outline-next', and their thread counterparts + behave just as the corresponding notmuch-tree navigation keys + when this mode is not enabled. + + - If, on the other hand, `notmuch-tree-outline-visibility' is + set to a non-nil value, these commands hiding the outlines of + the trees you are not reading as you move to new messages. + +To enable notmuch-tree-outline-mode by default in all +notmuch-tree buffers, just set +`notmuch-tree-outline-mode-enabled' to t." + :lighter notmuch-tree-outline-mode-lighter + :keymap `((,(kbd "TAB") . outline-cycle) + (,(kbd "M-TAB") . outline-cycle-buffer) + ("n" . notmuch-tree-outline-next) + ("p" . notmuch-tree-outline-previous) + (,(kbd "M-n") . notmuch-tree-outline-next-thread) + (,(kbd "M-p") . notmuch-tree-outline-previous-thread)) + (outline-minor-mode notmuch-tree-outline-mode) + (unless (derived-mode-p 'notmuch-tree-mode) + (user-error "notmuch-tree-outline-mode is only meaningful for notmuch trees!")) + (if notmuch-tree-outline-mode + (progn (setq-local outline-regexp "^[^\n]+") + (setq-local outline-level #'notmuch-tree-outline--level) + (notmuch-tree-outline--set-visibility)) + (setq-local outline-regexp (default-value 'outline-regexp)) + (setq-local outline-level (default-value 'outline-level)))) + ;;; _ (provide 'notmuch-tree) diff --git a/emacs/notmuch-wash.el b/emacs/notmuch-wash.el index 653ecc2a..fd8a9d1e 100644 --- a/emacs/notmuch-wash.el +++ b/emacs/notmuch-wash.el @@ -381,7 +381,7 @@ original filename the sender had." (notmuch-wash-subject-to-filename subject 52))) (defun notmuch-wash-convert-inline-patch-to-part (msg depth) - "Convert an inline patch into a fake 'text/x-diff' attachment. + "Convert an inline patch into a fake `text/x-diff' attachment. Given that this function guesses whether a buffer includes a patch and then guesses the extent of the patch, there is scope diff --git a/emacs/notmuch.el b/emacs/notmuch.el index 2ef67c0e..69aff347 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -90,11 +90,11 @@ ("tags" . "(%s)")) "Search result formatting. -Supported fields are: date, count, authors, subject, tags. -For example: - (setq notmuch-search-result-format - '((\"authors\" . \"%-40s\") - (\"subject\" . \"%s\"))) +List of pairs of (field . format-string). Supported field +strings are: \"date\", \"count\", \"authors\", \"subject\", +\"tags\". It is also supported to pass a function in place of a +field name. In this case the function is passed the thread +object (plist) and format string. Line breaks are permitted in format strings (though this is currently experimental). Note that a line break at the end of an @@ -102,7 +102,16 @@ currently experimental). Note that a line break at the end of an place it instead at the beginning of the following field. To enter a line break when setting this variable with setq, use \\n. To enter a line break in customize, press \\[quoted-insert] C-j." - :type '(alist :key-type string :value-type string) + :type '(alist + :key-type + (choice + (const :tag "Date" "date") + (const :tag "Count" "count") + (const :tag "Authors" "authors") + (const :tag "Subject" "subject") + (const :tag "Tags" "tags") + function) + :value-type (string :tag "Format")) :group 'notmuch-search) ;; The name of this variable `notmuch-init-file' is consistent with the @@ -117,13 +126,17 @@ there will be called at other points of notmuch execution." :type 'file :group 'notmuch) -(defcustom notmuch-search-hook '(notmuch-hl-line-mode) +(defcustom notmuch-search-hook nil "List of functions to call when notmuch displays the search results." :type 'hook - :options '(notmuch-hl-line-mode) :group 'notmuch-search :group 'notmuch-hooks) +(defcustom notmuch-hl-line t + "Use hl-line-mode to highlight current thread / message" + :type 'boolean + :group 'notmuch) + ;;; Mime Utilities (defun notmuch-foreach-mime-part (function mm-handle) @@ -178,9 +191,11 @@ there will be called at other points of notmuch execution." (define-key map "r" 'notmuch-search-reply-to-thread-sender) (define-key map "R" 'notmuch-search-reply-to-thread) (define-key map "o" 'notmuch-search-toggle-order) + (define-key map "i" 'notmuch-search-toggle-hide-excluded) (define-key map "c" 'notmuch-search-stash-map) (define-key map "t" 'notmuch-search-filter-by-tag) (define-key map "l" 'notmuch-search-filter) + (define-key map "E" 'notmuch-search-edit-search) (define-key map [mouse-1] 'notmuch-search-show-thread) (define-key map "k" 'notmuch-tag-jump) (define-key map "*" 'notmuch-search-tag-all) @@ -401,6 +416,8 @@ Complete list of currently available key bindings: (add-to-invisibility-spec (cons 'ellipsis t)) (setq truncate-lines t) (setq buffer-read-only t) + (when (boundp 'untrusted-content) + (setq untrusted-content t)) (setq imenu-prev-index-position-function #'notmuch-search-imenu-prev-index-position-function) (setq imenu-extract-index-name-function @@ -520,7 +537,9 @@ no messages in the region then return nil." With a prefix argument, invert the default value of `notmuch-show-only-matching-messages' when displaying the -thread." +thread. + +Return non-nil on success." (interactive "P") (let ((thread-id (notmuch-search-find-thread-id))) (if thread-id @@ -532,17 +551,24 @@ thread." (format "*%s*" (truncate-string-to-width (notmuch-search-find-subject) 30 nil nil t))) - (message "End of search results.")))) + (message "End of search results.") + nil))) (defun notmuch-tree-from-search-current-query () - "Call notmuch tree with the current query." + "Tree view of current query." (interactive) - (notmuch-tree notmuch-search-query-string)) + (notmuch-tree notmuch-search-query-string + nil nil nil nil nil nil + notmuch-search-oldest-first + notmuch-search-hide-excluded)) (defun notmuch-unthreaded-from-search-current-query () - "Call notmuch tree with the current query." + "Unthreaded view of current query." (interactive) - (notmuch-unthreaded notmuch-search-query-string)) + (notmuch-unthreaded notmuch-search-query-string + nil nil nil nil + notmuch-search-oldest-first + notmuch-search-hide-excluded)) (defun notmuch-tree-from-search-thread () "Show the selected thread with notmuch-tree." @@ -551,7 +577,9 @@ thread." notmuch-search-query-string nil (notmuch-prettify-subject (notmuch-search-find-subject)) - t nil (current-buffer))) + t nil (current-buffer) + notmuch-search-oldest-first + notmuch-search-hide-excluded)) (defun notmuch-search-reply-to-thread (&optional prompt-for-sender) "Begin composing a reply-all to the entire current thread in a new buffer." @@ -828,6 +856,7 @@ non-authors is found, assume that all of the authors match." overlay) (insert invisible-string) (setq overlay (make-overlay start (point))) + (overlay-put overlay 'evaporate t) (overlay-put overlay 'invisible 'ellipsis) (overlay-put overlay 'isearch-open-invisible #'delete-overlay))) (insert padding)))) @@ -880,6 +909,14 @@ sets the :orig-tag property." (setq notmuch-search-target-thread "found") (goto-char pos)))) +(defvar-local notmuch--search-hook-run nil + "Flag used to ensure the notmuch-search-hook is only run once per buffer") + +(defun notmuch--search-hook-wrapper () + (unless notmuch--search-hook-run + (setq notmuch--search-hook-run t) + (run-hooks 'notmuch-search-hook))) + (defun notmuch-search-process-filter (proc string) "Process and filter the output of \"notmuch search\"." (let ((results-buf (process-buffer proc)) @@ -892,7 +929,19 @@ sets the :orig-tag property." (goto-char (point-max)) (insert string)) (notmuch-sexp-parse-partial-list 'notmuch-search-append-result - results-buf))))) + results-buf)) + (with-current-buffer results-buf + (when (and notmuch-hl-line + ;; If we know where the cursor will end up (from + ;; the call to notmuch-search), avoid redrawing the + ;; hl-line overlay until the buffer is sufficiently + ;; filled. This check is intended as an + ;; optimization to reduce flicker. + (>= + (line-number-at-pos (point-max) t) + (or notmuch-search-target-line -1))) + (notmuch-hl-line-mode)) + (notmuch--search-hook-wrapper))))) ;;; Commands (and some helper functions used by them) @@ -905,7 +954,39 @@ See `notmuch-tag' for information on the format of TAG-CHANGES." (notmuch-search-get-tags-region (point-min) (point-max)) "Tag all"))) (notmuch-search-tag tag-changes (point-min) (point-max) t)) -(defun notmuch-search-buffer-title (query) +(defcustom notmuch-search-buffer-name-format "*notmuch-%t-%s*" + "Format for the name of search results buffers. + +In this spec, %s will be replaced by a description of the search +query and %t by its type (search, tree or unthreaded). The +buffer name is formatted using `format-spec': see its docstring +for additional parameters for the s and t format specifiers. + +See also `notmuch-saved-search-buffer-name-format'" + :type 'string + :group 'notmuch-search) + +(defcustom notmuch-saved-search-buffer-name-format "*notmuch-saved-%t-%s*" + "Format for the name of search results buffers for saved searches. + +In this spec, %s will be replaced by the saved search name and %t +by its type (search, tree or unthreaded). The buffer name is +formatted using `format-spec': see its docstring for additional +parameters for the s and t format specifiers. + +See also `notmuch-search-buffer-name-format'" + :type 'string + :group 'notmuch-search) + +(defun notmuch-search-format-buffer-name (query type saved) + "Compose a buffer name for the given QUERY, TYPE (search, tree, +unthreaded) and whether it's SAVED (t or nil)." + (let ((fmt (if saved + notmuch-saved-search-buffer-name-format + notmuch-search-buffer-name-format))) + (format-spec fmt `((?t . ,(or type "search")) (?s . ,query))))) + +(defun notmuch-search-buffer-title (query &optional type) "Returns the title for a buffer with notmuch search results." (let* ((saved-search (let (longest @@ -920,19 +1001,20 @@ See `notmuch-tag' for information on the format of TAG-CHANGES." do (setq longest tuple)) longest)) (saved-search-name (notmuch-saved-search-get saved-search :name)) + (saved-search-type (notmuch-saved-search-get saved-search :search-type)) (saved-search-query (notmuch-saved-search-get saved-search :query))) (cond ((and saved-search (equal saved-search-query query)) ;; Query is the same as saved search (ignoring case) - (concat "*notmuch-saved-search-" saved-search-name "*")) + (notmuch-search-format-buffer-name saved-search-name + saved-search-type + t)) (saved-search - (concat "*notmuch-search-" - (replace-regexp-in-string - (concat "^" (regexp-quote saved-search-query)) - (concat "[ " saved-search-name " ]") - query) - "*")) - (t - (concat "*notmuch-search-" query "*"))))) + (let ((query (replace-regexp-in-string + (concat "^" (regexp-quote saved-search-query)) + (concat "[ " saved-search-name " ]") + query))) + (notmuch-search-format-buffer-name query saved-search-type t))) + (t (notmuch-search-format-buffer-name query type nil))))) (defun notmuch-read-query (prompt) "Read a notmuch-query from the minibuffer with completion. @@ -978,14 +1060,16 @@ PROMPT is the string to prompt with." (put 'notmuch-search 'notmuch-doc "Search for messages.") ;;;###autoload -(defun notmuch-search (&optional query oldest-first target-thread target-line - no-display) +(defun notmuch-search (&optional query oldest-first hide-excluded target-thread + target-line no-display) "Display threads matching QUERY in a notmuch-search buffer. If QUERY is nil, it is read interactively from the minibuffer. Other optional parameters are used as follows: OLDEST-FIRST: A Boolean controlling the sort order of returned threads + HIDE-EXCLUDED: A boolean controlling whether to omit threads with excluded + tags. TARGET-THREAD: A thread ID (without the thread: prefix) that will be made current if it appears in the search results. TARGET-LINE: The line number to move to if the target thread does not @@ -1000,22 +1084,24 @@ the configured default sort order." (list ;; Prompt for a query nil - ;; Use the default search order (if we're doing a search from a - ;; search buffer, ignore any buffer-local overrides) - (default-value 'notmuch-search-oldest-first))) + ;; Use the default search order and exclude value (if we're doing a + ;; search from a search buffer, ignore any buffer-local overrides) + (default-value 'notmuch-search-oldest-first) + (default-value 'notmuch-search-hide-excluded))) (let* ((query (or query (notmuch-read-query "Notmuch search: "))) (buffer (get-buffer-create (notmuch-search-buffer-title query)))) - (if no-display - (set-buffer buffer) - (pop-to-buffer-same-window buffer)) + (set-buffer buffer) (notmuch-search-mode) + (unless no-display + (pop-to-buffer-same-window buffer)) ;; Don't track undo information for this buffer (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) + (setq notmuch-search-hide-excluded hide-excluded) (notmuch-tag-clear-cache) (when (get-buffer-process buffer) (error "notmuch search process already running for query `%s'" query)) @@ -1029,6 +1115,9 @@ the configured default sort order." (if oldest-first "--sort=oldest-first" "--sort=newest-first") + (if hide-excluded + "--exclude=true" + "--exclude=false") query))) ;; Use a scratch buffer to accumulate partial output. ;; This buffer will be killed by the sentinel, which @@ -1036,8 +1125,7 @@ the configured default sort order." (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))) + (set-process-query-on-exit-flag proc nil)))))) (defun notmuch-search-refresh-view () "Refresh the current view. @@ -1050,11 +1138,21 @@ same relative position within the new buffer." (interactive) (notmuch-search notmuch-search-query-string notmuch-search-oldest-first + notmuch-search-hide-excluded (notmuch-search-find-thread-id 'bare) (line-number-at-pos) t) (goto-char (point-min))) +(defun notmuch-search-toggle-hide-excluded () + "Toggle whether to hide excluded messages. + +This command toggles whether to hide excluded messages for the current +search. The default value for this is defined by `notmuch-search-hide-excluded'." + (interactive) + (setq notmuch-search-hide-excluded (not notmuch-search-hide-excluded)) + (notmuch-search-refresh-view)) + (defun notmuch-search-toggle-order () "Toggle the current search order. @@ -1083,7 +1181,8 @@ current search results AND the additional query string provided." (notmuch-search (if (string= grouped-original-query "*") grouped-query (concat grouped-original-query " and " grouped-query)) - notmuch-search-oldest-first))) + notmuch-search-oldest-first + notmuch-search-hide-excluded))) (defun notmuch-search-filter-by-tag (tag) "Filter the current search results based on a single TAG. @@ -1094,13 +1193,22 @@ search results and that are also tagged with the given TAG." (list (notmuch-select-tag-with-completion "Filter by tag: " notmuch-search-query-string))) (notmuch-search (concat notmuch-search-query-string " and tag:" tag) - notmuch-search-oldest-first)) + notmuch-search-oldest-first + notmuch-search-hide-excluded)) (defun notmuch-search-by-tag (tag) "Display threads matching TAG in a notmuch-search buffer." (interactive (list (notmuch-select-tag-with-completion "Notmuch search tag: "))) - (notmuch-search (concat "tag:" tag))) + (notmuch-search (concat "tag:" tag) + (default-value 'notmuch-search-oldest-first) + (default-value 'notmuch-search-hide-excluded))) + +(defun notmuch-search-edit-search (query) + "Edit the current search" + (interactive (list (read-from-minibuffer "Edit search: " + notmuch-search-query-string))) + (notmuch-search query notmuch-search-oldest-first)) ;;;###autoload (defun notmuch () diff --git a/hooks.c b/hooks.c index ec89b22e..0cf72e74 100644 --- a/hooks.c +++ b/hooks.c @@ -27,6 +27,7 @@ int notmuch_run_hook (notmuch_database_t *notmuch, const char *hook) { char *hook_path; + const char *config_path; int status = 0; pid_t pid; @@ -38,6 +39,12 @@ notmuch_run_hook (notmuch_database_t *notmuch, const char *hook) return 1; } + config_path = notmuch_config_path (notmuch); + if (setenv ("NOTMUCH_CONFIG", config_path, 1)) { + perror ("setenv"); + return 1; + } + /* Check access before fork() for speed and simplicity of error handling. */ if (access (hook_path, X_OK) == -1) { /* Ignore ENOENT. It's okay not to have a hook, hook dir, or even diff --git a/lib/Makefile.local b/lib/Makefile.local index 1378a74b..4e766305 100644 --- a/lib/Makefile.local +++ b/lib/Makefile.local @@ -64,7 +64,9 @@ libnotmuch_cxx_srcs = \ $(dir)/prefix.cc \ $(dir)/open.cc \ $(dir)/init.cc \ - $(dir)/parse-sexp.cc + $(dir)/parse-sexp.cc \ + $(dir)/sexp-fp.cc \ + $(dir)/lastmod-fp.cc libnotmuch_modules := $(libnotmuch_c_srcs:.c=.o) $(libnotmuch_cxx_srcs:.cc=.o) diff --git a/lib/built-with.c b/lib/built-with.c index 89958e12..275e72b8 100644 --- a/lib/built-with.c +++ b/lib/built-with.c @@ -32,7 +32,7 @@ notmuch_built_with (const char *name) return HAVE_XAPIAN_DB_RETRY_LOCK; } else if (STRNCMP_LITERAL (name, "session_key") == 0) { return true; - } else if (STRNCMP_LITERAL (name, "sexpr_query") == 0) { + } else if (STRNCMP_LITERAL (name, "sexp_queries") == 0) { return HAVE_SFSEXP; } else { return false; diff --git a/lib/config.cc b/lib/config.cc index 8775b00a..acb397ec 100644 --- a/lib/config.cc +++ b/lib/config.cc @@ -259,7 +259,15 @@ _notmuch_config_load_from_database (notmuch_database_t *notmuch) 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)); + char *normalized_val = NULL; + + /* If we opened from a given path, do not overwrite it */ + if (strcmp (key, "database.path") == 0 && + (notmuch->params & NOTMUCH_PARAM_DATABASE) && + notmuch->xapian_db) + continue; + + normalized_val = _expand_path (list, key, notmuch_config_list_value (list)); _notmuch_string_map_append (notmuch->config, key, normalized_val); talloc_free (normalized_val); } @@ -408,7 +416,8 @@ _expand_path (void *ctx, const char *key, const char *val) notmuch_status_t _notmuch_config_load_from_file (notmuch_database_t *notmuch, - GKeyFile *file) + GKeyFile *file, + char **status_string) { notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; gchar **groups = NULL, **keys, *val; @@ -427,11 +436,27 @@ _notmuch_config_load_from_file (notmuch_database_t *notmuch, 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); + GError *gerr = NULL; + + /* If we opened from a given path, do not overwrite it */ + if (strcmp (absolute_key, "database.path") == 0 && + (notmuch->params & NOTMUCH_PARAM_DATABASE) && + notmuch->xapian_db) + continue; + + val = g_key_file_get_string (file, *grp, *keys_p, &gerr); + if (gerr) { + if (status_string) + IGNORE_RESULT (asprintf (status_string, + "GLib: %s\n", + gerr->message)); + g_error_free (gerr); + } 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); @@ -581,6 +606,14 @@ _notmuch_config_key_to_string (notmuch_config_key_t key) return "user.name"; case NOTMUCH_CONFIG_AUTOCOMMIT: return "database.autocommit"; + case NOTMUCH_CONFIG_EXTRA_HEADERS: + return "show.extra_headers"; + case NOTMUCH_CONFIG_AUTHORS_SEPARATOR: + return "search.authors_separator"; + case NOTMUCH_CONFIG_AUTHORS_MATCHED_SEPARATOR: + return "search.authors_matched_separator"; + case NOTMUCH_CONFIG_INDEX_AS_TEXT: + return "index.as_text"; default: return NULL; } @@ -624,10 +657,16 @@ _notmuch_config_default (notmuch_database_t *notmuch, notmuch_config_key_t key) else email = _get_email_from_passwd_file (notmuch); return email; + case NOTMUCH_CONFIG_INDEX_AS_TEXT: case NOTMUCH_CONFIG_NEW_IGNORE: return ""; case NOTMUCH_CONFIG_AUTOCOMMIT: return "8000"; + case NOTMUCH_CONFIG_AUTHORS_SEPARATOR: + return ", "; + case NOTMUCH_CONFIG_AUTHORS_MATCHED_SEPARATOR: + return "| "; + case NOTMUCH_CONFIG_EXTRA_HEADERS: case NOTMUCH_CONFIG_HOOK_DIR: case NOTMUCH_CONFIG_BACKUP_DIR: case NOTMUCH_CONFIG_OTHER_EMAIL: @@ -642,6 +681,10 @@ notmuch_status_t _notmuch_config_load_defaults (notmuch_database_t *notmuch) { notmuch_config_key_t key; + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + + if (notmuch->config == NULL) + notmuch->config = _notmuch_string_map_create (notmuch); for (key = NOTMUCH_CONFIG_FIRST; key < NOTMUCH_CONFIG_LAST; @@ -651,11 +694,14 @@ _notmuch_config_load_defaults (notmuch_database_t *notmuch) val = _notmuch_string_map_get (notmuch->config, key_string); if (! val) { + if (key == NOTMUCH_CONFIG_MAIL_ROOT && (notmuch->params & NOTMUCH_PARAM_SPLIT)) + status = NOTMUCH_STATUS_NO_MAIL_ROOT; + _notmuch_string_map_set (notmuch->config, key_string, _notmuch_config_default (notmuch, key)); } } - return NOTMUCH_STATUS_SUCCESS; + return status; } const char * diff --git a/lib/database-private.h b/lib/database-private.h index 8b9d67fe..61232f1a 100644 --- a/lib/database-private.h +++ b/lib/database-private.h @@ -160,11 +160,12 @@ operator&= (_notmuch_features &a, _notmuch_features b) /* * Configuration options for xapian database fields */ -typedef enum notmuch_field_flags { +typedef enum { NOTMUCH_FIELD_NO_FLAGS = 0, NOTMUCH_FIELD_EXTERNAL = 1 << 0, NOTMUCH_FIELD_PROBABILISTIC = 1 << 1, NOTMUCH_FIELD_PROCESSOR = 1 << 2, + NOTMUCH_FIELD_STRIP_TRAILING_SLASH = 1 << 3, } notmuch_field_flag_t; /* @@ -190,6 +191,44 @@ operator& (notmuch_field_flag_t a, notmuch_field_flag_t b) Xapian::QueryParser::FLAG_WILDCARD | \ Xapian::QueryParser::FLAG_PURE_NOT) +/* + * explicit and implied parameters to open */ +typedef enum { + NOTMUCH_PARAM_NONE = 0, + /* database passed explicitely */ + NOTMUCH_PARAM_DATABASE = 1 << 0, + /* config file passed explicitely */ + NOTMUCH_PARAM_CONFIG = 1 << 1, + /* profile name passed explicitely */ + NOTMUCH_PARAM_PROFILE = 1 << 2, + /* split (e.g. XDG) configuration */ + NOTMUCH_PARAM_SPLIT = 1 << 3, +} notmuch_open_param_t; + +/* + * define bitwise operators to hide casts */ + +inline notmuch_open_param_t +operator| (notmuch_open_param_t a, notmuch_open_param_t b) +{ + return static_cast( + static_cast(a) | static_cast(b)); +} + +inline notmuch_open_param_t& +operator|= (notmuch_open_param_t &a, notmuch_open_param_t b) +{ + a = a | b; + return a; +} + +inline notmuch_open_param_t +operator& (notmuch_open_param_t a, notmuch_open_param_t b) +{ + return static_cast( + static_cast(a) & static_cast(b)); +} + struct _notmuch_database { bool exception_reported; @@ -249,6 +288,13 @@ struct _notmuch_database { /* Cached and possibly overridden configuration */ notmuch_string_map_t *config; + + /* Track what parameters were specified when opening */ + notmuch_open_param_t params; + + /* list of regular expressions to check for text indexing */ + regex_t *index_as_text; + size_t index_as_text_length; }; /* Prior to database version 3, features were implied by the database @@ -312,10 +358,6 @@ _notmuch_query_string_to_xapian_query (notmuch_database_t *notmuch, std::string query_string, Xapian::Query &output, std::string &msg); -/* parse-sexp.cc */ -notmuch_status_t -_notmuch_sexp_string_to_xapian_query (notmuch_database_t *notmuch, const char *querystr, - Xapian::Query &output); notmuch_status_t _notmuch_query_expand (notmuch_database_t *notmuch, const char *field, Xapian::Query subquery, @@ -338,5 +380,16 @@ notmuch_status_t _notmuch_sexp_string_to_xapian_query (notmuch_database_t *notmuch, const char *querystr, Xapian::Query &output); #endif + +/* parse-time-vrp.h */ +notmuch_status_t +_notmuch_date_strings_to_query (Xapian::valueno slot, const std::string &from, const std::string &to, + Xapian::Query &output, std::string &msg); + +/* lastmod-fp.h */ +notmuch_status_t +_notmuch_lastmod_strings_to_query (notmuch_database_t *notmuch, + const std::string &from, const std::string &to, + Xapian::Query &output, std::string &msg); #endif #endif diff --git a/lib/database.cc b/lib/database.cc index 7eb0de79..737a3f30 100644 --- a/lib/database.cc +++ b/lib/database.cc @@ -311,6 +311,8 @@ notmuch_status_to_string (notmuch_status_t status) return "Database exists, not recreated"; case NOTMUCH_STATUS_BAD_QUERY_SYNTAX: return "Syntax error in query"; + case NOTMUCH_STATUS_NO_MAIL_ROOT: + return "No mail root found"; default: case NOTMUCH_STATUS_LAST_STATUS: return "Unknown error status value"; @@ -474,6 +476,11 @@ _notmuch_database_ensure_writable (notmuch_database_t *notmuch) return NOTMUCH_STATUS_READ_ONLY_DATABASE; } + if (! notmuch->open) { + _notmuch_database_log (notmuch, "Cannot write to a closed database.\n"); + return NOTMUCH_STATUS_CLOSED_DATABASE; + } + return NOTMUCH_STATUS_SUCCESS; } @@ -590,10 +597,12 @@ notmuch_database_compact (const char *path, notmuch_database_t *notmuch = NULL; char *message = NULL; - ret = notmuch_database_open_verbose (path, - NOTMUCH_DATABASE_MODE_READ_WRITE, - ¬much, - &message); + ret = notmuch_database_open_with_config (path, + NOTMUCH_DATABASE_MODE_READ_WRITE, + "", + NULL, + ¬much, + &message); if (ret) { if (status_cb) status_cb (message, closure); return ret; @@ -751,6 +760,8 @@ notmuch_database_destroy (notmuch_database_t *notmuch) notmuch->date_range_processor = NULL; delete notmuch->last_mod_range_processor; notmuch->last_mod_range_processor = NULL; + delete notmuch->stemmer; + notmuch->stemmer = NULL; talloc_free (notmuch); @@ -846,9 +857,8 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, notmuch_query_t *query = NULL; unsigned int count = 0, total = 0; - status = _notmuch_database_ensure_writable (notmuch); - if (status) - return status; + if (_notmuch_database_mode (notmuch) != NOTMUCH_DATABASE_MODE_READ_WRITE) + return NOTMUCH_STATUS_READ_ONLY_DATABASE; db = notmuch->writable_xapian_db; @@ -1444,9 +1454,11 @@ notmuch_database_remove_message (notmuch_database_t *notmuch, &message); if (status == NOTMUCH_STATUS_SUCCESS && message) { - status = _notmuch_message_remove_filename (message, filename); + if (notmuch_message_count_files (message) > 1) { + status = _notmuch_message_remove_filename (message, filename); + } if (status == NOTMUCH_STATUS_SUCCESS) - _notmuch_message_delete (message); + status = _notmuch_message_delete (message); else if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) _notmuch_message_sync (message); @@ -1563,3 +1575,15 @@ notmuch_database_status_string (const notmuch_database_t *notmuch) { return notmuch->status_string; } + +bool +_notmuch_database_indexable_as_text (notmuch_database_t *notmuch, const char *mime_string) +{ + for (size_t i = 0; i < notmuch->index_as_text_length; i++) { + if (regexec (¬much->index_as_text[i], mime_string, 0, NULL, 0) == 0) { + return true; + } + } + + return false; +} diff --git a/lib/index.cc b/lib/index.cc index 728bfb22..629dcb22 100644 --- a/lib/index.cc +++ b/lib/index.cc @@ -380,6 +380,23 @@ _index_pkcs7_part (notmuch_message_t *message, GMimeObject *part, _notmuch_message_crypto_t *msg_crypto); +static bool +_indexable_as_text (notmuch_message_t *message, GMimeObject *part) +{ + GMimeContentType *content_type = g_mime_object_get_content_type (part); + notmuch_database_t *notmuch = notmuch_message_get_database (message); + + if (content_type) { + char *mime_string = g_mime_content_type_get_mime_type (content_type); + if (mime_string) { + bool ret = _notmuch_database_indexable_as_text (notmuch, mime_string); + g_free (mime_string); + return ret; + } + } + return false; +} + /* Callback to generate terms for each mime part of a message. */ static void _index_mime_part (notmuch_message_t *message, @@ -497,9 +514,11 @@ _index_mime_part (notmuch_message_t *message, _notmuch_message_add_term (message, "tag", "attachment"); _notmuch_message_gen_terms (message, "attachment", filename); - /* XXX: Would be nice to call out to something here to parse - * the attachment into text and then index that. */ - goto DONE; + if (! _indexable_as_text (message, part)) { + /* XXX: Would be nice to call out to something here to parse + * the attachment into text and then index that. */ + goto DONE; + } } byte_array = g_byte_array_new (); diff --git a/lib/indexopts.c b/lib/indexopts.c index 4a860858..2ffd1942 100644 --- a/lib/indexopts.c +++ b/lib/indexopts.c @@ -20,6 +20,10 @@ #include "notmuch-private.h" +struct _notmuch_indexopts { + _notmuch_crypto_t crypto; +}; + notmuch_indexopts_t * notmuch_database_get_default_indexopts (notmuch_database_t *db) { diff --git a/lib/lastmod-fp.cc b/lib/lastmod-fp.cc new file mode 100644 index 00000000..f85efd28 --- /dev/null +++ b/lib/lastmod-fp.cc @@ -0,0 +1,83 @@ +/* lastmod-fp.cc - lastmod range query glue + * + * This file is part of notmuch. + * + * Copyright © 2022 David Bremner + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/ . + * + * Author: David Bremner + */ + +#include "database-private.h" +#include "lastmod-fp.h" + +notmuch_status_t +_notmuch_lastmod_strings_to_query (notmuch_database_t *notmuch, + const std::string &from, const std::string &to, + Xapian::Query &output, std::string &msg) +{ + long from_idx = 0L, to_idx = LONG_MAX; + long current; + std::string str; + + /* revision should not change, but for the avoidance of doubt, + * grab for both ends of range, if needed*/ + current = notmuch_database_get_revision (notmuch, NULL); + + try { + if (from.empty ()) + from_idx = 0L; + else + from_idx = std::stol (from); + } catch (std::logic_error &e) { + msg = "bad 'from' revision: '" + from + "'"; + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + if (from_idx < 0) + from_idx += current; + + try { + if (EMPTY_STRING (to)) + to_idx = LONG_MAX; + else + to_idx = std::stol (to); + } catch (std::logic_error &e) { + msg = "bad 'to' revision: '" + to + "'"; + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + if (to_idx < 0) + to_idx += current; + + output = Xapian::Query (Xapian::Query::OP_VALUE_RANGE, NOTMUCH_VALUE_LAST_MOD, + Xapian::sortable_serialise (from_idx), + Xapian::sortable_serialise (to_idx)); + return NOTMUCH_STATUS_SUCCESS; +} + +Xapian::Query +LastModRangeProcessor::operator() (const std::string &begin, const std::string &end) +{ + + Xapian::Query output; + std::string msg; + + if (_notmuch_lastmod_strings_to_query (notmuch, begin, end, output, msg)) + throw Xapian::QueryParserError (msg); + + return output; +} + diff --git a/lib/lastmod-fp.h b/lib/lastmod-fp.h new file mode 100644 index 00000000..8168fe7b --- /dev/null +++ b/lib/lastmod-fp.h @@ -0,0 +1,41 @@ +/* lastmod-fp.h - database revision query glue + * + * This file is part of notmuch. + * + * Copyright © 2022 David Bremner + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/ . + * + * Author: David Bremner + */ + +#ifndef NOTMUCH_LASTMOD_FP_H +#define NOTMUCH_LASTMOD_FP_H + +#include + +class LastModRangeProcessor : public Xapian::RangeProcessor { +protected: + notmuch_database_t *notmuch; + +public: + LastModRangeProcessor (notmuch_database_t *notmuch_, const std::string prefix_) + : Xapian::RangeProcessor (NOTMUCH_VALUE_LAST_MOD, prefix_, 0), notmuch (notmuch_) + { + } + + Xapian::Query operator() (const std::string &begin, const std::string &end); +}; + +#endif /* NOTMUCH_LASTMOD_FP_H */ diff --git a/lib/message-property.cc b/lib/message-property.cc index d5afa30c..7f520340 100644 --- a/lib/message-property.cc +++ b/lib/message-property.cc @@ -25,6 +25,20 @@ #include "database-private.h" #include "message-private.h" +#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) +{ + notmuch_database_t *notmuch = notmuch_message_get_database (message); + + _notmuch_database_log (notmuch, + "A Xapian exception occurred at %s: %s\n", + where, + error.get_msg ().c_str ()); + notmuch->exception_reported = true; +} + notmuch_status_t notmuch_message_get_property (notmuch_message_t *message, const char *key, const char **value) { @@ -83,10 +97,15 @@ _notmuch_message_modify_property (notmuch_message_t *message, const char *key, c term = talloc_asprintf (message, "%s=%s", key, value); - if (delete_it) - private_status = _notmuch_message_remove_term (message, "property", term); - else - private_status = _notmuch_message_add_term (message, "property", term); + try { + if (delete_it) + private_status = _notmuch_message_remove_term (message, "property", term); + else + private_status = _notmuch_message_add_term (message, "property", term); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } if (private_status) return COERCE_STATUS (private_status, @@ -123,15 +142,22 @@ _notmuch_message_remove_all_properties (notmuch_message_t *message, const char * if (status) return status; - _notmuch_message_invalidate_metadata (message, "property"); if (key) term_prefix = talloc_asprintf (message, "%s%s%s", _find_prefix ("property"), key, prefix ? "" : "="); else term_prefix = _find_prefix ("property"); - /* XXX better error reporting ? */ - _notmuch_message_remove_terms (message, term_prefix); + try { + /* XXX better error reporting ? */ + _notmuch_message_remove_terms (message, term_prefix); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + + if (! _notmuch_message_frozen (message)) + _notmuch_message_sync (message); return NOTMUCH_STATUS_SUCCESS; } diff --git a/lib/message.cc b/lib/message.cc index 63b216b6..46638f80 100644 --- a/lib/message.cc +++ b/lib/message.cc @@ -169,6 +169,7 @@ _notmuch_message_create_for_document (const void *talloc_owner, message->doc = doc; message->termpos = 0; + message->modified = false; return message; } @@ -340,23 +341,6 @@ _notmuch_message_get_term (notmuch_message_t *message, return value; } -/* - * For special applications where we only want the thread id, reading - * in all metadata is a heavy I/O penalty. - */ -const char * -_notmuch_message_get_thread_id_only (notmuch_message_t *message) -{ - - Xapian::TermIterator i = message->doc.termlist_begin (); - Xapian::TermIterator end = message->doc.termlist_end (); - - message->thread_id = _notmuch_message_get_term (message, i, end, - _find_prefix ("thread")); - return message->thread_id; -} - - static void _notmuch_message_ensure_metadata (notmuch_message_t *message, void *field) { @@ -735,6 +719,8 @@ _notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix) /* Ignore failure to remove non-existent term. */ } } + + _notmuch_message_invalidate_metadata (message, "property"); } @@ -808,7 +794,7 @@ is_maildir (const char *p) } /* Add "folder:" term for directory. */ -static notmuch_status_t +NODISCARD static notmuch_status_t _notmuch_message_add_folder_terms (notmuch_message_t *message, const char *directory) { @@ -844,7 +830,10 @@ _notmuch_message_add_folder_terms (notmuch_message_t *message, *folder = '\0'; } - _notmuch_message_add_term (message, "folder", folder); + if (notmuch_status_t status = COERCE_STATUS (_notmuch_message_add_term (message, "folder", + folder), + "adding folder term")) + return status; talloc_free (folder); @@ -855,12 +844,17 @@ _notmuch_message_add_folder_terms (notmuch_message_t *message, #define RECURSIVE_SUFFIX "/**" /* Add "path:" terms for directory. */ -static notmuch_status_t +NODISCARD static notmuch_status_t _notmuch_message_add_path_terms (notmuch_message_t *message, const char *directory) { + notmuch_status_t status; + /* Add exact "path:" term. */ - _notmuch_message_add_term (message, "path", directory); + status = COERCE_STATUS (_notmuch_message_add_term (message, "path", directory), + "adding path term"); + if (status) + return status; if (strlen (directory)) { char *path, *p; @@ -873,7 +867,10 @@ _notmuch_message_add_path_terms (notmuch_message_t *message, for (p = path + strlen (path) - 1; p > path; p--) { if (*p == '/') { strcpy (p, RECURSIVE_SUFFIX); - _notmuch_message_add_term (message, "path", path); + status = COERCE_STATUS (_notmuch_message_add_term (message, "path", path), + "adding path term"); + if (status) + return status; } } @@ -881,7 +878,10 @@ _notmuch_message_add_path_terms (notmuch_message_t *message, } /* Recursive all-matching path:** for consistency. */ - _notmuch_message_add_term (message, "path", "**"); + status = COERCE_STATUS (_notmuch_message_add_term (message, "path", "**"), + "adding path term"); + if (status) + return status; return NOTMUCH_STATUS_SUCCESS; } @@ -900,6 +900,7 @@ _notmuch_message_add_directory_terms (void *ctx, notmuch_message_t *message) const char *direntry, *directory; char *colon; const std::string &term = *i; + notmuch_status_t term_status; /* Terminate loop at first term without desired prefix. */ if (strncmp (term.c_str (), direntry_prefix, direntry_prefix_len)) @@ -920,8 +921,13 @@ _notmuch_message_add_directory_terms (void *ctx, notmuch_message_t *message) message->notmuch, directory_id); - _notmuch_message_add_folder_terms (message, directory); - _notmuch_message_add_path_terms (message, directory); + term_status = _notmuch_message_add_folder_terms (message, directory); + if (term_status) + return term_status; + + term_status = _notmuch_message_add_path_terms (message, directory); + if (term_status) + return term_status; } return status; @@ -937,6 +943,7 @@ _notmuch_message_add_filename (notmuch_message_t *message, { const char *relative, *directory; notmuch_status_t status; + notmuch_private_status_t private_status; void *local = talloc_new (message); char *direntry; @@ -960,10 +967,25 @@ _notmuch_message_add_filename (notmuch_message_t *message, /* New file-direntry allows navigating to this message with * notmuch_directory_get_child_files() . */ - _notmuch_message_add_term (message, "file-direntry", direntry); + private_status = _notmuch_message_add_term (message, "file-direntry", direntry); + switch (private_status) { + case NOTMUCH_PRIVATE_STATUS_SUCCESS: + break; + case NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG: + _notmuch_database_log (message->notmuch, "filename too long for file-direntry term: %s\n", + filename); + return NOTMUCH_STATUS_PATH_ERROR; + default: + return COERCE_STATUS (private_status, "adding file-direntry term"); + } - _notmuch_message_add_folder_terms (message, directory); - _notmuch_message_add_path_terms (message, directory); + status = _notmuch_message_add_folder_terms (message, directory); + if (status) + return status; + + status = _notmuch_message_add_path_terms (message, directory); + if (status) + return status; talloc_free (local); @@ -1371,21 +1393,22 @@ _notmuch_message_delete (notmuch_message_t *message) if (status) return status; - message->notmuch->writable_xapian_db->delete_document (message->doc_id); - - /* if this was a ghost to begin with, we are done */ - private_status = _notmuch_message_has_term (message, "type", "ghost", &is_ghost); - if (private_status) - return COERCE_STATUS (private_status, - "Error trying to determine whether message was a ghost"); - if (is_ghost) - return NOTMUCH_STATUS_SUCCESS; - - /* 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; + /* look for a non-ghost message in the same thread */ + /* if this was a ghost to begin with, we are done */ + private_status = _notmuch_message_has_term (message, "type", "ghost", &is_ghost); + if (private_status) + return COERCE_STATUS (private_status, + "Error trying to determine whether message was a ghost"); + + message->notmuch->writable_xapian_db->delete_document (message->doc_id); + + if (is_ghost) + return NOTMUCH_STATUS_SUCCESS; + _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); @@ -1482,31 +1505,37 @@ _notmuch_message_close (notmuch_message_t *message) * * This change will not be reflected in the database until the next * call to _notmuch_message_sync. */ -notmuch_private_status_t +NODISCARD notmuch_private_status_t _notmuch_message_add_term (notmuch_message_t *message, const char *prefix_name, const char *value) { char *term; + notmuch_private_status_t status = NOTMUCH_PRIVATE_STATUS_SUCCESS; if (value == NULL) return NOTMUCH_PRIVATE_STATUS_NULL_POINTER; term = talloc_asprintf (message, "%s%s", _find_prefix (prefix_name), value); + if (strlen (term) > NOTMUCH_TERM_MAX) { + status = NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG; + goto DONE; + } - if (strlen (term) > NOTMUCH_TERM_MAX) - return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG; - - message->doc.add_term (term, 0); - message->modified = true; + try { + message->doc.add_term (term, 0); + message->modified = true; + _notmuch_message_invalidate_metadata (message, prefix_name); + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + status = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION; + } + DONE: talloc_free (term); - - _notmuch_message_invalidate_metadata (message, prefix_name); - - return NOTMUCH_PRIVATE_STATUS_SUCCESS; + return status; } /* Parse 'text' and add a term to 'message' for each parsed word. Each @@ -1551,7 +1580,7 @@ _notmuch_message_gen_terms (notmuch_message_t *message, * * This change will not be reflected in the database until the next * call to _notmuch_message_sync. */ -notmuch_private_status_t +NODISCARD notmuch_private_status_t _notmuch_message_remove_term (notmuch_message_t *message, const char *prefix_name, const char *value) @@ -1570,11 +1599,12 @@ _notmuch_message_remove_term (notmuch_message_t *message, try { message->doc.remove_term (term); message->modified = true; - } catch (const Xapian::InvalidArgumentError) { + } catch (const Xapian::InvalidArgumentError &error) { /* We'll let the philosophers try to wrestle with the * question of whether failing to remove that which was not * there in the first place is failure. For us, we'll silently * consider it all good. */ + LOG_XAPIAN_EXCEPTION (message, error); } talloc_free (term); @@ -2020,6 +2050,10 @@ notmuch_message_tags_to_maildir_flags (notmuch_message_t *message) char *to_set, *to_clear; notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + status = _notmuch_database_ensure_writable (message->notmuch); + if (status) + return status; + _get_maildir_flag_actions (message, &to_set, &to_clear); for (filenames = notmuch_message_get_filenames (message); @@ -2283,7 +2317,11 @@ notmuch_message_reindex (notmuch_message_t *message, if (thread_id == NULL) thread_id = orig_thread_id; - _notmuch_message_add_term (message, "thread", thread_id); + ret = COERCE_STATUS (_notmuch_message_add_term (message, "thread", thread_id), + "adding thread term"); + if (ret) + goto DONE; + /* Take header values only from first filename */ if (found == 0) _notmuch_message_set_header_values (message, date, from, subject); @@ -2301,7 +2339,11 @@ notmuch_message_reindex (notmuch_message_t *message, } if (found == 0) { /* put back thread id to help cleanup */ - _notmuch_message_add_term (message, "thread", orig_thread_id); + ret = COERCE_STATUS (_notmuch_message_add_term (message, "thread", orig_thread_id), + "adding thread term"); + if (ret) + goto DONE; + ret = _notmuch_message_delete (message); } else { _notmuch_message_sync (message); diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h index 093c29b1..367e23e6 100644 --- a/lib/notmuch-private.h +++ b/lib/notmuch-private.h @@ -121,7 +121,7 @@ typedef enum { */ #define NOTMUCH_MESSAGE_ID_MAX (200 - sizeof (NOTMUCH_METADATA_THREAD_ID_PREFIX)) -typedef enum _notmuch_private_status { +typedef enum { /* First, copy all the public status values. */ NOTMUCH_PRIVATE_STATUS_SUCCESS = NOTMUCH_STATUS_SUCCESS, NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY = NOTMUCH_STATUS_OUT_OF_MEMORY, @@ -144,6 +144,9 @@ typedef enum _notmuch_private_status { 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, + NOTMUCH_PRIVATE_STATUS_NO_MAIL_ROOT = NOTMUCH_STATUS_NO_MAIL_ROOT, + NOTMUCH_PRIVATE_STATUS_BAD_QUERY_SYNTAX = NOTMUCH_STATUS_BAD_QUERY_SYNTAX, + NOTMUCH_PRIVATE_STATUS_CLOSED_DATABASE = NOTMUCH_STATUS_CLOSED_DATABASE, /* Then add our own private values. */ NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG = NOTMUCH_STATUS_LAST_STATUS, @@ -173,7 +176,7 @@ typedef enum _notmuch_private_status { (notmuch_status_t) private_status) /* Flags shared by various lookup functions. */ -typedef enum _notmuch_find_flags { +typedef enum { /* Lookup without creating any documents. This is the default * behavior. */ NOTMUCH_FIND_LOOKUP = 0, @@ -256,6 +259,10 @@ _notmuch_database_filename_to_direntry (void *ctx, notmuch_find_flags_t flags, char **direntry); +bool +_notmuch_database_indexable_as_text (notmuch_database_t *notmuch, + const char *mime_string); + /* directory.cc */ notmuch_directory_t * @@ -586,9 +593,6 @@ _notmuch_message_add_reply (notmuch_message_t *message, void _notmuch_message_remove_unprefixed_terms (notmuch_message_t *message); -const char * -_notmuch_message_get_thread_id_only (notmuch_message_t *message); - size_t _notmuch_message_get_thread_depth (notmuch_message_t *message); void @@ -711,9 +715,7 @@ _notmuch_thread_create (void *ctx, /* indexopts.c */ -struct _notmuch_indexopts { - _notmuch_crypto_t crypto; -}; +struct _notmuch_indexopts; #define CONFIG_HEADER_PREFIX "index.header." @@ -724,7 +726,7 @@ 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_config_load_from_file (notmuch_database_t *db, GKeyFile *file, char **status_string); notmuch_status_t _notmuch_config_load_defaults (notmuch_database_t *db); @@ -755,6 +757,12 @@ _notmuch_talloc_steal (const void *new_ctx, const T *ptr) #undef talloc_steal #define talloc_steal _notmuch_talloc_steal #endif + +#if __cplusplus >= 201703L || __cppcheck__ +#define NODISCARD [[nodiscard]] +#else +#define NODISCARD /**/ +#endif #endif #endif diff --git a/lib/notmuch.h b/lib/notmuch.h index 074fc682..937fa24e 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 5 +#define LIBNOTMUCH_MINOR_VERSION 6 #define LIBNOTMUCH_MICRO_VERSION 0 @@ -112,7 +112,7 @@ typedef int notmuch_bool_t; * A zero value (NOTMUCH_STATUS_SUCCESS) indicates that the function * completed without error. Any other value indicates an error. */ -typedef enum _notmuch_status { +typedef enum { /** * No error occurred. */ @@ -224,6 +224,14 @@ typedef enum _notmuch_status { * Syntax error in query */ NOTMUCH_STATUS_BAD_QUERY_SYNTAX, + /** + * No mail root could be deduced from parameters and environment + */ + NOTMUCH_STATUS_NO_MAIL_ROOT, + /** + * Database is not fully opened, or has been closed + */ + NOTMUCH_STATUS_CLOSED_DATABASE, /** * Not an actual status value. Just a way to find out how many * valid status values there are. @@ -285,6 +293,8 @@ typedef struct _notmuch_indexopts notmuch_indexopts_t; * * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory. * + * NOTMUCH_STATUS_PATH_ERROR: filename is too long + * * NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to create the * database file (such as permission denied, or file not found, * etc.), or the database already exists. @@ -323,7 +333,7 @@ typedef enum { * config_path="" and error_message=NULL * @deprecated Deprecated as of libnotmuch 5.4 (notmuch 0.32) */ -/* NOTMUCH_DEPRECATED(5, 4) */ +NOTMUCH_DEPRECATED(5, 4) notmuch_status_t notmuch_database_open (const char *path, notmuch_database_mode_t mode, @@ -335,7 +345,7 @@ notmuch_database_open (const char *path, * @deprecated Deprecated as of libnotmuch 5.4 (notmuch 0.32) * */ -/* NOTMUCH_DEPRECATED(5, 4) */ +NOTMUCH_DEPRECATED(5, 4) notmuch_status_t notmuch_database_open_verbose (const char *path, notmuch_database_mode_t mode, @@ -433,6 +443,8 @@ notmuch_database_open_verbose (const char *path, * @retval NOTMUCH_STATUS_NULL_POINTER: The given \a database * argument is NULL. * + * @retval NOTMUCH_STATUS_NO_CONFIG: No config file was found. Fatal. + * * @retval NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory. * * @retval NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the @@ -458,6 +470,9 @@ notmuch_database_open_with_config (const char *database_path, * * For description of arguments, @see notmuch_database_open_with_config * + * For errors other then NO_DATABASE and NO_CONFIG, *database is set to + * NULL. + * * @retval NOTMUCH_STATUS_SUCCESS: Successfully loaded configuration. * * @retval NOTMUCH_STATUS_NO_CONFIG: No config file was loaded. Not fatal. @@ -489,6 +504,9 @@ notmuch_database_load_config (const char *database_path, * * For description of arguments, @see notmuch_database_open_with_config * + * In case of any failure, this function returns an error status and + * sets *database to NULL. + * * @retval NOTMUCH_STATUS_SUCCESS: Successfully created the database. * * @retval NOTMUCH_STATUS_DATABASE_EXISTS: Database already exists, not created @@ -704,7 +722,8 @@ notmuch_database_end_atomic (notmuch_database_t *notmuch); * * The UUID is a NUL-terminated opaque string that uniquely identifies * this database. Two revision numbers are only comparable if they - * have the same database UUID. + * have the same database UUID. The string 'uuid' is owned by notmuch + * and should not be freed or modified by the user. */ unsigned long notmuch_database_get_revision (notmuch_database_t *notmuch, @@ -1154,7 +1173,10 @@ notmuch_query_search_threads_st (notmuch_query_t *query, notmuch_threads_t **out * * query = notmuch_query_create (database, query_string); * - * for (messages = notmuch_query_search_messages (query); + * if (notmuch_query_search_messages (query, &messages) != NOTMUCH_STATUS_SUCCESS) + * return EXIT_FAILURE; + * + * for (; * notmuch_messages_valid (messages); * notmuch_messages_move_to_next (messages)) * { @@ -1678,7 +1700,7 @@ notmuch_message_reindex (notmuch_message_t *message, /** * Message flags. */ -typedef enum _notmuch_message_flag { +typedef enum { NOTMUCH_MESSAGE_FLAG_MATCH, NOTMUCH_MESSAGE_FLAG_EXCLUDED, @@ -2250,6 +2272,9 @@ notmuch_message_properties_destroy (notmuch_message_properties_t *properties); * valid string. Whereas when this function returns FALSE, * notmuch_tags_get will return NULL. * + * It is acceptable to pass NULL for 'tags', in which case this + * function will always return FALSE. + * See the documentation of notmuch_message_get_tags for example code * showing how to iterate over a notmuch_tags_t object. */ @@ -2524,7 +2549,7 @@ notmuch_config_list_destroy (notmuch_config_list_t *config_list); /** * Configuration keys known to libnotmuch */ -typedef enum _notmuch_config_key { +typedef enum { NOTMUCH_CONFIG_FIRST, NOTMUCH_CONFIG_DATABASE_PATH = NOTMUCH_CONFIG_FIRST, NOTMUCH_CONFIG_MAIL_ROOT, @@ -2538,6 +2563,10 @@ typedef enum _notmuch_config_key { NOTMUCH_CONFIG_OTHER_EMAIL, NOTMUCH_CONFIG_USER_NAME, NOTMUCH_CONFIG_AUTOCOMMIT, + NOTMUCH_CONFIG_EXTRA_HEADERS, + NOTMUCH_CONFIG_INDEX_AS_TEXT, + NOTMUCH_CONFIG_AUTHORS_SEPARATOR, + NOTMUCH_CONFIG_AUTHORS_MATCHED_SEPARATOR, NOTMUCH_CONFIG_LAST } notmuch_config_key_t; diff --git a/lib/open.cc b/lib/open.cc index 8a835e98..463e38bf 100644 --- a/lib/open.cc +++ b/lib/open.cc @@ -3,6 +3,7 @@ #include "database-private.h" #include "parse-time-vrp.h" +#include "lastmod-fp.h" #include "path-util.h" #if HAVE_XAPIAN_DB_RETRY_LOCK @@ -19,9 +20,8 @@ notmuch_database_open (const char *path, char *status_string = NULL; notmuch_status_t status; - status = notmuch_database_open_verbose (path, mode, database, - &status_string); - + status = notmuch_database_open_with_config (path, mode, "", NULL, + database, &status_string); if (status_string) { fputs (status_string, stderr); free (status_string); @@ -187,36 +187,35 @@ _db_dir_exists (const char *database_path, char **message) } static notmuch_status_t -_choose_database_path (void *ctx, +_choose_database_path (notmuch_database_t *notmuch, const char *profile, GKeyFile *key_file, const char **database_path, - bool *split, char **message) { + notmuch_status_t status; + 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); + char *path = g_key_file_get_string (key_file, "database", "path", NULL); if (path) { if (path[0] == '/') - *database_path = talloc_strdup (ctx, path); + *database_path = talloc_strdup (notmuch, path); else - *database_path = talloc_asprintf (ctx, "%s/%s", getenv ("HOME"), path); + *database_path = talloc_asprintf (notmuch, "%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); + *database_path = _xdg_dir (notmuch, "XDG_DATA_HOME", ".local/share", profile); status = _db_dir_exists (*database_path, message); if (status) { *database_path = NULL; } else { - *split = true; + notmuch->params |= NOTMUCH_PARAM_SPLIT; } } @@ -225,9 +224,7 @@ _choose_database_path (void *ctx, } if (! *database_path) { - notmuch_status_t status; - - *database_path = talloc_asprintf (ctx, "%s/mail", getenv ("HOME")); + *database_path = talloc_asprintf (notmuch, "%s/mail", getenv ("HOME")); status = _db_dir_exists (*database_path, message); if (status) { *database_path = NULL; @@ -243,11 +240,76 @@ _choose_database_path (void *ctx, *message = strdup ("Error: Database path must be absolute.\n"); return NOTMUCH_STATUS_PATH_ERROR; } + + status = _db_dir_exists (*database_path, message); + if (status) { + IGNORE_RESULT (asprintf (message, + "Error: database path '%s' does not exist or is not a directory.\n", + *database_path)); + return NOTMUCH_STATUS_NO_DATABASE; + } + + if (*message) { + free (*message); + *message = NULL; + } + + return NOTMUCH_STATUS_SUCCESS; +} + +static notmuch_status_t +_mkdir (const char *path, char **message) +{ + if (g_mkdir_with_parents (path, 0755)) { + IGNORE_RESULT (asprintf (message, "Error: Cannot create directory %s: %s.\n", + path, strerror (errno))); + return NOTMUCH_STATUS_FILE_ERROR; + } + return NOTMUCH_STATUS_SUCCESS; +} + +static notmuch_status_t +_create_database_path (notmuch_database_t *notmuch, + const char *profile, + GKeyFile *key_file, + const char **database_path, + char **message) +{ + notmuch_status_t status; + + if (! *database_path) { + *database_path = getenv ("NOTMUCH_DATABASE"); + } + + if (! *database_path && key_file) { + char *path = g_key_file_get_string (key_file, "database", "path", NULL); + if (path) { + if (path[0] == '/') + *database_path = talloc_strdup (notmuch, path); + else + *database_path = talloc_asprintf (notmuch, "%s/%s", getenv ("HOME"), path); + g_free (path); + } + } + + if (! *database_path) { + *database_path = _xdg_dir (notmuch, "XDG_DATA_HOME", ".local/share", profile); + notmuch->params |= NOTMUCH_PARAM_SPLIT; + } + + if (*database_path[0] != '/') { + *message = strdup ("Error: Database path must be absolute.\n"); + return NOTMUCH_STATUS_PATH_ERROR; + } + + if ((status = _mkdir (*database_path, message))) + return status; + return NOTMUCH_STATUS_SUCCESS; } static notmuch_database_t * -_alloc_notmuch () +_alloc_notmuch (const char *database_path, const char *config_path, const char *profile) { notmuch_database_t *notmuch; @@ -263,6 +325,17 @@ _alloc_notmuch () notmuch->transaction_count = 0; notmuch->transaction_threshold = 0; notmuch->view = 1; + notmuch->index_as_text = NULL; + notmuch->index_as_text_length = 0; + + notmuch->params = NOTMUCH_PARAM_NONE; + if (database_path) + notmuch->params |= NOTMUCH_PARAM_DATABASE; + if (config_path) + notmuch->params |= NOTMUCH_PARAM_CONFIG; + if (profile) + notmuch->params |= NOTMUCH_PARAM_PROFILE; + return notmuch; } @@ -361,6 +434,53 @@ _load_database_state (notmuch_database_t *notmuch) notmuch, notmuch->xapian_db->get_uuid ().c_str ()); } +/* XXX This should really be done lazily, but the error reporting path in the indexing code + * would need to be redone to report any errors. + */ +notmuch_status_t +_ensure_index_as_text (notmuch_database_t *notmuch, char **message) +{ + int nregex = 0; + regex_t *regexv = NULL; + + if (notmuch->index_as_text) + return NOTMUCH_STATUS_SUCCESS; + + for (notmuch_config_values_t *list = notmuch_config_get_values (notmuch, + NOTMUCH_CONFIG_INDEX_AS_TEXT); + notmuch_config_values_valid (list); + notmuch_config_values_move_to_next (list)) { + regex_t *new_regex; + int rerr; + const char *str = notmuch_config_values_get (list); + size_t len = strlen (str); + + /* str must be non-empty, because n_c_get_values skips empty + * strings */ + assert (len > 0); + + regexv = talloc_realloc (notmuch, regexv, regex_t, nregex + 1); + new_regex = ®exv[nregex]; + + rerr = regcomp (new_regex, str, REG_EXTENDED | REG_NOSUB); + if (rerr) { + size_t error_size = regerror (rerr, new_regex, NULL, 0); + char *error = (char *) talloc_size (str, error_size); + + regerror (rerr, new_regex, error, error_size); + IGNORE_RESULT (asprintf (message, "Error in index.as_text: %s: %s\n", error, str)); + + return NOTMUCH_STATUS_ILLEGAL_ARGUMENT; + } + nregex++; + } + + notmuch->index_as_text = regexv; + notmuch->index_as_text_length = nregex; + + return NOTMUCH_STATUS_SUCCESS; +} + static notmuch_status_t _finish_open (notmuch_database_t *notmuch, const char *profile, @@ -396,8 +516,6 @@ _finish_open (notmuch_database_t *notmuch, " 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; } @@ -414,8 +532,6 @@ _finish_open (notmuch_database_t *notmuch, " 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; } @@ -428,8 +544,7 @@ _finish_open (notmuch_database_t *notmuch, 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->last_mod_range_processor = new LastModRangeProcessor (notmuch, "lastmod:"); notmuch->query_parser->set_default_op (Xapian::Query::OP_AND); notmuch->query_parser->set_database (*notmuch->xapian_db); notmuch->stemmer = new Xapian::Stem ("english"); @@ -445,7 +560,7 @@ _finish_open (notmuch_database_t *notmuch, goto DONE; if (key_file) - status = _notmuch_config_load_from_file (notmuch, key_file); + status = _notmuch_config_load_from_file (notmuch, key_file, &message); if (status) goto DONE; @@ -470,6 +585,10 @@ _finish_open (notmuch_database_t *notmuch, if (status) goto DONE; + status = _ensure_index_as_text (notmuch, &message); + if (status) + goto DONE; + autocommit_str = notmuch_config_get (notmuch, NOTMUCH_CONFIG_AUTOCOMMIT); if (unlikely (! autocommit_str)) { INTERNAL_ERROR ("missing configuration for autocommit"); @@ -489,8 +608,6 @@ _finish_open (notmuch_database_t *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: @@ -508,15 +625,13 @@ notmuch_database_open_with_config (const char *database_path, 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; _notmuch_init (); - notmuch = _alloc_notmuch (); + notmuch = _alloc_notmuch (database_path, config_path, profile); if (! notmuch) { status = NOTMUCH_STATUS_OUT_OF_MEMORY; goto DONE; @@ -528,8 +643,8 @@ notmuch_database_open_with_config (const char *database_path, goto DONE; } - if ((status = _choose_database_path (local, profile, key_file, - &database_path, &split, + if ((status = _choose_database_path (notmuch, profile, key_file, + &database_path, &message))) goto DONE; @@ -547,8 +662,6 @@ notmuch_database_open_with_config (const char *database_path, status = _finish_open (notmuch, profile, mode, key_file, &message); DONE: - talloc_free (local); - if (key_file) g_key_file_free (key_file); @@ -559,10 +672,13 @@ notmuch_database_open_with_config (const char *database_path, free (message); } + if (status && notmuch) { + notmuch_database_destroy (notmuch); + notmuch = NULL; + } + if (database) *database = notmuch; - else - talloc_free (notmuch); if (notmuch) notmuch->open = true; @@ -607,13 +723,10 @@ notmuch_database_create_with_config (const char *database_path, const char *notmuch_path = NULL; char *message = NULL; GKeyFile *key_file = NULL; - void *local = talloc_new (NULL); - int err; - bool split = false; _notmuch_init (); - notmuch = _alloc_notmuch (); + notmuch = _alloc_notmuch (database_path, config_path, profile); if (! notmuch) { status = NOTMUCH_STATUS_OUT_OF_MEMORY; goto DONE; @@ -625,48 +738,45 @@ notmuch_database_create_with_config (const char *database_path, 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) + status = _choose_database_path (notmuch, profile, key_file, + &database_path, &message); + switch (status) { + case NOTMUCH_STATUS_SUCCESS: + break; + case NOTMUCH_STATUS_NO_DATABASE: + if ((status = _create_database_path (notmuch, profile, key_file, + &database_path, &message))) + goto DONE; + break; + default: goto DONE; + } _set_database_path (notmuch, database_path); - if (key_file && ! split) { + if (key_file && ! (notmuch->params & NOTMUCH_PARAM_SPLIT)) { char *mail_root = notmuch_canonicalize_file_name ( - g_key_file_get_value (key_file, "database", "mail_root", NULL)); + g_key_file_get_string (key_file, "database", "mail_root", NULL)); char *db_path = notmuch_canonicalize_file_name (database_path); - split = (mail_root && (0 != strcmp (mail_root, db_path))); + if (mail_root && (0 != strcmp (mail_root, db_path))) + notmuch->params |= NOTMUCH_PARAM_SPLIT; free (mail_root); free (db_path); } - if (split) { + if (notmuch->params & NOTMUCH_PARAM_SPLIT) { notmuch_path = database_path; } else { - if (! (notmuch_path = talloc_asprintf (local, "%s/%s", database_path, ".notmuch"))) { + if (! (notmuch_path = talloc_asprintf (notmuch, "%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; - } + status = _mkdir (notmuch_path, &message); + if (status) goto DONE; - } } if (! (notmuch->xapian_path = talloc_asprintf (notmuch, "%s/%s", notmuch_path, "xapian"))) { @@ -706,8 +816,6 @@ notmuch_database_create_with_config (const char *database_path, } DONE: - talloc_free (local); - if (key_file) g_key_file_free (key_file); @@ -717,10 +825,16 @@ notmuch_database_create_with_config (const char *database_path, else free (message); } + if (status && notmuch) { + notmuch_database_destroy (notmuch); + notmuch = NULL; + } + if (database) *database = notmuch; - else - talloc_free (notmuch); + + if (notmuch) + notmuch->open = true; return status; } @@ -801,15 +915,13 @@ notmuch_database_load_config (const char *database_path, 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; _notmuch_init (); - notmuch = _alloc_notmuch (); + notmuch = _alloc_notmuch (database_path, config_path, profile); if (! notmuch) { status = NOTMUCH_STATUS_OUT_OF_MEMORY; goto DONE; @@ -827,8 +939,8 @@ notmuch_database_load_config (const char *database_path, goto DONE; } - status = _choose_database_path (local, profile, key_file, - &database_path, &split, &message); + status = _choose_database_path (notmuch, profile, key_file, + &database_path, &message); switch (status) { case NOTMUCH_STATUS_NO_DATABASE: case NOTMUCH_STATUS_SUCCESS: @@ -854,7 +966,7 @@ notmuch_database_load_config (const char *database_path, } if (key_file) { - status = _notmuch_config_load_from_file (notmuch, key_file); + status = _notmuch_config_load_from_file (notmuch, key_file, &message); if (status) goto DONE; } @@ -863,11 +975,16 @@ notmuch_database_load_config (const char *database_path, goto DONE; DONE: - talloc_free (local); - if (status_string) *status_string = message; + if (status && + status != NOTMUCH_STATUS_NO_DATABASE + && status != NOTMUCH_STATUS_NO_CONFIG) { + notmuch_database_destroy (notmuch); + notmuch = NULL; + } + if (database) *database = notmuch; diff --git a/lib/parse-sexp.cc b/lib/parse-sexp.cc index 356c32ea..930888e9 100644 --- a/lib/parse-sexp.cc +++ b/lib/parse-sexp.cc @@ -3,6 +3,7 @@ #if HAVE_SFSEXP #include "sexp.h" #include "unicode-util.h" +#include "xapian-extra.h" /* _sexp is used for file scope symbols to avoid clashing with * definitions from sexp.h */ @@ -32,81 +33,106 @@ typedef enum { SEXP_FLAG_EXPAND = 1 << 6, SEXP_FLAG_DO_EXPAND = 1 << 7, SEXP_FLAG_ORPHAN = 1 << 8, + SEXP_FLAG_RANGE = 1 << 9, + SEXP_FLAG_PATHNAME = 1 << 10, } _sexp_flag_t; /* * define bitwise operators to hide casts */ -inline _sexp_flag_t +static inline _sexp_flag_t operator| (_sexp_flag_t a, _sexp_flag_t b) { return static_cast<_sexp_flag_t>( static_cast(a) | static_cast(b)); } -inline _sexp_flag_t +static inline _sexp_flag_t operator& (_sexp_flag_t a, _sexp_flag_t b) { return static_cast<_sexp_flag_t>( static_cast(a) & static_cast(b)); } +typedef enum { + SEXP_INITIAL_MATCH_ALL, + SEXP_INITIAL_MATCH_NOTHING, +} _sexp_initial_t; + +static inline Xapian::Query +_sexp_initial_query (_sexp_initial_t initial) +{ + switch (initial) { + case SEXP_INITIAL_MATCH_ALL: + return xapian_query_match_all (); + case SEXP_INITIAL_MATCH_NOTHING: + return Xapian::Query::MatchNothing; + } + INTERNAL_ERROR ("invalid initial sexp value %d", initial); +} + typedef struct { const char *name; Xapian::Query::op xapian_op; - Xapian::Query initial; + _sexp_initial_t initial; _sexp_flag_t flags; } _sexp_prefix_t; static _sexp_prefix_t prefixes[] = { - { "and", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "and", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_NONE }, - { "attachment", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "attachment", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_EXPAND }, - { "body", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "body", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_FIELD }, - { "from", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "date", Xapian::Query::OP_INVALID, SEXP_INITIAL_MATCH_ALL, + SEXP_FLAG_RANGE }, + { "from", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, - { "folder", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, - SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, - { "id", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, + { "folder", Xapian::Query::OP_OR, SEXP_INITIAL_MATCH_NOTHING, + SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND | + SEXP_FLAG_PATHNAME }, + { "id", Xapian::Query::OP_OR, SEXP_INITIAL_MATCH_NOTHING, SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX }, - { "infix", Xapian::Query::OP_INVALID, Xapian::Query::MatchAll, + { "infix", Xapian::Query::OP_INVALID, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_SINGLE | SEXP_FLAG_ORPHAN }, - { "is", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "is", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, - { "matching", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "lastmod", Xapian::Query::OP_INVALID, SEXP_INITIAL_MATCH_ALL, + SEXP_FLAG_RANGE }, + { "matching", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_DO_EXPAND }, - { "mid", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, + { "mid", Xapian::Query::OP_OR, SEXP_INITIAL_MATCH_NOTHING, SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX }, - { "mimetype", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "mimetype", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_EXPAND }, - { "not", Xapian::Query::OP_AND_NOT, Xapian::Query::MatchAll, + { "not", Xapian::Query::OP_AND_NOT, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_NONE }, - { "of", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "of", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_DO_EXPAND }, - { "or", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, + { "or", Xapian::Query::OP_OR, SEXP_INITIAL_MATCH_NOTHING, SEXP_FLAG_NONE }, - { "path", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, - SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX }, - { "property", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "path", Xapian::Query::OP_OR, SEXP_INITIAL_MATCH_NOTHING, + SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | + SEXP_FLAG_PATHNAME }, + { "property", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, - { "query", Xapian::Query::OP_INVALID, Xapian::Query::MatchNothing, + { "query", Xapian::Query::OP_INVALID, SEXP_INITIAL_MATCH_NOTHING, SEXP_FLAG_SINGLE | SEXP_FLAG_ORPHAN }, - { "regex", Xapian::Query::OP_INVALID, Xapian::Query::MatchAll, + { "regex", Xapian::Query::OP_INVALID, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_SINGLE | SEXP_FLAG_DO_REGEX }, - { "rx", Xapian::Query::OP_INVALID, Xapian::Query::MatchAll, + { "rx", Xapian::Query::OP_INVALID, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_SINGLE | SEXP_FLAG_DO_REGEX }, - { "starts-with", Xapian::Query::OP_WILDCARD, Xapian::Query::MatchAll, + { "starts-with", Xapian::Query::OP_WILDCARD, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_SINGLE }, - { "subject", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "subject", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, - { "tag", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "tag", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, - { "thread", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, + { "thread", Xapian::Query::OP_OR, SEXP_INITIAL_MATCH_NOTHING, SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, - { "to", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + { "to", Xapian::Query::OP_AND, SEXP_INITIAL_MATCH_ALL, SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_EXPAND }, { } }; @@ -179,6 +205,55 @@ _sexp_parse_phrase (std::string term_prefix, const char *phrase, Xapian::Query & return NOTMUCH_STATUS_SUCCESS; } +static notmuch_status_t +resolve_binding (notmuch_database_t *notmuch, const _sexp_binding_t *env, const char *name, + const _sexp_binding_t **out) +{ + for (; env; env = env->next) { + if (strcmp (name, env->name) == 0) { + *out = env; + return NOTMUCH_STATUS_SUCCESS; + } + } + + _notmuch_database_log (notmuch, "undefined parameter '%s'\n", name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; +} + +static notmuch_status_t +_sexp_expand_term (notmuch_database_t *notmuch, + const _sexp_prefix_t *prefix, + const _sexp_binding_t *env, + const sexp_t *sx, + const char **out) +{ + notmuch_status_t status; + + if (! out) + return NOTMUCH_STATUS_NULL_POINTER; + + while (sx->ty == SEXP_VALUE && sx->aty == SEXP_BASIC && sx->val[0] == ',') { + const char *name = sx->val + 1; + const _sexp_binding_t *binding; + + status = resolve_binding (notmuch, env, name, &binding); + if (status) + return status; + + sx = binding->sx; + env = binding->context; + } + + if (sx->ty != SEXP_VALUE) { + _notmuch_database_log (notmuch, "'%s' expects single atom as argument\n", + prefix->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + *out = sx->val; + return NOTMUCH_STATUS_SUCCESS; +} + static notmuch_status_t _sexp_parse_wildcard (notmuch_database_t *notmuch, const _sexp_prefix_t *parent, @@ -219,8 +294,8 @@ _sexp_parse_one_term (notmuch_database_t *notmuch, std::string term_prefix, cons notmuch_status_t _sexp_parse_regex (notmuch_database_t *notmuch, const _sexp_prefix_t *prefix, const _sexp_prefix_t *parent, - unused(const _sexp_binding_t *env), - std::string val, Xapian::Query &output) + const _sexp_binding_t *env, + const sexp_t *term, Xapian::Query &output) { if (! parent) { _notmuch_database_log (notmuch, "illegal '%s' outside field\n", @@ -235,9 +310,15 @@ _sexp_parse_regex (notmuch_database_t *notmuch, } std::string msg; /* ignored */ + const char *str; + notmuch_status_t status; + + status = _sexp_expand_term (notmuch, prefix, env, term, &str); + if (status) + return status; return _notmuch_regexp_to_query (notmuch, Xapian::BAD_VALUENO, parent->name, - val, output, msg); + str, output, msg); } @@ -255,7 +336,8 @@ _sexp_expand_query (notmuch_database_t *notmuch, return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; } - status = _sexp_combine_query (notmuch, NULL, NULL, prefix->xapian_op, prefix->initial, sx, + status = _sexp_combine_query (notmuch, NULL, NULL, prefix->xapian_op, + _sexp_initial_query (prefix->initial), sx, subquery); if (status) return status; @@ -307,7 +389,8 @@ _sexp_parse_header (notmuch_database_t *notmuch, const _sexp_prefix_t *parent, parent = &user_prefix; - return _sexp_combine_query (notmuch, parent, env, Xapian::Query::OP_AND, Xapian::Query::MatchAll, + return _sexp_combine_query (notmuch, parent, env, Xapian::Query::OP_AND, + xapian_query_match_all (), sx->list->next, output); } @@ -436,13 +519,80 @@ _sexp_expand_param (notmuch_database_t *notmuch, const _sexp_prefix_t *parent, const _sexp_binding_t *env, const char *name, Xapian::Query &output) { - for (; env; env = env->next) { - if (strcmp (name, env->name) == 0) { - return _sexp_to_xapian_query (notmuch, parent, env->context, env->sx, - output); + notmuch_status_t status; + + const _sexp_binding_t *binding; + + status = resolve_binding (notmuch, env, name, &binding); + if (status) + return status; + + return _sexp_to_xapian_query (notmuch, parent, binding->context, binding->sx, + output); +} + +static notmuch_status_t +_sexp_parse_range (notmuch_database_t *notmuch, const _sexp_prefix_t *prefix, + const sexp_t *sx, Xapian::Query &output) +{ + const char *from, *to; + std::string msg; + + /* empty range matches everything */ + if (! sx) { + output = xapian_query_match_all (); + return NOTMUCH_STATUS_SUCCESS; + } + + if (sx->ty == SEXP_LIST) { + _notmuch_database_log (notmuch, "expected atom as first argument of '%s'\n", prefix->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + from = sx->val; + if (strcmp (from, "*") == 0) + from = ""; + + to = from; + + if (sx->next) { + if (sx->next->ty == SEXP_LIST) { + _notmuch_database_log (notmuch, "expected atom as second argument of '%s'\n", + prefix->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + if (sx->next->next) { + _notmuch_database_log (notmuch, "'%s' expects maximum of two arguments\n", prefix->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + to = sx->next->val; + if (strcmp (to, "*") == 0) + to = ""; + } + + if (strcmp (prefix->name, "date") == 0) { + notmuch_status_t status; + status = _notmuch_date_strings_to_query (NOTMUCH_VALUE_TIMESTAMP, from, to, output, msg); + if (status) { + if (! msg.empty ()) + _notmuch_database_log (notmuch, "%s\n", msg.c_str ()); } + return status; } - _notmuch_database_log (notmuch, "undefined parameter %s\n", name); + + if (strcmp (prefix->name, "lastmod") == 0) { + notmuch_status_t status; + status = _notmuch_lastmod_strings_to_query (notmuch, from, to, output, msg); + if (status) { + if (! msg.empty ()) + _notmuch_database_log (notmuch, "%s\n", msg.c_str ()); + } + return status; + } + + _notmuch_database_log (notmuch, "unimplimented range prefix: '%s'\n", prefix->name); return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; } @@ -467,8 +617,13 @@ _sexp_to_xapian_query (notmuch_database_t *notmuch, const _sexp_prefix_t *parent return _sexp_parse_wildcard (notmuch, parent, env, "", output); } + char *atom = sx->val; + + if (parent && parent->flags & SEXP_FLAG_PATHNAME) + strip_trailing (atom, '/'); + if (parent && (parent->flags & SEXP_FLAG_BOOLEAN)) { - output = Xapian::Query (term_prefix + sx->val); + output = Xapian::Query (term_prefix + atom); return NOTMUCH_STATUS_SUCCESS; } @@ -493,7 +648,7 @@ _sexp_to_xapian_query (notmuch_database_t *notmuch, const _sexp_prefix_t *parent /* Empty list */ if (! sx->list) { - output = Xapian::Query::MatchAll; + output = xapian_query_match_all (); return NOTMUCH_STATUS_SUCCESS; } @@ -519,7 +674,7 @@ _sexp_to_xapian_query (notmuch_database_t *notmuch, const _sexp_prefix_t *parent for (_sexp_prefix_t *prefix = prefixes; prefix && prefix->name; prefix++) { if (strcmp (prefix->name, sx->list->val) == 0) { - if (prefix->flags & SEXP_FLAG_FIELD) { + if (prefix->flags & (SEXP_FLAG_FIELD | SEXP_FLAG_RANGE)) { if (parent) { _notmuch_database_log (notmuch, "nested field: '%s' inside '%s'\n", prefix->name, parent->name); @@ -541,6 +696,9 @@ _sexp_to_xapian_query (notmuch_database_t *notmuch, const _sexp_prefix_t *parent return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; } + if (prefix->flags & SEXP_FLAG_RANGE) + return _sexp_parse_range (notmuch, prefix, sx->list->next, output); + if (strcmp (prefix->name, "infix") == 0) { return _sexp_parse_infix (notmuch, sx->list->next, output); } @@ -549,18 +707,25 @@ _sexp_to_xapian_query (notmuch_database_t *notmuch, const _sexp_prefix_t *parent return _notmuch_query_name_to_query (notmuch, sx->list->next->val, output); } - if (prefix->xapian_op == Xapian::Query::OP_WILDCARD) - return _sexp_parse_wildcard (notmuch, parent, env, sx->list->next->val, output); + if (prefix->xapian_op == Xapian::Query::OP_WILDCARD) { + const char *str; + status = _sexp_expand_term (notmuch, prefix, env, sx->list->next, &str); + if (status) + return status; + + return _sexp_parse_wildcard (notmuch, parent, env, str, output); + } if (prefix->flags & SEXP_FLAG_DO_REGEX) { - return _sexp_parse_regex (notmuch, prefix, parent, env, sx->list->next->val, output); + return _sexp_parse_regex (notmuch, prefix, parent, env, sx->list->next, output); } if (prefix->flags & SEXP_FLAG_DO_EXPAND) { return _sexp_expand_query (notmuch, prefix, parent, env, sx->list->next, output); } - return _sexp_combine_query (notmuch, parent, env, prefix->xapian_op, prefix->initial, + return _sexp_combine_query (notmuch, parent, env, prefix->xapian_op, + _sexp_initial_query (prefix->initial), sx->list->next, output); } } diff --git a/lib/parse-time-vrp.cc b/lib/parse-time-vrp.cc index 22bf2ab5..6b07970b 100644 --- a/lib/parse-time-vrp.cc +++ b/lib/parse-time-vrp.cc @@ -24,22 +24,28 @@ #include "parse-time-vrp.h" #include "parse-time-string.h" -Xapian::Query -ParseTimeRangeProcessor::operator() (const std::string &begin, const std::string &end) +notmuch_status_t +_notmuch_date_strings_to_query (Xapian::valueno slot, + const std::string &begin, const std::string &end, + Xapian::Query &output, std::string &msg) { double from = DBL_MIN, to = DBL_MAX; time_t parsed_time, now; std::string str; /* Use the same 'now' for begin and end. */ - if (time (&now) == (time_t) -1) - throw Xapian::QueryParserError ("unable to get current time"); + if (time (&now) == (time_t) -1) { + msg = "unable to get current time"; + return NOTMUCH_STATUS_ILLEGAL_ARGUMENT; + } 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 (parse_time_string (begin.c_str (), &parsed_time, &now, PARSE_TIME_ROUND_DOWN)) { + msg = "Didn't understand date specification '" + begin + "'"; + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + from = (double) parsed_time; } if (! end.empty ()) { @@ -48,15 +54,30 @@ ParseTimeRangeProcessor::operator() (const std::string &begin, const std::string else str = end; - if (parse_time_string (str.c_str (), &parsed_time, &now, PARSE_TIME_ROUND_UP_INCLUSIVE)) - throw Xapian::QueryParserError ("Didn't understand date specification '" + str + "'"); - else - to = (double) parsed_time; + if (parse_time_string (str.c_str (), &parsed_time, &now, PARSE_TIME_ROUND_UP_INCLUSIVE)) { + msg = "Didn't understand date specification '" + str + "'"; + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + to = (double) parsed_time; } - return Xapian::Query (Xapian::Query::OP_VALUE_RANGE, slot, - Xapian::sortable_serialise (from), - Xapian::sortable_serialise (to)); + output = Xapian::Query (Xapian::Query::OP_VALUE_RANGE, slot, + Xapian::sortable_serialise (from), + Xapian::sortable_serialise (to)); + return NOTMUCH_STATUS_SUCCESS; +} + +Xapian::Query +ParseTimeRangeProcessor::operator() (const std::string &begin, const std::string &end) +{ + + Xapian::Query output; + std::string msg; + + if (_notmuch_date_strings_to_query (slot, begin, end, output, msg)) + throw Xapian::QueryParserError (msg); + + return output; } /* XXX TODO: is throwing an exception the right thing to do here? */ diff --git a/lib/prefix.cc b/lib/prefix.cc index 0d92bdd7..06e2333a 100644 --- a/lib/prefix.cc +++ b/lib/prefix.cc @@ -3,6 +3,7 @@ #include "thread-fp.h" #include "regexp-fields.h" #include "parse-time-vrp.h" +#include "sexp-fp.h" typedef struct { const char *name; @@ -46,7 +47,7 @@ prefix_t prefix_table[] = { { "mid", "Q", NOTMUCH_FIELD_EXTERNAL | NOTMUCH_FIELD_PROCESSOR }, { "path", "P", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROCESSOR }, + NOTMUCH_FIELD_PROCESSOR | NOTMUCH_FIELD_STRIP_TRAILING_SLASH }, { "property", "XPROPERTY", NOTMUCH_FIELD_EXTERNAL }, /* * Unconditionally add ':' to reduce potential ambiguity with @@ -55,11 +56,13 @@ prefix_t prefix_table[] = { * discussion. */ { "folder", "XFOLDER:", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROCESSOR }, + NOTMUCH_FIELD_PROCESSOR | NOTMUCH_FIELD_STRIP_TRAILING_SLASH }, { "date", NULL, NOTMUCH_FIELD_EXTERNAL | NOTMUCH_FIELD_PROCESSOR }, { "query", NULL, NOTMUCH_FIELD_EXTERNAL | NOTMUCH_FIELD_PROCESSOR }, + { "sexp", NULL, NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROCESSOR }, { "from", "XFROM", NOTMUCH_FIELD_EXTERNAL | NOTMUCH_FIELD_PROBABILISTIC | NOTMUCH_FIELD_PROCESSOR }, @@ -138,6 +141,8 @@ _setup_query_field (const prefix_t *prefix, notmuch_database_t *notmuch) 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 if (STRNCMP_LITERAL (prefix->name, "sexp") == 0) + fp = (new SexpFieldProcessor (notmuch))->release (); else fp = (new RegexpFieldProcessor (prefix->name, prefix->flags, *notmuch->query_parser, notmuch))->release (); diff --git a/lib/query.cc b/lib/query.cc index b0937fcc..1c60c122 100644 --- a/lib/query.cc +++ b/lib/query.cc @@ -20,6 +20,7 @@ #include "notmuch-private.h" #include "database-private.h" +#include "xapian-extra.h" #include /* GHashTable, GPtrArray */ @@ -186,7 +187,7 @@ _notmuch_query_string_to_xapian_query (notmuch_database_t *notmuch, { try { if (query_string == "" || query_string == "*") { - output = Xapian::Query::MatchAll; + output = xapian_query_match_all (); } else { output = notmuch->query_parser-> @@ -227,6 +228,7 @@ _notmuch_query_ensure_parsed_xapian (notmuch_query_t *query) return NOTMUCH_STATUS_SUCCESS; } +#if HAVE_SFSEXP static notmuch_status_t _notmuch_query_ensure_parsed_sexpr (notmuch_query_t *query) { @@ -243,6 +245,7 @@ _notmuch_query_ensure_parsed_sexpr (notmuch_query_t *query) _notmuch_query_cache_terms (query); return NOTMUCH_STATUS_SUCCESS; } +#endif static notmuch_status_t _notmuch_query_ensure_parsed (notmuch_query_t *query) diff --git a/lib/regexp-fields.cc b/lib/regexp-fields.cc index c6d9d94f..3a775261 100644 --- a/lib/regexp-fields.cc +++ b/lib/regexp-fields.cc @@ -25,6 +25,7 @@ #include "regexp-fields.h" #include "notmuch-private.h" #include "database-private.h" +#include "xapian-extra.h" notmuch_status_t compile_regex (regex_t ®exp, const char *str, std::string &msg) @@ -200,7 +201,7 @@ RegexpFieldProcessor::operator() (const std::string & str) if (str.empty ()) { if (options & NOTMUCH_FIELD_PROBABILISTIC) { return Xapian::Query (Xapian::Query::OP_AND_NOT, - Xapian::Query::MatchAll, + xapian_query_match_all (), Xapian::Query (Xapian::Query::OP_WILDCARD, term_prefix)); } else { return Xapian::Query (term_prefix); @@ -227,7 +228,8 @@ RegexpFieldProcessor::operator() (const std::string & str) * phrase parsing, when possible */ std::string query_str; - if (*str.rbegin () != '*' || str.find (' ') != std::string::npos) + if ((str.at (0) != '(' || *str.rbegin () != ')') && + (*str.rbegin () != '*' || str.find (' ') != std::string::npos)) query_str = '"' + str + '"'; else query_str = str; @@ -235,7 +237,15 @@ RegexpFieldProcessor::operator() (const std::string & str) return parser.parse_query (query_str, NOTMUCH_QUERY_PARSER_FLAGS, term_prefix); } else { /* Boolean prefix */ - std::string term = term_prefix + str; + std::string query_str; + std::string term; + + if (str.length () > 1 && str.at (str.size () - 1) == '/') + query_str = str.substr (0, str.size () - 1); + else + query_str = str; + + term = term_prefix + query_str; return Xapian::Query (term); } } diff --git a/lib/sexp-fp.cc b/lib/sexp-fp.cc new file mode 100644 index 00000000..1fdf5225 --- /dev/null +++ b/lib/sexp-fp.cc @@ -0,0 +1,44 @@ +/* sexp-fp.cc - "sexp:" field processor glue + * + * This file is part of notmuch. + * + * Copyright © 2022 David Bremner + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/ . + * + * Author: David Bremner + */ + +#include "database-private.h" +#include "sexp-fp.h" +#include + +Xapian::Query +SexpFieldProcessor::operator() (const std::string & query_string) +{ + notmuch_status_t status; + Xapian::Query output; + +#if HAVE_SFSEXP + status = _notmuch_sexp_string_to_xapian_query (notmuch, query_string.c_str (), output); + if (status) { + throw Xapian::QueryParserError ("error parsing " + query_string); + } +#else + throw Xapian::QueryParserError ("sexp query parser not available"); +#endif + + return output; + +} diff --git a/lib/sexp-fp.h b/lib/sexp-fp.h new file mode 100644 index 00000000..341dfa7e --- /dev/null +++ b/lib/sexp-fp.h @@ -0,0 +1,41 @@ +/* sexp-fp.h - sexp field processor glue + * + * This file is part of notmuch. + * + * Copyright © 2022 David Bremner + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/ . + * + * Author: David Bremner + */ + +#ifndef NOTMUCH_SEXP_FP_H +#define NOTMUCH_SEXP_FP_H + +#include +#include "notmuch.h" + +class SexpFieldProcessor : public Xapian::FieldProcessor { +protected: + notmuch_database_t *notmuch; + +public: + SexpFieldProcessor (notmuch_database_t *notmuch_) : notmuch (notmuch_) + { + }; + + Xapian::Query operator() (const std::string & query_string); +}; + +#endif /* NOTMUCH_SEXP_FP_H */ diff --git a/lib/string-map.c b/lib/string-map.c index e3a81b4f..99bc2ea2 100644 --- a/lib/string-map.c +++ b/lib/string-map.c @@ -86,10 +86,14 @@ _notmuch_string_map_append (notmuch_string_map_t *map, static int cmppair (const void *pa, const void *pb) { + int cmp = 0; notmuch_string_pair_t *a = (notmuch_string_pair_t *) pa; notmuch_string_pair_t *b = (notmuch_string_pair_t *) pb; - return strcmp (a->key, b->key); + cmp = strcmp (a->key, b->key); + if (cmp == 0) + cmp = strcmp (a->value, b->value); + return cmp; } static void diff --git a/lib/tags.c b/lib/tags.c index c7d3f66f..ec5366ff 100644 --- a/lib/tags.c +++ b/lib/tags.c @@ -48,7 +48,7 @@ _notmuch_tags_create (const void *ctx, notmuch_string_list_t *list) notmuch_bool_t notmuch_tags_valid (notmuch_tags_t *tags) { - return tags->iterator != NULL; + return tags && (tags->iterator != NULL); } const char * diff --git a/lib/thread.cc b/lib/thread.cc index 60e9a666..168a9e8b 100644 --- a/lib/thread.cc +++ b/lib/thread.cc @@ -122,21 +122,28 @@ _thread_add_matched_author (notmuch_thread_t *thread, /* Construct an authors string from matched_authors_array and * authors_array. The string contains matched authors first, then - * non-matched authors (with the two groups separated by '|'). Within - * each group, authors are listed in date order. */ + * non-matched authors (with the two groups separated by '|' or the custom + * separator defined in the configuration). Within each group, authors are + * listed in date order and separated by ',' or the custom separator defined in + * the configuration. */ static void _resolve_thread_authors_string (notmuch_thread_t *thread) { unsigned int i; char *author; int first_non_matched_author = 1; + const char *authors_sep = notmuch_config_get (thread->notmuch, + NOTMUCH_CONFIG_AUTHORS_SEPARATOR); + const char *authors_matched_sep = notmuch_config_get (thread->notmuch, + NOTMUCH_CONFIG_AUTHORS_MATCHED_SEPARATOR); /* First, list all matched authors in date order. */ for (i = 0; i < thread->matched_authors_array->len; i++) { author = (char *) g_ptr_array_index (thread->matched_authors_array, i); if (thread->authors) - thread->authors = talloc_asprintf (thread, "%s, %s", + thread->authors = talloc_asprintf (thread, "%s%s%s", thread->authors, + authors_sep, author); else thread->authors = author; @@ -149,12 +156,14 @@ _resolve_thread_authors_string (notmuch_thread_t *thread) author, NULL, NULL)) continue; if (first_non_matched_author) { - thread->authors = talloc_asprintf (thread, "%s| %s", + thread->authors = talloc_asprintf (thread, "%s%s%s", thread->authors, + authors_matched_sep, author); } else { - thread->authors = talloc_asprintf (thread, "%s, %s", + thread->authors = talloc_asprintf (thread, "%s%s%s", thread->authors, + authors_sep, author); } diff --git a/mime-node.c b/mime-node.c index d29c4e48..1c5d619b 100644 --- a/mime-node.c +++ b/mime-node.c @@ -78,13 +78,14 @@ mime_node_get_message_crypto_status (mime_node_t *node) notmuch_status_t mime_node_open (const void *ctx, notmuch_message_t *message, + int duplicate, _notmuch_crypto_t *crypto, mime_node_t **root_out) { const char *filename = notmuch_message_get_filename (message); mime_node_context_t *mctx; mime_node_t *root; notmuch_status_t status; - int fd; + int fd = -1; root = talloc_zero (ctx, mime_node_t); if (root == NULL) { @@ -103,20 +104,33 @@ mime_node_open (const void *ctx, notmuch_message_t *message, talloc_set_destructor (mctx, _mime_node_context_free); /* Fast path */ - fd = open (filename, O_RDONLY); + if (duplicate <= 0) + fd = open (filename, O_RDONLY); if (fd == -1) { - /* Slow path - for some reason the first file in the list is - * not available anymore. This is clearly a problem in the + /* Slow path - Either we are trying to open a specific file, or + * for some reason the first file in the list is + * not available anymore. The latter is clearly a problem in the * database, but we are not going to let this problem be a * show stopper */ notmuch_filenames_t *filenames; + int i = 1; + for (filenames = notmuch_message_get_filenames (message); notmuch_filenames_valid (filenames); - notmuch_filenames_move_to_next (filenames)) { - filename = notmuch_filenames_get (filenames); - fd = open (filename, O_RDONLY); - if (fd != -1) - break; + notmuch_filenames_move_to_next (filenames), i++) { + if (i >= duplicate) { + filename = notmuch_filenames_get (filenames); + fd = open (filename, O_RDONLY); + if (fd != -1) { + break; + } else { + if (duplicate > 0) { + fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno)); + status = NOTMUCH_STATUS_FILE_ERROR; + goto DONE; + } + } + } } talloc_free (filenames); diff --git a/notmuch-client.h b/notmuch-client.h index 96d81166..1a87240d 100644 --- a/notmuch-client.h +++ b/notmuch-client.h @@ -65,7 +65,7 @@ struct sprinter; struct notmuch_show_params; typedef struct notmuch_show_format { - struct sprinter *(*new_sprinter)(const void *ctx, FILE *stream); + struct sprinter *(*new_sprinter)(notmuch_database_t * db, FILE *stream); notmuch_status_t (*part)(const void *ctx, struct sprinter *sprinter, struct mime_node *node, int indent, const struct notmuch_show_params *params); @@ -75,7 +75,10 @@ typedef struct notmuch_show_params { bool entire_thread; bool omit_excluded; bool output_body; + int duplicate; int part; + int offset; + int limit; _notmuch_crypto_t crypto; bool include_html; GMimeStream *out_stream; @@ -229,6 +232,7 @@ show_one_part (const char *filename, int part); void format_part_sprinter (const void *ctx, struct sprinter *sp, mime_node_t *node, + int duplicate, bool output_body, bool include_html); @@ -388,7 +392,8 @@ struct mime_node { }; /* Construct a new MIME node pointing to the root message part of - * message. If crypto->verify is true, signed child parts will be + * message. Use the duplicate-th filename if that parameter is + * positive. If crypto->verify is true, signed child parts will be * verified. If crypto->decrypt is NOTMUCH_DECRYPT_TRUE, encrypted * child parts will be decrypted using either stored session keys or * asymmetric crypto. If crypto->decrypt is NOTMUCH_DECRYPT_AUTO, @@ -406,6 +411,7 @@ struct mime_node { */ notmuch_status_t mime_node_open (const void *ctx, notmuch_message_t *message, + int duplicate, _notmuch_crypto_t *crypto, mime_node_t **node_out); /* Return a new MIME node for the requested child part of parent. @@ -426,13 +432,13 @@ mime_node_seek_dfs (mime_node_t *node, int n); const _notmuch_message_crypto_t * mime_node_get_message_crypto_status (mime_node_t *node); -typedef enum dump_formats { +typedef enum { DUMP_FORMAT_AUTO, DUMP_FORMAT_BATCH_TAG, DUMP_FORMAT_SUP } dump_format_t; -typedef enum dump_includes { +typedef enum { DUMP_INCLUDE_TAGS = 1, DUMP_INCLUDE_CONFIG = 2, DUMP_INCLUDE_PROPERTIES = 4 @@ -499,11 +505,10 @@ int notmuch_minimal_options (const char *subcommand_name, struct _notmuch_client_indexing_cli_choices { int decrypt_policy; bool decrypt_policy_set; - notmuch_indexopts_t *opts; }; extern struct _notmuch_client_indexing_cli_choices indexing_cli_choices; extern const notmuch_opt_desc_t notmuch_shared_indexing_options []; notmuch_status_t -notmuch_process_shared_indexing_options (notmuch_database_t *notmuch); +notmuch_process_shared_indexing_options (notmuch_indexopts_t *opts); #endif diff --git a/notmuch-config.c b/notmuch-config.c index db00a26c..f7e59f1e 100644 --- a/notmuch-config.c +++ b/notmuch-config.c @@ -40,11 +40,13 @@ static const struct config_group { "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" + " Supported values are 'mail_root' and 'path'. The recommended option\n" + " is to set 'mail_root' to the directory where your mail currently exists\n" + " and to where mail will be delivered in the future. Files should be\n" + " individual email messages. By default notmuch will store its database\n" + " in $XDG_DATA_HOME/notmuch; you can override this by setting 'path'.\n" + " If only 'path' is set, this directory is for 'mail_root' and for\n" + " the database location (in a subdirectory called \".notmuch\").\n" }, { "user", @@ -278,18 +280,24 @@ notmuch_conffile_open (notmuch_database_t *notmuch, return NULL; } - if (config->is_new) - g_key_file_set_comment (config->key_file, NULL, NULL, - toplevel_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); + if (config->is_new && (i == 0) ) { + const char *comment; + + comment = talloc_asprintf (config, "%s\n%s", + toplevel_config_comment, + group_comment_table[i].comment); + g_key_file_set_comment (config->key_file, name, NULL, comment, + NULL); + } else { + g_key_file_set_comment (config->key_file, name, NULL, + group_comment_table[i].comment, NULL); + } } } return config; @@ -383,7 +391,10 @@ _config_set_list (notmuch_conffile_t *config, const char *list[], size_t length) { - g_key_file_set_string_list (config->key_file, group, key, list, length); + if (length > 1) + g_key_file_set_string_list (config->key_file, group, key, list, length); + else + g_key_file_set_string (config->key_file, group, key, list[0]); } void @@ -680,9 +691,9 @@ _notmuch_config_list_built_with () printf ("%sretry_lock=%s\n", BUILT_WITH_PREFIX, notmuch_built_with ("retry_lock") ? "true" : "false"); - printf ("%ssexpr_query=%s\n", + printf ("%ssexp_queries=%s\n", BUILT_WITH_PREFIX, - notmuch_built_with ("sexpr_query") ? "true" : "false"); + notmuch_built_with ("sexp_queries") ? "true" : "false"); } static int diff --git a/notmuch-git.py b/notmuch-git.py new file mode 100644 index 00000000..ee87bec6 --- /dev/null +++ b/notmuch-git.py @@ -0,0 +1,1250 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2011-2014 David Bremner +# W. Trevor King +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see https://www.gnu.org/licenses/ . + +""" +Manage notmuch tags with Git +""" + +from __future__ import print_function +from __future__ import unicode_literals + +import codecs as _codecs +import collections as _collections +import functools as _functools +import inspect as _inspect +import locale as _locale +import logging as _logging +import os as _os +import re as _re +import subprocess as _subprocess +import sys as _sys +import tempfile as _tempfile +import textwrap as _textwrap +from urllib.parse import quote as _quote +from urllib.parse import unquote as _unquote +import json as _json + +_LOG = _logging.getLogger('notmuch-git') +_LOG.setLevel(_logging.WARNING) +_LOG.addHandler(_logging.StreamHandler()) + +NOTMUCH_GIT_DIR = None +TAG_PREFIX = None +FORMAT_VERSION = 1 + +_HEX_ESCAPE_REGEX = _re.compile('%[0-9A-F]{2}') +_TAG_DIRECTORY = 'tags/' +_TAG_FILE_REGEX = ( _re.compile(_TAG_DIRECTORY + '(?P[^/]*)/(?P[^/]*)'), + _re.compile(_TAG_DIRECTORY + '([0-9a-f]{2}/){2}(?P[^/]*)/(?P[^/]*)')) + +# magic hash for Git (git hash-object -t blob /dev/null) +_EMPTYBLOB = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391' + +def _hex_quote(string, safe='+@=:,'): + """ + quote('abc def') -> 'abc%20def'. + + Wrap urllib.parse.quote with additional safe characters (in + addition to letters, digits, and '_.-') and lowercase hex digits + (e.g. '%3a' instead of '%3A'). + """ + uppercase_escapes = _quote(string, safe) + return _HEX_ESCAPE_REGEX.sub( + lambda match: match.group(0).lower(), + uppercase_escapes) + +def _xapian_quote(string): + """ + Quote a string for Xapian's QueryParser. + + Xapian uses double-quotes for quoting strings. You can escape + internal quotes by repeating them [1,2,3]. + + [1]: https://trac.xapian.org/ticket/128#comment:2 + [2]: https://trac.xapian.org/ticket/128#comment:17 + [3]: https://trac.xapian.org/changeset/13823/svn + """ + return '"{0}"'.format(string.replace('"', '""')) + + +def _xapian_unquote(string): + """ + Unquote a Xapian-quoted string. + """ + if string.startswith('"') and string.endswith('"'): + return string[1:-1].replace('""', '"') + return string + + +def timed(fn): + """Timer decorator""" + from time import perf_counter + + def inner(*args, **kwargs): + start_time = perf_counter() + rval = fn(*args, **kwargs) + end_time = perf_counter() + _LOG.info('{0}: {1:.8f}s elapsed'.format(fn.__name__, end_time - start_time)) + return rval + + return inner + + +class SubprocessError(RuntimeError): + "A subprocess exited with a nonzero status" + def __init__(self, args, status, stdout=None, stderr=None): + self.status = status + self.stdout = stdout + self.stderr = stderr + msg = '{args} exited with {status}'.format(args=args, status=status) + if stderr: + msg = '{msg}: {stderr}'.format(msg=msg, stderr=stderr) + super(SubprocessError, self).__init__(msg) + + +class _SubprocessContextManager(object): + """ + PEP 343 context manager for subprocesses. + + 'expect' holds a tuple of acceptable exit codes, otherwise we'll + raise a SubprocessError in __exit__. + """ + def __init__(self, process, args, expect=(0,)): + self._process = process + self._args = args + self._expect = expect + + def __enter__(self): + return self._process + + def __exit__(self, type, value, traceback): + for name in ['stdin', 'stdout', 'stderr']: + stream = getattr(self._process, name) + if stream: + stream.close() + setattr(self._process, name, None) + status = self._process.wait() + _LOG.debug( + 'collect {args} with status {status} (expected {expect})'.format( + args=self._args, status=status, expect=self._expect)) + if status not in self._expect: + raise SubprocessError(args=self._args, status=status) + + def wait(self): + return self._process.wait() + + +def _spawn(args, input=None, additional_env=None, wait=False, stdin=None, + stdout=None, stderr=None, encoding=_locale.getpreferredencoding(), + expect=(0,), **kwargs): + """Spawn a subprocess, and optionally wait for it to finish. + + This wrapper around subprocess.Popen has two modes, depending on + the truthiness of 'wait'. If 'wait' is true, we use p.communicate + internally to write 'input' to the subprocess's stdin and read + from it's stdout/stderr. If 'wait' is False, we return a + _SubprocessContextManager instance for fancier handling + (e.g. piping between processes). + + For 'wait' calls when you want to write to the subprocess's stdin, + you only need to set 'input' to your content. When 'input' is not + None but 'stdin' is, we'll automatically set 'stdin' to PIPE + before calling Popen. This avoids having the subprocess + accidentally inherit the launching process's stdin. + """ + _LOG.debug('spawn {args} (additional env. var.: {env})'.format( + args=args, env=additional_env)) + if not stdin and input is not None: + stdin = _subprocess.PIPE + if additional_env: + if not kwargs.get('env'): + kwargs['env'] = dict(_os.environ) + kwargs['env'].update(additional_env) + p = _subprocess.Popen( + args, stdin=stdin, stdout=stdout, stderr=stderr, **kwargs) + if wait: + if hasattr(input, 'encode'): + input = input.encode(encoding) + (stdout, stderr) = p.communicate(input=input) + status = p.wait() + _LOG.debug( + 'collect {args} with status {status} (expected {expect})'.format( + args=args, status=status, expect=expect)) + if stdout is not None: + stdout = stdout.decode(encoding) + if stderr is not None: + stderr = stderr.decode(encoding) + if status not in expect: + raise SubprocessError( + args=args, status=status, stdout=stdout, stderr=stderr) + return (status, stdout, stderr) + if p.stdin and not stdin: + p.stdin.close() + p.stdin = None + if p.stdin: + p.stdin = _codecs.getwriter(encoding=encoding)(stream=p.stdin) + stream_reader = _codecs.getreader(encoding=encoding) + if p.stdout: + p.stdout = stream_reader(stream=p.stdout) + if p.stderr: + p.stderr = stream_reader(stream=p.stderr) + return _SubprocessContextManager(args=args, process=p, expect=expect) + + +def _git(args, **kwargs): + args = ['git', '--git-dir', NOTMUCH_GIT_DIR] + list(args) + return _spawn(args=args, **kwargs) + + +def _get_current_branch(): + """Get the name of the current branch. + + Return 'None' if we're not on a branch. + """ + try: + (status, branch, stderr) = _git( + args=['symbolic-ref', '--short', 'HEAD'], + stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, wait=True) + except SubprocessError as e: + if 'not a symbolic ref' in e: + return None + raise + return branch.strip() + + +def _get_remote(): + "Get the default remote for the current branch." + local_branch = _get_current_branch() + (status, remote, stderr) = _git( + args=['config', 'branch.{0}.remote'.format(local_branch)], + stdout=_subprocess.PIPE, wait=True) + return remote.strip() + +def _tag_query(prefix=None): + if prefix is None: + prefix = TAG_PREFIX + return '(tag (starts-with "{:s}"))'.format(prefix.replace('"','\\\"')) + +def count_messages(prefix=None): + "count messages with a given prefix." + (status, stdout, stderr) = _spawn( + args=['notmuch', 'count', '--query=sexp', _tag_query(prefix)], + stdout=_subprocess.PIPE, wait=True) + if status != 0: + _LOG.error("failed to run notmuch config") + _sys.exit(1) + return int(stdout.rstrip()) + +def get_tags(prefix=None): + "Get a list of tags with a given prefix." + (status, stdout, stderr) = _spawn( + args=['notmuch', 'search', '--exclude=false', '--query=sexp', '--output=tags', _tag_query(prefix)], + stdout=_subprocess.PIPE, wait=True) + return [tag for tag in stdout.splitlines()] + +def archive(treeish='HEAD', args=()): + """ + Dump a tar archive of the current notmuch-git tag set. + + Using 'git archive'. + + Each tag $tag for message with Message-Id $id is written to + an empty file + + tags/hash1(id)/hash2(id)/encode($id)/encode($tag) + + The encoding preserves alphanumerics, and the characters + "+-_@=.:," (not the quotes). All other octets are replaced with + '%' followed by a two digit hex number. + """ + _git(args=['archive', treeish] + list(args), wait=True) + + +def clone(repository): + """ + Create a local notmuch-git repository from a remote source. + + This wraps 'git clone', adding some options to avoid creating a + working tree while preserving remote-tracking branches and + upstreams. + """ + with _tempfile.TemporaryDirectory(prefix='notmuch-git-clone.') as workdir: + _spawn( + args=[ + 'git', 'clone', '--no-checkout', '--separate-git-dir', NOTMUCH_GIT_DIR, + repository, workdir], + wait=True) + _git(args=['config', '--unset', 'core.worktree'], wait=True, expect=(0, 5)) + _git(args=['config', 'core.bare', 'true'], wait=True) + (status, stdout, stderr) = _git(args=['show-ref', '--verify', + '--quiet', + 'refs/remotes/origin/config'], + expect=(0,1), + wait=True) + if status == 0: + _git(args=['branch', 'config', 'origin/config'], wait=True) + existing_tags = get_tags() + if existing_tags: + _LOG.warning( + 'Not checking out to avoid clobbering existing tags: {}'.format( + ', '.join(existing_tags))) + else: + checkout() + + +def _is_committed(status): + return len(status['added']) + len(status['deleted']) == 0 + + +class CachedIndex: + def __init__(self, repo, treeish): + self.cache_path = _os.path.join(repo, 'notmuch', 'index_cache.json') + self.index_path = _os.path.join(repo, 'index') + self.current_treeish = treeish + # cached values + self.treeish = None + self.hash = None + self.index_checksum = None + + self._load_cache_file() + + def _load_cache_file(self): + try: + with open(self.cache_path) as f: + data = _json.load(f) + self.treeish = data['treeish'] + self.hash = data['hash'] + self.index_checksum = data['index_checksum'] + except FileNotFoundError: + pass + except _json.JSONDecodeError: + _LOG.error("Error decoding cache") + _sys.exit(1) + + def __enter__(self): + self.read_tree() + return self + + def __exit__(self, type, value, traceback): + checksum = _read_index_checksum(self.index_path) + (_, hash, _) = _git( + args=['rev-parse', self.current_treeish], + stdout=_subprocess.PIPE, + wait=True) + + with open(self.cache_path, "w") as f: + _json.dump({'treeish': self.current_treeish, + 'hash': hash.rstrip(), 'index_checksum': checksum }, f) + + @timed + def read_tree(self): + current_checksum = _read_index_checksum(self.index_path) + (_, hash, _) = _git( + args=['rev-parse', self.current_treeish], + stdout=_subprocess.PIPE, + wait=True) + current_hash = hash.rstrip() + + if self.current_treeish == self.treeish and \ + self.index_checksum and self.index_checksum == current_checksum and \ + self.hash and self.hash == current_hash: + return + + _git(args=['read-tree', self.current_treeish], wait=True) + + +def _check_fraction(change): + safe = 0.1 + conf = _notmuch_config_get ('git.safe_fraction') + if conf and conf != '': + safe=float(conf) + + total = count_messages (TAG_PREFIX) + if total == 0: + _LOG.error('No existing tags with given prefix, stopping.') + _LOG.error('Use --force to override.') + exit(1) + + fraction = change/total + _LOG.debug('total messages {:d}, change: {:d}, fraction: {:f}'.format(total,change,fraction)) + if fraction > safe: + _LOG.error('safe fraction {:f} exceeded, stopping.'.format(safe)) + _LOG.error('Use --force to override or reconfigure git.safe_fraction.') + exit(1) + +def check_safe_fraction(status): + + change = len(status['added'])+len(status['deleted']) + _check_fraction(change) + +def check_diff_fraction(): + + # check number of directories (i.e. messages) changed. + change_set = set() + + with _git(args=['diff', '--name-only', 'HEAD', '@{upstream}'], + stdout=_subprocess.PIPE) as git: + for path in git.stdout: + change_set.add(_os.path.dirname(path)) + + change=len(change_set) + _check_fraction(change) + + +def commit(treeish='HEAD', message=None, force=False): + """ + Commit prefix-matching tags from the notmuch database to Git. + """ + + status = get_status() + + if _is_committed(status=status): + _LOG.warning('Nothing to commit') + return + + if not force: + check_safe_fraction (status) + + with CachedIndex(NOTMUCH_GIT_DIR, treeish) as index: + try: + _update_index(status=status) + (_, tree, _) = _git( + args=['write-tree'], + stdout=_subprocess.PIPE, + wait=True) + (_, parent, _) = _git( + args=['rev-parse', treeish], + stdout=_subprocess.PIPE, + wait=True) + (_, commit, _) = _git( + args=['commit-tree', tree.strip(), '-p', parent.strip()], + input=message, + stdout=_subprocess.PIPE, + wait=True) + _git( + args=['update-ref', treeish, commit.strip()], + stdout=_subprocess.PIPE, + wait=True) + except Exception as e: + _git(args=['read-tree', '--empty'], wait=True) + _git(args=['read-tree', treeish], wait=True) + raise + +@timed +def _update_index(status): + with _git( + args=['update-index', '--index-info'], + stdin=_subprocess.PIPE) as p: + for id, tags in status['deleted'].items(): + for line in _index_tags_for_message(id=id, status='D', tags=tags): + p.stdin.write(line) + for id, tags in status['added'].items(): + for line in _index_tags_for_message(id=id, status='A', tags=tags): + p.stdin.write(line) + + +def fetch(remote=None): + """ + Fetch changes from the remote repository. + + See 'merge' to bring those changes into notmuch. + """ + args = ['fetch'] + if remote: + args.append(remote) + _git(args=args, wait=True) + + +def init(remote=None,format_version=None): + """ + Create an empty notmuch-git repository. + + This wraps 'git init' with a few extra steps to support subsequent + status and commit commands. + """ + from pathlib import Path + parent = Path(NOTMUCH_GIT_DIR).parent + try: + _os.makedirs(parent) + except FileExistsError: + pass + + if not format_version: + format_version = 1 + + format_version=int(format_version) + + if format_version > 1 or format_version < 0: + _LOG.error("Illegal format version {:d}".format(format_version)) + _sys.exit(1) + + _spawn(args=['git', '--git-dir', NOTMUCH_GIT_DIR, 'init', + '--initial-branch=master', '--quiet', '--bare'], wait=True) + _git(args=['config', 'core.logallrefupdates', 'true'], wait=True) + # create an empty blob (e69de29bb2d1d6434b8b29ae775ad8c2e48c5391) + _git(args=['hash-object', '-w', '--stdin'], input='', wait=True) + allow_empty=('--allow-empty',) + if format_version >= 1: + allow_empty=() + # create a blob for the FORMAT file + (status, stdout, _) = _git(args=['hash-object', '-w', '--stdin'], stdout=_subprocess.PIPE, + input='{:d}\n'.format(format_version), wait=True) + verhash=stdout.rstrip() + _LOG.debug('hash of FORMAT blob = {:s}'.format(verhash)) + # Add FORMAT to the index + _git(args=['update-index', '--add', '--cacheinfo', '100644,{:s},FORMAT'.format(verhash)], wait=True) + + _git( + args=[ + 'commit', *allow_empty, '-m', 'Start a new notmuch-git repository' + ], + additional_env={'GIT_WORK_TREE': NOTMUCH_GIT_DIR}, + wait=True) + + +def checkout(force=None): + """ + Update the notmuch database from Git. + + This is mainly useful to discard your changes in notmuch relative + to Git. + """ + status = get_status() + + if not force: + check_safe_fraction(status) + + with _spawn( + args=['notmuch', 'tag', '--batch'], stdin=_subprocess.PIPE) as p: + for id, tags in status['added'].items(): + p.stdin.write(_batch_line(action='-', id=id, tags=tags)) + for id, tags in status['deleted'].items(): + p.stdin.write(_batch_line(action='+', id=id, tags=tags)) + + +def _batch_line(action, id, tags): + """ + 'notmuch tag --batch' line for adding/removing tags. + + Set 'action' to '-' to remove a tag or '+' to add the tags to a + given message id. + """ + tag_string = ' '.join( + '{action}{prefix}{tag}'.format( + action=action, prefix=_ENCODED_TAG_PREFIX, tag=_hex_quote(tag)) + for tag in tags) + line = '{tags} -- id:{id}\n'.format( + tags=tag_string, id=_xapian_quote(string=id)) + return line + + +def _insist_committed(): + "Die if the the notmuch tags don't match the current HEAD." + status = get_status() + if not _is_committed(status=status): + _LOG.error('\n'.join([ + 'Uncommitted changes to {prefix}* tags in notmuch', + '', + "For a summary of changes, run 'notmuch-git status'", + "To save your changes, run 'notmuch-git commit' before merging/pull", + "To discard your changes, run 'notmuch-git checkout'", + ]).format(prefix=TAG_PREFIX)) + _sys.exit(1) + + +def pull(repository=None, refspecs=None): + """ + Pull (merge) remote repository changes to notmuch. + + 'pull' is equivalent to 'fetch' followed by 'merge'. We use the + Git-configured repository for your current branch + (branch..repository, likely 'origin', and + branch..merge, likely 'master'). + """ + _insist_committed() + if refspecs and not repository: + repository = _get_remote() + args = ['pull'] + if repository: + args.append(repository) + if refspecs: + args.extend(refspecs) + with _tempfile.TemporaryDirectory(prefix='notmuch-git-pull.') as workdir: + for command in [ + ['reset', '--hard'], + args]: + _git( + args=command, + additional_env={'GIT_WORK_TREE': workdir}, + wait=True) + checkout() + + +def merge(reference='@{upstream}'): + """ + Merge changes from 'reference' into HEAD and load the result into notmuch. + + The default reference is '@{upstream}'. + """ + _insist_committed() + with _tempfile.TemporaryDirectory(prefix='notmuch-git-merge.') as workdir: + for command in [ + ['reset', '--hard'], + ['merge', reference]]: + _git( + args=command, + additional_env={'GIT_WORK_TREE': workdir}, + wait=True) + checkout() + + +def log(args=()): + """ + A simple wrapper for 'git log'. + + After running 'notmuch-git fetch', you can inspect the changes with + 'notmuch-git log HEAD..@{upstream}'. + """ + # we don't want output trapping here, because we want the pager. + args = ['log', '--name-status', '--no-renames'] + list(args) + with _git(args=args, expect=(0, 1, -13)) as p: + p.wait() + + +def push(repository=None, refspecs=None): + "Push the local notmuch-git Git state to a remote repository." + if refspecs and not repository: + repository = _get_remote() + args = ['push'] + if repository: + args.append(repository) + if refspecs: + args.extend(refspecs) + _git(args=args, wait=True) + + +def reset(force=False): + """ + reset the local git branch to match the remote one + """ + if not force: + check_diff_fraction() + + _git(args=["reset","--soft","origin/master"],wait=True) + +def status(): + """ + Show pending updates in notmuch or git repo. + + Prints lines of the form + + ng Message-Id tag + + where n is a single character representing notmuch database status + + * A + + Tag is present in notmuch database, but not committed to notmuch-git + (equivalently, tag has been deleted in notmuch-git repo, e.g. by a + pull, but not restored to notmuch database). + + * D + + Tag is present in notmuch-git repo, but not restored to notmuch + database (equivalently, tag has been deleted in notmuch). + + * U + + Message is unknown (missing from local notmuch database). + + The second character (if present) represents a difference between + local and upstream branches. Typically 'notmuch-git fetch' needs to be + run to update this. + + * a + + Tag is present in upstream, but not in the local Git branch. + + * d + + Tag is present in local Git branch, but not upstream. + """ + status = get_status() + # 'output' is a nested defaultdict for message status: + # * The outer dict is keyed by message id. + # * The inner dict is keyed by tag name. + # * The inner dict values are status strings (' a', 'Dd', ...). + output = _collections.defaultdict( + lambda : _collections.defaultdict(lambda : ' ')) + for id, tags in status['added'].items(): + for tag in tags: + output[id][tag] = 'A' + for id, tags in status['deleted'].items(): + for tag in tags: + output[id][tag] = 'D' + for id, tags in status['missing'].items(): + for tag in tags: + output[id][tag] = 'U' + if _is_unmerged(): + for id, tag in _diff_refs(filter='A'): + output[id][tag] += 'a' + for id, tag in _diff_refs(filter='D'): + output[id][tag] += 'd' + for id, tag_status in sorted(output.items()): + for tag, status in sorted(tag_status.items()): + print('{status}\t{id}\t{tag}'.format( + status=status, id=id, tag=tag)) + + +def _is_unmerged(ref='@{upstream}'): + try: + (status, fetch_head, stderr) = _git( + args=['rev-parse', ref], + stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, wait=True) + except SubprocessError as e: + if 'No upstream configured' in e.stderr: + return + raise + (status, base, stderr) = _git( + args=['merge-base', 'HEAD', ref], + stdout=_subprocess.PIPE, wait=True) + return base != fetch_head + +class DatabaseCache: + def __init__(self): + try: + from notmuch2 import Database + self._notmuch = Database() + except ImportError: + self._notmuch = None + self._known = {} + + def known(self,id): + if id in self._known: + return self._known[id]; + + if self._notmuch: + try: + _ = self._notmuch.find(id) + self._known[id] = True + except LookupError: + self._known[id] = False + else: + (_, stdout, stderr) = _spawn( + args=['notmuch', 'search', '--exclude=false', '--output=files', 'id:{0}'.format(id)], + stdout=_subprocess.PIPE, + wait=True) + self._known[id] = stdout != None + return self._known[id] + +@timed +def get_status(): + status = { + 'deleted': {}, + 'missing': {}, + } + db = DatabaseCache() + with PrivateIndex(repo=NOTMUCH_GIT_DIR, prefix=TAG_PREFIX) as index: + maybe_deleted = index.diff(filter='D') + for id, tags in maybe_deleted.items(): + if db.known(id): + status['deleted'][id] = tags + else: + status['missing'][id] = tags + status['added'] = index.diff(filter='A') + + return status + +class PrivateIndex: + def __init__(self, repo, prefix): + try: + _os.makedirs(_os.path.join(repo, 'notmuch')) + except FileExistsError: + pass + + file_name = 'notmuch/index' + self.index_path = _os.path.join(repo, file_name) + self.cache_path = _os.path.join(repo, 'notmuch', '{:s}.json'.format(_hex_quote(file_name))) + + self.current_prefix = prefix + + self.prefix = None + self.uuid = None + self.lastmod = None + self.checksum = None + self._load_cache_file() + self.file_tree = None + self._index_tags() + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + checksum = _read_index_checksum(self.index_path) + (count, uuid, lastmod) = _read_database_lastmod() + with open(self.cache_path, "w") as f: + _json.dump({'prefix': self.current_prefix, 'uuid': uuid, 'lastmod': lastmod, 'checksum': checksum }, f) + + def _load_cache_file(self): + try: + with open(self.cache_path) as f: + data = _json.load(f) + self.prefix = data['prefix'] + self.uuid = data['uuid'] + self.lastmod = data['lastmod'] + self.checksum = data['checksum'] + except FileNotFoundError: + return None + except _json.JSONDecodeError: + _LOG.error("Error decoding cache") + _sys.exit(1) + + @timed + def _read_file_tree(self): + self.file_tree = {} + + with _git( + args=['ls-files', 'tags'], + additional_env={'GIT_INDEX_FILE': self.index_path}, + stdout=_subprocess.PIPE) as git: + for file in git.stdout: + dir=_os.path.dirname(file) + tag=_os.path.basename(file).rstrip() + if dir not in self.file_tree: + self.file_tree[dir]=[tag] + else: + self.file_tree[dir].append(tag) + + + def _clear_tags_for_message(self, id): + """ + Clear any existing index entries for message 'id' + + Neither 'id' nor the tags in 'tags' should be encoded/escaped. + """ + + if self.file_tree == None: + self._read_file_tree() + + dir = _id_path(id) + + if dir not in self.file_tree: + return + + for file in self.file_tree[dir]: + line = '0 0000000000000000000000000000000000000000\t{:s}/{:s}\n'.format(dir,file) + yield line + + + @timed + def _index_tags(self): + "Write notmuch tags to private git index." + prefix = '+{0}'.format(_ENCODED_TAG_PREFIX) + current_checksum = _read_index_checksum(self.index_path) + if (self.prefix == None or self.prefix != self.current_prefix + or self.checksum == None or self.checksum != current_checksum): + _git( + args=['read-tree', '--empty'], + additional_env={'GIT_INDEX_FILE': self.index_path}, wait=True) + + query = _tag_query() + clear_tags = False + (count,uuid,lastmod) = _read_database_lastmod() + if self.prefix == self.current_prefix and self.uuid \ + and self.uuid == uuid and self.checksum == current_checksum: + query = '(and (infix "lastmod:{:d}..")) {:s})'.format(self.lastmod+1, query) + clear_tags = True + with _spawn( + args=['notmuch', 'dump', '--format=batch-tag', '--query=sexp', '--', query], + stdout=_subprocess.PIPE) as notmuch: + with _git( + args=['update-index', '--index-info'], + stdin=_subprocess.PIPE, + additional_env={'GIT_INDEX_FILE': self.index_path}) as git: + for line in notmuch.stdout: + if line.strip().startswith('#'): + continue + (tags_string, id) = [_.strip() for _ in line.split(' -- id:')] + tags = [ + _unquote(tag[len(prefix):]) + for tag in tags_string.split() + if tag.startswith(prefix)] + id = _xapian_unquote(string=id) + if clear_tags: + for line in self._clear_tags_for_message(id=id): + git.stdin.write(line) + for line in _index_tags_for_message( + id=id, status='A', tags=tags): + git.stdin.write(line) + + @timed + def diff(self, filter): + """ + Get an {id: {tag, ...}} dict for a given filter. + + For example, use 'A' to find added tags, and 'D' to find deleted tags. + """ + s = _collections.defaultdict(set) + with _git( + args=[ + 'diff-index', '--cached', '--diff-filter', filter, + '--name-only', 'HEAD'], + additional_env={'GIT_INDEX_FILE': self.index_path}, + stdout=_subprocess.PIPE) as p: + # Once we drop Python < 3.3, we can use 'yield from' here + for id, tag in _unpack_diff_lines(stream=p.stdout): + s[id].add(tag) + return s + +def _read_index_checksum (index_path): + """Read the index checksum, as defined by index-format.txt in the git source + WARNING: assumes SHA1 repo""" + import binascii + try: + with open(index_path, 'rb') as f: + size=_os.path.getsize(index_path) + f.seek(size-20); + return binascii.hexlify(f.read(20)).decode('ascii') + except FileNotFoundError: + return None + +def _read_database_lastmod(): + with _spawn( + args=['notmuch', 'count', '--lastmod', '*'], + stdout=_subprocess.PIPE) as notmuch: + (count,uuid,lastmod_str) = notmuch.stdout.readline().split() + return (count,uuid,int(lastmod_str)) + +def _id_path(id): + hid=_hex_quote(string=id) + from hashlib import blake2b + + if FORMAT_VERSION==0: + return 'tags/{hid}'.format(hid=hid) + elif FORMAT_VERSION==1: + idhash = blake2b(hid.encode('utf8'), digest_size=2).hexdigest() + return 'tags/{dir1}/{dir2}/{hid}'.format( + hid=hid, + dir1=idhash[0:2],dir2=idhash[2:]) + else: + _LOG.error("Unknown format version",FORMAT_VERSION) + _sys.exit(1) + +def _index_tags_for_message(id, status, tags): + """ + Update the Git index to either create or delete an empty file. + + Neither 'id' nor the tags in 'tags' should be encoded/escaped. + """ + mode = '100644' + hash = _EMPTYBLOB + + if status == 'D': + mode = '0' + hash = '0000000000000000000000000000000000000000' + + for tag in tags: + path = '{ipath}/{tag}'.format(ipath=_id_path(id),tag=_hex_quote(string=tag)) + yield '{mode} {hash}\t{path}\n'.format(mode=mode, hash=hash, path=path) + + +def _diff_refs(filter, a='HEAD', b='@{upstream}'): + with _git( + args=['diff', '--diff-filter', filter, '--name-only', a, b], + stdout=_subprocess.PIPE) as p: + # Once we drop Python < 3.3, we can use 'yield from' here + for id, tag in _unpack_diff_lines(stream=p.stdout): + yield id, tag + + +def _unpack_diff_lines(stream): + "Iterate through (id, tag) tuples in a diff stream." + for line in stream: + match = _TAG_FILE_REGEX[FORMAT_VERSION].match(line.strip()) + if not match: + message = 'non-tag line in diff: {!r}'.format(line.strip()) + if line.startswith(_TAG_DIRECTORY): + raise ValueError(message) + _LOG.info(message) + continue + id = _unquote(match.group('id')) + tag = _unquote(match.group('tag')) + yield (id, tag) + + +def _help(parser, command=None): + """ + Show help for an notmuch-git command. + + Because some folks prefer: + + $ notmuch-git help COMMAND + + to + + $ notmuch-git COMMAND --help + """ + if command: + parser.parse_args([command, '--help']) + else: + parser.parse_args(['--help']) + +def _notmuch_config_get(key): + (status, stdout, stderr) = _spawn( + args=['notmuch', 'config', 'get', key], + stdout=_subprocess.PIPE, wait=True) + if status != 0: + _LOG.error("failed to run notmuch config") + _sys.exit(1) + return stdout.rstrip() + +def read_format_version(): + try: + (status, stdout, stderr) = _git( + args=['cat-file', 'blob', 'master:FORMAT'], + stdout=_subprocess.PIPE, stderr=_subprocess.PIPE, wait=True) + except SubprocessError as e: + _LOG.debug("failed to read FORMAT file from git, assuming format version 0") + return 0 + + return int(stdout) + +# based on BaseDirectory.save_data_path from pyxdg (LGPL2+) +def xdg_data_path(profile): + resource = _os.path.join('notmuch',profile,'git') + assert not resource.startswith('/') + _home = _os.path.expanduser('~') + xdg_data_home = _os.environ.get('XDG_DATA_HOME') or \ + _os.path.join(_home, '.local', 'share') + path = _os.path.join(xdg_data_home, resource) + return path + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser( + description=__doc__.strip(), + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + '-C', '--git-dir', metavar='REPO', + help='Git repository to operate on.') + parser.add_argument( + '-p', '--tag-prefix', metavar='PREFIX', + default = None, + help='Prefix of tags to operate on.') + parser.add_argument( + '-N', '--nmbug', action='store_true', + help='Set defaults for --tag-prefix and --git-dir for the notmuch bug tracker') + parser.add_argument( + '-l', '--log-level', + choices=['critical', 'error', 'warning', 'info', 'debug'], + help='Log verbosity. Defaults to {!r}.'.format( + _logging.getLevelName(_LOG.level).lower())) + + help = _functools.partial(_help, parser=parser) + help.__doc__ = _help.__doc__ + subparsers = parser.add_subparsers( + title='commands', + description=( + 'For help on a particular command, run: ' + "'%(prog)s ... --help'.")) + for command in [ + 'archive', + 'checkout', + 'clone', + 'commit', + 'fetch', + 'help', + 'init', + 'log', + 'merge', + 'pull', + 'push', + 'reset', + 'status', + ]: + func = locals()[command] + doc = _textwrap.dedent(func.__doc__).strip().replace('%', '%%') + subparser = subparsers.add_parser( + command, + help=doc.splitlines()[0], + description=doc, + formatter_class=argparse.RawDescriptionHelpFormatter) + subparser.set_defaults(func=func) + if command == 'archive': + subparser.add_argument( + 'treeish', metavar='TREE-ISH', nargs='?', default='HEAD', + help=( + 'The tree or commit to produce an archive for. Defaults ' + "to 'HEAD'.")) + subparser.add_argument( + 'args', metavar='ARG', nargs='*', + help=( + "Argument passed through to 'git archive'. Set anything " + 'before , see git-archive(1) for details.')) + elif command == 'checkout': + subparser.add_argument( + '-f', '--force', action='store_true', + help='checkout a large fraction of tags.') + elif command == 'clone': + subparser.add_argument( + 'repository', + help=( + 'The (possibly remote) repository to clone from. See the ' + 'URLS section of git-clone(1) for more information on ' + 'specifying repositories.')) + elif command == 'commit': + subparser.add_argument( + '-f', '--force', action='store_true', + help='commit a large fraction of tags.') + subparser.add_argument( + 'message', metavar='MESSAGE', default='', nargs='?', + help='Text for the commit message.') + elif command == 'fetch': + subparser.add_argument( + 'remote', metavar='REMOTE', nargs='?', + help=( + 'Override the default configured in branch..remote ' + 'to fetch from a particular remote repository (e.g. ' + "'origin').")) + elif command == 'help': + subparser.add_argument( + 'command', metavar='COMMAND', nargs='?', + help='The command to show help for.') + elif command == 'init': + subparser.add_argument( + '--format-version', metavar='VERSION', + default = None, + help='create format VERSION repository.') + elif command == 'log': + subparser.add_argument( + 'args', metavar='ARG', nargs='*', + help="Additional argument passed through to 'git log'.") + elif command == 'merge': + subparser.add_argument( + 'reference', metavar='REFERENCE', default='@{upstream}', + nargs='?', + help=( + 'Reference, usually other branch heads, to merge into ' + "our branch. Defaults to '@{upstream}'.")) + elif command == 'pull': + subparser.add_argument( + 'repository', metavar='REPOSITORY', default=None, nargs='?', + help=( + 'The "remote" repository that is the source of the pull. ' + 'This parameter can be either a URL (see the section GIT ' + 'URLS in git-pull(1)) or the name of a remote (see the ' + 'section REMOTES in git-pull(1)).')) + subparser.add_argument( + 'refspecs', metavar='REFSPEC', default=None, nargs='*', + help=( + 'Refspec (usually a branch name) to fetch and merge. See ' + 'the entry in the OPTIONS section of ' + 'git-pull(1) for other possibilities.')) + elif command == 'push': + subparser.add_argument( + 'repository', metavar='REPOSITORY', default=None, nargs='?', + help=( + 'The "remote" repository that is the destination of the ' + 'push. This parameter can be either a URL (see the ' + 'section GIT URLS in git-push(1)) or the name of a remote ' + '(see the section REMOTES in git-push(1)).')) + subparser.add_argument( + 'refspecs', metavar='REFSPEC', default=None, nargs='*', + help=( + 'Refspec (usually a branch name) to push. See ' + 'the entry in the OPTIONS section of ' + 'git-push(1) for other possibilities.')) + elif command == 'reset': + subparser.add_argument( + '-f', '--force', action='store_true', + help='reset a large fraction of tags.') + + args = parser.parse_args() + + nmbug_mode = False + notmuch_profile = _os.getenv('NOTMUCH_PROFILE','default') + + if args.nmbug or _os.path.basename(__file__) == 'nmbug': + nmbug_mode = True + + if args.git_dir: + NOTMUCH_GIT_DIR = args.git_dir + else: + if nmbug_mode: + default = _os.path.join('~', '.nmbug') + else: + default = _notmuch_config_get ('git.path') + if default == '': + default = xdg_data_path(notmuch_profile) + + NOTMUCH_GIT_DIR = _os.path.expanduser(_os.getenv('NOTMUCH_GIT_DIR', default)) + + _NOTMUCH_GIT_DIR = _os.path.join(NOTMUCH_GIT_DIR, '.git') + if _os.path.isdir(_NOTMUCH_GIT_DIR): + NOTMUCH_GIT_DIR = _NOTMUCH_GIT_DIR + + if args.tag_prefix: + TAG_PREFIX = args.tag_prefix + else: + if nmbug_mode: + prefix = 'notmuch::' + else: + prefix = _notmuch_config_get ('git.tag_prefix') + + TAG_PREFIX = _os.getenv('NOTMUCH_GIT_PREFIX', prefix) + + _ENCODED_TAG_PREFIX = _hex_quote(TAG_PREFIX, safe='+@=,') # quote ':' + + if args.log_level: + level = getattr(_logging, args.log_level.upper()) + _LOG.setLevel(level) + + # for test suite + for var in ['NOTMUCH_GIT_DIR', 'NOTMUCH_GIT_PREFIX', 'NOTMUCH_PROFILE', 'NOTMUCH_CONFIG' ]: + _LOG.debug('env {:s} = {:s}'.format(var, _os.getenv(var,'%None%'))) + + if _notmuch_config_get('built_with.sexp_queries') != 'true': + _LOG.error("notmuch git needs sexp query support") + _sys.exit(1) + + if not getattr(args, 'func', None): + parser.print_usage() + _sys.exit(1) + + # The following two lines are used by the test suite. + _LOG.debug('prefix = {:s}'.format(TAG_PREFIX)) + _LOG.debug('repository = {:s}'.format(NOTMUCH_GIT_DIR)) + + if args.func != init: + FORMAT_VERSION = read_format_version() + + _LOG.debug('FORMAT_VERSION={:d}'.format(FORMAT_VERSION)) + + if args.func == help: + arg_names = ['command'] + else: + (arg_names, varargs, varkw) = _inspect.getargs(args.func.__code__) + kwargs = {key: getattr(args, key) for key in arg_names if key in args} + try: + args.func(**kwargs) + except SubprocessError as e: + if _LOG.level == _logging.DEBUG: + raise # don't mask the traceback + _LOG.error(str(e)) + _sys.exit(1) diff --git a/notmuch-insert.c b/notmuch-insert.c index 72e2e35f..e44607ad 100644 --- a/notmuch-insert.c +++ b/notmuch-insert.c @@ -241,6 +241,26 @@ maildir_mktemp (const void *ctx, const char *maildir, bool world_readable, char return fd; } +static bool +write_buf (const char *buf, int fdout, ssize_t remain) +{ + const char *p = buf; + + do { + ssize_t written = write (fdout, p, remain); + if (written < 0 && errno == EINTR) + continue; + if (written <= 0) { + fprintf (stderr, "Error: writing to temporary file: %s", + strerror (errno)); + return false; + } + p += written; + remain -= written; + } while (remain > 0); + return true; +} + /* * Copy fdin to fdout, return true on success, and false on errors and * empty input. @@ -249,11 +269,13 @@ static bool copy_fd (int fdout, int fdin) { bool empty = true; + bool first = true; + const char *header = "X-Envelope-From: "; while (! interrupted) { ssize_t remain; char buf[4096]; - char *p; + const char *p = buf; remain = read (fdin, buf, sizeof (buf)); if (remain == 0) @@ -266,20 +288,18 @@ copy_fd (int fdout, int fdin) return false; } - p = buf; - do { - ssize_t written = write (fdout, p, remain); - if (written < 0 && errno == EINTR) - continue; - if (written <= 0) { - fprintf (stderr, "Error: writing to temporary file: %s", - strerror (errno)); + if (first && remain >= 5 && 0 == strncmp (buf, "From ", 5)) { + if (! write_buf (header, fdout, strlen (header))) return false; - } - p += written; - remain -= written; - empty = false; - } while (remain > 0); + p += 5; + remain -= 5; + } + + first = false; + + if (! write_buf (p, fdout, remain)) + return false; + empty = false; } return (! interrupted && ! empty); @@ -461,6 +481,8 @@ notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[]) char *maildir; char *newpath; int opt_index; + notmuch_indexopts_t *indexopts = notmuch_database_get_default_indexopts (notmuch); + void *local = talloc_new (NULL); notmuch_opt_desc_t options[] = { @@ -550,7 +572,7 @@ notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[]) return EXIT_FAILURE; } - status = notmuch_process_shared_indexing_options (notmuch); + status = notmuch_process_shared_indexing_options (indexopts); if (status != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "Error: Failed to process index options. (%s)\n", notmuch_status_to_string (status)); @@ -558,7 +580,7 @@ notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[]) } /* Index the message. */ - status = add_file (notmuch, newpath, tag_ops, synchronize_flags, keep, indexing_cli_choices.opts); + status = add_file (notmuch, newpath, tag_ops, synchronize_flags, keep, indexopts); /* Commit changes. */ close_status = notmuch_database_close (notmuch); diff --git a/notmuch-new.c b/notmuch-new.c index b7a5f2ea..4a53e3eb 100644 --- a/notmuch-new.c +++ b/notmuch-new.c @@ -45,6 +45,7 @@ typedef struct { const char *db_path; const char *mail_root; + notmuch_indexopts_t *indexopts; int output_is_a_tty; enum verbosity verbosity; bool debug; @@ -376,7 +377,7 @@ add_file (notmuch_database_t *notmuch, const char *filename, if (status) goto DONE; - status = notmuch_database_index_file (notmuch, filename, indexing_cli_choices.opts, &message); + status = notmuch_database_index_file (notmuch, filename, state->indexopts, &message); switch (status) { /* Success. */ case NOTMUCH_STATUS_SUCCESS: @@ -412,6 +413,10 @@ add_file (notmuch_database_t *notmuch, const char *filename, case NOTMUCH_STATUS_FILE_NOT_EMAIL: fprintf (stderr, "Note: Ignoring non-mail file: %s\n", filename); break; + case NOTMUCH_STATUS_PATH_ERROR: + fprintf (stderr, "Note: Ignoring non-indexable path: %s\n", filename); + (void) print_status_database ("add_file", notmuch, status); + break; case NOTMUCH_STATUS_FILE_ERROR: /* Someone renamed/removed the file between scandir and now. */ state->vanished_files++; @@ -600,11 +605,12 @@ add_files (notmuch_database_t *notmuch, continue; } - /* Ignore the .notmuch directory and any "tmp" directory + /* Ignore any top level .notmuch directory and any "tmp" directory * that appears within a maildir. */ if ((is_maildir && strcmp (entry->d_name, "tmp") == 0) || - strcmp (entry->d_name, ".notmuch") == 0) + (strcmp (entry->d_name, ".notmuch") == 0 + && (strcmp (path, state->mail_root)) == 0)) continue; next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name); @@ -1150,6 +1156,8 @@ notmuch_new_command (notmuch_database_t *notmuch, int argc, char *argv[]) else if (verbose) add_files_state.verbosity = VERBOSITY_VERBOSE; + add_files_state.indexopts = notmuch_database_get_default_indexopts (notmuch); + add_files_state.new_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_TAGS); if (print_status_database ( @@ -1217,7 +1225,7 @@ notmuch_new_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (notmuch == NULL) return EXIT_FAILURE; - status = notmuch_process_shared_indexing_options (notmuch); + status = notmuch_process_shared_indexing_options (add_files_state.indexopts); if (status != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "Error: Failed to process index options. (%s)\n", notmuch_status_to_string (status)); diff --git a/notmuch-reindex.c b/notmuch-reindex.c index 49eacd47..e9a65456 100644 --- a/notmuch-reindex.c +++ b/notmuch-reindex.c @@ -90,6 +90,7 @@ notmuch_reindex_command (notmuch_database_t *notmuch, int argc, char *argv[]) int opt_index; int ret; notmuch_status_t status; + notmuch_indexopts_t *indexopts = notmuch_database_get_default_indexopts (notmuch); /* Set up our handler for SIGINT */ memset (&action, 0, sizeof (struct sigaction)); @@ -110,7 +111,7 @@ notmuch_reindex_command (notmuch_database_t *notmuch, int argc, char *argv[]) notmuch_process_shared_options (notmuch, argv[0]); - status = notmuch_process_shared_indexing_options (notmuch); + status = notmuch_process_shared_indexing_options (indexopts); if (status != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "Error: Failed to process index options. (%s)\n", notmuch_status_to_string (status)); @@ -128,7 +129,7 @@ notmuch_reindex_command (notmuch_database_t *notmuch, int argc, char *argv[]) return EXIT_FAILURE; } - ret = reindex_query (notmuch, query_string, indexing_cli_choices.opts); + ret = reindex_query (notmuch, query_string, indexopts); notmuch_database_destroy (notmuch); diff --git a/notmuch-reply.c b/notmuch-reply.c index 9fca22db..44297251 100644 --- a/notmuch-reply.c +++ b/notmuch-reply.c @@ -663,7 +663,7 @@ do_reply (notmuch_database_t *notmuch, notmuch_messages_move_to_next (messages)) { message = notmuch_messages_get (messages); - if (mime_node_open (notmuch, message, ¶ms->crypto, &node)) + if (mime_node_open (notmuch, message, params->duplicate, ¶ms->crypto, &node)) return 1; reply = create_reply_message (notmuch, message, @@ -683,7 +683,7 @@ do_reply (notmuch_database_t *notmuch, /* Start the original */ sp->map_key (sp, "original"); - format_part_sprinter (notmuch, sp, node, true, false); + format_part_sprinter (notmuch, sp, node, params->duplicate, true, false); /* End */ sp->end (sp); @@ -715,6 +715,7 @@ notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[]) int opt_index; notmuch_show_params_t params = { .part = -1, + .duplicate = 0, .crypto = { .decrypt = NOTMUCH_DECRYPT_AUTO }, }; int format = FORMAT_DEFAULT; @@ -739,6 +740,7 @@ notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[]) { "auto", NOTMUCH_DECRYPT_AUTO }, { "true", NOTMUCH_DECRYPT_NOSTASH }, { 0, 0 } } }, + { .opt_int = ¶ms.duplicate, .name = "duplicate" }, { .opt_inherit = notmuch_shared_options }, { } }; diff --git a/notmuch-show.c b/notmuch-show.c index 2848c9c3..8c23f821 100644 --- a/notmuch-show.c +++ b/notmuch-show.c @@ -23,6 +23,21 @@ #include "sprinter.h" #include "zlib-extra.h" +static const char * +_get_filename (notmuch_message_t *message, int index) +{ + notmuch_filenames_t *filenames = notmuch_message_get_filenames (message); + int i = 1; + + for (; + notmuch_filenames_valid (filenames); + notmuch_filenames_move_to_next (filenames), i++) { + if (i >= index) + return notmuch_filenames_get (filenames); + } + return NULL; +} + static const char * _get_tags_as_string (const void *ctx, notmuch_message_t *message) { @@ -209,6 +224,30 @@ _is_from_line (const char *line) return 0; } +/* Output extra headers if configured with the `show.extra_headers' + * configuration option + */ +static void +format_extra_headers_sprinter (sprinter_t *sp, GMimeMessage *message) +{ + GMimeHeaderList *header_list = g_mime_object_get_header_list (GMIME_OBJECT (message)); + + for (notmuch_config_values_t *extra_headers = notmuch_config_get_values ( + sp->notmuch, NOTMUCH_CONFIG_EXTRA_HEADERS); + notmuch_config_values_valid (extra_headers); + notmuch_config_values_move_to_next (extra_headers)) { + GMimeHeader *header; + const char *header_name = notmuch_config_values_get (extra_headers); + + header = g_mime_header_list_get_header (header_list, header_name); + if (header == NULL) + continue; + + sp->map_key (sp, g_mime_header_get_name (header)); + sp->string (sp, g_mime_header_get_value (header)); + } +} + void format_headers_sprinter (sprinter_t *sp, GMimeMessage *message, bool reply, const _notmuch_message_crypto_t *msg_crypto) @@ -269,6 +308,9 @@ format_headers_sprinter (sprinter_t *sp, GMimeMessage *message, sp->string (sp, g_mime_message_get_date_string (sp, message)); } + /* Output extra headers the user has configured, if any */ + if (! reply) + format_extra_headers_sprinter (sp, message); sp->end (sp); talloc_free (local); } @@ -631,6 +673,7 @@ format_omitted_part_meta_sprinter (sprinter_t *sp, GMimeObject *meta, GMimePart void format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node, + int duplicate, bool output_body, bool include_html) { @@ -642,10 +685,13 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node, sp->begin_map (sp); format_message_sprinter (sp, node->envelope_file); + sp->map_key (sp, "duplicate"); + sp->integer (sp, duplicate > 0 ? duplicate : 1); + if (output_body) { sp->map_key (sp, "body"); sp->begin_list (sp); - format_part_sprinter (ctx, sp, mime_node_child (node, 0), true, include_html); + format_part_sprinter (ctx, sp, mime_node_child (node, 0), -1, true, include_html); sp->end (sp); } @@ -809,7 +855,7 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node, } for (i = 0; i < node->nchildren; i++) - format_part_sprinter (ctx, sp, mime_node_child (node, i), true, include_html); + format_part_sprinter (ctx, sp, mime_node_child (node, i), -1, true, include_html); /* Close content structures */ for (i = 0; i < nclose; i++) @@ -823,7 +869,8 @@ format_part_sprinter_entry (const void *ctx, sprinter_t *sp, mime_node_t *node, unused (int indent), const notmuch_show_params_t *params) { - format_part_sprinter (ctx, sp, node, params->output_body, params->include_html); + format_part_sprinter (ctx, sp, node, params->duplicate, params->output_body, + params->include_html); return NOTMUCH_STATUS_SUCCESS; } @@ -898,7 +945,7 @@ format_part_raw (unused (const void *ctx), unused (sprinter_t *sp), char buf[4096]; notmuch_status_t ret = NOTMUCH_STATUS_FILE_ERROR; - filename = notmuch_message_get_filename (node->envelope_file); + filename = _get_filename (node->envelope_file, params->duplicate); if (filename == NULL) { fprintf (stderr, "Error: Cannot get message filename.\n"); goto DONE; @@ -977,7 +1024,7 @@ show_message (void *ctx, session_key_count_error = notmuch_message_count_properties (message, "session-key", &session_keys); - status = mime_node_open (local, message, &(params->crypto), &root); + status = mime_node_open (local, message, params->duplicate, &(params->crypto), &root); if (status) goto DONE; part = mime_node_seek_dfs (root, (params->part < 0 ? 0 : params->part)); @@ -1112,6 +1159,18 @@ do_show_threaded (void *ctx, notmuch_thread_t *thread; notmuch_messages_t *messages; notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS; + int i; + + if (params->offset < 0) { + unsigned count; + notmuch_status_t s = notmuch_query_count_threads (query, &count); + if (print_status_query ("notmuch show", query, s)) + return 1; + + params->offset += count; + if (params->offset < 0) + params->offset = 0; + } status = notmuch_query_search_threads (query, &threads); if (print_status_query ("notmuch show", query, status)) @@ -1119,11 +1178,16 @@ do_show_threaded (void *ctx, sp->begin_list (sp); - for (; - notmuch_threads_valid (threads); - notmuch_threads_move_to_next (threads)) { + for (i = 0; + notmuch_threads_valid (threads) && (params->limit < 0 || i < params->offset + params->limit); + notmuch_threads_move_to_next (threads), i++) { thread = notmuch_threads_get (threads); + if (i < params->offset) { + notmuch_thread_destroy (thread); + continue; + } + messages = notmuch_thread_get_toplevel_messages (thread); if (messages == NULL) @@ -1154,6 +1218,18 @@ do_show_unthreaded (void *ctx, notmuch_message_t *message; notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS; notmuch_bool_t excluded; + int i; + + if (params->offset < 0) { + unsigned count; + notmuch_status_t s = notmuch_query_count_messages (query, &count); + if (print_status_query ("notmuch show", query, s)) + return 1; + + params->offset += count; + if (params->offset < 0) + params->offset = 0; + } status = notmuch_query_search_messages (query, &messages); if (print_status_query ("notmuch show", query, status)) @@ -1161,9 +1237,13 @@ do_show_unthreaded (void *ctx, sp->begin_list (sp); - for (; - notmuch_messages_valid (messages); - notmuch_messages_move_to_next (messages)) { + for (i = 0; + notmuch_messages_valid (messages) && (params->limit < 0 || i < params->offset + params->limit); + notmuch_messages_move_to_next (messages), i++) { + if (i < params->offset) { + continue; + } + sp->begin_list (sp); sp->begin_list (sp); @@ -1239,6 +1319,9 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[]) sprinter_t *sprinter; notmuch_show_params_t params = { .part = -1, + .duplicate = 0, + .offset = 0, + .limit = -1, /* unlimited */ .omit_excluded = true, .output_body = true, .crypto = { .decrypt = NOTMUCH_DECRYPT_AUTO }, @@ -1279,6 +1362,9 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[]) { .opt_bool = ¶ms.crypto.verify, .name = "verify" }, { .opt_bool = ¶ms.output_body, .name = "body" }, { .opt_bool = ¶ms.include_html, .name = "include-html" }, + { .opt_int = ¶ms.duplicate, .name = "duplicate" }, + { .opt_int = ¶ms.limit, .name = "limit" }, + { .opt_int = ¶ms.offset, .name = "offset" }, { .opt_inherit = notmuch_shared_options }, { } }; @@ -1297,6 +1383,9 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[]) /* specifying a part implies single message display */ single_message = params.part >= 0; + /* specifying a duplicate also implies single message display */ + single_message = single_message || (params.duplicate > 0); + if (format == NOTMUCH_FORMAT_NOT_SPECIFIED) { /* if part was requested and format was not specified, use format=raw */ if (params.part >= 0) @@ -1310,6 +1399,14 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[]) fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n"); return EXIT_FAILURE; } + if (params.crypto.decrypt != NOTMUCH_DECRYPT_FALSE + && params.crypto.decrypt != NOTMUCH_DECRYPT_AUTO) { + fprintf (stderr, "Warning: mbox format does not support decryption (ignored)\n"); + } + if (params.crypto.verify) { + fprintf (stderr, + "Warning: mbox format does not support signature verification (ignored)\n"); + } } else if (format == NOTMUCH_FORMAT_RAW) { /* raw format only supports single message display */ single_message = true; diff --git a/notmuch.c b/notmuch.c index 3fb58bf2..814b9e42 100644 --- a/notmuch.c +++ b/notmuch.c @@ -141,21 +141,18 @@ const notmuch_opt_desc_t notmuch_shared_indexing_options [] = { notmuch_status_t -notmuch_process_shared_indexing_options (notmuch_database_t *notmuch) +notmuch_process_shared_indexing_options (notmuch_indexopts_t *opts) { - if (indexing_cli_choices.opts == NULL) - indexing_cli_choices.opts = notmuch_database_get_default_indexopts (notmuch); + if (opts == NULL) + return NOTMUCH_STATUS_NULL_POINTER; + if (indexing_cli_choices.decrypt_policy_set) { 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, + status = notmuch_indexopts_set_decrypt_policy (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)); - notmuch_indexopts_destroy (indexing_cli_choices.opts); - indexing_cli_choices.opts = NULL; return status; } } @@ -214,12 +211,14 @@ typedef struct help_topic { } help_topic_t; static const help_topic_t help_topics[] = { - { "search-terms", - "Common search term syntax." }, { "hooks", "Hooks that will be run before or after certain commands." }, { "properties", "Message property conventions and documentation." }, + { "search-terms", + "Common infix search term syntax." }, + { "sexp-queries", + "Common s-expression search term syntax." }, }; static const command_t * @@ -332,9 +331,7 @@ exec_man (const char *page) static int _help_for (const char *topic_name) { - const command_t *command; - const help_topic_t *topic; - unsigned int i; + char *page; if (! topic_name) { printf ("The notmuch mail system.\n\n"); @@ -351,23 +348,9 @@ _help_for (const char *topic_name) return EXIT_SUCCESS; } - command = find_command (topic_name); - if (command) { - char *page = talloc_asprintf (NULL, "notmuch-%s", command->name); - exec_man (page); - } + page = talloc_asprintf (NULL, "notmuch-%s", topic_name); + exec_man (page); - for (i = 0; i < ARRAY_SIZE (help_topics); i++) { - topic = &help_topics[i]; - if (strcmp (topic_name, topic->name) == 0) { - char *page = talloc_asprintf (NULL, "notmuch-%s", topic->name); - exec_man (page); - } - } - - fprintf (stderr, - "\nSorry, %s is not a known command. There's not much I can do to help.\n\n", - topic_name); return EXIT_FAILURE; } @@ -446,11 +429,18 @@ notmuch_command (notmuch_database_t *notmuch, * false on errors. */ static bool -try_external_command (char *argv[]) +try_external_command (const char *config_file_name, char *argv[]) { char *old_argv0 = argv[0]; bool ret = true; + if (config_file_name) { + if (setenv ("NOTMUCH_CONFIG", config_file_name, 1)) { + perror ("setenv"); + exit (1); + } + } + argv[0] = talloc_asprintf (NULL, "notmuch-%s", old_argv0); /* @@ -510,7 +500,7 @@ main (int argc, char *argv[]) /* if command->function is NULL, try external command */ if (! command || ! command->function) { /* This won't return if the external command is found. */ - if (try_external_command (argv + opt_index)) + if (try_external_command (config_file_name, argv + opt_index)) fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n", command_name); ret = EXIT_FAILURE; @@ -541,7 +531,7 @@ main (int argc, char *argv[]) } if (status == NOTMUCH_STATUS_NO_CONFIG) - fputs ("Try running 'notmuch setup' to create a configuration.", stderr); + fputs ("Try running 'notmuch setup' to create a configuration.\n", stderr); return EXIT_FAILURE; } @@ -573,15 +563,16 @@ main (int argc, char *argv[]) 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; + if (status_string) { + fputs (status_string, stderr); + free (status_string); + status_string = NULL; } + switch (status) { case NOTMUCH_STATUS_NO_CONFIG: if (! (command->mode & NOTMUCH_COMMAND_CONFIG_CREATE)) { - fputs ("Try running 'notmuch setup' to create a configuration.", stderr); + fputs ("Try running 'notmuch setup' to create a configuration.\n", stderr); goto DONE; } break; @@ -599,6 +590,8 @@ main (int argc, char *argv[]) case NOTMUCH_STATUS_SUCCESS: break; default: + fputs ("Error: unable to load config file.\n", stderr); + ret = 1; goto DONE; } diff --git a/performance-test/README b/performance-test/README index 59b37b1b..1ca0df2f 100644 --- a/performance-test/README +++ b/performance-test/README @@ -24,11 +24,11 @@ Getting set up to run tests: First, you need to get the corpus. If you don't already have the gpg key for David Bremner, run - % gpg --search 'david@tethera.net' + % gpg --locate-external-key 'david@tethera.net' This should get you a key with fingerprint - 815B 6398 2A79 F8E7 C727 86C4 762B 57BB 7842 06AD + 7A18 807F 100A 4570 C596 8420 7E4E 65C8 720B 706B (the last 8 digits are printed as the "key id"). diff --git a/performance-test/T02-tag.sh b/performance-test/T02-tag.sh index 9c895d6a..47fdb0c2 100755 --- a/performance-test/T02-tag.sh +++ b/performance-test/T02-tag.sh @@ -11,4 +11,13 @@ time_run 'tag * +existing_tag' "notmuch tag +new_tag '*'" time_run 'tag * -existing_tag' "notmuch tag -new_tag '*'" time_run 'tag * -missing_tag' "notmuch tag -new_tag '*'" +time_run 'tag * +maildir_flag F' "notmuch tag +flagged '*'" +time_run 'tag * -maildir_flag F' "notmuch tag -flagged '*'" +time_run 'tag * +maildir_flag P' "notmuch tag +passed '*'" +time_run 'tag * -maildir_flag P' "notmuch tag -passed '*'" +time_run 'tag * +maildir_flag D' "notmuch tag +draft '*'" +time_run 'tag * -maildir_flag D' "notmuch tag -draft '*'" +time_run 'tag * +maildir_flag S' "notmuch tag -unread '*'" +time_run 'tag * -maildir_flag S' "notmuch tag +unread '*'" + time_done diff --git a/performance-test/T06-emacs.sh b/performance-test/T06-emacs.sh new file mode 100755 index 00000000..c92bbd66 --- /dev/null +++ b/performance-test/T06-emacs.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +test_description='emacs operations' + +. $(dirname "$0")/perf-test-lib.sh || exit 1 +. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1 + +test_require_emacs + +time_start + +print_emacs_header + +MSGS=$(notmuch search --output=messages "*" | shuf -n 50 | awk '{printf " \"%s\"",$1}') + +time_emacs "tag messages" \ +"(dolist (msg (list $MSGS)) + (notmuch-tag msg (list \"+test\")) + (notmuch-tag msg (list \"-test\"))))" + +time_emacs "show warmup" \ + '(notmuch-show "thread:{id:tip-4f8219875a0dad2cfad9e93a3fafcd9626db98d2@git.kernel.org}")' + +time_emacs "show thread #1" \ + '(notmuch-show "thread:{id:tip-4f8219875a0dad2cfad9e93a3fafcd9626db98d2@git.kernel.org}")' + +time_emacs "depth bound #1" \ + '(let ((notmuch-show-depth-limit 0)) + (notmuch-show "thread:{id:tip-4f8219875a0dad2cfad9e93a3fafcd9626db98d2@git.kernel.org}"))' + +time_emacs "height bound #1" \ + '(let ((notmuch-show-height-limit -1)) + (notmuch-show "thread:{id:tip-4f8219875a0dad2cfad9e93a3fafcd9626db98d2@git.kernel.org}"))' + +time_emacs "size bound #1" \ + '(let ((notmuch-show-max-text-part-size 1)) + (notmuch-show "thread:{id:tip-4f8219875a0dad2cfad9e93a3fafcd9626db98d2@git.kernel.org}"))' + +time_emacs "show thread #2" \ + '(notmuch-show "thread:{id:20101208005731.943729010@clark.site}")' + +time_emacs "depth bound #2" \ + '(let ((notmuch-show-depth-limit 0)) + (notmuch-show "thread:{id:20101208005731.943729010@clark.site}"))' + +time_emacs "height bound #2" \ + '(let ((notmuch-show-height-limit -1)) + (notmuch-show "thread:{id:20101208005731.943729010@clark.site}"))' + +time_emacs "size bound #2" \ + '(let ((notmuch-show-max-text-part-size 1)) + (notmuch-show "thread:{id:20101208005731.943729010@clark.site}"))' + +time_emacs "show thread #3" \ + '(notmuch-show "thread:{id:20120109014938.GE20796@mit.edu}")' + +time_emacs "depth bound #3" \ + '(let ((notmuch-show-depth-limit 0)) + (notmuch-show "thread:{id:20120109014938.GE20796@mit.edu}"))' + +time_emacs "height bound #3" \ + '(let ((notmuch-show-height-limit -1)) + (notmuch-show "thread:{id:20120109014938.GE20796@mit.edu}"))' + +time_emacs "size bound #3" \ + '(let ((notmuch-show-max-text-part-size 1)) + (notmuch-show "thread:{id:20120109014938.GE20796@mit.edu}"))' + +time_emacs "show thread #4" \ + '(notmuch-show "thread:{id:1280704593.25620.48.camel@mulgrave.site}")' + +time_emacs "depth bound #4" \ + '(let ((notmuch-show-depth-limit 0)) + (notmuch-show "thread:{id:1280704593.25620.48.camel@mulgrave.site}"))' + +time_emacs "height bound #4" \ + '(let ((notmuch-show-height-limit -1)) + (notmuch-show "thread:{id:1280704593.25620.48.camel@mulgrave.site}"))' + +time_emacs "size bound #4" \ + '(let ((notmuch-show-max-text-part-size 1)) + (notmuch-show "thread:{id:1280704593.25620.48.camel@mulgrave.site}"))' + +time_done diff --git a/performance-test/T07-git.sh b/performance-test/T07-git.sh new file mode 100755 index 00000000..11dfec05 --- /dev/null +++ b/performance-test/T07-git.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +test_description='notmuch-git' + +. $(dirname "$0")/perf-test-lib.sh || exit 1 + +time_start + +time_run 'init' "notmuch git init" + +time_run 'commit --force' "notmuch git commit --force" +time_run 'commit' "notmuch git -l error commit" +time_run 'commit' "notmuch git -l error commit" + +time_run 'checkout' "notmuch git checkout" + +time_run 'tag -inbox' "notmuch tag -inbox '*'" + +time_run 'checkout --force' "notmuch git checkout --force" + + + +time_done diff --git a/performance-test/download/notmuch-email-corpus-0.5.tar.xz.asc b/performance-test/download/notmuch-email-corpus-0.5.tar.xz.asc new file mode 100644 index 00000000..2318c2f6 --- /dev/null +++ b/performance-test/download/notmuch-email-corpus-0.5.tar.xz.asc @@ -0,0 +1,16 @@ +-----BEGIN PGP SIGNATURE----- + +iQIzBAABCAAdFiEEkiyHYXwaY0SiY6fqA0U5G1WqFSEFAmR119gACgkQA0U5G1Wq +FSHlSQ/+NSRj27PEZjaP2I+3j+rsMG3pnVckNcuOQyfgjJ+zEagMZyRu3vaIA/pX +xtBrNIX4l4CQIkqwyNjsuqJdzh6S3DeCWSEr1Q+GSBki+wQCBiRuDYY2HQoDezEK +4bMfniEWZpKJD8PfIabz0OOqMUsfXEYMd9kefew5/J4OGnDIv8E5pKfqvDNNO4rW +MhZ9w9uR9wkvmfmpO66kAgTfLllwiyNHWoWnzQfNmqM8eULFn7XxM1PEZShUEqXf +pTWCqqm5OyUcy8f+gy9Mb7DRRvnwLpHHRQlCzzH2c+ENQRpt1ErsgVKpHTVk4UsB +EML+zwyWEaQg7xVKWXRJDuGCF47S1GCQNUtvtx57HJl6Ds6N2mlr2KEGaI7qtiz5 +5hdaTc0L/TVN0WS+uCdfdDDozFErf1kwhA6Jnpi0YNNdK+wpFzj7ISvA+DNHwJ75 +TLBuJIU/h3QfX3NDC5xDbsWAgpv7a84e7ePO6+kAVkHsNYDbFjiunr5fRbqDsJcJ +B+aVGhKvFZbziC84Dn5Ae9Lpa40fQlxbdb+So2nDIiuR3P33vt7wr/e2ptVfrqkn +a1DM96n03VWexwEDMye3b3rOTXsN5Ul87zucg9xWm5JT75NGuqJ1WDJN/wwNPDro +ZXS1OHh7UKsU1tP2J9+gLiKYNBP4m4BQjEgYXpiYEoge9A1QplQ= +=5/Ep +-----END PGP SIGNATURE----- diff --git a/performance-test/perf-test-lib.sh b/performance-test/perf-test-lib.sh index 41b1ddfd..c34f8cd6 100644 --- a/performance-test/perf-test-lib.sh +++ b/performance-test/perf-test-lib.sh @@ -41,6 +41,8 @@ done # Ensure NOTMUCH_SRCDIR and NOTMUCH_BUILDDIR are set. . $(dirname "$0")/../test/export-dirs.sh || exit 1 +. "$NOTMUCH_SRCDIR/test/test-vars.sh" || exit 1 + # Where to run the tests TEST_DIRECTORY=$NOTMUCH_BUILDDIR/performance-test @@ -208,6 +210,11 @@ print_header () printf "\t\t\tWall(s)\tUsr(s)\tSys(s)\tRes(K)\tIn/Out(512B)\n" } +print_emacs_header () +{ + printf "\t\t\tWall(s)\tGCs\tGC time(s)\n" +} + time_run () { printf " %-22s" "$1" diff --git a/performance-test/version.sh b/performance-test/version.sh index f02527a7..357b9dad 100644 --- a/performance-test/version.sh +++ b/performance-test/version.sh @@ -1,3 +1,3 @@ # this should be both a valid Makefile fragment and valid POSIX(ish) shell. -PERFTEST_VERSION=0.4 +PERFTEST_VERSION=0.5 diff --git a/sprinter-json.c b/sprinter-json.c index c7f4851c..502f89fb 100644 --- a/sprinter-json.c +++ b/sprinter-json.c @@ -172,7 +172,7 @@ json_separator (struct sprinter *sp) } struct sprinter * -sprinter_json_create (const void *ctx, FILE *stream) +sprinter_json_create (notmuch_database_t *db, FILE *stream) { static const struct sprinter_json template = { .vtable = { @@ -192,11 +192,12 @@ sprinter_json_create (const void *ctx, FILE *stream) }; struct sprinter_json *res; - res = talloc (ctx, struct sprinter_json); + res = talloc (db, struct sprinter_json); if (! res) return NULL; *res = template; + res->vtable.notmuch = db; res->stream = stream; return &res->vtable; } diff --git a/sprinter-sexp.c b/sprinter-sexp.c index 63b25428..e37cb1f9 100644 --- a/sprinter-sexp.c +++ b/sprinter-sexp.c @@ -207,7 +207,7 @@ sexp_separator (struct sprinter *sp) } struct sprinter * -sprinter_sexp_create (const void *ctx, FILE *stream) +sprinter_sexp_create (notmuch_database_t *db, FILE *stream) { static const struct sprinter_sexp template = { .vtable = { @@ -227,11 +227,12 @@ sprinter_sexp_create (const void *ctx, FILE *stream) }; struct sprinter_sexp *res; - res = talloc (ctx, struct sprinter_sexp); + res = talloc (db, struct sprinter_sexp); if (! res) return NULL; *res = template; + res->vtable.notmuch = db; res->stream = stream; return &res->vtable; } diff --git a/sprinter-text.c b/sprinter-text.c index c75ec5be..99330a94 100644 --- a/sprinter-text.c +++ b/sprinter-text.c @@ -114,7 +114,7 @@ text_map_key (unused (struct sprinter *sp), unused (const char *key)) } struct sprinter * -sprinter_text_create (const void *ctx, FILE *stream) +sprinter_text_create (notmuch_database_t *db, FILE *stream) { static const struct sprinter_text template = { .vtable = { @@ -134,21 +134,22 @@ sprinter_text_create (const void *ctx, FILE *stream) }; struct sprinter_text *res; - res = talloc (ctx, struct sprinter_text); + res = talloc (db, struct sprinter_text); if (! res) return NULL; *res = template; + res->vtable.notmuch = db; res->stream = stream; return &res->vtable; } struct sprinter * -sprinter_text0_create (const void *ctx, FILE *stream) +sprinter_text0_create (notmuch_database_t *db, FILE *stream) { struct sprinter *sp; - sp = sprinter_text_create (ctx, stream); + sp = sprinter_text_create (db, stream); if (! sp) return NULL; diff --git a/sprinter.h b/sprinter.h index 528d8a2d..fd08641c 100644 --- a/sprinter.h +++ b/sprinter.h @@ -9,6 +9,11 @@ * (strings, integers and booleans). */ typedef struct sprinter { + /* + * Open notmuch database + */ + notmuch_database_t *notmuch; + /* Start a new map/dictionary structure. This should be followed by * a sequence of alternating calls to map_key and one of the * value-printing functions until the map is ended by end. @@ -65,20 +70,20 @@ typedef struct sprinter { /* Create a new unstructured printer that emits the default text format * for "notmuch search". */ struct sprinter * -sprinter_text_create (const void *ctx, FILE *stream); +sprinter_text_create (notmuch_database_t *db, FILE *stream); /* Create a new unstructured printer that emits the text format for * "notmuch search", with each field separated by a null character * instead of the newline character. */ struct sprinter * -sprinter_text0_create (const void *ctx, FILE *stream); +sprinter_text0_create (notmuch_database_t *db, FILE *stream); /* Create a new structure printer that emits JSON. */ struct sprinter * -sprinter_json_create (const void *ctx, FILE *stream); +sprinter_json_create (notmuch_database_t *db, FILE *stream); /* Create a new structure printer that emits S-Expressions. */ struct sprinter * -sprinter_sexp_create (const void *ctx, FILE *stream); +sprinter_sexp_create (notmuch_database_t *db, FILE *stream); #endif // NOTMUCH_SPRINTER_H diff --git a/test/README b/test/README index 10f127cb..a81808b1 100644 --- a/test/README +++ b/test/README @@ -137,6 +137,23 @@ detection of missing prerequisites. In the future we may treat tests unable to run because of missing prerequisites, but not explicitly skipped by the user, as failures. +Testing installed notmuch +------------------------- + +Systems integrators (e.g. Linux distros) may wish to test an installed +version of notmuch. This can be done be running + + $ NOTMUCH_TEST_INSTALLED=1 ./test/notmuch-test + +In this scenario the test suite does not assume a built tree, and in +particular cannot rely on the output of 'configure'. You may want to +set certain feature environment variables ('NOTMUCH_HAVE_*') directly +if you know those apply to your installed notmuch). Consider also +setting TERM=dumb if the value of TERM cannot be used (e.g. in a +chroot with missing terminfo). Note that having a built tree may cause +surprising/broken results for NOTMUCH_TEST_INSTALLED, so consider +cleaning first. + Writing Tests ------------- The test script is written as a shell script. It is to be named as diff --git a/test/T000-basic.sh b/test/T000-basic.sh index a2f4d93f..642f918d 100755 --- a/test/T000-basic.sh +++ b/test/T000-basic.sh @@ -66,6 +66,7 @@ test_begin_subtest 'NOTMUCH_CONFIG is set and points to an existing file' test_expect_success 'test -f "${NOTMUCH_CONFIG}"' test_begin_subtest 'PATH is set to build directory' +test_subtest_broken_for_installed test_expect_equal \ "$(dirname ${TEST_DIRECTORY})" \ "$(echo $PATH|cut -f1 -d: | sed -e 's,/test/valgrind/bin$,,')" diff --git a/test/T010-help-test.sh b/test/T010-help-test.sh index da45d3ae..827edc14 100755 --- a/test/T010-help-test.sh +++ b/test/T010-help-test.sh @@ -12,13 +12,16 @@ test_expect_success 'notmuch help' test_begin_subtest 'notmuch --version' test_expect_success 'notmuch --version' -if [ $NOTMUCH_HAVE_MAN -eq 1 ]; then +if [ "${NOTMUCH_HAVE_MAN-0}" = "1" ]; then test_begin_subtest 'notmuch --help tag' test_expect_success 'notmuch --help tag' test_begin_subtest 'notmuch help tag' test_expect_success 'notmuch help tag' else + if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + test_done + fi test_begin_subtest 'notmuch --help tag (man pages not available)' test_expect_success 'test_must_fail notmuch --help tag >/dev/null' diff --git a/test/T030-config.sh b/test/T030-config.sh index 3a585d1b..1d2b7df8 100755 --- a/test/T030-config.sh +++ b/test/T030-config.sh @@ -3,6 +3,8 @@ test_description='"notmuch config"' . $(dirname "$0")/test-lib.sh || exit 1 +cp notmuch-config initial-config + test_begin_subtest "Get string value" test_expect_equal "$(notmuch config get user.name)" "Notmuch Test Suite" @@ -51,15 +53,18 @@ cat < EXPECTED built_with.compact=something built_with.field_processor=something built_with.retry_lock=something -built_with.sexpr_query=something +built_with.sexp_queries=something database.autocommit=8000 database.mail_root=MAIL_DIR database.path=MAIL_DIR foo.list=this;is another;list value; foo.string=this is another string value +index.as_text= maildir.synchronize_flags=true new.ignore= new.tags=unread;inbox +search.authors_matched_separator=| +search.authors_separator=, search.exclude_tags= user.name=Notmuch Test Suite user.other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org @@ -67,6 +72,35 @@ user.primary_email=test_suite@notmuchmail.org EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "Round trip config item with leading spaces" +test_subtest_known_broken +notmuch config set foo.bar " thing" +output=$(notmuch config get foo.bar) +test_expect_equal "${output}" " thing" + +test_begin_subtest "Round trip config item with leading tab" +test_subtest_known_broken +notmuch config set foo.bar " thing" +output=$(notmuch config get foo.bar) +test_expect_equal "${output}" " thing" + +test_begin_subtest "Round trip config item with embedded tab" +notmuch config set foo.bar "thing other" +output=$(notmuch config get foo.bar) +test_expect_equal "${output}" "thing other" + +test_begin_subtest "Round trip config item with embedded backslash" +notmuch config set foo.bar 'thing\other' +output=$(notmuch config get foo.bar) +test_expect_equal "${output}" "thing\other" + +test_begin_subtest "Round trip config item with embedded NL/CR" +notmuch config set foo.bar 'thing + other' +output=$(notmuch config get foo.bar) +test_expect_equal "${output}" "thing + other" + test_begin_subtest "Top level --config=FILE option" cp "${NOTMUCH_CONFIG}" alt-config notmuch --config=alt-config config set user.name "Another Name" @@ -163,4 +197,17 @@ test_begin_subtest "get built_with.nonexistent prints false" output=$(notmuch config get built_with.nonexistent) test_expect_equal "$output" "false" +test_begin_subtest "Bad utf8 reported as error" +cp initial-config bad-config +printf '[query]\nq3=from:\xff\n' >>bad-config +test_expect_code 1 "notmuch --config=./bad-config config list" + +test_begin_subtest "Specific error message about bad utf8" +notmuch --config=./bad-config config list 2>ERRORS +cat < EXPECTED +GLib: Key file contains key “q3” with value “from:�” which is not UTF-8 +Error: unable to load config file. +EOF +test_expect_equal_file EXPECTED ERRORS + test_done diff --git a/test/T040-setup.sh b/test/T040-setup.sh index 42c621c8..39846d34 100755 --- a/test/T040-setup.sh +++ b/test/T040-setup.sh @@ -21,7 +21,15 @@ baz EOF expected_dir=$NOTMUCH_SRCDIR/test/setup.expected-output -test_expect_equal_file ${expected_dir}/config-with-comments new-notmuch-config +sed '/^$/d' < new-notmuch-config > filtered-config +test_expect_equal_file ${expected_dir}/config-with-comments filtered-config + +test_begin_subtest "setup consistent with config-set for single items" +# note this relies on the config state from the previous test. +notmuch --config=new-notmuch-config config list > list.setup +notmuch --config=new-notmuch-config config set search.exclude_tags baz +notmuch --config=new-notmuch-config config list > list.config +test_expect_equal_file list.setup list.config test_begin_subtest "notmuch with a config but without a database suggests notmuch new" notmuch 2>&1 | notmuch_dir_sanitize > OUTPUT diff --git a/test/T050-new.sh b/test/T050-new.sh index 1141c1e3..52888be2 100755 --- a/test/T050-new.sh +++ b/test/T050-new.sh @@ -329,6 +329,18 @@ notmuch config set new.tags "foo;;bar" output=$(NOTMUCH_NEW --quiet 2>&1) test_expect_equal "$output" "" +test_begin_subtest "leading/trailing whitespace in new.tags is ignored" +# avoid complications with leading spaces and "notmuch config" +sed -i 's/^tags=.*$/tags= fu bar ; ; bar /' notmuch-config +add_message +NOTMUCH_NEW --quiet +notmuch dump id:$gen_msg_id | sed 's/ --.*$//' > OUTPUT +cat <EXPECTED +#notmuch-dump batch-tag:3 config,properties,tags ++bar +fu%20bar +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "Tags starting with '-' in new.tags are forbidden" notmuch config set new.tags "-foo;bar" output=$(NOTMUCH_NEW --debug 2>&1) @@ -339,6 +351,16 @@ test_expect_code 1 "NOTMUCH_NEW --debug 2>&1" notmuch config set new.tags $OLDCONFIG +test_begin_subtest ".notmuch only ignored at top level" +generate_message '[dir]=foo/bar/.notmuch/cur' '[subject]="Do not ignore, very important"' +NOTMUCH_NEW > OUTPUT +notmuch search subject:Do-not-ignore | notmuch_search_sanitize >> OUTPUT +cat < EXPECTED +Added 1 new message to the database. +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Do not ignore, very important (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "RFC822 group names are indexed" test_subtest_known_broken generate_message [to]="undisclosed-recipients:" @@ -361,38 +383,48 @@ No new mail. Removed 1 message. EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "Long file names have reasonable diagnostics" +printf -v name 'f%.0s' {1..234} +generate_message "[filename]=$name" +notmuch new 2>&1 | notmuch_dir_sanitize >OUTPUT +rm ${MAIL_DIR}/${name} +cat < EXPECTED +Note: Ignoring non-indexable path: MAIL_DIR/$name +add_file: Path supplied is illegal for this function +filename too long for file-direntry term: MAIL_DIR/$name +Processed 1 file in almost no time. +No new mail. +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "Xapian exception: read only files" +test_subtest_broken_for_root chmod u-w ${MAIL_DIR}/.notmuch/xapian/*.* output=$(NOTMUCH_NEW --debug 2>&1 | sed 's/: .*$//' ) chmod u+w ${MAIL_DIR}/.notmuch/xapian/*.* test_expect_equal "$output" "A Xapian exception occurred opening database" -test_begin_subtest "Handle files vanishing between scandir and add_file" +make_shim dif-shim< -# A file for scandir to find. It won't get indexed, so can be empty. -touch ${MAIL_DIR}/vanish +WRAP_DLFUNC(notmuch_status_t, notmuch_database_index_file, \ + (notmuch_database_t *database, const char *filename, notmuch_indexopts_t *indexopts, notmuch_message_t **message)) -# Breakpoint to remove the file before indexing -cat < notmuch-new-vanish.gdb -set breakpoint pending on -set logging file notmuch-new-vanish-gdb.log -set logging on -break notmuch_database_index_file -commands -shell rm -f ${MAIL_DIR}/vanish -continue -end -run + if (unlink ("${MAIL_DIR}/vanish")) { + fprintf (stderr, "unlink failed\n"); + exit (42); + } + return notmuch_database_index_file_orig (database, filename, indexopts, message); +} EOF -${TEST_GDB} --batch-silent --return-child-result -x notmuch-new-vanish.gdb \ - --args notmuch new 2>OUTPUT 1>/dev/null -echo "exit status: $?" >> OUTPUT - -# Clean up the file in case gdb isn't available. -rm -f ${MAIL_DIR}/vanish +test_begin_subtest "Handle files vanishing between scandir and add_file" +# A file for scandir to find. It won't get indexed, so can be empty. +touch ${MAIL_DIR}/vanish +notmuch_with_shim dif-shim new 2>OUTPUT 1>/dev/null +echo "exit status: $?" >> OUTPUT cat < EXPECTED Unexpected error with file ${MAIL_DIR}/vanish add_file: Something went wrong trying to read or write a file diff --git a/test/T051-new-renames.sh b/test/T051-new-renames.sh new file mode 100755 index 00000000..ebd06be1 --- /dev/null +++ b/test/T051-new-renames.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +test_description='"notmuch new" with directory renames' +. $(dirname "$0")/test-lib.sh || exit 1 + +for loop in {1..10}; do + +rm -rf ${MAIL_DIR} + +for i in {1..10}; do + generate_message '[dir]=foo' '[subject]="Message foo $i"' +done + +for i in {1..10}; do + generate_message '[dir]=bar' '[subject]="Message bar $i"' +done + +test_begin_subtest "Index the messages, round $loop" +output=$(NOTMUCH_NEW) +test_expect_equal "$output" "Added 20 new messages to the database." + +all_files=$(notmuch search --output=files \*) +count_foo=$(notmuch count folder:foo) + +test_begin_subtest "Rename folder" +mv ${MAIL_DIR}/foo ${MAIL_DIR}/baz +output=$(NOTMUCH_NEW) +test_expect_equal "$output" "No new mail. Detected $count_foo file renames." + +test_begin_subtest "Rename folder back" +mv ${MAIL_DIR}/baz ${MAIL_DIR}/foo +output=$(NOTMUCH_NEW) +test_expect_equal "$output" "No new mail. Detected $count_foo file renames." + +test_begin_subtest "Files remain the same" +output=$(notmuch search --output=files \*) +test_expect_equal "$output" "$all_files" + +done + +test_done diff --git a/test/T055-path-config.sh b/test/T055-path-config.sh index ef22e964..049b745e 100755 --- a/test/T055-path-config.sh +++ b/test/T055-path-config.sh @@ -92,7 +92,20 @@ xdg_config () { notmuch --config=${CONFIG_PATH} config set database.path } -for config in traditional split XDG XDG+profile symlink home_mail maildir_env; do +mailroot_only_config () { + local dir + + backup_config + notmuch config set database.mail_root ${TMP_DIRECTORY}/mail + notmuch config set database.path + DATABASE_PATH="${HOME}/.local/share/notmuch/default" + rm -rf $DATABASE_PATH + mkdir -p $DATABASE_PATH + XAPIAN_PATH="${DATABASE_PATH}/xapian" + mv mail/.notmuch/xapian $DATABASE_PATH +} + +for config in traditional split XDG XDG+profile symlink home_mail maildir_env mailroot_only; do #start each set of tests with an known set of messages add_email_corpus @@ -122,6 +135,9 @@ for config in traditional split XDG XDG+profile symlink home_mail maildir_env; d maildir_env) maildir_env_config ;; + mailroot_only) + mailroot_only_config + ;; esac test_begin_subtest "count ($config)" @@ -277,15 +293,18 @@ EOF built_with.compact=something built_with.field_processor=something built_with.retry_lock=something -built_with.sexpr_query=something +built_with.sexp_queries=something database.autocommit=8000 database.backup_dir database.hook_dir database.mail_root=MAIL_DIR database.path +index.as_text= maildir.synchronize_flags=true new.ignore= new.tags=unread;inbox +search.authors_matched_separator=| +search.authors_separator=, search.exclude_tags= user.name=Notmuch Test Suite user.other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org @@ -293,6 +312,29 @@ user.primary_email=test_suite@notmuchmail.org EOF test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "Config list from python ($config)" + test_python < OUTPUT +from notmuch2 import Database +db=Database(config=Database.CONFIG.SEARCH) +for key in list(db.config): + print(key) +EOF + cat < EXPECTED +database.autocommit +database.backup_dir +database.hook_dir +database.mail_root +database.path +maildir.synchronize_flags +new.tags +search.authors_matched_separator +search.authors_separator +user.name +user.other_email +user.primary_email +EOF + test_expect_equal_file EXPECTED OUTPUT + case $config in XDG*) test_begin_subtest "Set shadowed config value in database ($config)" @@ -306,8 +348,50 @@ EOF output2=$(notmuch --config='' config get ${key}) notmuch config set ${key} test_expect_equal "${output}+${output2}" "${value}+" + ;& + split) + test_begin_subtest "'to' header does not crash (python-cffi) ($config)" + echo 'notmuch@notmuchmail.org' > EXPECTED + test_python < OUTPUT + notmuch search subject:Do-not-ignore | notmuch_search_sanitize >> OUTPUT + cat < EXPECTED +Added 1 new message to the database. +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Do not ignore, very important (inbox unread) +EOF + test_expect_equal_file EXPECTED OUTPUT + ;& + mailroot_only) + test_begin_subtest "create database parent dir ($config)" + rm -r ${DATABASE_PATH} + notmuch new + test_expect_equal "$(xapian-metadata get ${XAPIAN_PATH} version)" 3 + ;; + home_mail|maildir_env) + test_begin_subtest "No errors from config list ($config)" + notmuch config list 2>OUTPUT 1>/dev/null + test_expect_equal_file /dev/null OUTPUT + ;; + *) + backup_database + test_begin_subtest ".notmuch without xapian/ handled gracefully ($config)" + rm -r $XAPIAN_PATH + test_expect_success "notmuch new" + restore_database ;; esac + restore_config rm -rf home/.local rm -rf home/.config diff --git a/test/T060-count.sh b/test/T060-count.sh index 6ad80df9..d63333b6 100755 --- a/test/T060-count.sh +++ b/test/T060-count.sh @@ -102,22 +102,25 @@ output=$(sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' OUTPUT) test_expect_equal "${output}" "A Xapian exception occurred opening database" restore_database -cat < count-files.gdb -set breakpoint pending on -set logging file count-files-gdb.log -set logging on -break count_files -commands -shell cp /dev/null ${MAIL_DIR}/.notmuch/xapian/postlist.* -continue -end -run +make_shim qsm-shim< + +WRAP_DLFUNC (notmuch_status_t, notmuch_query_search_messages, (notmuch_query_t *query, notmuch_messages_t **messages)) + + /* XXX WARNING THIS CORRUPTS THE DATABASE */ + int fd = open ("target_postlist", O_WRONLY|O_TRUNC); + if (fd < 0) + exit (8); + close (fd); + + return notmuch_query_search_messages_orig(query, messages); +} EOF backup_database test_begin_subtest "error message from query_search_messages" -${TEST_GDB} --batch-silent --return-child-result -x count-files.gdb \ - --args notmuch count --output=files '*' 2>OUTPUT 1>/dev/null +ln -s ${MAIL_DIR}/.notmuch/xapian/postlist.* target_postlist +notmuch_with_shim qsm-shim count --output=files '*' 2>OUTPUT 1>/dev/null cat < EXPECTED notmuch count: A Xapian exception occurred A Xapian exception occurred performing query @@ -129,32 +132,57 @@ restore_database test_begin_subtest "count library function is non-destructive" cat< EXPECTED +== stdout == 1: 52 messages 2: 52 messages Exclude 'spam' 3: 52 messages 4: 52 messages +== stderr == EOF -test_python < OUTPUT +#include + +int main (int argc, char** argv) +{ + notmuch_database_t *db; + notmuch_status_t stat = NOTMUCH_STATUS_SUCCESS; + char *msg = NULL; + notmuch_query_t *query; + const char *str = "tag:inbox or tag:spam"; + const char *tag_str = "spam"; + unsigned int count; + + stat=notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + NULL, NULL, &db, &msg); + if (stat) { + fprintf (stderr, "open: %s\n", msg); + exit(1); + } + + EXPECT0(notmuch_query_create_with_syntax (db, str, + NOTMUCH_QUERY_SYNTAX_XAPIAN, &query)); + EXPECT0(notmuch_query_count_messages (query, &count)); + printf("1: %d messages\n", count); + EXPECT0(notmuch_query_count_messages (query, &count)); + printf("2: %d messages\n", count); + printf("Exclude '%s'\n",tag_str); + stat = notmuch_query_add_tag_exclude (query, tag_str); + if (stat && stat != NOTMUCH_STATUS_IGNORED) { + fprintf(stderr, "status=%d\n", stat); + exit(1); + } + EXPECT0(notmuch_query_count_messages (query, &count)); + printf("3: %d messages\n", count); + EXPECT0(notmuch_query_count_messages (query, &count)); + printf("4: %d messages\n", count); +} EOF test_expect_equal_file EXPECTED OUTPUT -if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then +if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then test_begin_subtest "and of exact terms (query=sexp)" output=$(notmuch count --query=sexp '(and "wonderful" "wizard")') diff --git a/test/T070-insert.sh b/test/T070-insert.sh index 208deb1c..73953272 100755 --- a/test/T070-insert.sh +++ b/test/T070-insert.sh @@ -43,13 +43,13 @@ test_begin_subtest "Permissions on inserted message should be 0600" test_expect_equal "600" "$(stat -c %a "$cur_msg_filename")" test_begin_subtest "Insert message adds default tags" -output=$(notmuch show --format=json "subject:insert-subject") +output=$(notmuch show --format=json "subject:insert-subject" | notmuch_json_show_sanitize) expected='[[[{ - "id": "'"${gen_msg_id}"'", + "id": "XXXXX", "crypto": {}, "match": true, "excluded": false, - "filename": ["'"${cur_msg_filename}"'"], + "filename": ["YYYYY"], "timestamp": 946728000, "date_relative": "2000-01-01", "tags": ["inbox","unread"], @@ -234,6 +234,18 @@ output=$(notmuch show --format=json id:$gen_msg_id) test_json_nodes <<<"$output" \ 'new_tags:[0][0][0]["tags"] = ["bar", "foo"]' +test_begin_subtest "leading/trailing whitespace in new.tags is ignored" +# avoid complications with leading spaces and "notmuch config" +sed -i 's/^tags=.*$/tags= fu bar ; ; bar /' notmuch-config +gen_insert_msg +notmuch insert < $gen_msg_filename +notmuch dump id:$gen_msg_id | sed 's/ --.*$//' > OUTPUT +cat <EXPECTED +#notmuch-dump batch-tag:3 config,properties,tags ++bar +fu%20bar +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "Tags starting with '-' in new.tags are forbidden" notmuch config set new.tags "-foo;bar" gen_insert_msg @@ -280,4 +292,9 @@ for code in OUT_OF_MEMORY XAPIAN_EXCEPTION ; do test_expect_code 0 "notmuch_with_shim shim-$code insert --keep < \"$gen_msg_filename\"" done +test_begin_subtest "insert converts mboxes on delivery" +notmuch insert +unmboxed < "${TEST_DIRECTORY}"/corpora/insert/mbox-attachment.eml +output=$(notmuch count tag:unmboxed) +test_expect_equal "${output}" 1 + test_done diff --git a/test/T080-search.sh b/test/T080-search.sh index a3f0dead..515eb88f 100755 --- a/test/T080-search.sh +++ b/test/T080-search.sh @@ -34,6 +34,12 @@ add_message '[subject]="search by id"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000" output=$(notmuch search id:${gen_msg_id} | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by id (inbox unread)" +test_begin_subtest "Message-Id with spaces" +test_subtest_known_broken +add_message '[id]="message id@example.com"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +output=$(notmuch search --output=messages id:"message id@example.com") +test_expect_equal "$output" "messageid@example.com" + test_begin_subtest "Search by mid:" add_message '[subject]="search by mid"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' output=$(notmuch search mid:${gen_msg_id} | notmuch_search_sanitize) @@ -132,6 +138,7 @@ thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (inbox unread) thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; subjectsearchtest (inbox unread) thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread) thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by id (inbox unread) +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; Message-Id with spaces (inbox unread) thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by mid (inbox unread) thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread) thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by thread (inbox unread) diff --git a/test/T081-sexpr-search.sh b/test/T081-sexpr-search.sh index 2a8ad5f1..8800b545 100755 --- a/test/T081-sexpr-search.sh +++ b/test/T081-sexpr-search.sh @@ -2,7 +2,7 @@ test_description='"notmuch search" in several variations' . $(dirname "$0")/test-lib.sh || exit 1 -if [ $NOTMUCH_HAVE_SFSEXP -ne 1 ]; then +if [ "${NOTMUCH_HAVE_SFSEXP-0}" != "1" ]; then printf "Skipping due to missing sfsexp library\n" test_done fi @@ -31,6 +31,13 @@ thread:XXX 2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "and of stemmed terms" +notmuch search --query=sexp '(and wonderful wizard)' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "or of exact terms" notmuch search --query=sexp '(or "php" "wizard")' | notmuch_search_sanitize > OUTPUT cat < EXPECTED @@ -39,6 +46,14 @@ thread:XXX 2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "or of exact terms via field processor" +notmuch search 'sexp:"(or ""php"" ""wizard"")"' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2010-12-29 [1/1] François Boulogne; [aur-general] Guidelines: cp, mkdir vs install (inbox unread) +thread:XXX 2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "single term in body" notmuch search --query=sexp 'wizard' | notmuch_search_sanitize>OUTPUT cat < EXPECTED @@ -185,6 +200,50 @@ notmuch search folder:'""' > EXPECTED notmuch search --query=sexp '(folder "")' > OUTPUT test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "Search by 'folder' with --output=files" +output=$(notmuch search --output=files --query=sexp '(folder bad/news)' | notmuch_search_files_sanitize) +test_expect_equal "$output" "MAIL_DIR/bad/news/msg-XXX +MAIL_DIR/duplicate/bad/news/msg-XXX" + +test_begin_subtest "Search by 'folder' with --output=files (trailing /)" +output=$(notmuch search --output=files --query=sexp '(folder bad/news/)' | notmuch_search_files_sanitize) +test_expect_equal "$output" "MAIL_DIR/bad/news/msg-XXX +MAIL_DIR/duplicate/bad/news/msg-XXX" + +test_begin_subtest "Search by 'folder' (multiple)" +output=$(notmuch search --query=sexp '(folder bad bad/news things/bad)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread) +thread:XXX 2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Bites, stings, sad feelings (inbox unread)" + +test_begin_subtest "Search by 'folder' (multiple, trailing /)" +output=$(notmuch search --query=sexp '(folder bad bad/news/ things/bad)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread) +thread:XXX 2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Bites, stings, sad feelings (inbox unread)" + +test_begin_subtest "Search by 'path' with --output=files" +output=$(notmuch search --output=files --query=sexp '(path bad/news)' | notmuch_search_files_sanitize) +test_expect_equal "$output" "MAIL_DIR/bad/news/msg-XXX +MAIL_DIR/duplicate/bad/news/msg-XXX" + +test_begin_subtest "Search by 'path' with --output=files (trailing /)" +output=$(notmuch search --output=files --query=sexp '(path bad/news/)' | notmuch_search_files_sanitize) +test_expect_equal "$output" "MAIL_DIR/bad/news/msg-XXX +MAIL_DIR/duplicate/bad/news/msg-XXX" + +test_begin_subtest "Search by 'path' specification (multiple)" +output=$(notmuch search --query=sexp '(path bad bad/news things/bad)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread) +thread:XXX 2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Bites, stings, sad feelings (inbox unread)" + +test_begin_subtest "Search by 'path' specification (multiple, trailing /)" +output=$(notmuch search --query=sexp '(path bad bad/news/ things/bad)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread) +thread:XXX 2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Bites, stings, sad feelings (inbox unread)" + test_begin_subtest "Search by 'id'" add_message '[subject]="search by id"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' output=$(notmuch search --query=sexp "(id ${gen_msg_id})" | notmuch_search_sanitize) @@ -346,7 +405,7 @@ output=$(notmuch search --query=sexp '(attachment (starts-with not))' | notmuch_ test_expect_equal "$output" 'thread:XXX 2009-11-18 [2/2] Lars Kellogg-Stedman; [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread)' test_begin_subtest "starts-with, folder" -notmuch search --output=files --query=sexp '(folder (starts-with bad))' | notmuch_dir_sanitize | sed 's/[0-9]*$/XXX/' > OUTPUT +notmuch search --output=files --query=sexp '(folder (starts-with bad))' | notmuch_search_files_sanitize > OUTPUT cat < EXPECTED MAIL_DIR/bad/msg-XXX MAIL_DIR/bad/news/msg-XXX @@ -663,6 +722,11 @@ notmuch search property:foo=bar > EXPECTED notmuch search --query=sexp '(property (rx foo=.*))' > OUTPUT test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "regexp 'property' search via field processor" +notmuch search property:foo=bar > EXPECTED +notmuch search 'sexp:"(property (rx foo=.*))"' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "anchored 'tag' search" notmuch search tag:signed > EXPECTED notmuch search --query=sexp '(tag (rx ^si))' > OUTPUT @@ -699,6 +763,13 @@ thread:XXX 2009-11-18 [7/7] Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packa EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "Compound subquery via field processor" +notmuch search 'sexp:"(thread (of (from keithp) (subject Maildir)))"' | notmuch_search_sanitize > OUTPUT +cat< EXPECTED +thread:XXX 2009-11-18 [7/7] Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth; [notmuch] Working with Maildir storage? (inbox signed unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "empty subquery" notmuch search --query=sexp '(thread (of))' 1>OUTPUT 2>&1 notmuch search '*' > EXPECTED @@ -768,6 +839,214 @@ notmuch search date:2009-11-18..2009-11-18 and tag:unread > EXPECTED notmuch search --query=sexp '(and (infix "date:2009-11-18..2009-11-18") (infix "tag:unread"))' > OUTPUT test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "date query, empty" +notmuch search from:keithp | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (date) (from keithp))'| notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "date query, one argument" +notmuch search date:2009-11-18 and from:keithp | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (date 2009-11-18) (from keithp))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "date query, two arguments" +notmuch search date:2009-11-17..2009-11-18 and from:keithp | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (date 2009-11-17 2009-11-18) (from keithp))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "date query, lower bound only" +notmuch search date:2009-11-18.. and from:keithp | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (date 2009-11-18 "") (from keithp))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "date query, upper bound only" +notmuch search date:..2009-11-17 and from:keithp | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (date "" 2009-11-17) (from keithp))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "date query, lower bound only, using *" +notmuch search date:2009-11-18.. and from:keithp | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (date 2009-11-18 *) (from keithp))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "date query, upper bound only, using *" +notmuch search date:..2009-11-17 and from:keithp | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (date * 2009-11-17) (from keithp))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "date query, illegal nesting 1" +notmuch search --query=sexp '(to (date))' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +nested field: 'date' inside 'to' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "date query, illegal nesting 2" +notmuch search --query=sexp '(to (date 2021-11-18))' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +nested field: 'date' inside 'to' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "date query, illegal nesting 3" +notmuch search --query=sexp '(date (to))' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +expected atom as first argument of 'date' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "date query, illegal nesting 4" +notmuch search --query=sexp '(date today (to))' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +expected atom as second argument of 'date' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "date query, too many arguments" +notmuch search --query=sexp '(date yesterday and tommorow)' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'date' expects maximum of two arguments +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "date query, bad date" +notmuch search --query=sexp '(date "hawaiian-pizza-day")' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +Didn't understand date specification 'hawaiian-pizza-day' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, empty" +notmuch search from:keithp | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (lastmod) (from keithp))'| notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, one argument" +notmuch tag +4EFC743A.3060609@april.org id:4EFC743A.3060609@april.org +revision=$(notmuch count --lastmod '*' | cut -f3) +notmuch search lastmod:$revision..$revision | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(and (lastmod $revision))" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, one argument (negative)" +notmuch tag +4EFC743A.3060609@april.org id:4EFC743A.3060609@april.org +revision=$(notmuch count --lastmod '*' | cut -f3) +revision1=$((revision - 1)) +notmuch search lastmod:$revision1..$revision1 | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(lastmod -1)" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "lastmod query, two arguments" +notmuch tag +keithp from:keithp +revision2=$(notmuch count --lastmod '*' | cut -f3) +notmuch search lastmod:$revision..$revision2 | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(and (lastmod $revision $revision2))" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, two arguments, first negative" +revdiff=$((revision2 - revision)) +notmuch search lastmod:$revision..$revision2 | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(lastmod -$revdiff $revision2)" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, two arguments, second negative" +revdiff=$((revision2 - revision)) +notmuch search lastmod:..$revision | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(lastmod 0 -$revdiff)" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, lower bound only" +notmuch search lastmod:$revision.. | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(lastmod $revision \"\")" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "lastmod query, lower bound only (negative)" +notmuch search lastmod:$revision.. | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(lastmod -$revdiff \"\")" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "lastmod query, upper bound only" +notmuch search lastmod:..$revision2 | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(lastmod \"\" $revision2)" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "lastmod query, upper bound only (negative)" +notmuch search lastmod:..$revision | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(lastmod \"\" -$revdiff)" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "lastmod query, lower bound only, using *" +notmuch search lastmod:$revision.. | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(lastmod $revision *)" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "lastmod query, upper bound only, using *" +notmuch search lastmod:..$revision2 | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp "(lastmod * $revision2)" | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "lastmod query, illegal nesting 1" +notmuch search --query=sexp '(to (lastmod))' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +nested field: 'lastmod' inside 'to' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, bad from revision" +notmuch search --query=sexp '(lastmod apples)' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +bad 'from' revision: 'apples' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, bad to revision" +notmuch search --query=sexp '(lastmod 0 apples)' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +bad 'to' revision: 'apples' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, illegal nesting 2" +notmuch search --query=sexp '(to (lastmod 2021-11-18))' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +nested field: 'lastmod' inside 'to' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, illegal nesting 3" +notmuch search --query=sexp '(lastmod (to))' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +expected atom as first argument of 'lastmod' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, illegal nesting 4" +notmuch search --query=sexp '(lastmod today (to))' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +expected atom as second argument of 'lastmod' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "lastmod query, too many arguments" +notmuch search --query=sexp '(lastmod yesterday and tommorow)' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'lastmod' expects maximum of two arguments +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "user header (unknown header)" notmuch search --query=sexp '(FooBar)' >& OUTPUT cat < EXPECTED @@ -787,6 +1066,11 @@ grep -Ril List-Id ${MAIL_DIR} | sort | notmuch_dir_sanitize > EXPECTED notmuch search --output=files --query=sexp '(List *)' | sort | notmuch_dir_sanitize > OUTPUT test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "wildcard search for user header via field processor" +grep -Ril List-Id ${MAIL_DIR} | sort | notmuch_dir_sanitize > EXPECTED +notmuch search --output=files 'sexp:"(List *)"' | sort | notmuch_dir_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "wildcard search for user header 2" grep -Ril List-Id ${MAIL_DIR} | sort | notmuch_dir_sanitize > EXPECTED notmuch search --output=files --query=sexp '(List (starts-with not))' | sort | notmuch_dir_sanitize > OUTPUT @@ -901,6 +1185,32 @@ too many arguments to macro EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "Saved Search: bad parameter syntax 5" +notmuch config set squery.Bad5 '(macro (thing) (tag (rx ,thing)))' +notmuch search --query=sexp '(Bad5 (1 2))' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'rx' expects single atom as argument +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Saved Search: bad parameter syntax 6" +notmuch config set squery.Bad6 '(macro (thing) (tag (starts-with ,thing)))' +notmuch search --query=sexp '(Bad6 (1 2))' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'starts-with' expects single atom as argument +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Saved Search: bad parameter syntax 7" +notmuch search --query=sexp '(subject (rx ,unknown))' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +undefined parameter 'unknown' +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "Saved Search: macro without body" notmuch config set squery.Bad3 '(macro (a b))' notmuch search --query=sexp '(Bad3)' >OUTPUT 2>&1 @@ -930,7 +1240,7 @@ notmuch config set squery.Bad6 '(macro (a) (and ,b (subject maildir)))' notmuch search --query=sexp '(Bad6 foo)' >OUTPUT 2>&1 cat < EXPECTED notmuch search: Syntax error in query -undefined parameter b +undefined parameter 'b' EOF test_expect_equal_file EXPECTED OUTPUT @@ -952,6 +1262,18 @@ notmuch config set squery.TagSubject2 '(macro (tagname subj) (and (tag ,tagname notmuch search --query=sexp '(TagSubject2 inbox maildir)' | notmuch_search_sanitize > OUTPUT test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "macro in regex" +notmuch search tag:inbox and date:2009-11-17 | notmuch_search_sanitize > EXPECTED +notmuch config set squery.D '(macro (tagname) (and (date 2009-11-17) (tag (rx ,tagname))))' +notmuch search --query=sexp '(D inbo)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "macro in wildcard" +notmuch search tag:inbox and date:2009-11-17 | notmuch_search_sanitize > EXPECTED +notmuch config set squery.W '(macro (tagname) (and (date 2009-11-17) (tag (starts-with ,tagname))))' +notmuch search --query=sexp '(W inbo)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + test_begin_subtest "nested macros (shadowing)" notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED notmuch config set squery.Inner '(macro (x) (subject ,x))' @@ -965,10 +1287,24 @@ notmuch config set squery.Outer2 '(macro (x y) (and (tag ,x) (Inner2 ,y)))' notmuch search --query=sexp '(Outer2 inbox maildir)' > OUTPUT 2>&1 cat < EXPECTED notmuch search: Syntax error in query -undefined parameter y +undefined parameter 'y' EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "nested macros (shadowing, regex)" +notmuch search tag:/inbo/ and subject:/Maildi/ | notmuch_search_sanitize > EXPECTED +notmuch config set squery.Inner3 '(macro (x) (subject (rx ,x)))' +notmuch config set squery.Outer3 '(macro (x y) (and (tag (rx ,x)) (Inner3 ,y)))' +notmuch search --query=sexp '(Outer3 inbo Maildi)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "nested macros (shadowing, wildcard)" +notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED +notmuch config set squery.Inner4 '(macro (x) (subject (starts-with ,x)))' +notmuch config set squery.Outer4 '(macro (x y) (and (tag (starts-with ,x)) (Inner4 ,y)))' +notmuch search --query=sexp '(Outer4 inbo maildi)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + test_begin_subtest "combine macro and user defined header" notmuch config set squery.About '(macro (name) (or (subject ,name) (List ,name)))' notmuch search subject:notmuch or List:notmuch | notmuch_search_sanitize > EXPECTED diff --git a/test/T090-search-output.sh b/test/T090-search-output.sh index bf28d220..0d85c609 100755 --- a/test/T090-search-output.sh +++ b/test/T090-search-output.sh @@ -435,7 +435,7 @@ test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "search for non-existent message prints nothing" notmuch search "no-message-matches-this" > OUTPUT -echo -n >EXPECTED +: >EXPECTED test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "search --format=json for non-existent message prints proper empty json" diff --git a/test/T095-address.sh b/test/T095-address.sh index 8bb3627a..0cafbe20 100755 --- a/test/T095-address.sh +++ b/test/T095-address.sh @@ -325,7 +325,7 @@ cat <EXPECTED EOF test_expect_equal_file EXPECTED OUTPUT -if [[ NOTMUCH_HAVE_SFSEXP = 1 ]]; then +if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then test_begin_subtest "sexpr query: all messages" notmuch address '*' > EXPECTED notmuch address --query=sexp '()' > OUTPUT diff --git a/test/T100-search-by-folder.sh b/test/T100-search-by-folder.sh index 409cfdcc..b4f6294e 100755 --- a/test/T100-search-by-folder.sh +++ b/test/T100-search-by-folder.sh @@ -18,6 +18,12 @@ test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; T thread:XXX 2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread) thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Bites, stings, sad feelings (inbox unread)" +test_begin_subtest "search by path: specification (multiple)" +output=$(notmuch search path:bad path:bad/news path:things/bad | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread) +thread:XXX 2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Bites, stings, sad feelings (inbox unread)" + test_begin_subtest "Top level folder" output=$(notmuch search folder:'""' | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Top level (inbox unread)" @@ -28,8 +34,13 @@ test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1(2)] Notmuch Test Suite test_begin_subtest "Folder search with --output=files" output=$(notmuch search --output=files folder:bad/news | notmuch_search_files_sanitize) -test_expect_equal "$output" "MAIL_DIR/bad/news/msg-003 -MAIL_DIR/duplicate/bad/news/msg-003" +test_expect_equal "$output" "MAIL_DIR/bad/news/msg-XXX +MAIL_DIR/duplicate/bad/news/msg-XXX" + +test_begin_subtest "Folder search with --output=files (trailing /)" +output=$(notmuch search --output=files folder:bad/news/ | notmuch_search_files_sanitize) +test_expect_equal "$output" "MAIL_DIR/bad/news/msg-XXX +MAIL_DIR/duplicate/bad/news/msg-XXX" test_begin_subtest "After removing duplicate instance of matching path" rm -r "${MAIL_DIR}/bad/news" @@ -39,7 +50,7 @@ test_expect_equal "$output" "" test_begin_subtest "Folder search with --output=files part #2" output=$(notmuch search --output=files folder:duplicate/bad/news | notmuch_search_files_sanitize) -test_expect_equal "$output" "MAIL_DIR/duplicate/bad/news/msg-003" +test_expect_equal "$output" "MAIL_DIR/duplicate/bad/news/msg-XXX" test_begin_subtest "After removing duplicate instance of matching path part #2" output=$(notmuch search folder:duplicate/bad/news | notmuch_search_sanitize) @@ -120,6 +131,13 @@ test_expect_equal "$output" "MAIL_DIR/bar/17:2, MAIL_DIR/bar/18:2, MAIL_DIR/cur/51:2," +test_begin_subtest "path: search (trailing /)" +output=$(notmuch search --output=files path:"bar/" | notmuch_search_files_sanitize | sort) +# cur/51:2, is a duplicate of bar/18:2, +test_expect_equal "$output" "MAIL_DIR/bar/17:2, +MAIL_DIR/bar/18:2, +MAIL_DIR/cur/51:2," + test_begin_subtest "top level path: search" output=$(notmuch search --output=files path:'""' | notmuch_search_files_sanitize | sort) test_expect_equal "$output" "MAIL_DIR/01:2, diff --git a/test/T131-show-limiting.sh b/test/T131-show-limiting.sh new file mode 100755 index 00000000..30d1f254 --- /dev/null +++ b/test/T131-show-limiting.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +test_description='"notmuch show" --offset and --limit parameters' +. $(dirname "$0")/test-lib.sh || exit 1 + +add_email_corpus + +show () { + local kind="$1" + shift + if [ "$kind" = messages ]; then + set -- --unthreaded "$@" + fi + notmuch show --body=false --format=text --entire-thread=false "$@" "*" | + sed -nre 's/^.message\{.*\.*/&/p' +} + +for outp in messages threads; do + test_begin_subtest "$outp: limit does the right thing" + show $outp | head -n 20 >expected + show $outp --limit=20 >output + test_expect_equal_file expected output + + test_begin_subtest "$outp: concatenation of limited shows" + show $outp | head -n 20 >expected + show $outp --limit=10 >output + show $outp --limit=10 --offset=10 >>output + test_expect_equal_file expected output + + test_begin_subtest "$outp: limit larger than result set" + N=$(notmuch count --output=$outp "*") + show $outp >expected + show $outp --limit=$((1 + N)) >output + test_expect_equal_file expected output + + test_begin_subtest "$outp: limit = 0" + test_expect_equal "$(show $outp --limit=0)" "" + + test_begin_subtest "$outp: offset does the right thing" + # note: tail -n +N is 1-based + show $outp | tail -n +21 >expected + show $outp --offset=20 >output + test_expect_equal_file expected output + + test_begin_subtest "$outp: offset = 0" + show $outp >expected + show $outp --offset=0 >output + test_expect_equal_file expected output + + test_begin_subtest "$outp: negative offset" + show $outp | tail -n 20 >expected + show $outp --offset=-20 >output + test_expect_equal_file expected output + + test_begin_subtest "$outp: negative offset" + show $outp | tail -n 1 >expected + show $outp --offset=-1 >output + test_expect_equal_file expected output + + test_begin_subtest "$outp: negative offset combined with limit" + show $outp | tail -n 20 | head -n 10 >expected + show $outp --offset=-20 --limit=10 >output + test_expect_equal_file expected output + + test_begin_subtest "$outp: negative offset combined with equal limit" + show $outp | tail -n 20 >expected + show $outp --offset=-20 --limit=20 >output + test_expect_equal_file expected output + + test_begin_subtest "$outp: negative offset combined with large limit" + show $outp | tail -n 20 >expected + show $outp --offset=-20 --limit=50 >output + test_expect_equal_file expected output + + test_begin_subtest "$outp: negative offset larger than results" + N=$(notmuch count --output=$outp "*") + show $outp >expected + show $outp --offset=-$((1 + N)) >output + test_expect_equal_file expected output +done + +test_done diff --git a/test/T150-tagging.sh b/test/T150-tagging.sh index 1a2fd77e..273c0af3 100755 --- a/test/T150-tagging.sh +++ b/test/T150-tagging.sh @@ -320,6 +320,7 @@ test_begin_subtest "Tag name beginning with -" test_expect_code 1 'notmuch tag +- One' test_begin_subtest "Xapian exception: read only files" +test_subtest_broken_for_root chmod u-w ${MAIL_DIR}/.notmuch/xapian/*.* output=$(notmuch tag +something '*' 2>&1 | sed 's/: .*$//' ) chmod u+w ${MAIL_DIR}/.notmuch/xapian/*.* @@ -327,7 +328,7 @@ test_expect_equal "$output" "A Xapian exception occurred opening database" add_email_corpus -if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then +if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then test_query_syntax '(and "wonderful" "wizard")' 'wonderful and wizard' test_query_syntax '(or "php" "wizard")' 'php or wizard' diff --git a/test/T160-json.sh b/test/T160-json.sh index 6a3e5812..318c9788 100755 --- a/test/T160-json.sh +++ b/test/T160-json.sh @@ -5,17 +5,17 @@ test_description="--format=json output" test_begin_subtest "Show message: json" add_message "[subject]=\"json-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[bcc]=\"test_suite+bcc@notmuchmail.org\"" "[reply-to]=\"test_suite+replyto@notmuchmail.org\"" "[body]=\"json-show-message\"" -output=$(notmuch show --format=json "json-show-message") -test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"${gen_msg_filename}\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite \", \"To\": \"Notmuch Test Suite \", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]" +output=$(notmuch show --format=json "json-show-message" | notmuch_json_show_sanitize) +test_expect_equal_json "$output" "[[[{\"id\": \"XXXXX\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"YYYYY\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite \", \"To\": \"Notmuch Test Suite \", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]" # This should be the same output as above. test_begin_subtest "Show message: json --body=true" -output=$(notmuch show --format=json --body=true "json-show-message") -test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"${gen_msg_filename}\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite \", \"To\": \"Notmuch Test Suite \", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]" +output=$(notmuch show --format=json --body=true "json-show-message" | notmuch_json_show_sanitize) +test_expect_equal_json "$output" "[[[{\"id\": \"XXXXX\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"YYYYY\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite \", \"To\": \"Notmuch Test Suite \", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]" test_begin_subtest "Show message: json --body=false" -output=$(notmuch show --format=json --body=false "json-show-message") -test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"${gen_msg_filename}\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite \", \"To\": \"Notmuch Test Suite \", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}}, []]]]" +output=$(notmuch show --format=json --body=false "json-show-message" | notmuch_json_show_sanitize) +test_expect_equal_json "$output" "[[[{\"id\": \"XXXXX\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"YYYYY\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite \", \"To\": \"Notmuch Test Suite \", \"Bcc\": \"test_suite+bcc@notmuchmail.org\", \"Reply-To\": \"test_suite+replyto@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}}, []]]]" test_begin_subtest "Search message: json" add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\"" @@ -33,8 +33,8 @@ test_expect_equal_json "$output" "[{\"thread\": \"XXX\", test_begin_subtest "Show message: json, utf-8" add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\"" -output=$(notmuch show --format=json "jsön-show-méssage") -test_expect_equal_json "$output" "[[[{\"id\": \"${gen_msg_id}\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"${gen_msg_filename}\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-utf8-body-sübjéct\", \"From\": \"Notmuch Test Suite \", \"To\": \"Notmuch Test Suite \", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"jsön-show-méssage\n\"}]}, []]]]" +output=$(notmuch show --format=json "jsön-show-méssage" | notmuch_json_show_sanitize) +test_expect_equal_json "$output" "[[[{\"id\": \"XXXXX\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"YYYYY\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-utf8-body-sübjéct\", \"From\": \"Notmuch Test Suite \", \"To\": \"Notmuch Test Suite \", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"jsön-show-méssage\n\"}]}, []]]]" test_begin_subtest "Show message: json, inline attachment filename" subject='json-show-inline-attachment-filename' @@ -49,7 +49,7 @@ output=$(notmuch show --format=json "id:$id") filename=$(notmuch search --output=files "id:$id") # Get length of README after base64-encoding, minus additional newline. attachment_length=$(( $(base64 $NOTMUCH_SRCDIR/test/README | wc -c) - 1 )) -test_expect_equal_json "$output" "[[[{\"id\": \"$id\", \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"$filename\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\"], \"headers\": {\"Subject\": \"$subject\", \"From\": \"Notmuch Test Suite \", \"To\": \"test_suite@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"multipart/mixed\", \"content\": [{\"id\": 2, \"content-type\": \"text/plain\", \"content\": \"This is a test message with inline attachment with a filename\"}, {\"id\": 3, \"content-type\": \"application/octet-stream\", \"content-length\": $attachment_length, \"content-transfer-encoding\": \"base64\", \"content-disposition\": \"inline\", \"filename\": \"README\"}]}]}, []]]]" +test_expect_equal_json "$output" "[[[{\"id\": \"$id\", \"duplicate\": 1, \"crypto\": {}, \"match\": true, \"excluded\": false, \"filename\": [\"$filename\"], \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\"], \"headers\": {\"Subject\": \"$subject\", \"From\": \"Notmuch Test Suite \", \"To\": \"test_suite@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"multipart/mixed\", \"content\": [{\"id\": 2, \"content-type\": \"text/plain\", \"content\": \"This is a test message with inline attachment with a filename\"}, {\"id\": 3, \"content-type\": \"application/octet-stream\", \"content-length\": $attachment_length, \"content-transfer-encoding\": \"base64\", \"content-disposition\": \"inline\", \"filename\": \"README\"}]}]}, []]]]" test_begin_subtest "Search message: json, utf-8" add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\"" @@ -65,8 +65,9 @@ test_expect_equal_json "$output" "[{\"thread\": \"XXX\", \"tags\": [\"inbox\", \"unread\"]}]" +if [ -z "${NOTMUCH_TEST_INSTALLED-}" ]; then test_begin_subtest "Search message: json, 64-bit timestamp" -if [ $NOTMUCH_HAVE_64BIT_TIME_T -ne 1 ]; then +if [ "${NOTMUCH_HAVE_64BIT_TIME_T-0}" != "1" ]; then test_subtest_known_broken fi add_message "[subject]=\"json-search-64bit-timestamp-subject\"" "[date]=\"Tue, 01 Jan 2999 12:00:00 -0000\"" "[body]=\"json-search-64bit-timestamp-message\"" @@ -81,6 +82,7 @@ test_expect_equal_json "$output" "[{\"thread\": \"XXX\", \"query\": [\"id:$gen_msg_id\", null], \"tags\": [\"inbox\", \"unread\"]}]" +fi # NOTMUCH_TEST_INSTALLED undefined / empty test_begin_subtest "Format version: too low" test_expect_code 20 "notmuch search --format-version=0 \\*" @@ -97,6 +99,7 @@ cat < EXPECTED [ { "date_relative": "2001-01-05", + "duplicate": 1, "excluded": false, "filename": [ "${MAIL_DIR}/copy1", @@ -132,6 +135,7 @@ cat < EXPECTED [ { "date_relative": "2001-01-05", + "duplicate": 1, "excluded": false, "filename": "${MAIL_DIR}/copy1", "headers": { @@ -156,4 +160,46 @@ EOF output=$(notmuch show --format=json --body=false --format-version=2 id:message-id@example.com) test_expect_equal_json "$output" "$(cat EXPECTED)" +test_begin_subtest "show extra headers" +add_message "[subject]=\"extra-headers\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[in-reply-to]=\"\"" "[body]=\"extra-headers test\""\ + "[header]=\"Received: from mail.example.com (mail.example.com [1.1.1.1]) + by mail.notmuchmail.org (some MTA) with ESMTP id 12345678 + for ; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)\"" \ + +notmuch config set show.extra_headers "in-reply-to;received" +output=$(notmuch show --format=json --body=false id:${gen_msg_id} | notmuch_json_show_sanitize) +cat < EXPECTED +[ + [ + [ + { + "crypto": {}, + "date_relative": "2000-01-01", + "excluded": false, + "filename": [ + "YYYYY" + ], + "headers": { + "Date": "Sat, 01 Jan 2000 12:00:00 +0000", + "From": "Notmuch Test Suite ", + "In-Reply-To": "", + "Received": "from mail.example.com (mail.example.com [1.1.1.1])\tby mail.notmuchmail.org (some MTA) with ESMTP id 12345678\tfor ; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)", + "Subject": "extra-headers", + "To": "Notmuch Test Suite " + }, + "id": "XXXXX", + "match": true, + "tags": [ + "inbox", + "unread" + ], + "timestamp": 946728000 + }, + [] + ] + ] +] +EOF +test_expect_equal_json "${output}" "$(cat EXPECTED)" + test_done diff --git a/test/T170-sexp.sh b/test/T170-sexp.sh index 18084273..0be94bd2 100755 --- a/test/T170-sexp.sh +++ b/test/T170-sexp.sh @@ -5,32 +5,32 @@ test_description="--format=sexp output" test_begin_subtest "Show message: sexp" add_message "[subject]=\"sexp-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[bcc]=\"test_suite+bcc@notmuchmail.org\"" "[reply-to]=\"test_suite+replyto@notmuchmail.org\"" "[body]=\"sexp-show-message\"" -output=$(notmuch show --format=sexp "sexp-show-message") -test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename (\"${gen_msg_filename}\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"sexp-show-message\n\")) :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite \" :To \"Notmuch Test Suite \" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))" +output=$(notmuch show --format=sexp "sexp-show-message" | notmuch_sexp_show_sanitize) +test_expect_equal "$output" "((((:id \"XXXXX\" :match t :excluded nil :filename (\"YYYYY\") :timestamp 42 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"sexp-show-message\n\")) :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite \" :To \"Notmuch Test Suite \" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"GENERATED_DATE\")) ())))" # This should be the same output as above. test_begin_subtest "Show message: sexp --body=true" -output=$(notmuch show --format=sexp --body=true "sexp-show-message") -test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename (\"${gen_msg_filename}\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"sexp-show-message\n\")) :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite \" :To \"Notmuch Test Suite \" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))" +output=$(notmuch show --format=sexp --body=true "sexp-show-message" | notmuch_sexp_show_sanitize) +test_expect_equal "$output" "((((:id \"XXXXX\" :match t :excluded nil :filename (\"YYYYY\") :timestamp 42 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"sexp-show-message\n\")) :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite \" :To \"Notmuch Test Suite \" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"GENERATED_DATE\")) ())))" test_begin_subtest "Show message: sexp --body=false" -output=$(notmuch show --format=sexp --body=false "sexp-show-message") -test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename (\"${gen_msg_filename}\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite \" :To \"Notmuch Test Suite \" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))" +output=$(notmuch show --format=sexp --body=false "sexp-show-message" | notmuch_sexp_show_sanitize) +test_expect_equal "$output" "((((:id \"XXXXX\" :match t :excluded nil :filename (\"YYYYY\") :timestamp 42 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :crypto () :headers (:Subject \"sexp-show-subject\" :From \"Notmuch Test Suite \" :To \"Notmuch Test Suite \" :Bcc \"test_suite+bcc@notmuchmail.org\" :Reply-To \"test_suite+replyto@notmuchmail.org\" :Date \"GENERATED_DATE\")) ())))" test_begin_subtest "Search message: sexp" add_message "[subject]=\"sexp-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"sexp-search-message\"" -output=$(notmuch search --format=sexp "sexp-search-message" | notmuch_search_sanitize) -test_expect_equal "$output" "((:thread \"0000000000000002\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-subject\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))" +output=$(notmuch search --format=sexp "sexp-search-message" | notmuch_sexp_search_sanitize) +test_expect_equal "$output" "((:thread \"XXX\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-subject\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))" test_begin_subtest "Show message: sexp, utf-8" add_message "[subject]=\"sexp-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\"" -output=$(notmuch show --format=sexp "jsön-show-méssage") -test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename (\"${gen_msg_filename}\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"jsön-show-méssage\n\")) :crypto () :headers (:Subject \"sexp-show-utf8-body-sübjéct\" :From \"Notmuch Test Suite \" :To \"Notmuch Test Suite \" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))" +output=$(notmuch show --format=sexp "jsön-show-méssage" | notmuch_sexp_show_sanitize) +test_expect_equal "$output" "((((:id \"XXXXX\" :match t :excluded nil :filename (\"YYYYY\") :timestamp 42 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :body ((:id 1 :content-type \"text/plain\" :content \"jsön-show-méssage\n\")) :crypto () :headers (:Subject \"sexp-show-utf8-body-sübjéct\" :From \"Notmuch Test Suite \" :To \"Notmuch Test Suite \" :Date \"GENERATED_DATE\")) ())))" test_begin_subtest "Search message: sexp, utf-8" add_message "[subject]=\"sexp-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\"" -output=$(notmuch search --format=sexp "jsön-search-méssage" | notmuch_search_sanitize) -test_expect_equal "$output" "((:thread \"0000000000000004\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-utf8-body-sübjéct\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))" +output=$(notmuch search --format=sexp "jsön-search-méssage" | notmuch_sexp_search_sanitize) +test_expect_equal "$output" "((:thread \"XXX\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-utf8-body-sübjéct\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))" test_begin_subtest "Show message: sexp, inline attachment filename" subject='sexp-show-inline-attachment-filename' @@ -45,6 +45,19 @@ output=$(notmuch show --format=sexp "id:$id") filename=$(notmuch search --output=files "id:$id") # Get length of README after base64-encoding, minus additional newline. attachment_length=$(( $(base64 $NOTMUCH_SRCDIR/test/README | wc -c) - 1 )) -test_expect_equal "$output" "((((:id \"$id\" :match t :excluded nil :filename (\"$filename\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\") :body ((:id 1 :content-type \"multipart/mixed\" :content ((:id 2 :content-type \"text/plain\" :content \"This is a test message with inline attachment with a filename\") (:id 3 :content-type \"application/octet-stream\" :content-disposition \"inline\" :filename \"README\" :content-transfer-encoding \"base64\" :content-length $attachment_length)))) :crypto () :headers (:Subject \"sexp-show-inline-attachment-filename\" :From \"Notmuch Test Suite \" :To \"test_suite@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))" +test_expect_equal "$output" "((((:id \"$id\" :match t :excluded nil :filename (\"$filename\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\") :duplicate 1 :body ((:id 1 :content-type \"multipart/mixed\" :content ((:id 2 :content-type \"text/plain\" :content \"This is a test message with inline attachment with a filename\") (:id 3 :content-type \"application/octet-stream\" :content-disposition \"inline\" :filename \"README\" :content-transfer-encoding \"base64\" :content-length $attachment_length)))) :crypto () :headers (:Subject \"sexp-show-inline-attachment-filename\" :From \"Notmuch Test Suite \" :To \"test_suite@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\")) ())))" + +test_begin_subtest "show extra headers" +add_message "[subject]=\"extra-headers\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[in-reply-to]=\"\"" "[body]=\"extra-headers test\""\ + "[header]=\"Received: from mail.example.com (mail.example.com [1.1.1.1]) + by mail.notmuchmail.org (some MTA) with ESMTP id 12345678 + for ; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)\"" \ + +notmuch config set show.extra_headers "in-reply-to;received" +notmuch show --format=sexp --body=false id:${gen_msg_id} | notmuch_sexp_show_sanitize > OUTPUT +cat < EXPECTED +((((:id "XXXXX" :match t :excluded nil :filename ("YYYYY") :timestamp 42 :date_relative "2000-01-01" :tags ("inbox" "unread") :crypto () :headers (:Subject "extra-headers" :From "Notmuch Test Suite " :To "Notmuch Test Suite " :Date "GENERATED_DATE" :In-Reply-To "" :Received "from mail.example.com (mail.example.com [1.1.1.1])\011by mail.notmuchmail.org (some MTA) with ESMTP id 12345678\011for ; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)")) ()))) +EOF +test_expect_equal_file EXPECTED OUTPUT test_done diff --git a/test/T190-multipart.sh b/test/T190-multipart.sh index 3545a599..cfe48ac5 100755 --- a/test/T190-multipart.sh +++ b/test/T190-multipart.sh @@ -376,18 +376,18 @@ test_begin_subtest "--format=text --part=8, no part, expect error" test_expect_success "notmuch show --format=text --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org'" test_begin_subtest "--format=json --part=0, full message" -notmuch show --format=json --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +notmuch show --format=json --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' | notmuch_json_show_sanitize >OUTPUT cat <EXPECTED -{"id": "87liy5ap00.fsf@yoom.home.cworth.org", "crypto": {}, "match": true, "excluded": false, "filename": ["${MAIL_DIR}/multipart"], "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["attachment","inbox","signed","unread"], "headers": {"Subject": "Multipart message", "From": "Carl Worth ", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [ +{"id": "XXXXX", "crypto": {}, "match": true, "excluded": false, "filename": ["YYYYY"], "timestamp": 42, "date_relative": "2001-01-05", "tags": ["attachment","inbox","signed","unread"], "headers": {"Subject": "Multipart message", "From": "Carl Worth ", "To": "cworth@cworth.org", "Date": "GENERATED_DATE"}, "body": [ {"id": 1, "content-type": "multipart/signed", "content": [ {"id": 2, "content-type": "multipart/mixed", "content": [ -{"id": 3, "content-type": "message/rfc822", "content-disposition": "inline", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth ", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [ +{"id": 3, "content-type": "message/rfc822", "content-disposition": "inline", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth ", "To": "cworth@cworth.org", "Date": "GENERATED_DATE"}, "body": [ {"id": 4, "content-type": "multipart/alternative", "content": [ -{"id": 5, "content-type": "text/html", "content-length": 71}, +{"id": 5, "content-type": "text/html", "content-length": "NONZERO"}, {"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}, {"id": 7, "content-type": "text/plain", "content-disposition": "attachment", "filename": "attachment", "content": "This is a text attachment.\n"}, {"id": 8, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"}]}, -{"id": 9, "content-type": "application/pgp-signature", "content-length": 197}]}]} +{"id": 9, "content-type": "application/pgp-signature", "content-length": "NONZERO"}]}]} EOF test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)" @@ -485,7 +485,7 @@ notmuch show --format=raw 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT test_expect_equal_file "${MAIL_DIR}"/multipart OUTPUT test_begin_subtest "--format=raw --part=0, full message" -notmuch show --format=raw --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +notmuch show --format=raw --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' | notmuch_json_show_sanitize >OUTPUT test_expect_equal_file "${MAIL_DIR}"/multipart OUTPUT test_begin_subtest "--format=raw --part=1, message body" @@ -683,7 +683,7 @@ test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)" test_begin_subtest "'notmuch show --part' does not corrupt a part with CRLF pair" notmuch show --format=raw --part=3 id:base64-part-with-crlf > crlf.out -echo -n -e "\xEF\x0D\x0A" > crlf.expected +printf "\xEF\x0D\x0A" > crlf.expected test_expect_equal_file crlf.out crlf.expected @@ -727,10 +727,10 @@ notmuch new > /dev/null cat_expected_head () { cat <", "Subject": "html message", "To": "B "}, @@ -742,8 +742,8 @@ EOF cat_expected_head > EXPECTED.nohtml cat <> EXPECTED.nohtml "content": [ - { "id": 2, "content-charset": "UTF-8", "content-length": 21, "content-type": "text/html"}, - { "id": 3, "content-charset": "ISO-8859-1", "content-length": 20, "content-type": "text/html"}, + { "id": 2, "content-charset": "UTF-8", "content-length": "NONZERO", "content-type": "text/html"}, + { "id": 3, "content-charset": "ISO-8859-1", "content-length": "NONZERO", "content-type": "text/html"}, { "id": 4, "content-type": "text/plain", "content": "0.5 equals \\u00bd\\n"} ]}]},[]]]] EOF @@ -759,11 +759,11 @@ cat <> EXPECTED.withhtml EOF test_begin_subtest "html parts excluded by default" -notmuch show --format=json id:htmlmessage > OUTPUT +notmuch show --format=json id:htmlmessage | notmuch_json_show_sanitize > OUTPUT test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED.nohtml)" test_begin_subtest "html parts included" -notmuch show --format=json --include-html id:htmlmessage > OUTPUT +notmuch show --format=json --include-html id:htmlmessage | notmuch_json_show_sanitize > OUTPUT test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED.withhtml)" test_begin_subtest "indexes mime-type #1" diff --git a/test/T206-author-separator.sh b/test/T206-author-separator.sh new file mode 100755 index 00000000..b8b1afb4 --- /dev/null +++ b/test/T206-author-separator.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +test_description="custom separator for authors" +. $(dirname "$0")/test-lib.sh || exit 1 + +test_begin_subtest "Adding parent message" +generate_message [body]=findme [id]=new-parent-id [subject]=author-reorder-threadtest '[from]="User "' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +output=$(NOTMUCH_NEW) +test_expect_equal "$output" "Added 1 new message to the database." + +test_begin_subtest "Adding initial child message" +generate_message [body]=findme "[in-reply-to]=\" [subject]=author-reorder-threadtest '[from]="last name, first name "' '[date]="Sat, 01 Jan 2000 12:01:00 -0000"' +output=$(NOTMUCH_NEW) +test_expect_equal "$output" "Added 1 new message to the database." + +test_begin_subtest "Adding second child message" +generate_message [body]=findme "[in-reply-to]=\" [subject]=author-reorder-threadtest '[from]="first last "' '[date]="Sat, 01 Jan 2000 12:02:00 -0000"' +output=$(NOTMUCH_NEW) +test_expect_equal "$output" "Added 1 new message to the database." + +test_begin_subtest "Custom separarator is used" +notmuch config set search.authors_matched_separator "#" +notmuch config set search.authors_separator ";" +output=$(notmuch search from:user | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/3] User#last name, first name;first last; author-reorder-threadtest (inbox unread)" + +test_done diff --git a/test/T210-raw.sh b/test/T210-raw.sh index e1d50bf9..44082028 100755 --- a/test/T210-raw.sh +++ b/test/T210-raw.sh @@ -64,4 +64,15 @@ for pow in {10..20}; do test_expect_success "notmuch show --format=raw subject:$size > /dev/null" done +add_email_corpus duplicate +ID=87r2ecrr6x.fsf@zephyr.silentflame.com +test_begin_subtest "raw content, duplicate files" +rm -f OUTPUT.raw +for dup in {1..5}; do + notmuch show --format=raw --duplicate=${dup} --format=raw id:${ID} | md5sum | cut -f1 -d' ' >> OUTPUT.raw +done +sort OUTPUT.raw > OUTPUT +notmuch search --output=files id:${ID} | xargs md5sum | cut -f1 -d ' ' | sort > EXPECTED +test_expect_equal_file_nonempty EXPECTED OUTPUT + test_done diff --git a/test/T220-reply.sh b/test/T220-reply.sh index 4e9984d2..120d7138 100755 --- a/test/T220-reply.sh +++ b/test/T220-reply.sh @@ -24,7 +24,7 @@ test_begin_subtest "Basic reply" notmuch reply id:${gen_msg_id} >OUTPUT 2>&1 && echo OK >> OUTPUT test_expect_equal_file basic.expected OUTPUT -if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then +if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then test_begin_subtest "Basic reply (query=sexp)" notmuch reply --query=sexp "(id ${gen_msg_id})" >OUTPUT 2>&1 && echo OK >> OUTPUT @@ -298,7 +298,8 @@ On Tue, 05 Jan 2010 15:43:56 -0000, ☃ wrote: OK" test_begin_subtest "Reply with RFC 2047-encoded headers (JSON)" -output=$(echo '{"answer":' && notmuch reply --format=json id:${gen_msg_id} 2>&1 && echo ', "success": "OK"}') +output=$(echo '{"answer":' && notmuch reply --format=json id:${gen_msg_id} 2>&1 | notmuch_json_show_sanitize \ + && echo ', "success": "OK"}') test_expect_equal_json "$output" ' { "answer": { "original": { @@ -312,14 +313,14 @@ test_expect_equal_json "$output" ' "crypto": {}, "date_relative": "2010-01-05", "excluded": false, - "filename": ["'${MAIL_DIR}'/msg-015"], + "filename": ["YYYYY"], "headers": { "Date": "Tue, 05 Jan 2010 15:43:56 +0000", "From": "\u2603 ", "Subject": "\u00e0\u00df\u00e7", "To": "Notmuch Test Suite " }, - "id": "'${gen_msg_id}'", + "id": "XXXXX", "match": false, "tags": [ "inbox", @@ -352,4 +353,40 @@ On Thu, 16 Jun 2016 22:14:41 -0400, Alice wrote: > Note the Cc: and cc: headers. OK" +add_email_corpus duplicate + +ID1=debian/2.6.1.dfsg-4-1-g87ea161@87ea161e851dfb1ea324af00e4ecfccc18875e15 + +test_begin_subtest "format json, --duplicate=2, duplicate key" +output=$(notmuch reply --format=json --duplicate=2 id:${ID1}) +test_json_nodes <<<"$output" "dup:['original']['duplicate']=2" + +test_begin_subtest "format json, subject, --duplicate=1" +output=$(notmuch reply --format=json --duplicate=1 id:${ID1}) +file=$(notmuch search --output=files id:${ID1} | head -n 1) +subject=$(sed -n 's/^Subject: \(.*\)$/\1/p' < $file) +test_json_nodes <<<"$output" "subject:['reply-headers']['Subject']=\"Re: $subject\"" + +test_begin_subtest "format json, subject, --duplicate=2" +output=$(notmuch reply --format=json --duplicate=2 id:${ID1}) +file=$(notmuch search --output=files id:${ID1} | tail -n 1) +subject=$(sed -n 's/^Subject: \(.*\)$/\1/p' < $file) +test_json_nodes <<<"$output" "subject:['reply-headers']['Subject']=\"Re: $subject\"" + +ID2=87r2geywh9.fsf@tethera.net +for dup in {1..2}; do + test_begin_subtest "format json, body, --duplicate=${dup}" + output=$(notmuch reply --format=json --duplicate=${dup} id:${ID2} | \ + $NOTMUCH_PYTHON -B "$NOTMUCH_SRCDIR"/test/json_check_nodes.py "body:['original']['body'][0]['content']" | \ + grep '^# body') + test_expect_equal "$output" "# body ${dup}" +done + +ID3=87r2ecrr6x.fsf@zephyr.silentflame.com +for dup in {1..5}; do + test_begin_subtest "format json, --duplicate=${dup}, 'duplicate' key" + output=$(notmuch reply --format=json --duplicate=${dup} id:${ID3}) + test_json_nodes <<<"$output" "dup:['original']['duplicate']=${dup}" +done + test_done diff --git a/test/T240-dump-restore.sh b/test/T240-dump-restore.sh index a86f0fb7..c3f18839 100755 --- a/test/T240-dump-restore.sh +++ b/test/T240-dump-restore.sh @@ -118,7 +118,7 @@ notmuch dump -- from:cworth > dump-dash-cworth.actual test_expect_equal_file dump-cworth.expected dump-dash-cworth.actual -if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then +if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then test_begin_subtest "dump --query=sexp -- '(from cworth)'" notmuch dump --query=sexp -- '(from cworth)' > dump-dash-cworth.actual2 @@ -139,6 +139,7 @@ notmuch dump --output=dump-outfile-dash-inbox.actual -- from:cworth test_expect_equal_file dump-cworth.expected dump-outfile-dash-inbox.actual test_begin_subtest "Check for a safe set of message-ids" +test_subtest_broken_for_installed notmuch search --output=messages from:cworth | sed s/^id:// > EXPECTED notmuch search --output=messages from:cworth | sed s/^id:// |\ $TEST_DIRECTORY/hex-xcode --direction=encode > OUTPUT @@ -246,6 +247,7 @@ notmuch dump --format=batch-tag > OUTPUT.$test_count test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count test_begin_subtest 'format=batch-tag, checking encoded output' +test_subtest_broken_for_installed 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 diff --git a/test/T310-emacs.sh b/test/T310-emacs.sh index 1b6660f0..e96c1601 100755 --- a/test/T310-emacs.sh +++ b/test/T310-emacs.sh @@ -130,75 +130,6 @@ test_emacs '(notmuch-search "tag:inbox") (test-output)' test_expect_equal_file $EXPECTED/notmuch-show-thread-maildir-storage OUTPUT -test_begin_subtest "Add tag from search view" -os_x_darwin_thread=$(notmuch search --output=threads id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com) -test_emacs "(notmuch-search \"$os_x_darwin_thread\") - (notmuch-test-wait) - (execute-kbd-macro \"+tag-from-search-view\")" -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 tag-from-search-view unread)" - -test_begin_subtest "Remove tag from search view" -test_emacs "(notmuch-search \"$os_x_darwin_thread\") - (notmuch-test-wait) - (execute-kbd-macro \"-tag-from-search-view\")" -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 "Add tag (large query)" -# We use a long query to force us into batch mode and use a funny tag -# that requires escaping for batch tagging. -test_emacs "(notmuch-tag (concat \"$os_x_darwin_thread\" \" or \" (mapconcat #'identity (make-list notmuch-tag-argument-limit \"x\") \"-\")) (list \"+tag-from-%-large-query\"))" -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 tag-from-%-large-query unread)" -notmuch tag -tag-from-%-large-query $os_x_darwin_thread - -test_begin_subtest "notmuch-show: add single tag to single message" -test_emacs "(notmuch-show \"$os_x_darwin_thread\") - (execute-kbd-macro \"+tag-from-show-view\")" -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 tag-from-show-view unread)" - -test_begin_subtest "notmuch-show: remove single tag from single message" -test_emacs "(notmuch-show \"$os_x_darwin_thread\") - (execute-kbd-macro \"-tag-from-show-view\")" -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: add multiple tags to single message" -test_emacs "(notmuch-show \"$os_x_darwin_thread\") - (execute-kbd-macro \"+tag1-from-show-view +tag2-from-show-view\")" -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 tag1-from-show-view tag2-from-show-view unread)" - -test_begin_subtest "notmuch-show: remove multiple tags from single message" -test_emacs "(notmuch-show \"$os_x_darwin_thread\") - (execute-kbd-macro \"-tag1-from-show-view -tag2-from-show-view\")" -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\"") @@ -327,6 +258,7 @@ EOF test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "Verify that sent messages are saved/searchable (via FCC)" +test_subtest_broken_for_installed notmuch new > /dev/null output=$(notmuch search 'subject:"testing message sent via SMTP"' | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; Testing message sent via SMTP (inbox)" @@ -419,6 +351,7 @@ EOF test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "Reply within emacs" +test_subtest_broken_for_installed test_emacs '(let ((message-hidden-headers ''())) (notmuch-search "subject:\"testing message sent via SMTP\"") (notmuch-test-wait) @@ -485,6 +418,31 @@ Sender writes: EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "Reply with show.extra_headers set" +notmuch config set show.extra_headers Received +add_message '[from]="Sender "' \ + [to]=test_suite_other@notmuchmail.org + +test_emacs "(let ((message-hidden-headers '())) + (notmuch-search \"id:\\\"${gen_msg_id}\\\"\") + (notmuch-test-wait) + (notmuch-search-reply-to-thread) + (test-output))" +cat <EXPECTED +From: Notmuch Test Suite +To: Sender +Subject: Re: ${test_subtest_name} +In-Reply-To: <${gen_msg_id}> +Fcc: ${MAIL_DIR}/sent +References: <${gen_msg_id}> +--text follows this line-- +Sender writes: + +> This is just a test message (#${gen_msg_cnt}) +EOF +notmuch config set show.extra_headers +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "Reply from address in named group list within emacs" add_message '[from]="Sender "' \ '[to]=group:test_suite@notmuchmail.org,someone@example.com\;' \ @@ -680,7 +638,7 @@ References: --text follows this line-- test_suite@notmuchmail.org writes: -> This is just a test message (#7) +> This is just a test message (#${gen_msg_cnt}) EOF test_expect_equal_file EXPECTED OUTPUT @@ -1074,7 +1032,7 @@ End of search results. YYY/notmuch_fail exited with status 1 (see *Notmuch errors* for more details) === ERROR === YYY/notmuch_fail exited with status 1 -command: YYY/notmuch_fail search --format\=sexp --format-version\=5 --sort\=newest-first tag\:inbox +command: YYY/notmuch_fail search --format\=sexp --format-version\=5 --sort\=newest-first --exclude\=false tag\:inbox exit status: 1" test_begin_subtest "Search handles subprocess warnings" @@ -1108,30 +1066,6 @@ This is a warning (see *Notmuch errors* for more details) This is a warning This is another warning" -test_begin_subtest "Search thread tag operations are race-free" -add_message '[subject]="Search race test"' -gen_msg_id_1=$gen_msg_id -generate_message '[in-reply-to]="<'$gen_msg_id_1'>"' \ - '[references]="<'$gen_msg_id_1'>"' \ - '[subject]="Search race test two"' -test_emacs '(notmuch-search "subject:\"search race test\"") - (notmuch-test-wait) - (notmuch-poll) - (execute-kbd-macro "+search-thread-race-tag")' -output=$(notmuch search --output=messages 'tag:search-thread-race-tag') -test_expect_equal "$output" "id:$gen_msg_id_1" - -test_begin_subtest "Search global tag operations are race-free" -generate_message '[in-reply-to]="<'$gen_msg_id_1'>"' \ - '[references]="<'$gen_msg_id_1'>"' \ - '[subject]="Re: Search race test"' -test_emacs '(notmuch-search "subject:\"search race test\" -subject:two") - (notmuch-test-wait) - (notmuch-poll) - (execute-kbd-macro "*+search-global-race-tag")' -output=$(notmuch search --output=messages 'tag:search-global-race-tag') -test_expect_equal "$output" "id:$gen_msg_id_1" - test_begin_subtest "Term escaping" output=$(test_emacs "(mapcar 'notmuch-escape-boolean-term (list \"\" diff --git a/test/T312-emacs-hl-line.sh b/test/T312-emacs-hl-line.sh new file mode 100755 index 00000000..3402811c --- /dev/null +++ b/test/T312-emacs-hl-line.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +test_description="emacs hl-line-mode" +. $(dirname "$0")/test-lib.sh || exit 1 +. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1 + +EXPECTED=$NOTMUCH_SRCDIR/test/emacs.expected-output + +test_require_emacs +add_email_corpus + +test_begin_subtest "line 1, search" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-search "tag:inbox") + (notmuch-test-wait) + (notmuch-test-expect-equal + (let ((start (overlay-start hl-line-overlay)) + (end (overlay-end hl-line-overlay))) + (list start (> end start))) + (list 1 t)))' + +test_begin_subtest "line 1, tree" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (notmuch-test-expect-equal + (let ((start (overlay-start hl-line-overlay)) + (end (overlay-end hl-line-overlay))) + (list start (> end start))) + (list 1 t)))' + +test_begin_subtest "line 1, unthreaded" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (notmuch-test-expect-equal + (let ((start (overlay-start hl-line-overlay)) + (end (overlay-end hl-line-overlay))) + (list start (> end start))) + (list 1 t)))' + +test_begin_subtest "line 1, search, refresh" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-search "tag:inbox") + (notmuch-test-wait) + (notmuch-refresh-this-buffer) + (notmuch-test-wait) + (notmuch-test-expect-equal (overlay-start hl-line-overlay) 1))' + +test_begin_subtest "line 1, tree, refresh" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (notmuch-refresh-this-buffer) + (notmuch-test-wait) + (notmuch-test-expect-equal + (let ((start (overlay-start hl-line-overlay)) + (end (overlay-end hl-line-overlay))) + (list start (> end start))) + (list 1 t)))' + +test_begin_subtest "line 1, unthreaded, refresh" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (notmuch-refresh-this-buffer) + (notmuch-test-wait) + (notmuch-test-expect-equal + (let ((start (overlay-start hl-line-overlay)) + (end (overlay-end hl-line-overlay))) + (list start (> end start))) + (list 1 t)))' + + +test_begin_subtest "line 12, notmuch-search" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-search "tag:inbox") + (notmuch-test-wait) + (forward-line 11) + (notmuch-post-command) + (notmuch-test-expect-equal + (line-number-at-pos (overlay-start hl-line-overlay)) 12))' + +test_begin_subtest "line 12, tree" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (forward-line 11) + (notmuch-post-command) + (notmuch-test-expect-equal + (line-number-at-pos (overlay-start hl-line-overlay)) 12))' + +test_begin_subtest "line 12, unthreaded" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-unthreaded "tag:inbox") + (notmuch-test-wait) + (forward-line 11) + (notmuch-post-command) + (notmuch-test-expect-equal + (line-number-at-pos (overlay-start hl-line-overlay)) 12))' + +test_begin_subtest "line 12, search, refresh" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-search "tag:inbox") + (notmuch-test-wait) + (forward-line 11) + (notmuch-post-command) + (notmuch-refresh-this-buffer) + (notmuch-test-wait) + (notmuch-test-expect-equal + (line-number-at-pos (overlay-start hl-line-overlay)) 12))' + +test_begin_subtest "line 12, tree, refresh" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (forward-line 11) + (notmuch-post-command) + (notmuch-refresh-this-buffer) + (notmuch-test-wait) + (notmuch-test-expect-equal + (line-number-at-pos (overlay-start hl-line-overlay)) 12))' + +test_begin_subtest "line 12, unthreaded, refresh" +test_emacs_expect_t '(let ((notmuch-hl-line t)) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (forward-line 11) + (notmuch-post-command) + (notmuch-refresh-this-buffer) + (notmuch-test-wait) + (notmuch-test-expect-equal + (line-number-at-pos (overlay-start hl-line-overlay)) 12))' + +test_begin_subtest "hl-line disabled, search" +test_emacs_expect_t '(let ((notmuch-hl-line nil)) + (notmuch-search "tag:inbox") + (notmuch-test-wait) + (notmuch-test-expect-equal + (or hl-line-mode hl-line-overlay) + nil))' + +test_begin_subtest "hl-line disabled, tree" +test_emacs_expect_t '(let ((notmuch-hl-line nil)) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (notmuch-test-expect-equal + (or hl-line-mode hl-line-overlay) + nil))' + +test_begin_subtest "hl-line disabled, unthreaded" +test_emacs_expect_t '(let ((notmuch-hl-line nil)) + (notmuch-unthreaded "tag:inbox") + (notmuch-test-wait) + (notmuch-test-expect-equal + (or hl-line-mode hl-line-overlay) + nil))' + +test_done + diff --git a/test/T315-emacs-tagging.sh b/test/T315-emacs-tagging.sh new file mode 100755 index 00000000..c26413ce --- /dev/null +++ b/test/T315-emacs-tagging.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash + +test_description="emacs interface" +. $(dirname "$0")/test-lib.sh || exit 1 +. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1 + +EXPECTED=$NOTMUCH_SRCDIR/test/emacs.expected-output + +test_require_emacs +add_email_corpus + +test_begin_subtest "Add tag from search view" +os_x_darwin_thread=$(notmuch search --output=threads id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com) +test_emacs "(notmuch-search \"$os_x_darwin_thread\") + (notmuch-test-wait) + (execute-kbd-macro \"+tag-from-search-view\")" +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 tag-from-search-view unread)" + +test_begin_subtest "Remove tag from search view" +test_emacs "(notmuch-search \"$os_x_darwin_thread\") + (notmuch-test-wait) + (execute-kbd-macro \"-tag-from-search-view\")" +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 "Add tag (large query)" +# We use a long query to force us into batch mode and use a funny tag +# that requires escaping for batch tagging. +test_emacs "(notmuch-tag (concat \"$os_x_darwin_thread\" \" or \" (mapconcat #'identity (make-list notmuch-tag-argument-limit \"x\") \"-\")) (list \"+tag-from-%-large-query\"))" +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 tag-from-%-large-query unread)" +notmuch tag -tag-from-%-large-query $os_x_darwin_thread + +test_begin_subtest "notmuch-show: add single tag to single message" +test_emacs "(notmuch-show \"$os_x_darwin_thread\") + (execute-kbd-macro \"+tag-from-show-view\")" +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 tag-from-show-view unread)" + +test_begin_subtest "notmuch-show: remove single tag from single message" +test_emacs "(notmuch-show \"$os_x_darwin_thread\") + (execute-kbd-macro \"-tag-from-show-view\")" +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: add multiple tags to single message" +test_emacs "(notmuch-show \"$os_x_darwin_thread\") + (execute-kbd-macro \"+tag1-from-show-view +tag2-from-show-view\")" +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 tag1-from-show-view tag2-from-show-view unread)" + +test_begin_subtest "notmuch-show: remove multiple tags from single message" +test_emacs "(notmuch-show \"$os_x_darwin_thread\") + (execute-kbd-macro \"-tag1-from-show-view -tag2-from-show-view\")" +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 "Search thread tag operations are race-free" +add_message '[subject]="Search race test"' +gen_msg_id_1=$gen_msg_id +generate_message '[in-reply-to]="<'$gen_msg_id_1'>"' \ + '[references]="<'$gen_msg_id_1'>"' \ + '[subject]="Search race test two"' +test_emacs '(notmuch-search "subject:\"search race test\"") + (notmuch-test-wait) + (notmuch-poll) + (execute-kbd-macro "+search-thread-race-tag")' +output=$(notmuch search --output=messages 'tag:search-thread-race-tag') +test_expect_equal "$output" "id:$gen_msg_id_1" + +test_begin_subtest "Search global tag operations are race-free" +generate_message '[in-reply-to]="<'$gen_msg_id_1'>"' \ + '[references]="<'$gen_msg_id_1'>"' \ + '[subject]="Re: Search race test"' +test_emacs '(notmuch-search "subject:\"search race test\" -subject:two") + (notmuch-test-wait) + (notmuch-poll) + (execute-kbd-macro "*+search-global-race-tag")' +output=$(notmuch search --output=messages 'tag:search-global-race-tag') +test_expect_equal "$output" "id:$gen_msg_id_1" + +test_begin_subtest "undo with empty history is an error" +test_emacs "(let ((notmuch-tag-history nil)) + (test-log-error + (notmuch-tag-undo))) + " +cat < EXPECTED +(error no further notmuch undo information) +EOF +test_expect_equal_file EXPECTED MESSAGES + +for mode in search show tree unthreaded; do + test_begin_subtest "undo tagging in $mode mode" + test_emacs "(let ((notmuch-tag-history nil)) + (notmuch-$mode \"$os_x_darwin_thread\") + (notmuch-test-wait) + (execute-kbd-macro \"+tag-to-be-undone-$mode\") + (notmuch-tag-undo) + (notmuch-test-wait))" + count=$(notmuch count "tag:tag-to-be-undone-$mode") + test_expect_equal "$count" "0" + + test_begin_subtest "undo tagging in $mode mode (multiple operations)" + test_emacs "(let ((notmuch-tag-history nil)) + (notmuch-$mode \"$os_x_darwin_thread\") + (notmuch-test-wait) + (execute-kbd-macro \"+one-$mode\") + (execute-kbd-macro \"+two-$mode\") + (notmuch-tag-undo) + (notmuch-test-wait) + (execute-kbd-macro \"+three-$mode\"))" + output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize) + notmuch tag "-one-$mode" "-three-$mode" $os_x_darwin_thread + test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox one-$mode three-$mode unread)" + + test_begin_subtest "undo tagging in $mode mode (multiple undo)" + test_emacs "(let ((notmuch-tag-history nil)) + (notmuch-$mode \"$os_x_darwin_thread\") + (notmuch-test-wait) + (execute-kbd-macro \"+one-$mode\") + (execute-kbd-macro \"+two-$mode\") + (notmuch-tag-undo) + (notmuch-test-wait) + (notmuch-tag-undo) + (notmuch-test-wait) + (execute-kbd-macro \"+three-$mode\"))" + output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize) + notmuch tag "-one-$mode" "-three-$mode" $os_x_darwin_thread + test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox three-$mode unread)" + + test_begin_subtest "undo tagging in $mode mode (via binding)" + test_emacs "(let ((notmuch-tag-history nil)) + (notmuch-$mode \"$os_x_darwin_thread\") + (notmuch-test-wait) + (execute-kbd-macro \"+tag-to-be-undone-$mode\") + (execute-kbd-macro (kbd \"C-x u\")) + (notmuch-test-wait))" + count=$(notmuch count "tag:tag-to-be-undone-$mode") + test_expect_equal "$count" "0" +done + +test_done diff --git a/test/T350-crypto.sh b/test/T350-crypto.sh index 8dbf8935..712a0c07 100755 --- a/test/T350-crypto.sh +++ b/test/T350-crypto.sh @@ -13,16 +13,30 @@ test_description='PGP/MIME signature verification and decryption' test_require_emacs add_gnupg_home -test_begin_subtest "emacs delivery of signed message" +test_begin_subtest "emacs delivery of signed message via fcc" test_expect_success \ 'emacs_fcc_message \ "test signed message 001" \ "This is a test signed message." \ "(mml-secure-message-sign)"' +test_begin_subtest "emacs delivery of signed message via fcc and smtp" +emacs_deliver_message \ + 'signed message sent via SMTP' \ + 'This is a test that messages are sent via SMTP' \ + "(add-hook 'message-send-mail-hook (lambda () (sleep-for 1))) + (mml-secure-message-sign)" +msg_file=$(notmuch search --output=files subject:signed-message-sent-via-SMTP) +test_expect_equal_message_body sent_message "$msg_file" + test_begin_subtest "signed part content-type indexing" -output=$(notmuch search mimetype:multipart/signed and mimetype:application/pgp-signature | notmuch_search_sanitize) -test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; test signed message 001 (inbox signed)" +test_subtest_broken_for_installed +notmuch search mimetype:multipart/signed and mimetype:application/pgp-signature | notmuch_search_sanitize > OUTPUT +cat <EXPECTED +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; test signed message 001 (inbox signed) +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; signed message sent via SMTP (inbox signed) +EOF +test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "signature verification" output=$(notmuch show --format=json --verify subject:"test signed message 001" \ @@ -439,6 +453,7 @@ y | gpg --no-tty --quiet --import output=$(notmuch show --format=json --verify subject:"test signed message 001" \ | notmuch_json_show_sanitize \ + | sed -e 's/"key-\(revoked\|missing\)"/"key-revoked"/g' \ | sed -e 's|"created": [1234567890]*|"created": 946728000|') expected='[[[{"id": "XXXXX", "match": true, @@ -454,7 +469,7 @@ expected='[[[{"id": "XXXXX", "Date": "Sat, 01 Jan 2000 12:00:00 +0000"}, "body": [{"id": 1, "sigstatus": [{"status": "error", - "keyid": "6D92612D94E46381", + "keyid": "'$(echo $FINGERPRINT | cut -c 25-)'", "errors": {"key-revoked": true}}], "content-type": "multipart/signed", "content": [{"id": 2, diff --git a/test/T355-smime.sh b/test/T355-smime.sh index 31fa4b4e..d2118c04 100755 --- a/test/T355-smime.sh +++ b/test/T355-smime.sh @@ -35,6 +35,11 @@ EOF test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "signature verification (notmuch CLI)" +if [ $NOTMUCH_GMIME_EMITS_ANGLE_BRACKETS == 1 ]; then + EXPECTED_EMAIL_ADDR='' +else + EXPECTED_EMAIL_ADDR='test_suite@notmuchmail.org' +fi output=$(notmuch show --format=json --verify subject:"test signed message 001" \ | notmuch_json_show_sanitize \ | sed -e 's|"created": [-1234567890]*|"created": 946728000|g' \ @@ -46,7 +51,7 @@ expected='[[[{"id": "XXXXX", "timestamp": 946728000, "date_relative": "2000-01-01", "tags": ["inbox","signed"], - "crypto": {"signed": {"status": [{"fingerprint": "'$FINGERPRINT'", "status": "good","userid": "CN=Notmuch Test Suite", "email": "", "expires": 424242424, "created": 946728000}]}}, + "crypto": {"signed": {"status": [{"fingerprint": "'$FINGERPRINT'", "status": "good","userid": "CN=Notmuch Test Suite", "email": "'$EXPECTED_EMAIL_ADDR'", "expires": 424242424, "created": 946728000}]}}, "headers": {"Subject": "test signed message 001", "From": "Notmuch Test Suite ", "To": "test_suite@notmuchmail.org", @@ -55,7 +60,7 @@ expected='[[[{"id": "XXXXX", "sigstatus": [{"fingerprint": "'$FINGERPRINT'", "status": "good", "userid": "CN=Notmuch Test Suite", - "email": "", + "email": "'$EXPECTED_EMAIL_ADDR'", "expires": 424242424, "created": 946728000}], "content-type": "multipart/signed", @@ -178,8 +183,9 @@ test_begin_subtest "show PKCS#7 SignedData outputs valid JSON" output=$(notmuch show --format=json id:smime-onepart-signed@protected-headers.example) test_valid_json "$output" +if [ -z "${NOTMUCH_TEST_INSTALLED-}" ]; then test_begin_subtest "Verify signature on PKCS#7 SignedData message" -if [ $NOTMUCH_HAVE_64BIT_TIME_T -ne 1 ]; then +if [ "${NOTMUCH_HAVE_64BIT_TIME_T-0}" != "1" ]; then test_subtest_known_broken fi output=$(notmuch show --format=json id:smime-onepart-signed@protected-headers.example) @@ -189,6 +195,7 @@ test_json_nodes <<<"$output" \ 'expires:[0][0][0]["crypto"]["signed"]["status"][0]["expires"]=2611032858' \ 'fingerprint:[0][0][0]["crypto"]["signed"]["status"][0]["fingerprint"]="702BA4B157F1E2B7D16B0C6A5FFC8A7DE2057DEB"' \ 'status:[0][0][0]["crypto"]["signed"]["status"][0]["status"]="good"' +fi # NOTMUCH_TEST_INSTALLED undefined / empty test_begin_subtest "Verify signature on PKCS#7 SignedData message signer User ID" if [ $NOTMUCH_GMIME_X509_CERT_VALIDITY -ne 1 ]; then diff --git a/test/T356-protected-headers.sh b/test/T356-protected-headers.sh index f0aba14e..9f640331 100755 --- a/test/T356-protected-headers.sh +++ b/test/T356-protected-headers.sh @@ -69,12 +69,12 @@ test_json_nodes <<<"$output" \ test_begin_subtest "show cryptographic envelope on signed mail" output=$(notmuch show --verify --format=json id:simple-signed-mail@crypto.notmuchmail.org) test_json_nodes <<<"$output" \ - 'crypto:[0][0][0]["crypto"]={"signed": {"status": [{"created": 1525609971, "fingerprint": "'$FINGERPRINT'", "email": "'"$SELF_EMAIL"'", "userid": "'"$SELF_USERID"'", "status": "good"}]}}' + 'crypto:[0][0][0]["crypto"]={"signed": {"status": [{"created": 1662554210, "fingerprint": "'$FINGERPRINT'", "email": "'"$SELF_EMAIL"'", "userid": "'"$SELF_USERID"'", "status": "good"}]}}' test_begin_subtest "verify signed protected header" output=$(notmuch show --verify --format=json id:signed-protected-header@crypto.notmuchmail.org) test_json_nodes <<<"$output" \ - 'crypto:[0][0][0]["crypto"]={"signed": {"status": [{"created": 1525350527, "fingerprint": "'$FINGERPRINT'", "email": "'"$SELF_EMAIL"'", "userid": "'"$SELF_USERID"'", "status": "good"}], "headers": ["Subject"]}}' + 'crypto:[0][0][0]["crypto"]={"signed": {"status": [{"created": 1662554263, "fingerprint": "'$FINGERPRINT'", "email": "'"$SELF_EMAIL"'", "userid": "'"$SELF_USERID"'", "status": "good"}], "headers": ["Subject"]}}' test_begin_subtest "protected subject does not leak by default in replies" output=$(notmuch reply --decrypt=true --format=json id:protected-header@crypto.notmuchmail.org) @@ -115,7 +115,7 @@ test_begin_subtest "verify protected header is both signed and encrypted" output=$(notmuch show --decrypt=true --format=json id:encrypted-signed@crypto.notmuchmail.org) test_json_nodes <<<"$output" \ 'crypto:[0][0][0]["crypto"]={ - "signed":{"status": [{"status": "good", "fingerprint": "'$FINGERPRINT'", "email": "'"$SELF_EMAIL"'", "userid": "'"$SELF_USERID"'", "created": 1525812676}], + "signed":{"status": [{"status": "good", "fingerprint": "'$FINGERPRINT'", "email": "'"$SELF_EMAIL"'", "userid": "'"$SELF_USERID"'", "created": 1662550328}], "encrypted": true, "headers": ["Subject"]},"decrypted": {"status": "full", "header-mask": {"Subject": "Subject Unavailable"}}}' \ 'subject:[0][0][0]["headers"]["Subject"]="Rhinoceros dinner"' @@ -123,7 +123,7 @@ test_begin_subtest "verify protected header is signed even when not masked" output=$(notmuch show --decrypt=true --format=json id:encrypted-signed-not-masked@crypto.notmuchmail.org) test_json_nodes <<<"$output" \ 'crypto:[0][0][0]["crypto"]={ - "signed":{"status": [{"status": "good", "fingerprint": "'$FINGERPRINT'", "userid": "'"$SELF_USERID"'", "email": "'"$SELF_EMAIL"'", "created": 1525812676}], + "signed":{"status": [{"status": "good", "fingerprint": "'$FINGERPRINT'", "userid": "'"$SELF_USERID"'", "email": "'"$SELF_EMAIL"'", "created": 1662550328}], "encrypted": true, "headers": ["Subject"]},"decrypted": {"status": "full"}}' \ 'subject:[0][0][0]["headers"]["Subject"]="Rhinoceros dinner"' diff --git a/test/T360-symbol-hiding.sh b/test/T360-symbol-hiding.sh index 642457bf..ff06ff69 100755 --- a/test/T360-symbol-hiding.sh +++ b/test/T360-symbol-hiding.sh @@ -11,6 +11,10 @@ test_description='exception symbol hiding' . $(dirname "$0")/test-lib.sh || exit 1 +if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + test_done +fi + test_begin_subtest 'running test' run_test mkdir -p ${PWD}/fakedb/.notmuch $TEST_DIRECTORY/symbol-test ${PWD}/fakedb ${PWD}/nonexistent 2>&1 \ diff --git a/test/T370-search-folder-coherence.sh b/test/T370-search-folder-coherence.sh index 0a2727e7..cf202bb3 100755 --- a/test/T370-search-folder-coherence.sh +++ b/test/T370-search-folder-coherence.sh @@ -24,8 +24,8 @@ test_expect_equal "$output" "No new mail." test_begin_subtest "Multiple files for same message" cat <EXPECTED -MAIL_DIR/msg-001 -MAIL_DIR/spam/msg-001 +MAIL_DIR/msg-XXX +MAIL_DIR/spam/msg-XXX EOF notmuch search --output=files id:$id_x | notmuch_search_files_sanitize >OUTPUT test_expect_equal_file EXPECTED OUTPUT diff --git a/test/T380-atomicity.sh b/test/T380-atomicity.sh index afe49d93..0f9e6d2e 100755 --- a/test/T380-atomicity.sh +++ b/test/T380-atomicity.sh @@ -71,8 +71,8 @@ if test_require_external_prereq gdb; then # Check output against golden output outcount=$(cat outcount) - echo -n > searchall - echo -n > expectall + : > searchall + : > expectall for ((i = 0; i < $outcount; i++)); do if ! cmp -s search.$i expected; then # Find the range of interruptions that match this output diff --git a/test/T390-python.sh b/test/T390-python.sh deleted file mode 100755 index 9f71ce3c..00000000 --- a/test/T390-python.sh +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env bash -test_description="python bindings" -. $(dirname "$0")/test-lib.sh || exit 1 - -test_require_external_prereq ${NOTMUCH_PYTHON} - -add_email_corpus -add_gnupg_home - -test_begin_subtest "compare thread ids" -test_python < EXPECTED -test_expect_equal_file EXPECTED OUTPUT - -test_begin_subtest "compare message ids" -test_python < EXPECTED -test_expect_equal_file EXPECTED OUTPUT - -test_begin_subtest "get non-existent file" -test_python < CLEAN -cat <<'EOF' >EXPECTED -UUID 53 -EOF -test_expect_equal_file EXPECTED CLEAN - -grep '^[0-9a-f]' OUTPUT > INITIAL_OUTPUT - -test_begin_subtest "output of count matches test code" -notmuch count --lastmod '*' | cut -f2-3 > OUTPUT -test_expect_equal_file INITIAL_OUTPUT OUTPUT -add_message '[content-type]="text/plain; charset=iso-8859-2"' \ - '[content-transfer-encoding]=8bit' \ - '[subject]="ISO-8859-2 encoded message"' \ - "[body]=$'Czech word tu\350\362\341\350\350\355 means pinguin\'s.'" # ISO-8859-2 characters are generated by shell's escape sequences -test_begin_subtest "Add ISO-8859-2 encoded message, call get_message_parts" -test_python < EXPECTED -test_expect_equal_file EXPECTED OUTPUT - -# TODO currently these tests for setting and getting config values are -# somewhat interdependent. This is because the config values stored in the -# database are not cleaned up after each test, so they remain there for the -# next test. The ./README file states that this can happen so it seems kind -# of ok. - -test_begin_subtest "set and get config values" -test_python <<'EOF' -import notmuch -db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) -db.set_config('testkey1', 'testvalue1') -db.set_config('testkey2', 'testvalue2') -v1 = db.get_config('testkey1') -v2 = db.get_config('testkey2') -print('testkey1 = ' + v1) -print('testkey2 = ' + v2) -EOF -cat <<'EOF' >EXPECTED -testkey1 = testvalue1 -testkey2 = testvalue2 -EOF -test_expect_equal_file EXPECTED OUTPUT - -test_begin_subtest "get_configs with no match returns empty generator" -test_python <<'EOF' -import notmuch -db = notmuch.Database() -v = db.get_configs('nonexistent') -print(list(v) == []) -EOF -test_expect_equal "$(cat OUTPUT)" "True" - -test_begin_subtest "get_configs with no arguments returns all pairs" -test_python <<'EOF' -import notmuch -db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) -db.set_config("zzzafter", "afterval") -db.set_config("aaabefore", "beforeval") -v = db.get_configs() -for index, keyval in enumerate(v): - key, val = keyval - print('{}: {} => {}'.format(index, key, val)) -EOF -cat <<'EOF' >EXPECTED -0: aaabefore => beforeval -1: testkey1 => testvalue1 -2: testkey2 => testvalue2 -3: zzzafter => afterval -EOF -test_expect_equal_file EXPECTED OUTPUT - -test_begin_subtest "get_configs prefix is used to match keys" -test_python <<'EOF' -import notmuch -db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) -db.set_config('testkey1', 'testvalue1') -db.set_config('testkey2', 'testvalue2') -v = db.get_configs('testkey') -for index, keyval in enumerate(v): - key, val = keyval - print('{}: {} => {}'.format(index, key, val)) -EOF -cat <<'EOF' >EXPECTED -0: testkey1 => testvalue1 -1: testkey2 => testvalue2 -EOF -test_expect_equal_file EXPECTED OUTPUT - -test_begin_subtest "set_config with no value will unset config entries" -test_python <<'EOF' -import notmuch -db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) -db.set_config('testkey1', '') -db.set_config('testkey2', '') -db.set_config("zzzafter", '') -db.set_config("aaabefore", '') -v = db.get_configs() -print(list(v) == []) -EOF -test_expect_equal "$(cat OUTPUT)" "True" - -mkdir -p "${MAIL_DIR}/cur" -fname="${MAIL_DIR}/cur/simplemsg.eml" -cat < "$fname" -From: test_suite@notmuchmail.org -To: test_suite@notmuchmail.org -Subject: encrypted message -Date: Sat, 01 Jan 2000 12:00:00 +0000 -Message-ID: -MIME-Version: 1.0 -Content-Type: multipart/encrypted; boundary="=-=-="; - protocol="application/pgp-encrypted" - ---=-=-= -Content-Type: application/pgp-encrypted - -Version: 1 - ---=-=-= -Content-Type: application/octet-stream - -$(printf 'Content-Type: text/plain\n\nThis is the sekrit message\n' | gpg --no-tty --batch --quiet --trust-model=always --encrypt --armor --recipient test_suite@notmuchmail.org) ---=-=-=-- -EOF - -test_begin_subtest "index message with decryption" -test_python < EXPECTED -test_expect_equal_file EXPECTED OUTPUT - -test_done diff --git a/test/T391-python-cffi.sh b/test/T391-python-cffi.sh index d54bad27..0059b050 100755 --- a/test/T391-python-cffi.sh +++ b/test/T391-python-cffi.sh @@ -2,13 +2,22 @@ test_description="python bindings (pytest)" . $(dirname "$0")/test-lib.sh || exit 1 -if [ $NOTMUCH_HAVE_PYTHON3_CFFI -eq 0 -o $NOTMUCH_HAVE_PYTHON3_PYTEST -eq 0 ]; then +if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then test_done fi +if [ "${NOTMUCH_HAVE_PYTHON3_CFFI-0}" = "0" -o "${NOTMUCH_HAVE_PYTHON3_PYTEST-0}" = "0" ]; then + test_done +fi + +test_begin_subtest "python cffi tests (NOTMUCH_CONFIG set)" +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 --verbose --log-file=$TMP_DIRECTORY/test.output)" -test_begin_subtest "python cffi tests" +test_begin_subtest "python cffi tests (NOTMUCH_CONFIG unset)" pytest_dir=$NOTMUCH_BUILDDIR/bindings/python-cffi/build/stage printf "[pytest]\nminversion = 3.0\naddopts = -ra\n" > $pytest_dir/pytest.ini +unset NOTMUCH_CONFIG test_expect_success "(cd $pytest_dir && ${NOTMUCH_PYTHON} -m pytest --verbose --log-file=$TMP_DIRECTORY/test.output)" test_done diff --git a/test/T392-python-cffi-notmuch.sh b/test/T392-python-cffi-notmuch.sh new file mode 100755 index 00000000..06161219 --- /dev/null +++ b/test/T392-python-cffi-notmuch.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +test_description="python bindings (notmuch test suite)" +. $(dirname "$0")/test-lib.sh || exit 1 + +if [ "${NOTMUCH_HAVE_PYTHON3_CFFI-0}" = "0" -o "${NOTMUCH_HAVE_PYTHON3_PYTEST-0}" = "0" ]; then + test_done +fi + +add_email_corpus + +cat < recurse.py +from notmuch2 import Database +def show_msgs(msgs, level): + print('{:s} {:s}'.format(' ' * level*4, type(msgs).__name__)) + for msg in msgs: + print('{:s} {:s}'.format(' ' * (level*4+2), type(msg).__name__)) + replies=msg.replies() + show_msgs(replies, level+1) +db = Database(config=Database.CONFIG.SEARCH) +msg=db.find("87ocn0qh6d.fsf@yoom.home.cworth.org") +threads = db.threads(query="thread:"+msg.threadid) +thread = next (threads) +show_msgs(thread, 0) +EOF + +test_begin_subtest "recursive traversal of replies (no crash)" +test_python < recurse.py +error=$? +test_expect_equal "${error}" 0 + +test_begin_subtest "recursive traversal of replies (output)" +test_python < recurse.py +tail -n 10 < OUTPUT > OUTPUT.sample +cat < EXPECTED + OwnedMessage + MessageIter + OwnedMessage + MessageIter + OwnedMessage + MessageIter + OwnedMessage + MessageIter + OwnedMessage + MessageIter +EOF +test_expect_equal_file EXPECTED OUTPUT.sample + +test_done diff --git a/test/T395-ruby.sh b/test/T395-ruby.sh index e828efed..d0c6bb17 100755 --- a/test/T395-ruby.sh +++ b/test/T395-ruby.sh @@ -2,7 +2,7 @@ test_description="ruby bindings" . $(dirname "$0")/test-lib.sh || exit 1 -if [ "${NOTMUCH_HAVE_RUBY_DEV}" = "0" ]; then +if [ -z "${NOTMUCH_TEST_INSTALLED-}" -a "${NOTMUCH_HAVE_RUBY_DEV-0}" = "0" ]; then test_subtest_missing_external_prereq_["ruby development files"]=t fi @@ -12,10 +12,14 @@ test_ruby() { ( cat <<-EOF require 'notmuch' - db = Notmuch::Database.new('$MAIL_DIR') + db = Notmuch::Database.new() EOF cat - ) | $NOTMUCH_RUBY -I "$NOTMUCH_BUILDDIR/bindings/ruby"> OUTPUT + ) | if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + ruby + else + $NOTMUCH_RUBY -I "$NOTMUCH_BUILDDIR/bindings/ruby" + fi> OUTPUT test_expect_equal_file EXPECTED OUTPUT } diff --git a/test/T400-hooks.sh b/test/T400-hooks.sh index 0c84b7dd..35bf70c0 100755 --- a/test/T400-hooks.sh +++ b/test/T400-hooks.sh @@ -15,6 +15,15 @@ EOF echo "${TOKEN}" > ${2} } +create_printenv_hook () { + mkdir -p ${HOOK_DIR} + cat <"${HOOK_DIR}/${1}" +#!/bin/sh +printenv "${2}" > "${3}" +EOF + chmod +x "${HOOK_DIR}/${1}" +} + create_write_hook () { local TOKEN="${RANDOM}" mkdir -p ${HOOK_DIR} @@ -53,8 +62,11 @@ add_message # create maildir structure for notmuch-insert mkdir -p "$MAIL_DIR"/{cur,new,tmp} +ORIG_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} for config in traditional profile explicit relative XDG split; do unset NOTMUCH_PROFILE + export NOTMUCH_CONFIG=${ORIG_NOTMUCH_CONFIG} + EXPECTED_CONFIG=${NOTMUCH_CONFIG} notmuch config set database.hook_dir notmuch config set database.path ${MAIL_DIR} case $config in @@ -65,8 +77,10 @@ for config in traditional profile explicit relative XDG split; do dir=${HOME}/.config/notmuch/other mkdir -p ${dir} HOOK_DIR=${dir}/hooks - cp ${NOTMUCH_CONFIG} ${dir}/config + EXPECTED_CONFIG=${dir}/config + cp ${NOTMUCH_CONFIG} ${EXPECTED_CONFIG} export NOTMUCH_PROFILE=other + unset NOTMUCH_CONFIG ;; explicit) HOOK_DIR=${HOME}/.notmuch-hooks @@ -200,6 +214,23 @@ EOF EOF test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "NOTMUCH_CONFIG is set" + create_printenv_hook "pre-new" NOTMUCH_CONFIG OUTPUT + NOTMUCH_NEW + cat < EXPECTED +${EXPECTED_CONFIG} +EOF + test_expect_equal_file_nonempty EXPECTED OUTPUT + + test_begin_subtest "NOTMUCH_CONFIG is set by --config" + create_printenv_hook "pre-new" NOTMUCH_CONFIG OUTPUT + cp "${EXPECTED_CONFIG}" "${EXPECTED_CONFIG}.alternate" + notmuch --config "${EXPECTED_CONFIG}.alternate" new + cat < EXPECTED +${EXPECTED_CONFIG}.alternate +EOF + test_expect_equal_file_nonempty EXPECTED OUTPUT + rm -rf ${HOOK_DIR} done test_done diff --git a/test/T405-external.sh b/test/T405-external.sh new file mode 100755 index 00000000..0e1d9646 --- /dev/null +++ b/test/T405-external.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +test_description='hooks' +. $(dirname "$0")/test-lib.sh || exit 1 + +create_echo_script () { + local TOKEN="${RANDOM}" + mkdir -p ${BIN_DIR} + cat <"${BIN_DIR}/${1}" +#!/bin/sh +echo "${TOKEN}" > ${3} +EOF + chmod +x "${BIN_DIR}/${1}" + echo "${TOKEN}" > ${2} +} + +create_printenv_script () { + mkdir -p ${BIN_DIR} + cat <"${BIN_DIR}/${1}" +#!/bin/sh +printenv "${2}" > "${3}" +EOF + chmod +x "${BIN_DIR}/${1}" +} + +# add a message to generate mail dir and database +add_message + +BIN_DIR=`pwd`/bin +PATH=$BIN_DIR:$PATH + +test_begin_subtest "'notmuch foo' runs notmuch-foo" +rm -rf ${BIN_DIR} +create_echo_script "notmuch-foo" EXPECTED OUTPUT $HOOK_DIR +notmuch foo +test_expect_equal_file_nonempty EXPECTED OUTPUT + +create_printenv_script "notmuch-printenv" NOTMUCH_CONFIG OUTPUT + +test_begin_subtest "NOTMUCH_CONFIG is set" +notmuch printenv +cat < EXPECTED +${NOTMUCH_CONFIG} +EOF +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "NOTMUCH_CONFIG is set by --config" +cp "${NOTMUCH_CONFIG}" "${NOTMUCH_CONFIG}.alternate" +cat < EXPECTED +${NOTMUCH_CONFIG}.alternate +EOF +notmuch --config "${NOTMUCH_CONFIG}.alternate" printenv +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_done diff --git a/test/T410-argument-parsing.sh b/test/T410-argument-parsing.sh index d9aa8e2d..40b625fe 100755 --- a/test/T410-argument-parsing.sh +++ b/test/T410-argument-parsing.sh @@ -2,6 +2,10 @@ test_description="argument parsing" . $(dirname "$0")/test-lib.sh || exit 1 +if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + test_done +fi + test_begin_subtest "sanity check" $TEST_DIRECTORY/arg-test pos1 --keyword=one --boolean --string=foo pos2 --int=7 --flag=one --flag=three > OUTPUT cat < EXPECTED diff --git a/test/T440-emacs-hello.sh b/test/T440-emacs-hello.sh index a1ed1c2b..842781a4 100755 --- a/test/T440-emacs-hello.sh +++ b/test/T440-emacs-hello.sh @@ -68,6 +68,16 @@ test_emacs '(notmuch-hello) notmuch tag -$tag '*' test_expect_equal_file $EXPECTED/notmuch-hello-long-names OUTPUT +test_begin_subtest "All tags show up" +tag=exclude_me +notmuch tag +$tag '*' +notmuch config set search.exclude_tags $tag +test_emacs '(notmuch-hello) + (test-output)' +notmuch tag -$tag '*' +test_expect_equal_file $EXPECTED/notmuch-hello-all-tags OUTPUT + +test_done test_begin_subtest "notmuch-hello with nonexistent CWD" test_emacs ' (notmuch-hello) diff --git a/test/T450-emacs-show.sh b/test/T450-emacs-show.sh index 4b5f5fde..ff340f06 100755 --- a/test/T450-emacs-show.sh +++ b/test/T450-emacs-show.sh @@ -62,6 +62,17 @@ test_emacs '(let ((notmuch-crypto-process-mime nil)) (test-visible-output))' test_expect_equal_file $EXPECTED/notmuch-show-process-crypto-mime-parts-on OUTPUT +test_begin_subtest "notmuch-search-show-thread returns non-nil on success" +test_emacs_expect_t '(notmuch-search "id:20091117203301.GV3165@dottiness.seas.harvard.edu") + (notmuch-test-wait) + (and (notmuch-search-show-thread) + (not (notmuch-show-next-thread)))' + +test_begin_subtest "notmuch-search-show-thread returns nil when there are no messages" +test_emacs_expect_t '(notmuch-search "id:non-existing-id") + (notmuch-test-wait) + (not (notmuch-search-show-thread))' + test_begin_subtest "notmuch-show: don't elide non-matching messages" test_emacs '(let ((notmuch-show-only-matching-messages nil)) (notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"") @@ -80,6 +91,62 @@ test_emacs '(let ((notmuch-show-only-matching-messages t)) (test-visible-output))' test_expect_equal_file $EXPECTED/notmuch-show-elide-non-matching-messages-on OUTPUT +test_begin_subtest "Hide bodies of messages by depth" +test_emacs '(let ((notmuch-show-depth-limit -1)) + (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}") + (notmuch-test-wait) + (notmuch-search-show-thread) + (notmuch-test-wait) + (test-visible-output))' +test_expect_equal_file $EXPECTED/notmuch-show-depth OUTPUT + + +test_begin_subtest "Hide bodies of messages by height" +test_emacs '(let ((notmuch-show-height-limit -1)) + (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}") + (notmuch-test-wait) + (notmuch-search-show-thread) + (notmuch-test-wait) + (test-visible-output))' +# folding all messages by height or depth should look the same +test_expect_equal_file $EXPECTED/notmuch-show-depth OUTPUT + +test_begin_subtest "Hide bodies of messages; show only leaves." +test_emacs '(let ((notmuch-show-height-limit 0)) + (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}") + (notmuch-test-wait) + (notmuch-search-show-thread) + (notmuch-test-wait) + (test-visible-output))' +test_expect_equal_file $EXPECTED/notmuch-show-height-0 OUTPUT + +test_begin_subtest "Hide bodies of messages (depth > 1)" +test_emacs '(let ((notmuch-show-depth-limit 1)) + (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}") + (notmuch-test-wait) + (notmuch-search-show-thread) + (notmuch-test-wait) + (test-visible-output))' +test_expect_equal_file $EXPECTED/notmuch-show-depth-1 OUTPUT + +test_begin_subtest "Hide bodies of messages by size" +test_emacs '(let ((notmuch-show-max-text-part-size 1)) + (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}") + (notmuch-test-wait) + (notmuch-search-show-thread) + (notmuch-test-wait) + (test-visible-output))' +test_expect_equal_file $EXPECTED/notmuch-show-size OUTPUT + +test_begin_subtest "Hide bodies of messages by size > 450" +test_emacs '(let ((notmuch-show-max-text-part-size 450)) + (notmuch-search "thread:{id:87ocn0qh6d.fsf@yoom.home.cworth.org}") + (notmuch-test-wait) + (notmuch-search-show-thread) + (notmuch-test-wait) + (test-visible-output))' +test_expect_equal_file $EXPECTED/notmuch-show-size-450 OUTPUT + test_begin_subtest "notmuch-show: elide non-matching messages (w/ notmuch-show-toggle-elide-non-matching)" test_emacs '(let ((notmuch-show-only-matching-messages nil)) (notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"") @@ -209,6 +276,10 @@ test_emacs '(notmuch-show "id:'$gen_msg_id'") output=$(head -1 OUTPUT.raw|cut -f1-4 -d' ') test_expect_equal "$output" "Notmuch Test Suite " +test_begin_subtest "multipart/alternative hides html by default" +test_emacs '(notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com") + (test-visible-output)' +test_expect_equal_file $EXPECTED/notmuch-show-multipart-alternative OUTPUT # switching to the crypto corpus, using gpg from here on: add_gnupg_home @@ -220,7 +291,9 @@ test_emacs '(notmuch-show "id:basic-encrypted@crypto.notmuchmail.org") test_expect_equal_file $EXPECTED/notmuch-show-decrypted-message OUTPUT test_begin_subtest "show encrypted rfc822 message" -test_subtest_known_broken +if ${TEST_EMACS} --quick --batch --eval '(kill-emacs (if (version< emacs-version "28") 0 1))'; then + test_subtest_known_broken +fi test_emacs '(notmuch-show "id:encrypted-rfc822-attachment@crypto.notmuchmail.org") (test-visible-output)' test_expect_code 1 'fgrep "!!!" OUTPUT' @@ -243,4 +316,163 @@ test_emacs "(test-log-error (notmuch-show \"$tid\")))" test_expect_equal "$(cat MESSAGES)" "COMPLETE" +add_email_corpus attachment + +test_begin_subtest "display of text/calendar" +test_emacs '(let ((notmuch-show-all-multipart/alternative-parts t)) + (notmuch-show "id:YT3PR01MB10572EFC9F7C81F9446214768CEF72@YT3PR01MB10572.CANPRD01.PROD.OUTLOOK.COM")) + (test-visible-output "OUTPUT")' +cat < EXPECTED +David Bremner (1970-01-01) (inbox) +Subject: test +To: "david@tethera.net" +Date: Thu, 01 Jan 1970 00:00:00 +0000 + +[ multipart/alternative ] +[ text/plain ] +This meeting will could have been an email +[ text/calendar ] +%%(and (diary-cyclic 1 5 27 2024) (diary-block 5 27 2024 8 27 2024)) 17:00-17:30 test [In-person] + Desc: This meeting will could have been an email + + + Organizer: mailto:bremner@example.com +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "display of text/calendar, symlinked tmpdir" +mkdir real-tmp +ln -s real-tmp tmp +test_emacs "(let ((notmuch-show-all-multipart/alternative-parts t) + (temporary-file-directory \"tmp\") + (find-file-visit-truename t)) + (notmuch-show \"id:YT3PR01MB10572EFC9F7C81F9446214768CEF72@YT3PR01MB10572.CANPRD01.PROD.OUTLOOK.COM\")) + (test-visible-output \"OUTPUT\")" +cat < EXPECTED +David Bremner (1970-01-01) (inbox) +Subject: test +To: "david@tethera.net" +Date: Thu, 01 Jan 1970 00:00:00 +0000 + +[ multipart/alternative ] +[ text/plain ] +This meeting will could have been an email +[ text/calendar ] +%%(and (diary-cyclic 1 5 27 2024) (diary-block 5 27 2024 8 27 2024)) 17:00-17:30 test [In-person] + Desc: This meeting will could have been an email + + + Organizer: mailto:bremner@example.com +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "tar not inlined by default" +test_emacs '(notmuch-show "id:874llc2bkp.fsf@curie.anarc.at") + (test-visible-output "OUTPUT")' +cat < EXPECTED +Antoine Beaupré (2018-03-19) (attachment inbox) +Subject: Re: bug: "no top level messages" crash on Zen email loops +To: David Bremner , notmuch@notmuchmail.org +Date: Mon, 19 Mar 2018 13:56:54 -0400 + +[ multipart/mixed ] +[ text/plain ] +And obviously I forget the frigging attachment. +[ zendesk-email-loop2.tgz: application/x-gtar-compressed ] +[ text/plain ] + +PS: don't we have a "you forgot to actually attach the damn file" plugin +when we detect the word "attachment" and there's no attach? :p +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "tar not inlined by default on refresh" +test_emacs '(notmuch-show "id:874llc2bkp.fsf@curie.anarc.at") + (notmuch-show-refresh-view) + (test-visible-output "OUTPUT")' +cat < EXPECTED +Antoine Beaupré (2018-03-19) (attachment inbox) +Subject: Re: bug: "no top level messages" crash on Zen email loops +To: David Bremner , notmuch@notmuchmail.org +Date: Mon, 19 Mar 2018 13:56:54 -0400 + +[ multipart/mixed ] +[ text/plain ] +And obviously I forget the frigging attachment. +[ zendesk-email-loop2.tgz: application/x-gtar-compressed ] +[ text/plain ] + +PS: don't we have a "you forgot to actually attach the damn file" plugin +when we detect the word "attachment" and there's no attach? :p +EOF +test_expect_equal_file EXPECTED OUTPUT + +add_email_corpus duplicate + +ID3=87r2ecrr6x.fsf@zephyr.silentflame.com +test_begin_subtest "duplicate=3, subject" +test_emacs "(notmuch-show \"id:${ID3}\") + (notmuch-show-choose-duplicate 3) + (test-visible-output \"OUTPUT\")" +output=$(grep "Subject:" OUTPUT) +file=$(notmuch search --output=files id:${ID3} | head -n 3 | tail -n 1) +subject=$(grep '^Subject:' $file) +test_expect_equal "$output" "$subject" + +FILE3=$(notmuch search --output=files --duplicate=3 "id:${ID3}") +test_begin_subtest "duplicate=3, stash" +test_emacs_expect_t \ + "(notmuch-show \"id:${ID3}\") + (notmuch-show-choose-duplicate 3) + (notmuch-show-stash-filename) + (notmuch-test-expect-equal (list (car kill-ring)) (list \"${FILE3}\"))" + +test_begin_subtest "duplicate=0" +test_emacs "(test-log-error + (notmuch-show \"id:${ID3}\") + (notmuch-show-choose-duplicate 0))" +cat < EXPECTED +(error Duplicate 0 out of range [1,5]) +EOF +test_expect_equal_file EXPECTED MESSAGES + +test_begin_subtest "duplicate=1000" +test_emacs "(test-log-error + (notmuch-show \"id:${ID3}\") + (notmuch-show-choose-duplicate 1000))" +cat < EXPECTED +(error Duplicate 1000 out of range [1,5]) +EOF +test_expect_equal_file EXPECTED MESSAGES +test_begin_subtest "duplicate=4" +test_emacs "(notmuch-show \"id:${ID3}\") + (notmuch-show-choose-duplicate 4) + (test-visible-output \"OUTPUT\")" +test_expect_equal_file_nonempty $EXPECTED/notmuch-show-duplicate-4 OUTPUT + +FILE4=$(notmuch search --output=files --duplicate=4 "id:${ID3}") +test_begin_subtest "duplicate=4, raw" +test_emacs "(notmuch-show \"id:${ID3}\") + (notmuch-show-choose-duplicate 4) + (notmuch-show-view-raw-message) + (test-visible-output \"OUTPUT\")" +subject4=$(grep '^Subject:' $FILE4) +subject=$(grep '^Subject:' OUTPUT) +test_expect_equal "$subject4" "$subject" + +test_begin_subtest "notmuch-show for message with subject with embedded CRNL" +add_message "[subject]=\"=?UTF-8?B?8J+Pi++4jw==?= A SALE to boost your =?UTF-8?Q?workout=0D=0A?=\" [body]=the-message-body" +test_emacs "(notmuch-show \"id:${gen_msg_id}\") + (test-output \"OUTPUT.raw\")" +cat <EXPECTED +Notmuch Test Suite (2001-01-05) (inbox) +Subject: 🏋️ A SALE to boost your workout +To: Notmuch Test Suite +Date: GENERATED_DATE + +the-message-body +EOF +notmuch_date_sanitize < OUTPUT.raw > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + test_done diff --git a/test/T453-emacs-reply.sh b/test/T453-emacs-reply.sh new file mode 100755 index 00000000..0a27d066 --- /dev/null +++ b/test/T453-emacs-reply.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +test_description="emacs reply" +. $(dirname "$0")/test-lib.sh || exit 1 +. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1 + +EXPECTED=$NOTMUCH_SRCDIR/test/emacs-reply.expected-output + +test_require_emacs + +add_email_corpus attachment + +test_begin_subtest "tar not inlined by default" +test_emacs '(notmuch-mua-new-reply "id:874llc2bkp.fsf@curie.anarc.at") + (test-visible-output "OUTPUT.raw")' +cat < EXPECTED +From: Notmuch Test Suite +To: Antoine Beaupré +Subject: Re: bug: "no top level messages" crash on Zen email loops +In-Reply-To: <874llc2bkp.fsf@curie.anarc.at> +Fcc: MAIL_DIR/sent +--text follows this line-- +Antoine Beaupré writes: + +> And obviously I forget the frigging attachment. +> +> +> PS: don't we have a "you forgot to actually attach the damn file" plugin +> when we detect the word "attachment" and there's no attach? :p +EOF +notmuch_dir_sanitize < OUTPUT.raw > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +add_email_corpus duplicate + +ID2=87r2geywh9.fsf@tethera.net +for dup in {1..2}; do + test_begin_subtest "body, duplicate=${dup}" + test_emacs "(notmuch-show \"id:${ID2}\") + (notmuch-test-wait) + (notmuch-show-choose-duplicate $dup) + (notmuch-test-wait) + (notmuch-show-reply) + (test-visible-output \"OUTPUT.raw\")" + output=$(grep '^> # body' OUTPUT.raw) + test_expect_equal "$output" "> # body ${dup}" +done + +ID3=87r2ecrr6x.fsf@zephyr.silentflame.com +test_begin_subtest "duplicate=3, subject" +test_emacs "(notmuch-show \"id:${ID3}\") + (notmuch-test-wait) + (notmuch-show-choose-duplicate 3) + (notmuch-test-wait) + (notmuch-show-reply) + (test-visible-output \"OUTPUT\")" +output=$(sed -n 's/^Subject: //p' OUTPUT) +file=$(notmuch search --output=files id:${ID3} | head -n 3 | tail -n 1) +subject=$(sed -n 's/^Subject: //p' $file) +test_expect_equal "$output" "Re: $subject" + +test_begin_subtest "duplicate=4" +test_emacs "(notmuch-show \"id:${ID3}\") + (notmuch-show-choose-duplicate 4) + (notmuch-test-wait) + (notmuch-show-reply) + (test-visible-output \"OUTPUT.raw\")" +notmuch_dir_sanitize < OUTPUT.raw > OUTPUT +test_expect_equal_file_nonempty $EXPECTED/notmuch-reply-duplicate-4 OUTPUT + +test_done diff --git a/test/T454-emacs-dont-reply-names.sh b/test/T454-emacs-dont-reply-names.sh new file mode 100755 index 00000000..3a770177 --- /dev/null +++ b/test/T454-emacs-dont-reply-names.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +test_description="message-dont-reply-to-names in emacs replies" +. $(dirname "$0")/test-lib.sh || exit 1 +. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1 + +EXPECTED=$NOTMUCH_SRCDIR/test/emacs-show.expected-output + +test_require_emacs + +add_email_corpus default + +test_begin_subtest "regular expression" +test_emacs '(let ((message-dont-reply-to-names "notmuchmail\\|noreply\\|harvard")) + (notmuch-mua-new-reply + "id:20091117203301.GV3165@dottiness.seas.harvard.edu" nil t) + (test-visible-output "OUTPUT-FULL.raw"))' + +notmuch_dir_sanitize < OUTPUT-FULL.raw > OUTPUT-FULL +head -6 OUTPUT-FULL > OUTPUT + +cat < EXPECTED +From: Notmuch Test Suite +To: Mikhail Gusarov +Subject: Re: [notmuch] Working with Maildir storage? +In-Reply-To: <20091117203301.GV3165@dottiness.seas.harvard.edu> +Fcc: MAIL_DIR/sent +--text follows this line-- +EOF + +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "predicate" +test_emacs '(let ((message-dont-reply-to-names + (lambda (m) (string-prefix-p "Mikhail" m)))) + (notmuch-mua-new-reply + "id:20091117203301.GV3165@dottiness.seas.harvard.edu" nil t) + (test-visible-output "OUTPUT-FULL-PRED.raw"))' + +notmuch_dir_sanitize < OUTPUT-FULL-PRED.raw > OUTPUT-FULL-PRED +head -7 OUTPUT-FULL-PRED > OUTPUT-PRED + +cat < EXPECTED-PRED +From: Notmuch Test Suite +To: Lars Kellogg-Stedman +Cc: notmuch@notmuchmail.org +Subject: Re: [notmuch] Working with Maildir storage? +In-Reply-To: <20091117203301.GV3165@dottiness.seas.harvard.edu> +Fcc: MAIL_DIR/sent +--text follows this line-- +EOF + +test_expect_equal_file EXPECTED-PRED OUTPUT-PRED + +test_begin_subtest "nil value" +test_emacs '(let ((message-dont-reply-to-names nil)) + (notmuch-mua-new-reply + "id:20091117203301.GV3165@dottiness.seas.harvard.edu" nil t) + (test-visible-output "OUTPUT-FULL-NIL.raw"))' + +notmuch_dir_sanitize < OUTPUT-FULL-NIL.raw > OUTPUT-FULL-NIL +head -7 OUTPUT-FULL-NIL > OUTPUT-NIL + +cat < EXPECTED-NIL +From: Notmuch Test Suite +To: Lars Kellogg-Stedman , Mikhail Gusarov +Cc: notmuch@notmuchmail.org +Subject: Re: [notmuch] Working with Maildir storage? +In-Reply-To: <20091117203301.GV3165@dottiness.seas.harvard.edu> +Fcc: MAIL_DIR/sent +--text follows this line-- +EOF + +test_expect_equal_file EXPECTED-NIL OUTPUT-NIL + +test_done diff --git a/test/T460-emacs-tree.sh b/test/T460-emacs-tree.sh index 0f23b418..4243f65a 100755 --- a/test/T460-emacs-tree.sh +++ b/test/T460-emacs-tree.sh @@ -100,7 +100,7 @@ test_emacs '(notmuch-hello) (notmuch-test-wait) (test-output) (delete-other-windows)' -test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox OUTPUT +test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox-oldest-first OUTPUT test_begin_subtest "Tree view of a single thread (from search)" test_emacs '(notmuch-hello) @@ -200,4 +200,56 @@ test_emacs '(test-log-error (notmuch-tree "*")))' test_expect_equal "$(cat MESSAGES)" "COMPLETE" +# reinitialize database for outline tests +add_email_corpus + +test_begin_subtest "start in outline mode" +test_emacs '(let ((notmuch-tree-outline-enabled t)) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (test-visible-output))' +# folding all messages by height or depth should look the same +test_expect_equal_file $EXPECTED/inbox-outline OUTPUT + +test_begin_subtest "outline-cycle-buffer" +test_emacs '(let ((notmuch-tree-outline-enabled t)) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (outline-cycle-buffer) + (outline-cycle-buffer) + (notmuch-test-wait) + (test-visible-output))' +# folding all messages by height or depth should look the same +test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox OUTPUT + +test_begin_subtest "notmuch-tree for message with subject with embedded CRNL" +add_message "[subject]=\"=?UTF-8?B?8J+Pi++4jw==?= A SALE to boost your =?UTF-8?Q?workout=0D=0A?=\" [body]=the-message-body" +test_emacs "(notmuch-tree \"id:${gen_msg_id}\") + (notmuch-test-wait) + (test-output)" +# one line of output, plus "End of search results." +test_expect_equal "$(wc -l < OUTPUT)" 2 + +add_email_corpus duplicate + +ID3=87r2ecrr6x.fsf@zephyr.silentflame.com +test_begin_subtest "duplicate=3, subject" +test_emacs "(notmuch-tree \"id:${ID3}\") + (notmuch-test-wait) + (notmuch-tree-show-message t) + (notmuch-show-choose-duplicate 3) + (test-visible-output \"OUTPUT\")" +output=$(grep "Subject:" OUTPUT) +file=$(notmuch search --output=files id:${ID3} | head -n 3 | tail -n 1) +subject=$(grep '^Subject:' $file) +test_expect_equal "$output" "$subject" + +test_begin_subtest "duplicate=4" +test_emacs "(notmuch-show \"id:${ID3}\") + (notmuch-test-wait) + (notmuch-tree-show-message t) + (notmuch-show-choose-duplicate 4) + (test-visible-output \"OUTPUT\")" +test_expect_equal_file_nonempty $NOTMUCH_SRCDIR/test/emacs-show.expected-output/notmuch-show-duplicate-4 OUTPUT + test_done diff --git a/test/T461-emacs-search-exclude.sh b/test/T461-emacs-search-exclude.sh new file mode 100755 index 00000000..47f74682 --- /dev/null +++ b/test/T461-emacs-search-exclude.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash + +test_description="exclude options persist between Emacs search and tree modes" +. $(dirname "$0")/test-lib.sh || exit 1 +. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1 + +EXPECTED=$NOTMUCH_SRCDIR/test/emacs-exclude.expected-output + +test_require_emacs +add_email_corpus +notmuch config set search.exclude_tags deleted +notmuch tag +deleted -- 'from:"Stewart Smith"' or 'from:"Chris Wilson"' + +# Basic test cases just asserting exclude option is working and consistent. + +test_begin_subtest "Search doesn't contain excluded mail by default" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-search-tag-inbox-without-excluded OUTPUT + +test_begin_subtest "Toggling exclude in search will show excluded mail" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-search-toggle-hide-excluded) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-search-tag-inbox-with-excluded OUTPUT + +test_begin_subtest "Tree search doesn't contain excluded mail by default" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-tree-from-search-current-query) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox-without-excluded OUTPUT + +test_begin_subtest "Toggling exclude in tree search will show excluded mail" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-tree-from-search-current-query) + (notmuch-test-wait) + (notmuch-tree-toggle-hide-excluded) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox-with-excluded OUTPUT + +test_begin_subtest "Unthreaded search doesn't contain excluded mail by default" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-unthreaded-from-search-current-query) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-unthreaded-tag-inbox-without-excluded OUTPUT + +test_begin_subtest "Toggling exclude in unthreaded will show excluded mail" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-unthreaded-from-search-current-query) + (notmuch-test-wait) + (notmuch-tree-toggle-hide-excluded) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-unthreaded-tag-inbox-with-excluded OUTPUT + +# Cycling from search to tree to unthreaded and vice versa will persist the current +# value of notmuch-search-hide-excluded. + +test_begin_subtest "Value of hide-excluded from search persists into tree search" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-search-toggle-hide-excluded) + (notmuch-test-wait) + (notmuch-tree-from-search-current-query) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox-with-excluded OUTPUT + +test_begin_subtest "Value of hide-excluded from search persists into unthreaded" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-search-toggle-hide-excluded) + (notmuch-test-wait) + (notmuch-unthreaded-from-search-current-query) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-unthreaded-tag-inbox-with-excluded OUTPUT + +test_begin_subtest "Value of hide-excluded from tree persists into search" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-tree-from-search-current-query) + (notmuch-test-wait) + (notmuch-tree-toggle-hide-excluded) + (notmuch-test-wait) + (notmuch-search-from-tree-current-query) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-search-tag-inbox-with-excluded OUTPUT + +test_begin_subtest "Value of hide-excluded from tree persists into unthreaded" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-tree-from-search-current-query) + (notmuch-test-wait) + (notmuch-tree-toggle-hide-excluded) + (notmuch-test-wait) + (notmuch-unthreaded-from-tree-current-query) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-unthreaded-tag-inbox-with-excluded OUTPUT + +test_begin_subtest "Value of hide-excluded from unthreaded persists into tree" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-unthreaded-from-search-current-query) + (notmuch-test-wait) + (notmuch-tree-toggle-hide-excluded) + (notmuch-test-wait) + (notmuch-tree-from-unthreaded-current-query) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox-with-excluded OUTPUT + +test_begin_subtest "Value of hide-excluded from unthreaded persists into search" +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (1- (point))) + (notmuch-test-wait) + (notmuch-unthreaded-from-search-current-query) + (notmuch-test-wait) + (notmuch-tree-toggle-hide-excluded) + (notmuch-test-wait) + (notmuch-search-from-tree-current-query) + (notmuch-test-wait) + (test-output) + (delete-other-windows)' +test_expect_equal_file $EXPECTED/notmuch-search-tag-inbox-with-excluded OUTPUT + +test_done diff --git a/test/T465-emacs-unthreaded.sh b/test/T465-emacs-unthreaded.sh index e7bc1439..a3ff85fd 100755 --- a/test/T465-emacs-unthreaded.sh +++ b/test/T465-emacs-unthreaded.sh @@ -57,4 +57,29 @@ test_emacs '(test-log-error (notmuch-unthreaded "*")))' test_expect_equal "$(cat MESSAGES)" "COMPLETE" +add_email_corpus duplicate + +ID3=87r2ecrr6x.fsf@zephyr.silentflame.com +test_begin_subtest "duplicate=3, subject" +test_emacs "(let ((notmuch-tree-show-out t)) + (notmuch-unthreaded \"id:${ID3}\") + (notmuch-test-wait) + (notmuch-tree-show-message nil) + (notmuch-show-choose-duplicate 3) + (test-visible-output \"OUTPUT\"))" +output=$(grep "Subject:" OUTPUT) +file=$(notmuch search --output=files id:${ID3} | head -n 3 | tail -n 1) +subject=$(grep '^Subject:' $file) +test_expect_equal "$output" "$subject" + +test_begin_subtest "duplicate=4" +test_emacs "(let ((notmuch-tree-show-out t)) + (notmuch-unthreaded \"id:${ID3}\") + (notmuch-test-wait) + (notmuch-tree-show-message nil) + (notmuch-show-choose-duplicate 4) + (test-visible-output \"OUTPUT\"))" +test_expect_equal_file_nonempty $NOTMUCH_SRCDIR/test/emacs-show.expected-output/notmuch-show-duplicate-4 OUTPUT + + test_done diff --git a/test/T480-hex-escaping.sh b/test/T480-hex-escaping.sh index b2eb80b9..8bddf3e7 100755 --- a/test/T480-hex-escaping.sh +++ b/test/T480-hex-escaping.sh @@ -2,6 +2,10 @@ test_description="hex encoding and decoding" . $(dirname "$0")/test-lib.sh || exit 1 +if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + test_done +fi + test_begin_subtest "round trip" find $NOTMUCH_SRCDIR/test/corpora/default -type f -print | sort | xargs cat > EXPECTED $TEST_DIRECTORY/hex-xcode --direction=encode < EXPECTED | $TEST_DIRECTORY/hex-xcode --direction=decode > OUTPUT diff --git a/test/T490-parse-time-string.sh b/test/T490-parse-time-string.sh index f89755ed..3b6e48c4 100755 --- a/test/T490-parse-time-string.sh +++ b/test/T490-parse-time-string.sh @@ -2,6 +2,10 @@ test_description="date/time parser module" . $(dirname "$0")/test-lib.sh || exit 1 +if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + test_done +fi + # Sanity/smoke tests for the date/time parser independent of notmuch _date () { diff --git a/test/T520-show.sh b/test/T520-show.sh index 12bde6c7..1f7f6aa8 100755 --- a/test/T520-show.sh +++ b/test/T520-show.sh @@ -17,6 +17,25 @@ notmuch show foo.. exit_code=$? test_expect_equal 1 $exit_code +test_begin_subtest "warning for --mbox --decrypt" +notmuch show --format=mbox --decrypt=true '*' 1>/dev/null 2>OUTPUT +echo $? >> OUTPUT +cat < EXPECTED +Warning: mbox format does not support decryption (ignored) +Warning: mbox format does not support signature verification (ignored) +0 +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "warning for --mbox --verify" +notmuch show --format=mbox --verify '*' 1>/dev/null 2>OUTPUT +echo $? >> OUTPUT +cat < EXPECTED +Warning: mbox format does not support signature verification (ignored) +0 +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "notmuch show --sort=newest-first" notmuch show --entire-thread=true '*' > EXPECTED notmuch show --entire-thread=true --sort=newest-first '*' > OUTPUT @@ -35,7 +54,7 @@ notmuch show --entire-thread=true --sort=oldest-first $QUERY > OUTPUT test_expect_equal_file EXPECTED OUTPUT -if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then +if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then test_query_syntax '(and "wonderful" "wizard")' 'wonderful and wizard' test_query_syntax '(or "php" "wizard")' 'php or wizard' @@ -45,4 +64,46 @@ if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then fi +test_begin_subtest "quoting in From" +test_subtest_known_broken +add_message '[from]="=?UTF-8?Q?=3Cfoo=40example.org=3E=2C?= "' +output=$(notmuch show id:${gen_msg_id}|grep From:) +test_expect_equal "${output}" 'From: "," ' + +add_email_corpus duplicate + +ID1=debian/2.6.1.dfsg-4-1-g87ea161@87ea161e851dfb1ea324af00e4ecfccc18875e15 + +test_begin_subtest "format json, --duplicate=2, duplicate key" +output=$(notmuch show --format=json --duplicate=2 id:${ID1}) +test_json_nodes <<<"$output" "dup:['duplicate']=2" + +test_begin_subtest "format json, subject, --duplicate=1" +output=$(notmuch show --format=json --duplicate=1 id:${ID1}) +file=$(notmuch search --output=files id:${ID1} | head -n 1) +subject=$(sed -n 's/^Subject: \(.*\)$/\1/p' < $file) +test_json_nodes <<<"$output" "subject:['headers']['Subject']=\"$subject\"" + +test_begin_subtest "format json, subject, --duplicate=2" +output=$(notmuch show --format=json --duplicate=2 id:${ID1}) +file=$(notmuch search --output=files id:${ID1} | tail -n 1) +subject=$(sed -n 's/^Subject: \(.*\)$/\1/p' < $file) +test_json_nodes <<<"$output" "subject:['headers']['Subject']=\"$subject\"" + +ID2=87r2geywh9.fsf@tethera.net +for dup in {1..2}; do + test_begin_subtest "format json, body, --duplicate=${dup}" + output=$(notmuch show --format=json --duplicate=${dup} id:${ID2} | \ + $NOTMUCH_PYTHON -B "$NOTMUCH_SRCDIR"/test/json_check_nodes.py "body:['body'][0]['content']" | \ + grep '^# body') + test_expect_equal "$output" "# body ${dup}" +done + +ID3=87r2ecrr6x.fsf@zephyr.silentflame.com +for dup in {1..5}; do + test_begin_subtest "format json, --duplicate=${dup}, 'duplicate' key" + output=$(notmuch show --format=json --duplicate=${dup} id:${ID3}) + test_json_nodes <<<"$output" "dup:['duplicate']=${dup}" +done + test_done diff --git a/test/T550-db-features.sh b/test/T550-db-features.sh index 9d5a9e70..3048c7c4 100755 --- a/test/T550-db-features.sh +++ b/test/T550-db-features.sh @@ -3,6 +3,10 @@ test_description="database version and feature compatibility" . $(dirname "$0")/test-lib.sh || exit 1 +if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + test_done +fi + test_begin_subtest "future database versions abort open" ${TEST_DIRECTORY}/make-db-version ${MAIL_DIR} 9999 "" output=$(notmuch search x 2>&1 | sed 's/\(database at\) .*/\1 FILENAME/') diff --git a/test/T560-lib-error.sh b/test/T560-lib-error.sh index 1f4482cb..78cae1cd 100755 --- a/test/T560-lib-error.sh +++ b/test/T560-lib-error.sh @@ -16,7 +16,11 @@ int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; - stat = notmuch_database_open (NULL, 0, 0); + char* msg = NULL; + stat = notmuch_database_open_with_config (NULL, + NOTMUCH_DATABASE_MODE_READ_ONLY, + "", NULL, &db, &msg); + if (msg) fputs (msg, stderr); } EOF cat <<'EOF' >EXPECTED @@ -34,7 +38,11 @@ int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; - stat = notmuch_database_open ("./nonexistent/foo", 0, 0); + char *msg = NULL; + stat = notmuch_database_open_with_config ("./nonexistent/foo", + NOTMUCH_DATABASE_MODE_READ_ONLY, + "", NULL, &db, &msg); + if (msg) fputs (msg, stderr); } EOF cat <<'EOF' >EXPECTED @@ -52,7 +60,10 @@ int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; - stat = notmuch_database_create ("./nonexistent/foo", &db); + char *msg = NULL; + + stat = notmuch_database_create_with_config ("./nonexistent/foo", "", NULL, &db, &msg); + if (msg) fputs (msg, stderr); } EOF cat <<'EOF' >EXPECTED @@ -70,13 +81,17 @@ int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; - stat = notmuch_database_open (argv[1], 0, 0); + char* msg = NULL; + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + "", NULL, &db, &msg); + if (msg) fputs (msg, stderr); } EOF cat <<'EOF' >EXPECTED == stdout == == stderr == -Error: Cannot open database at CWD/nonexistent/foo: No such file or directory. +Error: database path 'CWD/nonexistent/foo' does not exist or is not a directory. EOF test_expect_equal_file EXPECTED OUTPUT @@ -87,13 +102,17 @@ test_C <<'EOF' int main (int argc, char** argv) { notmuch_status_t stat; - stat = notmuch_database_create (NULL, NULL); + char *msg = NULL; + + stat = notmuch_database_create_with_config (NULL, "", NULL, NULL, &msg); + printf ("%s\n", notmuch_status_to_string (stat)); + if (msg) fputs (msg, stderr); } EOF cat <<'EOF' >EXPECTED == stdout == +No mail root found == stderr == -Error: could not locate database. EOF test_expect_equal_file EXPECTED OUTPUT @@ -105,13 +124,17 @@ int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; - stat = notmuch_database_create (argv[1], &db); + char *msg = NULL; + + stat = notmuch_database_create_with_config (argv[1], "", NULL, &db, &msg); + printf ("%d\n", stat == NOTMUCH_STATUS_SUCCESS); + if (msg) fputs (msg, stderr); } EOF cat <<'EOF' >EXPECTED == stdout == +1 == stderr == -Error: Cannot open database at CWD/nonexistent/foo: No such file or directory. EOF test_expect_equal_file EXPECTED OUTPUT @@ -123,7 +146,11 @@ int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; - stat = notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, &db); + char* msg = NULL; + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + "", NULL, &db, &msg); + if (msg) fputs (msg, stderr); if (stat != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "error opening database: %d\n", stat); } @@ -148,7 +175,9 @@ int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; - stat = notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db); + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + "", NULL, &db, NULL); if (stat != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "error opening database: %d\n", stat); } @@ -206,7 +235,9 @@ int main (int argc, char** argv) char *msg = NULL; int fd; - stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg); + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + NULL, NULL, &db, &msg); if (stat != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : ""); exit (1); @@ -245,24 +276,6 @@ EOF test_expect_equal_file EXPECTED OUTPUT.clean restore_database -backup_database -test_begin_subtest "Xapian exception getting tags" -cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH} - { - notmuch_tags_t *tags = NULL; - tags = notmuch_database_get_all_tags (db); - stat = (tags == NULL); - } -EOF -sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean -cat <<'EOF' >EXPECTED -== stdout == -== stderr == -A Xapian exception occurred getting tags -EOF -test_expect_equal_file EXPECTED OUTPUT.clean -restore_database - backup_database test_begin_subtest "Xapian exception creating directory" cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH} diff --git a/test/T562-lib-database.sh b/test/T562-lib-database.sh index 769fe86e..fedfc9ed 100755 --- a/test/T562-lib-database.sh +++ b/test/T562-lib-database.sh @@ -9,17 +9,17 @@ test_begin_subtest "building database" test_expect_success "NOTMUCH_NEW" cat < c_head -#include -#include #include -#include + int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat = NOTMUCH_STATUS_SUCCESS; char *msg = NULL; - stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg); + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + NULL, NULL, &db, &msg); if (stat != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : ""); exit (1); @@ -82,7 +82,7 @@ cat < EXPECTED == stdout == 0 == stderr == -A Xapian exception occurred at lib/database.cc:XXX: Database has been closed +A Xapian exception occurred at database.cc:XXX: Database has been closed EOF test_expect_equal_file EXPECTED OUTPUT @@ -147,7 +147,7 @@ cat < EXPECTED == stdout == 1 == stderr == -A Xapian exception occurred at lib/database.cc:XXX: Database has been closed +A Xapian exception occurred at database.cc:XXX: Database has been closed EOF test_expect_equal_file EXPECTED OUTPUT @@ -243,14 +243,14 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} const char *path = talloc_asprintf(db, "%s/01:2,", argv[1]); EXPECT0(notmuch_database_close (db)); stat = notmuch_database_index_file (db, path, NULL, &msg); - printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf ("%d\n", stat == NOTMUCH_STATUS_CLOSED_DATABASE); } EOF cat < EXPECTED == stdout == 1 == stderr == -A Xapian exception occurred finding message: Database has been closed. +Cannot write to a closed database. EOF test_expect_equal_file EXPECTED OUTPUT @@ -358,14 +358,14 @@ 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_CLOSED_DATABASE); } EOF cat < EXPECTED == stdout == 1 == stderr == -Error: A Xapian exception occurred setting metadata: Database has been closed +Cannot write to a closed database. EOF test_expect_equal_file EXPECTED OUTPUT diff --git a/test/T563-lib-directory.sh b/test/T563-lib-directory.sh index 28325ff2..4711fcdf 100755 --- a/test/T563-lib-directory.sh +++ b/test/T563-lib-directory.sh @@ -9,10 +9,8 @@ test_begin_subtest "building database" test_expect_success "NOTMUCH_NEW" cat < c_head -#include -#include #include -#include + int main (int argc, char** argv) { notmuch_database_t *db; @@ -20,7 +18,9 @@ int main (int argc, char** argv) notmuch_status_t stat = NOTMUCH_STATUS_SUCCESS; char *msg = NULL; - stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg); + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + NULL, NULL, &db, &msg); if (stat != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : ""); exit (1); @@ -53,7 +53,7 @@ cat < EXPECTED == stdout == 1 == stderr == -A Xapian exception occurred at lib/directory.cc:XXX: Database has been closed +A Xapian exception occurred at directory.cc:XXX: Database has been closed EOF test_expect_equal_file EXPECTED OUTPUT @@ -70,7 +70,7 @@ cat < EXPECTED == stdout == 1 == stderr == -A Xapian exception occurred at lib/directory.cc:XXX: Database has been closed +A Xapian exception occurred at directory.cc:XXX: Database has been closed EOF test_expect_equal_file EXPECTED OUTPUT @@ -79,32 +79,14 @@ test_begin_subtest "delete directory document for a closed db" cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { stat = notmuch_directory_delete (dir); - printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION); - } -EOF -cat < EXPECTED -== stdout == -1 -== stderr == -A Xapian exception occurred deleting directory entry: Database has been closed. -EOF -test_expect_equal_file EXPECTED OUTPUT -restore_database - -backup_database -test_begin_subtest "get/set mtime of directory for a closed db" -cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} - { - time_t stamp = notmuch_directory_get_mtime (dir); - stat = notmuch_directory_set_mtime (dir, stamp); - printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf ("%d\n", stat == NOTMUCH_STATUS_CLOSED_DATABASE); } EOF cat < EXPECTED == stdout == 1 == stderr == -A Xapian exception occurred setting directory mtime: Database has been closed. +Cannot write to a closed database. EOF test_expect_equal_file EXPECTED OUTPUT restore_database @@ -115,14 +97,14 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { time_t stamp = notmuch_directory_get_mtime (dir); stat = notmuch_directory_set_mtime (dir, stamp); - printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf ("%d\n", stat == NOTMUCH_STATUS_CLOSED_DATABASE); } EOF cat < EXPECTED == stdout == 1 == stderr == -A Xapian exception occurred setting directory mtime: Database has been closed. +Cannot write to a closed database. EOF test_expect_equal_file EXPECTED OUTPUT restore_database diff --git a/test/T564-lib-query.sh b/test/T564-lib-query.sh index 50b0a88e..53a63bf6 100755 --- a/test/T564-lib-query.sh +++ b/test/T564-lib-query.sh @@ -9,17 +9,17 @@ test_begin_subtest "building database" test_expect_success "NOTMUCH_NEW" cat < c_head -#include -#include #include -#include + int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; char *msg = NULL; - stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg); + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + NULL, NULL, &db, &msg); if (stat != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : ""); exit (1); diff --git a/test/T565-lib-tags.sh b/test/T565-lib-tags.sh new file mode 100755 index 00000000..2a59f8dd --- /dev/null +++ b/test/T565-lib-tags.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +test_description="API tests for tags" + +. $(dirname "$0")/test-lib.sh || exit 1 + +add_email_corpus + +test_begin_subtest "building database" +test_expect_success "NOTMUCH_NEW" + +cat < c_head +#include +#include +#include +#include +#include +#include + +int main (int argc, char** argv) +{ + notmuch_database_t *db; + notmuch_status_t stat; + char *path; + char *msg = NULL; + int fd; + + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + NULL, NULL, &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 <<'EOF' > c_tail + if (stat) { + const char *stat_str = notmuch_database_status_string (db); + if (stat_str) + fputs (stat_str, stderr); + } + +} +EOF + +POSTLIST_PATH=(${MAIL_DIR}/.notmuch/xapian/postlist.*) + +backup_database +test_begin_subtest "Xapian exception getting tags" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH} + { + notmuch_tags_t *tags = NULL; + fd = open(argv[2],O_WRONLY|O_TRUNC); + if (fd < 0) { + fprintf (stderr, "error opening %s\n", argv[1]); + exit (1); + } + tags = notmuch_database_get_all_tags (db); + stat = (tags == NULL); + } +EOF +sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean +cat <<'EOF' >EXPECTED +== stdout == +== stderr == +A Xapian exception occurred getting tags +EOF +test_expect_equal_file EXPECTED OUTPUT.clean +restore_database + +test_begin_subtest "NULL tags are not valid" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} + { + notmuch_bool_t valid = TRUE; + valid = notmuch_tags_valid (NULL); + fprintf(stdout, "valid = %d\n", valid); + } +EOF +cat <<'EOF' >EXPECTED +== stdout == +valid = 0 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_done diff --git a/test/T566-lib-message.sh b/test/T566-lib-message.sh index ee55ef29..69051937 100755 --- a/test/T566-lib-message.sh +++ b/test/T566-lib-message.sh @@ -3,6 +3,10 @@ test_description="API tests for notmuch_message_*" . $(dirname "$0")/test-lib.sh || exit 1 +if [ -n "${NOTMUCH_TEST_INSTALLED}" ]; then + test_done +fi + add_email_corpus test_begin_subtest "building database" @@ -19,18 +23,19 @@ cat <<'EOF' > c_tail EOF cat < c_head0 -#include -#include #include + int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; char *msg = NULL; notmuch_message_t *message = NULL; - const char *id = "1258471718-6781-1-git-send-email-dottedmag@dottedmag.net"; + const char *id = "87pr7gqidx.fsf@yoom.home.cworth.org"; - stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg); + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + NULL, NULL, &db, &msg); if (stat != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : ""); exit (1); @@ -83,7 +88,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} EOF cat < EXPECTED == stdout == -1258471718-6781-1-git-send-email-dottedmag@dottedmag.net +87pr7gqidx.fsf@yoom.home.cworth.org 1 == stderr == EOF @@ -155,7 +160,7 @@ cat c_head0 - c_tail <<'EOF' | test_C ${MAIL_DIR} EOF cat < EXPECTED == stdout == -MAIL_DIR/01:2, +MAIL_DIR/cur/40:2, SUCCESS == stderr == EOF @@ -230,7 +235,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { notmuch_status_t status; status = notmuch_message_add_tag (message, "boom"); - printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_CLOSED_DATABASE); } EOF cat < EXPECTED @@ -246,7 +251,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { notmuch_status_t status; status = notmuch_message_remove_tag (message, "boom"); - printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_CLOSED_DATABASE); } EOF cat < EXPECTED @@ -306,12 +311,126 @@ cat < EXPECTED EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "_notmuch_message_add_term catches exceptions" +cat c_head0 - c_tail <<'EOF' | test_private_C ${MAIL_DIR} + { + notmuch_private_status_t status; + /* This relies on Xapian throwing an exception for adding empty terms */ + status = _notmuch_message_add_term (message, "body", ""); + printf("%d\n%d\n", message != NULL, status != NOTMUCH_STATUS_SUCCESS ); + } +EOF +cat < EXPECTED +== stdout == +1 +1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "_notmuch_message_remove_term catches exceptions" +cat c_head0 - c_tail <<'EOF' | test_private_C ${MAIL_DIR} + { + notmuch_private_status_t status; + /* Xapian throws the same exception for empty and non-existent terms; + * error string varies between Xapian versions. */ + status = _notmuch_message_remove_term (message, "tag", "nonexistent"); + printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_SUCCESS ); + } +EOF +cat < EXPECTED +== stdout == +1 +1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "_notmuch_message_add_filename on closed db" +cat c_head - c_tail <<'EOF' | test_private_C ${MAIL_DIR} + { + notmuch_private_status_t status; + status = _notmuch_message_add_filename (message, "some-filename"); + printf("%d\n%d\n", message != NULL, status != NOTMUCH_STATUS_SUCCESS); + } +EOF +cat < EXPECTED +== stdout == +1 +1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "_notmuch_message_remove_filename on closed db" +cat c_head - c_tail <<'EOF' | test_private_C ${MAIL_DIR} + { + notmuch_private_status_t status; + status = _notmuch_message_remove_filename (message, "some-filename"); + printf("%d\n%d\n", message != NULL, status != NOTMUCH_STATUS_SUCCESS); + } +EOF +cat < EXPECTED +== stdout == +1 +1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Handle converting tags to maildir flags with closed db" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} + { + notmuch_status_t status; + status = notmuch_message_tags_to_maildir_flags (message); + printf("%d\n%d\n", message != NULL, status != NOTMUCH_STATUS_SUCCESS); + } +EOF +cat < EXPECTED +== stdout == +1 +1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +POSTLIST_PATH=(${MAIL_DIR}/.notmuch/xapian/postlist.*) +test_begin_subtest "Handle converting tags to maildir flags with corrupted db" +backup_database +cat c_head0 - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH} + { + notmuch_status_t status; + + status = notmuch_message_add_tag (message, "draft"); + if (status) exit(1); + + int fd = open(argv[2],O_WRONLY|O_TRUNC); + if (fd < 0) { + fprintf (stderr, "error opening %s\n", argv[1]); + exit (1); + } + + status = notmuch_message_tags_to_maildir_flags (message); + printf("%d\n%d\n", message != NULL, status != NOTMUCH_STATUS_SUCCESS); + } +EOF +cat < EXPECTED +== stdout == +1 +1 +== stderr == +EOF +restore_database +notmuch new +notmuch tag -draft id:87pr7gqidx.fsf@yoom.home.cworth.org +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "Handle removing all tags with closed db" 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_CLOSED_DATABASE); } EOF cat < EXPECTED @@ -327,7 +446,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_CLOSED_DATABASE); } EOF cat < EXPECTED @@ -343,7 +462,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_CLOSED_DATABASE); } EOF cat < EXPECTED @@ -401,4 +520,31 @@ cat < EXPECTED EOF test_expect_equal_file EXPECTED OUTPUT +TERMLIST_PATH=(${MAIL_DIR}/.notmuch/xapian/termlist.*) +test_begin_subtest "remove message with corrupted db" +backup_database +cat c_head0 - c_tail <<'EOF' | test_private_C ${MAIL_DIR} ${TERMLIST_PATH} + { + notmuch_status_t status; + + int fd = open(argv[2],O_WRONLY|O_TRUNC); + if (fd < 0) { + fprintf (stderr, "error opening %s\n", argv[1]); + exit (1); + } + + stat = _notmuch_message_delete (message); + printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + } +EOF +cat < EXPECTED +== stdout == +1 +== stderr == +A Xapian exception occurred at message.cc:XXX: EOF reading block YYY +EOF +sed 's/EOF reading block [0-9]*/EOF reading block YYY/' < OUTPUT > OUTPUT.clean +test_expect_equal_file EXPECTED OUTPUT.clean +restore_database + test_done diff --git a/test/T568-lib-thread.sh b/test/T568-lib-thread.sh index 088e66dd..b4c24ca2 100755 --- a/test/T568-lib-thread.sh +++ b/test/T568-lib-thread.sh @@ -24,9 +24,8 @@ cat <<'EOF' > c_tail EOF cat < c_head -#include -#include #include + int main (int argc, char** argv) { notmuch_database_t *db; @@ -37,7 +36,9 @@ int main (int argc, char** argv) notmuch_query_t *query = NULL; const char *id = "${THREAD}"; - stat = notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db, &msg); + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + NULL, NULL, &db, &msg); if (stat != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : ""); exit (1); diff --git a/test/T570-revision-tracking.sh b/test/T570-revision-tracking.sh index a59e7c98..bcc97dd9 100755 --- a/test/T570-revision-tracking.sh +++ b/test/T570-revision-tracking.sh @@ -19,7 +19,12 @@ int main (int argc, char** argv) unsigned long rev; - stat = notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, &db); + char* msg = NULL; + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + "", NULL, &db, &msg); + if (msg) fputs (msg, stderr); + if (stat) fputs ("open failed\n", stderr); revision = notmuch_database_get_revision (db, &uuid); @@ -90,4 +95,31 @@ subtotal=$(notmuch count lastmod:..$lastmod) result=$(($subtotal == $total-1)) test_expect_equal 1 "$result" +if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then + test_begin_subtest 'exclude one message using negative lastmod (sexp)' + total=$(notmuch count '*') + notmuch tag +${RANDOM} id:4EFC743A.3060609@april.org + count=$(notmuch count --query=sexp '(lastmod -1 *)') + test_expect_equal 1 "$count" +fi + +test_begin_subtest 'exclude one message using negative lastmod' +total=$(notmuch count '*') +notmuch tag +${RANDOM} id:4EFC743A.3060609@april.org +count=$(notmuch count lastmod:-1..) +test_expect_equal 1 "$count" + +test_begin_subtest 'exclude one message using negative lastmod (second param)' +total=$(notmuch count '*') +notmuch tag +${RANDOM} id:4EFC743A.3060609@april.org +count=$(notmuch count lastmod:..-1) +test_expect_equal 51 "$count" + +test_begin_subtest 'negative lastmod (two parameters)' +notmuch tag +${RANDOM} '*' +before=$(notmuch count --lastmod '*' | cut -f3) +notmuch tag +${RANDOM} id:4EFC743A.3060609@april.org +count=$(notmuch count lastmod:-100..$before) +test_expect_equal 51 "$count" + test_done diff --git a/test/T590-libconfig.sh b/test/T590-libconfig.sh index a9566c13..9b364895 100755 --- a/test/T590-libconfig.sh +++ b/test/T590-libconfig.sh @@ -23,8 +23,6 @@ EOF } cat < c_head -#include -#include #include int main (int argc, char** argv) @@ -272,6 +270,29 @@ EOF test_expect_equal_file EXPECTED OUTPUT restore_database +test_begin_subtest "notmuch_config_get_values (ignore leading/trailing whitespace)" +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 ; d ")); + 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 +d +== 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% { @@ -418,6 +439,10 @@ cat <<'EOF' >EXPECTED 09: 'NULL' 10: 'USER_FULL_NAME' 11: '8000' +12: 'NULL' +13: '' +14: ', ' +15: '| ' == stderr == EOF unset MAILDIR @@ -616,8 +641,6 @@ cp notmuch-config.bak notmuch-config test_expect_equal_file EXPECTED OUTPUT cat < c_head2 -#include -#include #include int main (int argc, char** argv) @@ -704,7 +727,10 @@ 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 search.authors_matched_separator "| " +notmuch config set search.authors_separator ", " notmuch config set new.ignore "sekrit_junk" +notmuch config set index.as_text "text/" cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL% { notmuch_config_key_t key; @@ -730,6 +756,10 @@ cat <<'EOF' >EXPECTED 09: 'test_suite_other@notmuchmail.org;test_suite@otherdomain.org' 10: 'Notmuch Test Suite' 11: '8000' +12: 'NULL' +13: 'text/' +14: ', ' +15: '| ' == stderr == EOF test_expect_equal_file EXPECTED OUTPUT @@ -763,6 +793,10 @@ cat <<'EOF' >EXPECTED 09: 'NULL' 10: 'USER_FULL_NAME' 11: '8000' +12: 'NULL' +13: '' +14: ', ' +15: '| ' == stderr == EOF test_expect_equal_file EXPECTED OUTPUT.clean @@ -834,11 +868,15 @@ database.backup_dir MAIL_DIR/.notmuch/backups database.hook_dir MAIL_DIR/.notmuch/hooks database.mail_root MAIL_DIR database.path MAIL_DIR +index.as_text text/ key with spaces value, with, spaces! maildir.synchronize_flags true new.ignore sekrit_junk new.tags unread;inbox +search.authors_matched_separator | +search.authors_separator , search.exclude_tags foo;bar;fub +show.extra_headers (null) test.key1 testvalue1 test.key2 testvalue2 user.name Notmuch Test Suite @@ -849,4 +887,169 @@ zzzafter afterval EOF test_expect_equal_file EXPECTED OUTPUT +cat < c_head3 +#include +int main (int argc, char **argv) { + notmuch_status_t stat; + notmuch_database_t *db = NULL; +EOF + +cat < c_tail3 + printf("db == NULL: %d\n", db == NULL); +} +EOF + +test_begin_subtest "open: database set to null on missing config" +cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} + notmuch_status_t st = notmuch_database_open_with_config(argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + "/nonexistent", NULL, &db, NULL); +EOF +cat < EXPECTED +== stdout == +db == NULL: 1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "open: database set to null on missing config (env)" +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +export NOTMUCH_CONFIG="/nonexistent" +cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} + notmuch_status_t st = notmuch_database_open_with_config(argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + NULL, NULL, &db, NULL); +EOF +export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +cat < EXPECTED +== stdout == +db == NULL: 1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "create: database set to null on missing config" +cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} "/nonexistent" + notmuch_status_t st = notmuch_database_create_with_config(argv[1],argv[2], NULL, &db, NULL); +EOF +cat < EXPECTED +== stdout == +db == NULL: 1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "create: database set to null on missing config (env)" +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +export NOTMUCH_CONFIG="/nonexistent" +cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} + notmuch_status_t st = notmuch_database_create_with_config(argv[1], + NULL, NULL, &db, NULL); +EOF +export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +cat < EXPECTED +== stdout == +db == NULL: 1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "load_config: database set non-null on missing config" +cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} "/nonexistent" + notmuch_status_t st = notmuch_database_load_config(argv[1],argv[2], NULL, &db, NULL); +EOF +cat < EXPECTED +== stdout == +db == NULL: 0 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "load_config: database non-null on missing config (env)" +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +export NOTMUCH_CONFIG="/nonexistent" +cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} + notmuch_status_t st = notmuch_database_load_config(argv[1], NULL, NULL, &db, NULL); +EOF +export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +cat < EXPECTED +== stdout == +db == NULL: 0 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "load_config: database set to NULL on fatal error" +cat c_head3 - c_tail3 <<'EOF' | test_C + notmuch_status_t st = notmuch_database_load_config("relative", NULL, NULL, &db, NULL); +EOF +cat < EXPECTED +== stdout == +db == NULL: 1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "open: database parameter overrides implicit config" +cp $NOTMUCH_CONFIG ${NOTMUCH_CONFIG}.bak +notmuch config set database.path ${MAIL_DIR}/nonexistent +cat c_head3 - c_tail3 <<'EOF' | test_C ${MAIL_DIR} + const char *path = NULL; + notmuch_status_t st = notmuch_database_open_with_config(argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + NULL, NULL, &db, NULL); + printf ("status: %d\n", st); + path = notmuch_database_get_path (db); + printf ("path: %s\n", path ? path : "(null)"); +EOF +cp ${NOTMUCH_CONFIG}.bak ${NOTMUCH_CONFIG} +cat < EXPECTED +== stdout == +status: 0 +path: MAIL_DIR +db == NULL: 0 +== stderr == +EOF +notmuch_dir_sanitize < OUTPUT > OUTPUT.clean +test_expect_equal_file EXPECTED OUTPUT.clean + +cat < c_body + notmuch_status_t st = notmuch_database_open_with_config(NULL, + NOTMUCH_DATABASE_MODE_READ_ONLY, + "", NULL, &db, NULL); + printf ("status == SUCCESS: %d\n", st == NOTMUCH_STATUS_SUCCESS); + if (db) { + const char *mail_root = NULL; + mail_root = notmuch_config_get (db, NOTMUCH_CONFIG_MAIL_ROOT); + printf ("mail_root: %s\n", mail_root ? mail_root : "(null)"); + } +EOF + +cat < EXPECTED.common +== stdout == +status == SUCCESS: 0 +db == NULL: 1 +== stderr == +EOF + +test_begin_subtest "open/error: config=empty with no mail root in db " +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +unset NOTMUCH_CONFIG +cat c_head3 c_body c_tail3 | test_C +export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +notmuch_dir_sanitize < OUTPUT > OUTPUT.clean +test_expect_equal_file EXPECTED.common OUTPUT.clean + +test_begin_subtest "open/error: config=empty with no mail root in db (xdg)" +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +unset NOTMUCH_CONFIG +backup_database +mkdir -p home/.local/share/notmuch +mv mail/.notmuch home/.local/share/notmuch/default +cat c_head3 c_body c_tail3 | test_C +restore_database +export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +notmuch_dir_sanitize < OUTPUT > OUTPUT.clean +test_expect_equal_file EXPECTED.common OUTPUT.clean + test_done diff --git a/test/T592-thread-breakage.sh b/test/T592-thread-breakage.sh index 92a70e3e..2334fcaf 100755 --- a/test/T592-thread-breakage.sh +++ b/test/T592-thread-breakage.sh @@ -21,6 +21,10 @@ test_description='thread breakage during reindexing' . $(dirname "$0")/test-lib.sh || exit 1 +if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + test_done +fi + message_a () { mkdir -p ${MAIL_DIR}/cur cat > ${MAIL_DIR}/cur/a < c_head -#include -#include #include int main (int argc, char** argv) diff --git a/test/T610-message-property.sh b/test/T610-message-property.sh index d0e52f4a..04a5d488 100755 --- a/test/T610-message-property.sh +++ b/test/T610-message-property.sh @@ -6,17 +6,13 @@ test_description="message property API" add_email_corpus cat < c_head -#include -#include -#include -#include #include void print_properties (notmuch_message_t *message, const char *prefix, notmuch_bool_t exact) { notmuch_message_properties_t *list; for (list = notmuch_message_get_properties (message, prefix, exact); notmuch_message_properties_valid (list); notmuch_message_properties_move_to_next (list)) { - printf("%s\n", notmuch_message_properties_value(list)); + printf("%s = %s\n", notmuch_message_properties_key(list), notmuch_message_properties_value(list)); } notmuch_message_properties_destroy (list); } @@ -27,8 +23,12 @@ int main (int argc, char** argv) notmuch_message_t *message = NULL; const char *val; notmuch_status_t stat; + char* msg = NULL; - EXPECT0(notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db)); + EXPECT0(notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + "", NULL, &db, &msg)); + if (msg) fputs (msg, stderr); EXPECT0(notmuch_database_find_message(db, "4EFC743A.3060609@april.org", &message)); if (message == NULL) { fprintf (stderr, "unable to find message"); @@ -89,17 +89,6 @@ testkey2 = NULL EOF test_expect_equal_file EXPECTED OUTPUT -test_begin_subtest "notmuch_message_remove_all_properties" -cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} -EXPECT0(notmuch_message_remove_all_properties (message, NULL)); -print_properties (message, "", FALSE); -EOF -cat <<'EOF' >EXPECTED -== stdout == -== stderr == -EOF -test_expect_equal_file EXPECTED OUTPUT - test_begin_subtest "testing string map binary search (via message properties)" cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { @@ -157,7 +146,28 @@ print_properties (message, "testkey1", TRUE); EOF cat <<'EOF' >EXPECTED == stdout == -testvalue1 +testkey1 = testvalue1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "notmuch_message_remove_all_properties" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +EXPECT0(notmuch_message_remove_all_properties (message, NULL)); +EXPECT0(notmuch_database_destroy(db)); +EXPECT0(notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + "", NULL, &db, &msg)); +if (msg) fputs (msg, stderr); +EXPECT0(notmuch_database_find_message(db, "4EFC743A.3060609@april.org", &message)); +if (message == NULL) { + fprintf (stderr, "unable to find message"); + exit (1); +} +print_properties (message, "", FALSE); +EOF +cat <<'EOF' >EXPECTED +== stdout == == stderr == EOF test_expect_equal_file EXPECTED OUTPUT @@ -171,10 +181,9 @@ print_properties (message, "testkey1", TRUE); EOF cat <<'EOF' >EXPECTED == stdout == -alice -bob -testvalue1 -testvalue2 +testkey1 = alice +testkey1 = bob +testkey1 = testvalue2 == stderr == EOF test_expect_equal_file EXPECTED OUTPUT @@ -186,27 +195,14 @@ EXPECT0(notmuch_message_add_property (message, "testkey3", "testvalue3")); EXPECT0(notmuch_message_add_property (message, "testkey3", "alice3")); print_properties (message, "testkey", FALSE); EOF -# expected: 4 values for testkey1, 3 values for testkey3 -# they are not guaranteed to be sorted, so sort them, leaving the first -# line '== stdout ==' and the end ('== stderr ==' and whatever error -# may have been printed) alone -mv OUTPUT unsorted_OUTPUT -awk ' NR == 1 { print; next } \ - NR < 6 { print | "sort"; next } \ - NR == 6 { close("sort") } \ - NR < 9 { print | "sort"; next } \ - NR == 9 { close("sort") } \ - { print }' unsorted_OUTPUT > OUTPUT -rm unsorted_OUTPUT cat <<'EOF' >EXPECTED == stdout == -alice -bob -testvalue1 -testvalue2 -alice3 -bob3 -testvalue3 +testkey1 = alice +testkey1 = bob +testkey1 = testvalue2 +testkey3 = alice3 +testkey3 = bob3 +testkey3 = testvalue3 == stderr == EOF test_expect_equal_file EXPECTED OUTPUT @@ -248,7 +244,7 @@ test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "dump message properties" cat < PROPERTIES -#= 4EFC743A.3060609@april.org fancy%20key%20with%20%c3%a1cc%c3%a8nts=import%20value%20with%20= testkey1=alice testkey1=bob testkey1=testvalue1 testkey1=testvalue2 testkey3=alice3 testkey3=bob3 testkey3=testvalue3 +#= 4EFC743A.3060609@april.org fancy%20key%20with%20%c3%a1cc%c3%a8nts=import%20value%20with%20= testkey1=alice testkey1=bob testkey1=testvalue2 testkey3=alice3 testkey3=bob3 testkey3=testvalue3 EOF cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} EXPECT0(notmuch_message_add_property (message, "fancy key with áccènts", "import value with =")); @@ -259,7 +255,7 @@ test_expect_equal_file PROPERTIES OUTPUT test_begin_subtest "dump _only_ message properties" cat < EXPECTED #notmuch-dump batch-tag:3 properties -#= 4EFC743A.3060609@april.org fancy%20key%20with%20%c3%a1cc%c3%a8nts=import%20value%20with%20= testkey1=alice testkey1=bob testkey1=testvalue1 testkey1=testvalue2 testkey3=alice3 testkey3=bob3 testkey3=testvalue3 +#= 4EFC743A.3060609@april.org fancy%20key%20with%20%c3%a1cc%c3%a8nts=import%20value%20with%20= testkey1=alice testkey1=bob testkey1=testvalue2 testkey3=alice3 testkey3=bob3 testkey3=testvalue3 EOF notmuch dump --include=properties > OUTPUT test_expect_equal_file EXPECTED OUTPUT @@ -303,48 +299,32 @@ id:4EFC743A.3060609@april.org EOF test_expect_equal_file EXPECTED OUTPUT -test_begin_subtest "msg.get_property (python)" +test_begin_subtest "msg.properties (python)" test_python <<'EOF' -import notmuch -db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) -msg = db.find_message("4EFC743A.3060609@april.org") -print("testkey1 = {0}".format(msg.get_property("testkey1"))) -print("testkey3 = {0}".format(msg.get_property("testkey3"))) -EOF -cat <<'EOF' > EXPECTED -testkey1 = alice -testkey3 = alice3 -EOF -test_expect_equal_file EXPECTED OUTPUT - -test_begin_subtest "msg.get_properties (python)" -test_python <<'EOF' -import notmuch -db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY) -msg = db.find_message("4EFC743A.3060609@april.org") -for (key,val) in msg.get_properties("testkey1"): +import notmuch2 +db = notmuch2.Database(mode=notmuch2.Database.MODE.READ_ONLY) +msg = db.find("4EFC743A.3060609@april.org") +for (key,val) in msg.properties.getall("testkey1"): print("{0} = {1}".format(key,val)) EOF cat <<'EOF' > EXPECTED testkey1 = alice testkey1 = bob -testkey1 = testvalue1 testkey1 = testvalue2 EOF test_expect_equal_file EXPECTED OUTPUT -test_begin_subtest "msg.get_properties (python, prefix)" +test_begin_subtest "msg.properties (python, prefix)" test_python <<'EOF' -import notmuch -db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY) -msg = db.find_message("4EFC743A.3060609@april.org") -for (key,val) in msg.get_properties("testkey"): +import notmuch2 +db = notmuch2.Database(mode=notmuch2.Database.MODE.READ_ONLY) +msg = db.find("4EFC743A.3060609@april.org") +for (key,val) in msg.properties.getall("testkey"): print("{0} = {1}".format(key,val)) EOF cat <<'EOF' > EXPECTED testkey1 = alice testkey1 = bob -testkey1 = testvalue1 testkey1 = testvalue2 testkey3 = alice3 testkey3 = bob3 @@ -352,14 +332,59 @@ testkey3 = testvalue3 EOF test_expect_equal_file EXPECTED OUTPUT -test_begin_subtest "msg.get_properties (python, exact)" +test_begin_subtest "msg.properties (python, exact)" test_python <<'EOF' -import notmuch -db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY) -msg = db.find_message("4EFC743A.3060609@april.org") -for (key,val) in msg.get_properties("testkey",True): +import notmuch2 +db = notmuch2.Database(mode=notmuch2.Database.MODE.READ_ONLY) +msg = db.find("4EFC743A.3060609@april.org") +for (key,val) in msg.properties("testkey",True): print("{0} = {1}".format(key,val)) EOF test_expect_equal_file /dev/null OUTPUT +test_begin_subtest "notmuch_message_remove_all_properties_with_prefix" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +EXPECT0(notmuch_message_remove_all_properties_with_prefix (message, "testkey3")); +print_properties (message, "", FALSE); +EOF +cat <<'EOF' >EXPECTED +== stdout == +fancy key with áccènts = import value with = +testkey1 = alice +testkey1 = bob +testkey1 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "edit property on removed message without uncaught exception" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +EXPECT0(notmuch_database_remove_message (db, notmuch_message_get_filename (message))); +stat = notmuch_message_remove_property (message, "example", "example"); +if (stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION) + fprintf (stderr, "unable to remove properties on message"); +EOF +cat <<'EOF' >EXPECTED +== stdout == +== stderr == +unable to remove properties on message +EOF +test_expect_equal_file EXPECTED OUTPUT + +add_email_corpus + +test_begin_subtest "remove all properties on removed message without uncaught exception" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +EXPECT0(notmuch_database_remove_message (db, notmuch_message_get_filename (message))); +stat = notmuch_message_remove_all_properties_with_prefix (message, ""); +if (stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION) + fprintf (stderr, "unable to remove properties on message"); +EOF +cat <<'EOF' >EXPECTED +== stdout == +== stderr == +unable to remove properties on message +EOF +test_expect_equal_file EXPECTED OUTPUT + test_done diff --git a/test/T620-lock.sh b/test/T620-lock.sh index 7aaaff2a..99cc7010 100755 --- a/test/T620-lock.sh +++ b/test/T620-lock.sh @@ -9,9 +9,6 @@ if [ $NOTMUCH_HAVE_XAPIAN_DB_RETRY_LOCK -ne 1 ]; then test_subtest_known_broken fi test_C ${MAIL_DIR} <<'EOF' -#include -#include -#include #include void @@ -43,15 +40,25 @@ main (int argc, char **argv) if (child == 0) { notmuch_database_t *db2; + char* msg = NULL; sleep (1); - EXPECT0 (notmuch_database_open (path, NOTMUCH_DATABASE_MODE_READ_WRITE, &db2)); + + EXPECT0(notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + "", NULL, &db2, &msg)); + if (msg) fputs (msg, stderr); + taggit (db2, "child"); EXPECT0 (notmuch_database_close (db2)); } else { notmuch_database_t *db; + char* msg = NULL; - EXPECT0 (notmuch_database_open (path, NOTMUCH_DATABASE_MODE_READ_WRITE, &db)); + EXPECT0(notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + "", NULL, &db, &msg)); + if (msg) fputs (msg, stderr); taggit (db, "parent"); sleep (2); EXPECT0 (notmuch_database_close (db)); diff --git a/test/T630-emacs-draft.sh b/test/T630-emacs-draft.sh index c443f417..7d0da21a 100755 --- a/test/T630-emacs-draft.sh +++ b/test/T630-emacs-draft.sh @@ -71,4 +71,36 @@ Fcc: MAIL_DIR/sent <#secure method=pgpmime mode=sign> EOF test_expect_equal_file EXPECTED OUTPUT.clean + +add_email_corpus attachment +test_begin_subtest "Saving a draft keeps hidden headers" +test_emacs '(notmuch-mua-new-reply "id:874llc2bkp.fsf@curie.anarc.at") + (message-goto-subject) + (delete-region (line-beginning-position) (line-beginning-position 2)) + (insert "Subject: draft-test-reply\n") + (test-output "DRAFT") + (notmuch-draft-postpone) + (notmuch-show "subject:draft-test-reply") + (notmuch-show-resume-message) + (test-output)' +notmuch_dir_sanitize OUTPUT > OUTPUT.clean + +cat < EXPECTED +References: <87d10042pu.fsf@curie.anarc.at> <87woy8vx7i.fsf@tesseract.cs.unb.ca> <87a7v42bv9.fsf@curie.anarc.at> <874llc2bkp.fsf@curie.anarc.at> +From: Notmuch Test Suite +To: Antoine Beaupré +Subject: draft-test-reply +In-Reply-To: <874llc2bkp.fsf@curie.anarc.at> +Fcc: MAIL_DIR/sent +--text follows this line-- +Antoine Beaupré writes: + +> And obviously I forget the frigging attachment. +> +> +> PS: don't we have a "you forgot to actually attach the damn file" plugin +> when we detect the word "attachment" and there's no attach? :p +EOF +test_expect_equal_file EXPECTED OUTPUT.clean + test_done diff --git a/test/T640-database-modified.sh b/test/T640-database-modified.sh index 274105c7..2c3fa735 100755 --- a/test/T640-database-modified.sh +++ b/test/T640-database-modified.sh @@ -10,11 +10,8 @@ test_begin_subtest "catching DatabaseModifiedError in _notmuch_message_ensure_me first_id=$(notmuch search --output=messages '*'| head -1 | sed s/^id://) test_C ${MAIL_DIR} < -#include #include -#include -#include + int main (int argc, char **argv) { @@ -26,14 +23,22 @@ main (int argc, char **argv) notmuch_query_t *query; notmuch_tags_t *tags; int i; + char* msg = NULL; - EXPECT0 (notmuch_database_open (path, NOTMUCH_DATABASE_MODE_READ_ONLY, &ro_db)); + EXPECT0(notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + "", NULL, &ro_db, &msg)); + if (msg) fputs (msg, stderr); assert(ro_db); EXPECT0 (notmuch_database_find_message (ro_db, "${first_id}", &ro_message)); assert(ro_message); - EXPECT0 (notmuch_database_open (path, NOTMUCH_DATABASE_MODE_READ_WRITE, &rw_db)); + EXPECT0(notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + "", NULL, &rw_db, &msg)); + if (msg) fputs (msg, stderr); + query = notmuch_query_create(rw_db, ""); EXPECT0 (notmuch_query_search_messages (query, &messages)); diff --git a/test/T650-regexp-query.sh b/test/T650-regexp-query.sh index 55dc6c88..a9844501 100755 --- a/test/T650-regexp-query.sh +++ b/test/T650-regexp-query.sh @@ -65,6 +65,31 @@ thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; - (inbox unread) EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "bracketed subject search (with dquotes)" +notmuch search subject:notmuch and subject:show > EXPECTED +notmuch search 'subject:"(show notmuch)"' > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "bracketed subject search (with dquotes and operator 'or')" +notmuch search subject:notmuch or subject:show > EXPECTED +notmuch search 'subject:"(notmuch or show)"' > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "bracketed subject search (with dquotes and operator 'and')" +notmuch search subject:notmuch and subject:show > EXPECTED +notmuch search 'subject:"(notmuch and show)"' > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "bracketed subject search (with phrase, operator 'or')" +notmuch search 'subject:"mailing list"' or subject:FreeBSD > EXPECTED +notmuch search 'subject:"(""mailing list"" or FreeBSD)"' > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "bracketed subject search (with phrase, operator 'and')" +notmuch search search 'subject:"notmuch show"' and subject:commands > EXPECTED +notmuch search 'subject:"(""notmuch show"" and commands)"' > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + test_begin_subtest "xapian wildcard search for from:" notmuch search --output=messages 'from:cwo*' > OUTPUT test_expect_equal_file cworth.msg-ids OUTPUT diff --git a/test/T700-reindex.sh b/test/T700-reindex.sh index 347f8483..af34ad7c 100755 --- a/test/T700-reindex.sh +++ b/test/T700-reindex.sh @@ -5,7 +5,7 @@ test_description='reindexing messages' add_email_corpus -if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then +if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then count=$(notmuch count --lastmod '*' | cut -f 3) for query in '()' '(not)' '(and)' '(or ())' '(or (not))' '(or (and))' \ diff --git a/test/T710-message-id.sh b/test/T710-message-id.sh index 5129d84c..a2d8ec71 100755 --- a/test/T710-message-id.sh +++ b/test/T710-message-id.sh @@ -3,6 +3,10 @@ test_description="message id parsing" . $(dirname "$0")/test-lib.sh || exit 1 +if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + test_done +fi + test_begin_subtest "good message ids" ${TEST_DIRECTORY}/message-id-parse <OUTPUT <018b1a8f2d1df62e804ce88b65401304832dfbbf.1346614915.git.jani@nikula.org> diff --git a/test/T720-lib-lifetime.sh b/test/T720-lib-lifetime.sh index 3d94d4df..e5afeaa2 100755 --- a/test/T720-lib-lifetime.sh +++ b/test/T720-lib-lifetime.sh @@ -23,7 +23,13 @@ int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; - stat = notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, &db); + char* msg = NULL; + + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + "", NULL, &db, &msg); + if (msg) fputs (msg, stderr); + if (stat != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "error opening database: %d\n", stat); exit (1); diff --git a/test/T750-gzip.sh b/test/T750-gzip.sh index 4408d085..5648896f 100755 --- a/test/T750-gzip.sh +++ b/test/T750-gzip.sh @@ -58,13 +58,13 @@ test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "notmuch search --output=files with partially gzipped mail store" notmuch search --output=files '*' | notmuch_search_files_sanitize > OUTPUT cat < EXPECTED -MAIL_DIR/msg-001.gz -MAIL_DIR/msg-002.gz -MAIL_DIR/msg-003.gz -MAIL_DIR/msg-004 -MAIL_DIR/msg-005.gz -MAIL_DIR/msg-006 -MAIL_DIR/msg-007.gz +MAIL_DIR/msg-XXX.gz +MAIL_DIR/msg-XXX.gz +MAIL_DIR/msg-XXX.gz +MAIL_DIR/msg-XXX +MAIL_DIR/msg-XXX.gz +MAIL_DIR/msg-XXX +MAIL_DIR/msg-XXX.gz EOF test_expect_equal_file EXPECTED OUTPUT diff --git a/test/T760-as-text.sh b/test/T760-as-text.sh new file mode 100755 index 00000000..744567f2 --- /dev/null +++ b/test/T760-as-text.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +test_description='index attachments as text' +. $(dirname "$0")/test-lib.sh || exit 1 + +add_email_corpus indexing +test_begin_subtest "empty as_text; skip text/x-diff" +messages=$(notmuch count id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain) +count=$(notmuch count id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain and ersatz) +test_expect_equal "$messages,$count" "1,0" + +notmuch config set index.as_text "^text/" +add_email_corpus indexing + +test_begin_subtest "as_index is text/; find text/x-diff" +notmuch search id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain > EXPECTED +notmuch search id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain and ersatz > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "reindex with empty as_text, skips text/x-diff" +notmuch config set index.as_text +notmuch reindex '*' +messages=$(notmuch count id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain) +count=$(notmuch count id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain and ersatz) +test_expect_equal "$messages,$count" "1,0" + +test_begin_subtest "reindex with empty as_text; skips application/pdf" +notmuch config set index.as_text +notmuch reindex '*' +gmessages=$(notmuch count id:871qo9p4tf.fsf@tethera.net) +count=$(notmuch count id:871qo9p4tf.fsf@tethera.net and body:not-really-PDF) +test_expect_equal "$messages,$count" "1,0" + +test_begin_subtest "reindex with as_text as text/; finds text/x-diff" +notmuch config set index.as_text "^text/" +notmuch reindex '*' +notmuch search id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain > EXPECTED +notmuch search id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain and ersatz > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "reindex with as_text as text/; skips application/pdf" +notmuch config set index.as_text "^text/" +notmuch config set index.as_text +notmuch reindex '*' +messages=$(notmuch count id:871qo9p4tf.fsf@tethera.net) +count=$(notmuch count id:871qo9p4tf.fsf@tethera.net and body:not-really-PDF) +test_expect_equal "$messages,$count" "1,0" + +test_begin_subtest "as_text has multiple regexes" +notmuch config set index.as_text "blahblah;^text/" +notmuch reindex '*' +notmuch search id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain > EXPECTED +notmuch search id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain and ersatz > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "as_text is non-anchored regex" +notmuch config set index.as_text "e.t/" +notmuch reindex '*' +notmuch search id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain > EXPECTED +notmuch search id:20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain and ersatz > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "as_text is 'application/pdf'" +notmuch config set index.as_text "^application/pdf$" +notmuch reindex '*' +notmuch search id:871qo9p4tf.fsf@tethera.net > EXPECTED +notmuch search id:871qo9p4tf.fsf@tethera.net and '"not really PDF"' > OUTPUT +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "as_text is bad regex" +notmuch config set index.as_text '[' +notmuch reindex '*' >& OUTPUT +cat< EXPECTED +Error in index.as_text: Invalid regular expression: [ +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_done diff --git a/test/T770-emacs-subject-warnings.sh b/test/T770-emacs-subject-warnings.sh new file mode 100755 index 00000000..f9d27140 --- /dev/null +++ b/test/T770-emacs-subject-warnings.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +test_description="emacs subject warnings" +. $(dirname "$0")/test-lib.sh || exit 1 +. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1 + +test_require_emacs + +test_begin_subtest "notmuch-test-subject-warning part 1" +test_emacs_expect_t '(notmuch-test-subject-warning-1)' + +test_done diff --git a/test/T800-asan.sh b/test/T800-asan.sh new file mode 100755 index 00000000..9ce6baa7 --- /dev/null +++ b/test/T800-asan.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +test_description='run code with ASAN enabled against the library' +. $(dirname "$0")/test-lib.sh || exit 1 + +if [ "${NOTMUCH_HAVE_ASAN-0}" != "1" ]; then + printf "Skipping due to missing ASAN support\n" + test_done +fi + +if [ -n "${LD_PRELOAD-}" ]; then + printf "Skipping due to ASAN LD_PRELOAD restrictions\n" + test_done +fi + +add_email_corpus + +TEST_CFLAGS="${TEST_CFLAGS:-} -fsanitize=address" + +test_begin_subtest "open and destroy" +test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} < +#include + +int main(int argc, char **argv) { + notmuch_database_t *db = NULL; + + notmuch_status_t st = notmuch_database_open_with_config(argv[1], + NOTMUCH_DATABASE_MODE_READ_ONLY, + argv[2], NULL, &db, NULL); + + printf("db != NULL: %d\n", db != NULL); + if (db != NULL) + notmuch_database_destroy(db); + return 0; +} +EOF +cat < EXPECTED +== stdout == +db != NULL: 1 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_done diff --git a/test/T810-tsan.sh b/test/T810-tsan.sh new file mode 100755 index 00000000..1829a733 --- /dev/null +++ b/test/T810-tsan.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash + +test_directory=$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null && pwd) + +test_description='run code with TSan enabled against the library' +# Note it is hard to ensure race conditions are deterministic so this +# only provides best effort detection. Compile Notmuch with +# make CFLAGS=-fsanitize=thread LDFLAGS=-fsanitize=thread + +. "$test_directory"/test-lib.sh || exit 1 + +if [ "${NOTMUCH_HAVE_TSAN-0}" != "1" ]; then + printf "Skipping due to missing TSan support\n" + test_done +fi + +export TSAN_OPTIONS="suppressions=$test_directory/T810-tsan.suppressions" +TEST_CFLAGS="${TEST_CFLAGS:-} -fsanitize=thread" + +cp -r ${MAIL_DIR} ${MAIL_DIR}-2 + +test_begin_subtest "create" +test_C ${MAIL_DIR} ${MAIL_DIR}-2 < +#include + +void *thread (void *arg) { + char *mail_dir = arg; + /* + * Calls into notmuch_query_search_messages which was using the thread-unsafe + * Xapian::Query::MatchAll. + */ + EXPECT0(notmuch_database_create (mail_dir, NULL)); + return NULL; +} + +int main (int argc, char **argv) { + pthread_t t1, t2; + EXPECT0(pthread_create (&t1, NULL, thread, argv[1])); + EXPECT0(pthread_create (&t2, NULL, thread, argv[2])); + EXPECT0(pthread_join (t1, NULL)); + EXPECT0(pthread_join (t2, NULL)); + return 0; +} +EOF +cat < EXPECTED +== stdout == +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +add_email_corpus +rm -r ${MAIL_DIR}-2 +cp -r ${MAIL_DIR} ${MAIL_DIR}-2 + +test_begin_subtest "query" +test_C ${MAIL_DIR} ${MAIL_DIR}-2 < +#include + +void *thread (void *arg) { + char *mail_dir = arg; + notmuch_database_t *db; + /* + * 'from' is NOTMUCH_FIELD_PROBABILISTIC | NOTMUCH_FIELD_PROCESSOR and an + * empty string gets us to RegexpFieldProcessor::operator which was using + * the tread-unsafe Xapian::Query::MatchAll. + */ + EXPECT0(notmuch_database_open_with_config (mail_dir, + NOTMUCH_DATABASE_MODE_READ_ONLY, + NULL, NULL, &db, NULL)); + notmuch_query_t *query = notmuch_query_create (db, "from:\"\""); + notmuch_messages_t *messages; + EXPECT0(notmuch_query_search_messages (query, &messages)); + return NULL; +} + +int main (int argc, char **argv) { + pthread_t t1, t2; + EXPECT0(pthread_create (&t1, NULL, thread, argv[1])); + EXPECT0(pthread_create (&t2, NULL, thread, argv[2])); + EXPECT0(pthread_join (t1, NULL)); + EXPECT0(pthread_join (t2, NULL)); + return 0; +} +EOF +cat < EXPECTED +== stdout == +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then + test_begin_subtest "sexp query" + test_C ${MAIL_DIR} ${MAIL_DIR}-2 < +#include + +void *thread (void *arg) { + char *mail_dir = arg; + notmuch_database_t *db; + /* + * Query generation from s-expression used the tread-unsafe + * Xapian::Query::MatchAll. + */ + EXPECT0(notmuch_database_open_with_config (mail_dir, + NOTMUCH_DATABASE_MODE_READ_ONLY, + NULL, NULL, &db, NULL)); + notmuch_query_t *query; + EXPECT0(notmuch_query_create_with_syntax (db, "(from *)", NOTMUCH_QUERY_SYNTAX_SEXP, &query)); + notmuch_messages_t *messages; + EXPECT0(notmuch_query_search_messages (query, &messages)); + return NULL; +} + +int main (int argc, char **argv) { + pthread_t t1, t2; + EXPECT0(pthread_create (&t1, NULL, thread, argv[1])); + EXPECT0(pthread_create (&t2, NULL, thread, argv[2])); + EXPECT0(pthread_join (t1, NULL)); + EXPECT0(pthread_join (t2, NULL)); + return 0; +} +EOF + cat < EXPECTED +== stdout == +== stderr == +EOF + test_expect_equal_file EXPECTED OUTPUT +fi + +test_done diff --git a/test/T810-tsan.suppressions b/test/T810-tsan.suppressions new file mode 100644 index 00000000..80dc062f --- /dev/null +++ b/test/T810-tsan.suppressions @@ -0,0 +1,3 @@ +# It's unclear how TSan-friendly GLib is: +# https://gitlab.gnome.org/GNOME/glib/-/issues/1672 +called_from_lib:libglib*.so diff --git a/test/T850-git.sh b/test/T850-git.sh new file mode 100755 index 00000000..2591521b --- /dev/null +++ b/test/T850-git.sh @@ -0,0 +1,446 @@ +#!/usr/bin/env bash +test_description='"notmuch git" to save and restore tags' +. $(dirname "$0")/test-lib.sh || exit 1 + +if [ "${NOTMUCH_HAVE_SFSEXP-0}" != "1" ]; then + printf "Skipping due to missing sfsexp library\n" + test_done +fi + +# be very careful using backup_database / restore_database in this +# file, as they fool the cache invalidation checks in notmuch-git. + +add_email_corpus + +git config --global user.email notmuch@example.org +git config --global user.name "Notmuch Test Suite" + +test_begin_subtest "init" +test_expect_success "notmuch git -p '' -C remote.git init" + +test_begin_subtest "init (git.path)" +notmuch config set git.path configured.git +notmuch git init +notmuch config set git.path +output=$(git -C configured.git rev-parse --is-bare-repository) +test_expect_equal "$output" "true" + +test_begin_subtest "clone" +test_expect_success "notmuch git -p '' -C tags.git clone remote.git" + +test_begin_subtest "initial commit needs force" +test_expect_code 1 "notmuch git -C tags.git commit" + +test_begin_subtest "committing new prefix requires force" +notmuch git -C force-prefix.git init +notmuch tag +new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu +test_expect_code 1 "notmuch git -l debug -p 'new-prefix::' -C force-prefix.git commit" +notmuch tag -new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu + +test_begin_subtest "committing new prefix works with force" +notmuch tag +new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -l debug -p 'new-prefix::' -C force-prefix.git commit --force +git -C force-prefix.git ls-tree -r --name-only HEAD | notmuch_git_sanitize | xargs dirname | sort -u > OUTPUT +notmuch tag -new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu +cat <EXPECTED +20091117190054.GU3165@dottiness.seas.harvard.edu +EOF +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "checkout new prefix requires force" +test_expect_code 1 "notmuch git -l debug -p 'new-prefix::' -C force-prefix.git checkout" + +test_begin_subtest "checkout new prefix works with force" +notmuch dump > BEFORE +notmuch git -l debug -p 'new-prefix::' -C force-prefix.git checkout --force +notmuch dump --include=tags id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT +notmuch restore < BEFORE +cat < EXPECTED ++inbox +new-prefix%3a%3afoo +signed +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu +EOF +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "commit" +notmuch git -C tags.git commit --force +git -C tags.git ls-tree -r --name-only HEAD | notmuch_git_sanitize | xargs dirname | sort -u > OUTPUT +notmuch search --output=messages '*' | sed s/^id:// | sort > EXPECTED +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "commit --force succeeds" +notmuch git -C force.git init +test_expect_success "notmuch git -C force.git commit --force" + +test_begin_subtest "changing git.safe_fraction succeeds" +notmuch config set git.safe_fraction 1 +notmuch git -C force2.git init +test_expect_success "notmuch git -C force2.git commit" +notmuch config set git.safe_fraction + +test_begin_subtest "commit, with quoted tag" +notmuch git -C clone2.git clone tags.git +git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > BEFORE +notmuch tag '+"quoted tag"' '*' +notmuch git -C clone2.git commit +notmuch tag '-"quoted tag"' '*' +git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > AFTER +test_expect_equal_file_nonempty BEFORE AFTER + +test_begin_subtest "commit (incremental)" +notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C tags.git commit +git -C tags.git ls-tree -r --name-only HEAD | notmuch_git_sanitize | \ + grep 20091117190054 | sort > OUTPUT +echo "--------------------------------------------------" >> OUTPUT +notmuch tag -test id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C tags.git commit +git -C tags.git ls-tree -r --name-only HEAD | notmuch_git_sanitize | \ + grep 20091117190054 | sort >> OUTPUT +cat < EXPECTED +20091117190054.GU3165@dottiness.seas.harvard.edu/inbox +20091117190054.GU3165@dottiness.seas.harvard.edu/signed +20091117190054.GU3165@dottiness.seas.harvard.edu/test +20091117190054.GU3165@dottiness.seas.harvard.edu/unread +-------------------------------------------------- +20091117190054.GU3165@dottiness.seas.harvard.edu/inbox +20091117190054.GU3165@dottiness.seas.harvard.edu/signed +20091117190054.GU3165@dottiness.seas.harvard.edu/unread +EOF +test_expect_equal_file_nonempty EXPECTED OUTPUT + +test_begin_subtest "commit (change prefix)" +notmuch tag +test::one id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C tags.git -p 'test::' commit --force +git -C tags.git ls-tree -r --name-only HEAD | + grep 20091117190054 | notmuch_git_sanitize | sort > OUTPUT +echo "--------------------------------------------------" >> OUTPUT +notmuch tag -test::one id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C tags.git commit --force +git -C tags.git ls-tree -r --name-only HEAD | notmuch_git_sanitize | \ + grep 20091117190054 | sort >> OUTPUT +cat < EXPECTED +20091117190054.GU3165@dottiness.seas.harvard.edu/one +-------------------------------------------------- +20091117190054.GU3165@dottiness.seas.harvard.edu/inbox +20091117190054.GU3165@dottiness.seas.harvard.edu/signed +20091117190054.GU3165@dottiness.seas.harvard.edu/unread +EOF +test_expect_equal_file_nonempty EXPECTED OUTPUT + +backup_database +test_begin_subtest "large checkout needs --force" +notmuch tag -inbox '*' +test_expect_code 1 "notmuch git -C tags.git checkout" +restore_database + +test_begin_subtest "checkout (git.safe_fraction)" +notmuch git -C force3.git clone tags.git +notmuch dump > BEFORE +notmuch tag -inbox '*' +notmuch config set git.safe_fraction 1 +notmuch git -C force3.git checkout +notmuch config set git.safe_fraction +notmuch dump > AFTER +test_expect_equal_file_nonempty BEFORE AFTER + +test_begin_subtest "checkout" +notmuch dump > BEFORE +notmuch tag -inbox '*' +notmuch git -C tags.git checkout --force +notmuch dump > AFTER +test_expect_equal_file_nonempty BEFORE AFTER + +test_begin_subtest "archive" +notmuch git -C tags.git archive | tar tf - | \ + grep 20091117190054.GU3165@dottiness.seas.harvard.edu | notmuch_git_sanitize | sort > OUTPUT +cat < EXPECTED +20091117190054.GU3165@dottiness.seas.harvard.edu/ +20091117190054.GU3165@dottiness.seas.harvard.edu/inbox +20091117190054.GU3165@dottiness.seas.harvard.edu/signed +20091117190054.GU3165@dottiness.seas.harvard.edu/unread +EOF +notmuch git -C tags.git checkout +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "status" +notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C tags.git status > OUTPUT +cat < EXPECTED +A 20091117190054.GU3165@dottiness.seas.harvard.edu test +EOF +notmuch git -C tags.git checkout +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "status (global config argument)" +cp notmuch-config notmuch-config.new +notmuch --config=notmuch-config.new config set git.path tags.git +notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch --config=./notmuch-config.new git status > OUTPUT +cat < EXPECTED +A 20091117190054.GU3165@dottiness.seas.harvard.edu test +EOF +notmuch --config=notmuch-config.new git checkout +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "fetch" +notmuch tag +test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C remote.git commit --force +notmuch tag -test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C tags.git fetch +notmuch git -C tags.git status > OUTPUT +cat < EXPECTED + a 20091117190054.GU3165@dottiness.seas.harvard.edu test2 +EOF +notmuch git -C tags.git checkout +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "merge" +notmuch git -C tags.git merge +notmuch dump id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT +cat < EXPECTED ++inbox +signed +test2 +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "push" +notmuch tag +test3 id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C tags.git commit +notmuch tag -test3 id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C tags.git push +notmuch git -C remote.git checkout +notmuch dump id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT +cat < EXPECTED ++inbox +signed +test2 +test3 +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "reset" +notmuch git -C reset.git -p '' clone remote.git +notmuch git -C reset.git checkout --force +notmuch tag +test4 id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C remote.git commit --force +notmuch tag -test4 id:20091117190054.GU3165@dottiness.seas.harvard.edu +notmuch git -C reset.git fetch +notmuch git -C reset.git reset +notmuch git -C reset.git checkout --force +notmuch dump id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT +cat < EXPECTED ++inbox +signed +test2 +test3 +test4 +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "reset (require force for large change)" +notmuch git -C reset2.git -p '' clone remote.git +notmuch git -C reset2.git checkout --force +notmuch tag +test5 '*' +notmuch git -C remote.git commit --force +notmuch tag -test5 '*' +notmuch git -C reset2.git fetch +test_expect_code 1 "notmuch git -C reset2.git -l debug reset" + +test_begin_subtest "reset (don't require force for large change to one message)" +notmuch git -C reset3.git -p '' clone remote.git +notmuch git -C reset3.git checkout --force +notmuch dump id:20091117190054.GU3165@dottiness.seas.harvard.edu > BEFORE +for tag in $(seq 1 100); do + notmuch tag +$tag id:20091117190054.GU3165@dottiness.seas.harvard.edu +done +notmuch git -C remote.git commit --force +notmuch restore < BEFORE +notmuch git -C reset3.git fetch +test_expect_code 0 "notmuch git -C reset3.git -l debug reset" + +test_begin_subtest "reset --force" +notmuch git -C reset4.git -p '' clone remote.git +notmuch git -C reset4.git checkout --force +notmuch tag +test6 '*' +notmuch git -C remote.git commit --force +notmuch tag -test6 '*' +notmuch git -C reset4.git fetch +test_expect_code 0 "notmuch git -C reset4.git -l debug reset --force" + +test_begin_subtest "environment passed through when run as 'notmuch git'" +env NOTMUCH_GIT_DIR=foo NOTMUCH_GIT_PREFIX=bar NOTMUCH_PROFILE=default notmuch git -C tags.git -p '' -ldebug status |& \ + grep '^env ' | notmuch_dir_sanitize > OUTPUT +cat < EXPECTED +env NOTMUCH_GIT_DIR = foo +env NOTMUCH_GIT_PREFIX = bar +env NOTMUCH_PROFILE = default +env NOTMUCH_CONFIG = CWD/notmuch-config +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "--nmbug argument sets defaults" +notmuch git -ldebug --nmbug status |& grep '^\(prefix\|repository\)' | notmuch_dir_sanitize > OUTPUT +cat < EXPECTED +prefix = notmuch:: +repository = CWD/home/.nmbug +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "invoke as nmbug sets defaults" +test_subtest_broken_for_installed +"$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^\(prefix\|repository\)' | notmuch_dir_sanitize > OUTPUT +cat < EXPECTED +prefix = notmuch:: +repository = CWD/home/.nmbug +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "env variable NOTMUCH_GIT_DIR works when invoked as nmbug" +test_subtest_broken_for_installed +NOTMUCH_GIT_DIR=`pwd`/foo "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT +cat < EXPECTED +repository = CWD/foo +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "env variable NOTMUCH_GIT_DIR works when invoked as 'notmuch git'" +NOTMUCH_GIT_DIR=`pwd`/remote.git notmuch git -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT +cat < EXPECTED +repository = CWD/remote.git +EOF +test_expect_equal_file EXPECTED OUTPUT + + +test_begin_subtest "env variable NOTMUCH_GIT_DIR overrides config when invoked as 'nmbug'" +test_subtest_broken_for_installed +notmuch config set git.path `pwd`/bar +NOTMUCH_GIT_DIR=`pwd`/remote.git "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT +notmuch config set git.path +cat < EXPECTED +repository = CWD/remote.git +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "env variable NOTMUCH_GIT_DIR overrides config when invoked as 'notmuch git'" +notmuch config set git.path `pwd`/bar +NOTMUCH_GIT_DIR=`pwd`/remote.git notmuch git -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT +notmuch config set git.path +cat < EXPECTED +repository = CWD/remote.git +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "env variable NOTMUCH_GIT_PREFIX works when invoked as 'nmbug'" +test_subtest_broken_for_installed +NOTMUCH_GIT_PREFIX=env:: "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT +cat < EXPECTED +prefix = env:: +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "env variable NOTMUCH_GIT_PREFIX works when invoked as nmbug" +test_subtest_broken_for_installed +NOTMUCH_GIT_PREFIX=foo:: "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT +cat < EXPECTED +prefix = foo:: +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "env variable NOTMUCH_GIT_PREFIX overrides config when invoked as 'nmbug'" +test_subtest_broken_for_installed +notmuch config set git.tag_prefix config:: +NOTMUCH_GIT_PREFIX=env:: "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT +notmuch config set git.path +cat < EXPECTED +prefix = env:: +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "env variable NOTMUCH_GIT_PREFIX overrides config when invoked as 'notmuch git'" +notmuch config set git.tag_prefix config:: +NOTMUCH_GIT_PREFIX=env:: notmuch git -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT +notmuch config set git.path +cat < EXPECTED +prefix = env:: +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "init, xdg default location" +repo=home/.local/share/notmuch/default/git +notmuch git -ldebug init |& grep '^repository' | notmuch_dir_sanitize > OUTPUT +git -C $repo rev-parse --absolute-git-dir | notmuch_dir_sanitize >> OUTPUT +cat < EXPECTED +repository = CWD/$repo +CWD/$repo +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "init, xdg default location, with profile" +repo=home/.local/share/notmuch/work/git +NOTMUCH_PROFILE=work notmuch git -ldebug init |& grep '^repository' | notmuch_dir_sanitize > OUTPUT +git -C $repo rev-parse --absolute-git-dir | notmuch_dir_sanitize >> OUTPUT +cat < EXPECTED +repository = CWD/$repo +CWD/$repo +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "init, configured location" +repo=configured-tags +notmuch config set git.path `pwd`/$repo +notmuch git -ldebug init |& grep '^repository' | notmuch_dir_sanitize > OUTPUT +notmuch config set git.path +git -C $repo rev-parse --absolute-git-dir | notmuch_dir_sanitize >> OUTPUT +cat < EXPECTED +repository = CWD/$repo +CWD/$repo +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "configured tag prefix" +notmuch config set git.tag_prefix test:: +notmuch git -ldebug status |& grep '^prefix' > OUTPUT +notmuch config set git.tag_prefix +cat < EXPECTED +prefix = test:: +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "default version is 1" +notmuch git -l debug -C default-version.git init +output=$(git -C default-version.git cat-file blob HEAD:FORMAT) +test_expect_equal "${output}" 1 + +test_begin_subtest "illegal version" +test_expect_code 1 "notmuch git -l debug -C default-version.git init --format-version=42" + +hash=("" "8d/c3/") # for use in synthetic repo contents. +for ver in {0..1}; do + test_begin_subtest "init version=${ver}" + notmuch git -C version-${ver}.git -p "test${ver}::" init --format-version=${ver} + output=$(git -C version-${ver}.git ls-tree -r --name-only HEAD) + expected=("" "FORMAT") + test_expect_equal "${output}" "${expected[${ver}]}" + + test_begin_subtest "initial commit version=${ver}" + notmuch tag "+test${ver}::a" "+test${ver}::b" id:20091117190054.GU3165@dottiness.seas.harvard.edu + notmuch git -C version-${ver}.git -p "test${ver}::" commit --force + git -C version-${ver}.git ls-tree -r --name-only HEAD | grep -v FORMAT > OUTPUT +cat < EXPECTED +tags/${hash[${ver}]}20091117190054.GU3165@dottiness.seas.harvard.edu/a +tags/${hash[${ver}]}20091117190054.GU3165@dottiness.seas.harvard.edu/b +EOF + test_expect_equal_file_nonempty EXPECTED OUTPUT + + test_begin_subtest "second commit repo=${ver}" + notmuch tag "+test${ver}::c" "+test${ver}::d" id:20091117190054.GU3165@dottiness.seas.harvard.edu + notmuch git -C version-${ver}.git -p "test${ver}::" commit --force + git -C version-${ver}.git ls-tree -r --name-only HEAD | grep -v FORMAT > OUTPUT +cat < EXPECTED +tags/${hash[$ver]}20091117190054.GU3165@dottiness.seas.harvard.edu/a +tags/${hash[$ver]}20091117190054.GU3165@dottiness.seas.harvard.edu/b +tags/${hash[$ver]}20091117190054.GU3165@dottiness.seas.harvard.edu/c +tags/${hash[$ver]}20091117190054.GU3165@dottiness.seas.harvard.edu/d +EOF + test_expect_equal_file_nonempty EXPECTED OUTPUT + + test_begin_subtest "checkout repo=${ver} " + notmuch dump > BEFORE + notmuch tag -test::${ver}::a '*' + notmuch git -C version-${ver}.git -p "test${ver}::" checkout --force + notmuch dump > AFTER + test_expect_equal_file_nonempty BEFORE AFTER +done + +test_done diff --git a/test/corpora/attachment/text-calendar.eml b/test/corpora/attachment/text-calendar.eml new file mode 100644 index 00000000..8e064f9b --- /dev/null +++ b/test/corpora/attachment/text-calendar.eml @@ -0,0 +1,55 @@ +From: David Bremner +To: "david@tethera.net" +Subject: test +Date: Thu Jan 01 00:00:10 +0000 +Message-ID: + +Accept-Language: en-CA, en-US +Content-Language: en-CA +Content-Type: multipart/alternative; + boundary="_000_YT3PR01MB10572EFC9F7C81F9446214768CEF72YT3PR01MB10572CA_" +MIME-Version: 1.0 + +--_000_YT3PR01MB10572EFC9F7C81F9446214768CEF72YT3PR01MB10572CA_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +This meeting will could have been an email + + +--_000_YT3PR01MB10572EFC9F7C81F9446214768CEF72YT3PR01MB10572CA_ +Content-Type: text/calendar; charset="utf-8"; method=REQUEST +Content-Transfer-Encoding: base64 + +QkVHSU46VkNBTEVOREFSDQpNRVRIT0Q6UkVRVUVTVA0KUFJPRElEOk1pY3Jvc29mdCBFeGNoYW5n +ZSBTZXJ2ZXIgMjAxMA0KVkVSU0lPTjoyLjANCkJFR0lOOlZUSU1FWk9ORQ0KVFpJRDpBdGxhbnRp +YyBTdGFuZGFyZCBUaW1lDQpCRUdJTjpTVEFOREFSRA0KRFRTVEFSVDoxNjAxMDEwMVQwMjAwMDAN +ClRaT0ZGU0VURlJPTTotMDMwMA0KVFpPRkZTRVRUTzotMDQwMA0KUlJVTEU6RlJFUT1ZRUFSTFk7 +SU5URVJWQUw9MTtCWURBWT0xU1U7QllNT05USD0xMQ0KRU5EOlNUQU5EQVJEDQpCRUdJTjpEQVlM +SUdIVA0KRFRTVEFSVDoxNjAxMDEwMVQwMjAwMDANClRaT0ZGU0VURlJPTTotMDQwMA0KVFpPRkZT +RVRUTzotMDMwMA0KUlJVTEU6RlJFUT1ZRUFSTFk7SU5URVJWQUw9MTtCWURBWT0yU1U7QllNT05U +SD0zDQpFTkQ6REFZTElHSFQNCkVORDpWVElNRVpPTkUNCkJFR0lOOlZFVkVOVA0KT1JHQU5JWkVS +O0NOPURhdmlkIEJyZW1uZXI6bWFpbHRvOmJyZW1uZXJAZXhhbXBsZS5jb20NCkFUVEVOREVFO1JP +TEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPVRSVUU7Q049ZGF2 +aWRAdGV0aA0KIGVyYS5uZXQ6bWFpbHRvOmRhdmlkQHRldGhlcmEubmV0DQpERVNDUklQVElPTjtM +QU5HVUFHRT1lbi1DQTpUaGlzIG1lZXRpbmcgd2lsbCBjb3VsZCBoYXZlIGJlZW4gYW4gZW1haWxc +blxuDQpSUlVMRTpGUkVRPURBSUxZO1VOVElMPTIwMjQwODI3VDE3MDAwMFo7SU5URVJWQUw9MQ0K +VUlEOjA0MDAwMDAwODIwMEUwMDA3NEM1QjcxMDFBODJFMDA4MDAwMDAwMDAwRUE4NDFFNzk5QUZE +QTAxMDAwMDAwMDAwMDAwMDAwDQogMDEwMDAwMDAwNjkyRkRFNzQzMzNGQ0I0N0JFNEY0REU5MDk0 +OThDQkUNClNVTU1BUlk7TEFOR1VBR0U9ZW4tQ0E6dGVzdCBbSW4tcGVyc29uXQ0KRFRTVEFSVDtU +WklEPUF0bGFudGljIFN0YW5kYXJkIFRpbWU6MjAyNDA1MjdUMTQwMDAwDQpEVEVORDtUWklEPUF0 +bGFudGljIFN0YW5kYXJkIFRpbWU6MjAyNDA1MjdUMTQzMDAwDQpDTEFTUzpQVUJMSUMNClBSSU9S +SVRZOjUNCkRUU1RBTVA6MjAyNDA1MjZUMTgyNDEwWg0KVFJBTlNQOk9QQVFVRQ0KU1RBVFVTOkNP +TkZJUk1FRA0KU0VRVUVOQ0U6MA0KTE9DQVRJT047TEFOR1VBR0U9ZW4tQ0E6DQpYLU1JQ1JPU09G +VC1DRE8tQVBQVC1TRVFVRU5DRTowDQpYLU1JQ1JPU09GVC1DRE8tT1dORVJBUFBUSUQ6MjEyMjcw +MDA0Ng0KWC1NSUNST1NPRlQtQ0RPLUJVU1lTVEFUVVM6VEVOVEFUSVZFDQpYLU1JQ1JPU09GVC1D +RE8tSU5URU5ERURTVEFUVVM6QlVTWQ0KWC1NSUNST1NPRlQtQ0RPLUFMTERBWUVWRU5UOkZBTFNF +DQpYLU1JQ1JPU09GVC1DRE8tSU1QT1JUQU5DRToxDQpYLU1JQ1JPU09GVC1DRE8tSU5TVFRZUEU6 +MQ0KWC1NSUNST1NPRlQtRE9OT1RGT1JXQVJETUVFVElORzpGQUxTRQ0KWC1NSUNST1NPRlQtRElT +QUxMT1ctQ09VTlRFUjpGQUxTRQ0KWC1NSUNST1NPRlQtUkVRVUVTVEVEQVRURU5EQU5DRU1PREU6 +SU5QRVJTT05SRVFVSVJFRA0KWC1NSUNST1NPRlQtSVNSRVNQT05TRVJFUVVFU1RFRDpUUlVFDQpY +LU1JQ1JPU09GVC1MT0NBVElPTlM6W10NCkJFR0lOOlZBTEFSTQ0KREVTQ1JJUFRJT046UkVNSU5E +RVINClRSSUdHRVI7UkVMQVRFRD1TVEFSVDotUFQxNU0NCkFDVElPTjpESVNQTEFZDQpFTkQ6VkFM +QVJNDQpFTkQ6VkVWRU5UDQpFTkQ6VkNBTEVOREFSDQo= + +--_000_YT3PR01MB10572EFC9F7C81F9446214768CEF72YT3PR01MB10572CA_-- diff --git a/test/corpora/attachment/x-gtar-compressed.eml b/test/corpora/attachment/x-gtar-compressed.eml new file mode 100644 index 00000000..258a74d1 --- /dev/null +++ b/test/corpora/attachment/x-gtar-compressed.eml @@ -0,0 +1,136 @@ +Return-path: +Envelope-to: david@tethera.net +Delivery-date: Mon, 19 Mar 2018 13:56:54 -0400 +Received: from marcos.anarc.at ([206.248.172.91]) + by fethera.tethera.net with esmtp (Exim 4.89) + (envelope-from ) + id 1exz1i-0002aa-If + for david@tethera.net; Mon, 19 Mar 2018 13:56:54 -0400 +Received: from [127.0.0.1] (localhost [127.0.0.1]) (Authenticated sender: anarcat) with ESMTPSA id 718A610E04F +From: =?utf-8?Q?Antoine_Beaupr=C3=A9?= +To: David Bremner , notmuch@notmuchmail.org +Subject: Re: bug: "no top level messages" crash on Zen email loops +In-Reply-To: <87a7v42bv9.fsf@curie.anarc.at> +References: <87d10042pu.fsf@curie.anarc.at> <87woy8vx7i.fsf@tesseract.cs.unb.ca> <87a7v42bv9.fsf@curie.anarc.at> +Date: Mon, 19 Mar 2018 13:56:54 -0400 +Message-ID: <874llc2bkp.fsf@curie.anarc.at> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=-=-=" +X-Spam_score: 0.0 +X-Spam_score_int: 0 +X-Spam_bar: / + +--=-=-= +Content-Type: text/plain + +And obviously I forget the frigging attachment. + + +--=-=-= +Content-Type: application/x-gtar-compressed +Content-Disposition: attachment; filename=zendesk-email-loop2.tgz +Content-Transfer-Encoding: base64 + +H4sIAJX1r1oAA+xbaVPbyNbO19Gv6CF1K0mBrM2SLYOY8QoGbAzegKlbVFtq28KyJNSSF269//09 +Lclm35LJTebOiARbvZw+a5+npcPIDh08EMwoED58r0uEKyeK7FPKqfc+V9cHSZFVJZeTlWz2gyjJ +OUn9gNTvxtGdK6IhDhD6gF0cmDh8dtxr/X/Ra3Rrf0mVpaym5FQlc6orWl7P5hQpmxdzmi4qSj4D +Y2xSkLdq7XeuwQysPWt/WZIVOba/LGqalJPA/jktJ35A4neR+MH1N7f/KQmjwOVbOBwX0A6NfN8L +wt8Tr8iY3nSXO+OPA3tku9jhO14BpXrYTIb8Ht9mcMhViGPPSECsF0edEpPAMKuAhoE3RVNo9mhm +1Y0+O56JnbFHQ/QHbAIZEX6kf3/hfhkskZUssLwzugUDh/biC5rb4RhV241OC9kWkrJFVZHEqqiK +3C9DL0A7z7Czu406EdlCkoIa4ASyKOXhV0HOFZQc4sWsKKLP1UrnC+ig7eMpXx4Tc0ICvkcCantu +AbHWIqWYUttFSiabkdBnoKLCXF7Of0Ge+1DEFakjMiNOAa1u2yEOI1pATW8LUdMLiMFLGRUF5Dqy +QaeGmhFRSGhIjVLxvNq+VMWtymG9cdmu7zWrlS3ul/iuVzyqV7b2O42jy9pxs3N5dNy/LMOX02K7 +kzQ3qu12ca+6dVruVS7rzctKs91nrRXuFxyFnkNw4BpjPEXru0tQoEkM10OzRGojlhMY3wvI0rFp +WGCmwUtiIUXVECWm51oUgcV8MM8IBoEoiobAYIkunle6hkDbD33EIkMSgA6yGd+ztIyNLSlzY9El +Ze6JPr/c/YekyxlJkzIS7GqSpKe+NF287EVt5kY1WS7HbpStfbUbaffc6IFkXhRKTwv1bM99eUSR +yfM5Au8boc5ReyZl5EQK0/bHJEDFarmyzxerbVnV+PZ+EbxT1QT4jwZ2SL+wyU0PmY5N3BCZJAjt +oQ0CktjxwN2ItYq9F23wnAbL2Ww+q0lFuShKr6qwTyxQYfZWhSKoUCpkRbTJ0gT63O2U36HC9U6S +ib9Z3hRDjD7aVZ4n8MzmUiqWq2VZlHM5XftamRT9JZluiGsROkm4mHvBRHrOF0AMSc5AalbFrxVH +L5WknMzScFX9k8SpgAMVXhvL1UDUAtqzwyM8QO0k7zydgE6J7yzjrPKW0Wxc8Z4Ez0vENQhs3iPC +1yuQ/HQlp5801AtFv1QxzmsDSckOzeHgUsplAQANTT2HZXOg582cpOetS+oHUfj7HWvtcnWXv+X2 +DsH7o55cSoVslWVLkcH9pbRc9qml2tHgipiw+Z57UbDSTKqQVfiiMYaNmBAXGhIX4xr2lNzmLykj +cmXPDSH8+c7SB7NNIye0fRyEAnZCErg4hGnbHBp4kWvhYGls8LxhXE6BDBu1Zl+XMAjzWFOypGU3 +YL45xgEloRGFQz5/u2aAXQpbC191Tc+CXayAcrAzQXIpQv4BXVLfcynhmcUDsBWY1nG4uA/kn9ph +yKKG5Sp+RFwSgOdZMLmBbYcEBXSRKAwl99CRgpQlHzOwAO1BGgY+bIvP5lVFy+miLMt5GJlO5Zmb +8kXTBPFDvg6LgVxDmYgcS7h82x6BhqIAFIdmhrSNsBFQzNMxhj12G5lGAJlxQSwh/dxGlnHHiqCX +a8NyqRAuwm1EV11AJzQAjYt6HlC4zrQ/NpRNOdob+eOjA9ITx7WS2r7qdxZd3PA79SNrTq8ts7G8 +tptddWTAjLFhsTBkG0ohiF0y9Arwb5p6vG0VbJdf99DUm5hd+TTRF8zUSiHzjPXNymQkNRljzzgy +G/Ogrzfz+pV442Fts0lmvd5hq7dcqBPVy17NDhzizlsnh61KLWfnG/kDf7h57Cw2ezUXq6Q66YkL +pxYe1azRebBnXWiT3nH7qnR81VfzWAoWVDmzRq4iir090eotdGtfEMpWcZSjEC65aqPeqmXdUjXn +BV4w3FzOTFKpn8tndauy31ke2KcTTdZPz9STzRPjK03Hge1GkEQcQDGTyM/cteP/ihkPi8L1/szO +iaMDoX5Fg9HeZrNfypYOdHvzpNs5L0/y43arNtwTivsDyTpzrIDcXC33RktJGeXV3l4Qnkxvjqwr +eeDX/H1ltF+JDs91dzZoNoh/le+PvRHBfpdG+PrcOVnsh2fyvjsrDejgeKYuOuc3m1HPOSlNatHs +tGtP8y0tS2oz/8yiE41cn12U+62uV5meluoGBGm5UazybQaXC0hc3cPm7yypDXvFzIDkiMyZUTlo +j/dPawOwqoToFH6FgQFnA2y0XMc5HjQkd+HOLYWqejAVvBPDKEhSDjzi0C4vDiriYrNF1F6xfXI4 +PfcOAnMZHXswRGcU6hOzM9mviMOLRrEgiTDp4sKNzsODidf30hZf7FuNKh4W4SrkoaHIF5f62UxJ +GhiZmyKuRxd86JxUJVO1nIYNk3UYelI9syrHchQq5ym1DolOrhs6vZ3cbk+Pa9VieU1f79jl42sq +t7JLOgi7XbN6YVbPvSXNFxnfMEKdNaIbrB8oZvekeDNeQG5mlC77l+3LXA8w/Em62jC4yRaj8ogf +d1kL25p76UHIut1hEwhaduA41EM8Kh9Vi80/xUD9iXM17VnnEzyw5ovh9awYNI+/0UAzudI6mam8 +M5y/0WSMzn2bobebjE2+bzP0fpMxIk/YDL3FZKtse0TcETvmZ3NanuM4nn9/Kn+AFlgOFXwHkPV7 +Uvx15EGe5v3AdkM8cAjHffzIoxYcNylBbJdCS4Zp4l0N4YE3g9axTZFjuwTxHz9y3L79AONtcVxn +jN0Jm4kYimX7HDZD5pX3UWOGASZkYjelH3oJccK2dnZIxQbnQjNohfVhy0IumSPbBapTQENwpI+n +QK8NMQD0+mNAF/HCc2yHW/G3KV6ioe0anIXGxPGHkXOPApxEmISWZ0ZTUFLSCGuPw9CnBUGADpq5 +RbUZZIC9GN+eDzDHSjhO1mcHbQpRyCSNKAK5YkluxcQOhSNeQNixLplicBTNbByzMCcDpq/p3dVT +FHmHAWFsCsTlIyqkyJIKoJQM6hgcKCKezxiCdSHUrZWC/MCzIjNkCvkUkHSPwG6iEtC4SyH9xlyw +FQ2OIdeQ4GkMW7HjxEQswshSMILJcl4QWwywKCS3kNGKXQVWAj9K9dQnyPG8CWNrjoOYG7YgW5yZ +BnCk58zWPpYAZTqGxZ3lrxxXYrcBGcFMugVORVb+wzjjuIodL4kmrjf/ba3jCSE+bFnYnCBvGLMO +H/ECicZpLDaIY4NnRnCSdsEIiMYPfVK9rNTAlMCMArFBgiE2ya+oZ1P7JesAKUaeAqKBaQ8sP/KQ +ZQP8DxNfX6kTwioK7/lR7AAG95ILrI2vy2pWBs9nDwqgD3zYSsVgJksmA6n7gsRxiVkvCWz4Shbs +C+APAuHi4lFqoLXOMqgax6TpGRxbArTIpBqAT9jDoW3CSYVJNPQcx5tnwDSe+ymMDZOQWWkTJ+gd ++ZhSOE5bv6HSMgmSJGa2WNCsBs3B9QwODht8EjJWzDWz+CAKkyC3wbwuSdwcnImEqQ+m5CHuwTUz +6JQpC0INGIh3kHX/nUhL1XsHSgpwmBkLM1lwPFCIsJp0GTBSbLepxeLGerZdGgYQYLB5ULalxDG3 +WiXhLNnVgNERe/oBQkIMASrlvCdVlEHNRHmJkKBptj2z6ZSQp9z61ySLvHRBCK13V/gC9ifBzGbe +wJ51JLG1hequmQETrq4/kgMyz07I/zb+vFQ1DqfOt2WqnV8rx+XOeauKGC3U6paO6mW0wQtCXykL +QqVTQWfsSSs7WqOYos3sgx1BqDY30AazPBh+Pje4eWauZLxgJHROhQWjJrHp6dcEoKdzM1Zobexy +O6yHfRBs7XII7UwhmGNf4tlz4pmhVDbuCr2BUrTPOm7lX4uvVGIFGNwGEmJ6NFw6SQZezzAp3WB9 +CIWJL1joP/EtQgNwM9CWCQ6JfQoqXn3bjgf8H6MoxCSBZyFhemfgWZCvWCNbYm5bDJBIovivX+0p +c0XswhEKTljg/AAZt8GfrcQU4nbMx45lz27nr3sl0V+g7Rgh8LDLjsZwtJfy/mIbwhe0McRT21kW +0KejyLQtjPYMDtRrkU9bPRJYsPlsFQMbO1sUdM6Dg9rDdCK1b0AwSWaUQDwvKHzMxtd2qpX7/CRD +0MeByn5gzNuAjcGtoM2OAPRSyv7uY5izI/i70PFmrGNwr6GdN2Mdg3sN7bwZ6wDCYmvvgPMGZMgU +9wzw2QC2HdbvAq6IH0EHG7v3xhrc7egdAe9mUKqi96MlSKNP46Wn0dIT3K+zpsG9Ap1eFOxe8jW4 +F8gkEnfeiMHA6V9FYW/EYJDcX0BhqQneBsRA7U9CsZTIPTy2Mwhgq3oAytKBgMwSUt+OzSCgDe49 +2OwlX7BBV29x58eQi1n3PrK7Ded3Y7uHLBrcG2He60yDX71MInHTF9FiqljYGb4ZLaZY0eBeQIsr +n3kbZIRj3Kug8Y2QkUGv10Djk5DxsYsZ3LvwI6z5rCkBW74TjQK1xKoP8ajBfQ0ifRKPQuC8H5Ey +y8bII06kO9QHO63y86c0hdfi69PuPZgJWAXG7u4IDKLEiCVBWyzB2yGZUhPSSfxtBZBSLEfNMQgV +I7nYBdOXShu3sMCyqe/AjrIsuJ5LNnZT2MBowZ7psyE4VtrG21aCgBsJPZvMi8ms3RgvgGdO7tGM +AmfjhX0pNjZEydNBLyREY4h5l6iLp/eBJeMj1r7BgYexaYnuk99fCd7h3PCjK2P+HtdT9V9y5lRS +JFXKaWpeVHN6Ts3ns+LXF4C9Vv8lyuK6/ksVFVb/lctK/9R//Teu/8X6L12vFtVvqP9SxIKq//D6 +LzmTf6r+q9g/2kpqwMQfXgP2TKXWMxUoL3c/qNTS3lOplVdK+do3VGo9NvjjMqNnSp+e7XlQqaX8 +lyu13l4FFGuwokga7PG6JGez361S6xkVvr1S6+3lZ3H1WbVUrspSuVSVf85KrfeJU6xVS1oizute +/rer1Hpv+dQ3VGq9tyhsXam1Ppq6XvyYYx4fJf3fvltRFs4puvWUUvKS9NcqypLyP2VRFhlqN165 +oUuji173Cotdunm1lLs9s3W4SZvjC8+cuYqiTIY31R9QzRPOWsv6jVKea5KrRjK1j+3lqV1tFA9J +ZVqN5EpjJLWpqmQ3xVZrNuiMgqgf9AcXE+fgZqlqp5V6y86OvcPNrq2NbkTBHmud5cnZnhb6tQa5 +yC7qWX3veulH0ol85kndWaT1W+cVkndzFbl0IOTnLW0zur7YrPhFnHOspi17/n5vNrW0vcNydCQO +m3mnP7Mq0+yPLcr6uc1Ir498tecfDoOlM+rIzdzNYE4qVLFMs3rWyGbn3qQhBAv3Iqv7ujA/rxw0 +G4PhsJRfnp1dYKl0IB6eDh2hW7v2Wvnzi9Z5kJPxZlXWpe5o8+ZakrsHttroyz133mgdLIr+5pk+ +PiG6WLxpErNUuTruurp6bZcP3Vnvahx2hKMeaU310qA5znfnZbWvXI+F69pVb/KeoqzTxrGVHVxv +/hxFWWzWdNY8xAvXGbv7IZ87qJzW6XM1WHbZPpbdw8XeeLQq8SkuOmU6j/hnirweFW49KgJiRF4v +3Ko07bl3SDRvOVxWrvDZk0VAjNSDKiAYNArq067bwycVcrg3lTsdr14syPL3Kel63rw/T0nX39Xg +jwrCNEl5oSDsJRjxkxSEvVaF8KAmgXvwWjXGvpKyFf/ZjhT/FRLHziDxKjalEfn35wc1G/HT8YSK +MMcONOGJJ8RjqSCpX7bYw++AIMszOELZawZKyJQ9uWYPsd3lGg1m0DJ5GxC/12GPtZOJsPIUogzu +oNlz2YN0VlNjw4ERwxnQS56uU9siAxyw1xGA4tNH7n/EbCD2x1mg3ydZ/2JwyGYP2sMImF8iipc0 +eYgfT2Vv/7RbFjlunghjszdlHlhljWVH3m8/uAjkve75TxHIP0Ugf5kikHuUE5UApvThyKP6iw0E +QBTz64dylQ15Y3cnsVGsVdYkiQYn/gvcgDhOqirWLCYt1MfmbUtizeSGvcgJg+QtEBh8RQ8chVlq +4yn9A0vMXKlPxGxK0GR57EiIPpoq+9lOXlcBVdjNHrL6NYwyTkB5ibsygryDQcVhYWizI8JquVtx +Hl9MwBl24PSRSAisPyWgmIoYf2yj1HGz4HZ3Vkkp2tMRKyV24ri7TTcbKPFM1pplvAfmvbdzxITT +T2YU4BlYNkiyTPzV4ARFHUhkIIqiCacXrCoS0YaKaWr5vDYksiX/xhwVqP1/O2e32yYQROH7fQpk +KelNaQA7TVW6lnrTF2h7XYEgjpWAkXGUKk/fmeFvzQKmaVRZyvkUyXgJy3J2dnYHxqy8yygrwiS9 +jR7l/FL7xfLrRfCN/qhVZclPzI1giYq3GcUuJW0Env+RPurD3er07ifvQ5FvpGLWfVPpQ3ObNL8e +bRz09wcg9WnId7u49LPDD0BD8cNuQouXveQISTFVFTruUxrfbw9ubUL7KNnybXGR2812z4M7rDKt +qEM6s2Kdr47658vVIRk0BdPSxRzZLw4YgumeFp3piOEcn6loD6eqhv1Q7YWa79/J/zg/8y3NP73S +1lNp9f5HdLfLolFnJcIMeMLahcQ0JPmunhfSOsB0K8fetnF3fuwnfmoZeQ+qimaP/S7frI/XVzwD +SPH4sfzYvldVp9w/6aaVqdwp3ZaWbsF1q5tWnXL+tSHnhHSxYEmnlbXknJBjXDaaHzhv7SEqSxbq +OXHrldnCnpCCOEiD25HZUNTTqtXPktl5V0lHW42WzpiKq+K3Vn0dg06wZq6g2ao43U6t7Ja+sJ1a +2S2d007Os5I0DXJwi7WEBloZwcFAAti8OKFKyVFVUk6VnOQY2Tm04NpvZJW2+BU/RPl9l3o1XD/N +OL0zcJqOEY3Mj0X4FwqT0cj8WIR8exeNTGtl5CgNysG5LycFkWs+Gd/wwGkuvEoIe8mYEUvU6jXG +jFgiafU3tjgVmGkllyXLyRm+xJwU6UuzWKLNKOYF1tr4L9kve6i8XbAi6+rss66momVkXb056vyv +PH06h/e/Lb0b35P3v3krvP/tf1D3/yErzqX//er9f+h/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABg +lD9gaWU0AHgAAA== +--=-=-= +Content-Type: text/plain + + +PS: don't we have a "you forgot to actually attach the damn file" plugin +when we detect the word "attachment" and there's no attach? :p + +--=-=-=-- diff --git a/test/corpora/crypto/basic-encrypted.eml b/test/corpora/crypto/basic-encrypted.eml index 1ba4698a..b139a735 100644 --- a/test/corpora/crypto/basic-encrypted.eml +++ b/test/corpora/crypto/basic-encrypted.eml @@ -17,12 +17,11 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBBACp70e7KPy9OYaheIrkLzmhq1lRqmy51aL1jBL0K/qN7rfK -BZEG1cR8jeLjTFdPKPLVKJI80r7FgKI0ywvWvl6R1aE1Ty5BnVXT9XzCrEH7fqCl -SKK82EvolXTohAZHUrh6K66eQQTTIAC1n7B0A8hErzkgaM4+seN3LlvezT6TLNKM -ATpqsEbM2MVrGgw0b3oUsGGAPEt2MmjNEYsriKnqwt6dJDZc//XyhjgMQayiD8da -N1gT3oqgu/gKCpBZDYzHf9OtVi2UnlFDWy6rrMZLjWDnIv4ve9Pn/qolwHVjzdJ1 -ZfjNC5t0z3XADKGrjN9wutr4qm7STW1rHAXHP68TQTxI0qgJKjPXNKWEw6g= -=pJG4 +hF4DHXHP849rSK8SAQdAYbv9NFaU2Fbd6JbfE87h/yZNyWLJYZ2EseU0WyOz7Agw +/+KTbbIqRcEYhnpQhQXBQ2wqIN5gmdRhaqrj5q0VLV2BOKNJKqXGs/W4DghXwfAu +0oMBqjTd/mMbF0nJLw3bPX+LW47RHQdZ8vUVPlPr0ALg8kqgcfy95Qqy5h796Uyq +xs+I/UUOt7fzTDAw0B4qkRbdSangwYy80N4X43KrAfKSstBH3/7O4285XZr86YhF +rEtsBuwhoXI+DaG3uYZBBMTkzfButmBKHwB2CmWutmVpQL087A== +=lhSz -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/crypto/encrypted-rfc822-attachment b/test/corpora/crypto/encrypted-rfc822-attachment index 56fe3169..e7bad9b0 100644 --- a/test/corpora/crypto/encrypted-rfc822-attachment +++ b/test/corpora/crypto/encrypted-rfc822-attachment @@ -20,33 +20,28 @@ Content-Type: application/octet-stream; charset="us-ascii" -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBBACGKSDv5/rcwScSf9n33cZZPPxltQgxkDaClMGY2DARgebE -9rpE2O/4eoaEbdu+2shahPLIbD0wuRiGVpMIIloNNucl3f15h1wXPZbTwK7sNxJq -HycSf8sT1fkolmC9s9X0r5xHgk0G4klAqr5C3GOk7Y6wsHTYGqzDvBFEB0LvaoUC -DANw48DehwaEUQEP/iaiYeMDUsOzFZodKZOlQWese2JPTsRwF7KrTl8C93MrZqAh -A1pQjUH9cafj8mwDXA9ZCsYZs7r84IxShI+dUhinBSCq8F61OlLP859wq+wpKU7n -PNVA5bfd//4hRFvDT33ZlgeeeCcRo7h4IDjJwFDYsf0Ysqvo+IKipVNXXlAcGYYI -DVBucB0fYaVHWRKxw50mo02zKP2/GW6K3p1nxGTf73cKjc9of+VOvvMByODaJ+ne -WIuzZMqz0vfQ0UvRVBjlsXtB7VpCqJZWkubqqwwP6+2WOCA/c5LC3z2f/BK90EQh -/JrKfDR083UNhu4SjNwL/TF4ET33JHf1u5Gmzqx+eO00pfhVvkyz6LYImkE8ky/+ -bXyJY8iDq7dxtfqhzZbeNe4fafU/avXxTA5UkWTnYhCqyd2bvAYH3Ep3L7lSv6SQ -Tsy0jjTsWJoSq6jRIzJuo2mX2MBKPBfLZs4tH71/4RppECletNnS4ZlxiV4LNrWE -LrXQvE1V+mJ82muucIe7w52nf3UWjQqTA73+Ml0aK+lIhbckRIovAw1sGzRrbTEX -xLCgz7BYDMhs5mgtfiMAzGox4xGxi56Ge519vdbddan4G92mPlLl1IPOXkO8GyLO -D4IiPp5ilPy6uThuxxIFemxxUREbPrfLJNA8W7aRPrHz4YcgZhrAV9I4C+xE0ukB -i8MoJeFvbCGPyTwVDn8XfFKynlZYm1f8NIVMSj5JfV8J3Om9jzDp6hx+52iUQEbW -C9g4kfPZY8h0RMggdOlZsaR8j26xuW+fEtz7ucJIqfJ/ElTH+4bm8MK2qPZniRWv -ej2md4bP4Bo5DXydzxz7O7TBL6/Jsp7pJhHUUb36OnTWvInyg71LPT1QIxdRvXr9 -vNhrEBDX3MNf7RyXczvBcc+cLRo+zV+T4b8wd2kwXskWgKrGUJEe2TItdsafaQ9B -BkuVGu6cIDa6STyCJiOB68KIXiDuADSWJiiR+gZr4eU6vzfhR27LMQt/g+oPW+U4 -1AvzUl9uXjTMC2vFuTQ4M2g0WmksCNpPpzOu/QlBmRqpP2Fg9UuLv6ITWeCxp439 -g5NT5KXE2IiruL/DS0KEpWVNe4ayGzRvMawFuU582xbIzXjvilUZrW+p6req+oeF -QWTWHGDojTvULQPV2c91lWnLaNXVethfF4hrM5MIL+EwVs3sUXFMr1kX7VNrK0QH -Uos7nc4G9xngpdvwF4ImldD83O8qxOVzIfk5Dhz+etTH4HbnnDvmQ/FIYvjzGviR -ZeVwdCjv/9TC4yY3nJFKMwGp70jVa1vbmWL68HVNRyAVwnQnu2UlI8UR43iVZyUH -ZY0Nr0rbse/pvZyX4//EVOFLUR7K4GG0N4Kz41q5ZB8rI4Fl6QJhgIFJds13iM77 -n+wqLQE7kllgI32E4U9B -=YlXg +hF4DHXHP849rSK8SAQdAtWMNBhjdh9Xyig/GYM1RN27B7LmCk0oRkguuFMROXk8w +wLmcm3HiikqziJzlPY8sBLEx7cLQCkCrg6KarCHGvcqajUJluLAZgrwUsmUpOFy0 +0ukBQ6EnlpNlxdcmOElG9ZYn2Dgp1Gq4eo9rHb9f/TBLxuTr1Yr0vZ5f38bVn+tB +0FupuYE5h/PAM2JPjVg4rh1pIN2yWvNdFBHCm0Yt3AquS9bwQymUOjh0ZCey5ERy +WJtUQ7u+f2Hfm5K9CPPqlM7GPqeR8nv2/cgJxsA9nGn4tco7pHEkM1Kwe1dLlTbh +ZqEMKu+dVK/RqRUjDVdAonM7wz3Uah6HJH3a6W4g2mPiPPkOpnbTClCLiK0H8NRv +9rbO69kHoNar0YTpTAAbkxlKkdKeM/xAS2UFIdE+tXSofeCHmsFCmcohKH7Rt66l +rU7BJE7dtTiaPFYgHBuzbrkA+oH8gU9l7PQ1L16zo1rRVRb8IIBPSHY64Ekpy3L4 +FQ3Ttg8e26d2X+6eGca6RIXksTfqOniC33Fal6gBIUqP1w4ky1wjJ+wujkSuf0QB +yFWPP3o16o9X3nEIa2L8v/engSM5p7pezZfVJej/5hWnuPP3YmbDpn0ir21Qye96 +qxq7XI7GSWukxisxC8WakNA1LHnXo5DvHB8wm3lJIfEfIJ9RHhZqyRMptC563XCZ +A1NvY+4sKgx9g8PBRai019ZaTMYJRQqE3VlU+xHbJWMf4aymYyeY8ut8HvFhC9Tk +4D3eol7/LgHhXA9Db4L5MJX8iHtXhqyt62vmMXcFzC1Wi8Co/HnchzGQx2vXnUIZ +IlomEAvN1RZC2UV8q2NHKzKnfr43WfnIsdt1fQwEfQeTKX6wfhq8FDR5NmEeLeTw +ddGA95bZEkT2xYh+2g7G1c5Z33l5WLeriA/SUMWK1oafUtcx81lJnmK/xSIg3PKZ +jKIbTwv4tKjmGUJE5xb/RTUfOTG+v7HOBo0avwofvqz5k7Do8v0BEEmhewjcIKBf +h2Go8VxFtDj/OG0J98PS3OVQtnv4jzepXUxr2ZW7Z0aPFUHQ2F97Yio3nxb7r7um +OjKhNgIPp+iAWisqqw+kwWxPx/5GHxVYsOM+fqpFEjELGGNv+ImTG7jS1V5agqIr +qU+dxYydDKnQi/5xXeE4TO0MEFhCIttOQARw3DPtZmjLuXFjqvaasTV4YyXwrKCJ +0sfemg3wxkZxY0NI13g3CaJGtSzXdZVfkzpdVxRAZoETT7rLNDbawuq0AzSava2u +nB/vgcDWfPr4jW21 +=r4Vv -----END PGP MESSAGE----- --===============9060418334135509864==-- diff --git a/test/corpora/crypto/encrypted-signed.eml b/test/corpora/crypto/encrypted-signed.eml index 0345e3e9..f5d5d120 100644 --- a/test/corpora/crypto/encrypted-signed.eml +++ b/test/corpora/crypto/encrypted-signed.eml @@ -17,19 +17,18 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBBAC9z781zV7QAInGMKHX6TKU5Xw/OkoWXahpDL88F6Ocm5R9 -7M9z2ocvlyrbgRhqE+nvFeGH/K7rVkBBT6TAcdIe/C8Qzbd3stPPcx1PlunGROj7 -H/WAcmDksK3HkXpHwmInUtzNw1pkhOoLy/sFSbPvtyg8GCUzXbafHAIIo0rB2tLB -DwGWD3l4WdcyQWuYD9QJKuDIqdWo8E3TTcKkiOAt/6liwPNZ0jGzDeCuSTnWFj6Z -AiXGeNtD3I1tCN/8T3NjEKOCQ+bdT5Y06dDaL61FpQ23eIuSUgksVxjnkEAb6iPe -07gjzcyNuGP3WPI/0qu0wtZwpAQxvaNygDsQj/OjR5kn9luBd/VqodM3TWWS8miV -m0z1tYbqYAQWW6TS7fXlsyXoOxTLW5MCfe3D36VSErL/NJItETklVKzNfKjMmRKx -CI2ZUzugxPWSLQzOp5yl7iICk8e+vS9TkQw2j0nXAQYLYgmqZMhf4av5GlFv3tQu -heO4XLT6NBDTHMFTDbgW42kE0N4MDPc29AqVFGImcTHvflF4Vp0qIbSJdIcHwKkU -5LKqvicAa0lsIoJbsW3lHrzowyjov2vLH/VGd/wIX+MS3KT7cySdyp8HVMcwwyZu -Y9nrTN/7G1FwKWlcGa4uJNcFFkYlcEymZj1EX2cyrdezPtX7K5vhwBYddptFD+Bn -IVkghRut3UDeXe83F8OutWiZfK5EVYABq/aP3//hIbQl2o4Dkd3z9m+8LobrIV5s -NXjAjU5WQOjRLoHBebG2HkMpFsWhXD/Fb/Bb58VOpdI= -=x12v +hF4DHXHP849rSK8SAQdAwRMXQWEiw2ZldkVtILgy2w/vFrKxAgAxFmyDGvtbiTow +xLMHH7PgH+S0JIjEOM0jFglpyMdzvNBWII726THKbFnOcR38CsbK55lCYw2uhABS +0sD1AU/yQZNNDwjuJT6wjtxMdfI4DaFMD5XTpioNtRcphLpN7MHbYr4MYCxszRY7 +ZLIAkUCoY/3D26dcOkB0EMGELsRsPmKaCq/m3FVvxHHku94MzLnjV/eGRE40AiSe +JzteAKwXzUAYzYw+LCj9WPZvy1Rs37I4VgukEgSYXMidgc9fUQ4yYKesbyQ8/iMW +Ryo+X2yu7Iv3a7pp2qdArsJwatWpyRuASVVA7nZlNQS0YGh/fuSX3x8TJuSPliFr +sdTVuE6sJIlqttH9CRgxMjLY5YbpF3lBTqlv2tmz0VERhWKKsh5VOiUvJvZ3p5hn +FqnD1bcoWoBPZRNOoF6PzGkvWQIqysGLgg24wZr7ZRgZ8mHRgykBN6cmZHQKf16m +zqqf8sppaRal6d/L72EzmeHEusUn6FlWcEDlXLc5anghYdna7qhbLqsoY7X504SQ +Mx4tj1P4XVhgoXdLRR2EHOLrzUCrkiQ9cKfoAcUFJTcbGGYYiYwzUCkZWhsHOKsQ +y7tajvWUzGaJ8aiZ1dfdUraOzrvOOif4TdnFJrTpM0Agy6IH8tbm8EnhNOxkjQPr +3t7eS3JcuC0= +=qz8x -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/default/bar/baz/05:2, b/test/corpora/default/bar/baz/05:2, index 75b05fa4..ce174b52 100644 --- a/test/corpora/default/bar/baz/05:2, +++ b/test/corpora/default/bar/baz/05:2, @@ -63,7 +63,7 @@ and +Envelope-to: david@tethera.net +Delivery-date: Wed, 28 Nov 2012 18:41:46 -0400 +Received: from [199.188.72.155] (helo=yantan.tethera.net) + by tesseract.cs.unb.ca with esmtps (TLS1.0:RSA_AES_256_CBC_SHA1:32) + (Exim 4.72) + (envelope-from ) + id 1TdqKA-00017j-3c + for david@tethera.net; Wed, 28 Nov 2012 18:41:46 -0400 +Received: from wagner.debian.org ([217.196.43.132]) + by yantan.tethera.net with esmtp (Exim 4.72) + (envelope-from ) + id 1TdqK9-00072z-AF + for david@tethera.net; Wed, 28 Nov 2012 18:41:45 -0400 +Received: from localhost ([::1] helo=wagner.debian.org) + by wagner.debian.org with esmtp (Exim 4.72) + (envelope-from ) + id 1TdqK8-0007GZ-67 + for david@tethera.net; Wed, 28 Nov 2012 22:41:44 +0000 +Received: from vasks.debian.org ([217.196.43.140]) + by wagner.debian.org with esmtp (Exim 4.72) + (envelope-from ) + id 1TdqIc-0006jm-OC; Wed, 28 Nov 2012 22:40:11 +0000 +Received: from gladky-anton-guest by vasks.debian.org with local (Exim 4.72) + (envelope-from ) + id 1TdqIc-0003j1-DE; Wed, 28 Nov 2012 22:40:10 +0000 +Date: Wed, 28 Nov 2012 22:40:10 +0000 +From: Anton Gladky +To: 691896@bugs.debian.org, control@bugs.debian.org, + 691896-submitter@bugs.debian.org +Message-ID: +X-PTS-Approved: Yes +X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on wagner.debian.org +X-Spam-Level: +X-Spam-Status: No, score=-1.9 required=5.0 tests=BAYES_00,FREEMAIL_FROM + autolearn=ham version=3.3.1 +Subject: [87ea161] Fix for Bug#691896 committed to git +X-BeenThere: debian-science-maintainers@lists.alioth.debian.org +X-Mailman-Version: 2.1.13 +Precedence: list +List-Id: Mailing list for maintainer discussions and BTS messages + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +MIME-Version: 1.0 +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org +Errors-To: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org +X-SA-Exim-Connect-IP: ::1 +X-SA-Exim-Mail-From: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org +X-SA-Exim-Scanned: No (on wagner.debian.org); SAEximRunCond expanded to false +X-Spam-Score: 1.3 +X-Spam_bar: + + + +tags 691896 + pending +thanks + +Hello, + + The following change has been committed for this bug by + Anton Gladky on Wed, 31 Oct 2012 08:16:42 +0100. + The fix will be in the next upload. +==================================== +Minor fixes in README.Debian. (Closes: #691896) + + +==================================== + +You can check the diff of the fix at: + + ;a=commitdiff;h=87ea161 + + + +-- +debian-science-maintainers mailing list +debian-science-maintainers@lists.alioth.debian.org +http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-science-maintainers diff --git a/test/corpora/duplicate/msg-1-2:2, b/test/corpora/duplicate/msg-1-2:2, new file mode 100644 index 00000000..dc0476e7 --- /dev/null +++ b/test/corpora/duplicate/msg-1-2:2, @@ -0,0 +1,113 @@ +Return-path: +Envelope-to: david@tethera.net +Delivery-date: Wed, 28 Nov 2012 18:42:39 -0400 +Received: from [199.188.72.155] (helo=yantan.tethera.net) + by tesseract.cs.unb.ca with esmtps (TLS1.0:RSA_AES_256_CBC_SHA1:32) + (Exim 4.72) + (envelope-from ) + id 1TdqL1-00017s-3b + for david@tethera.net; Wed, 28 Nov 2012 18:42:39 -0400 +Received: from wagner.debian.org ([217.196.43.132]) + by yantan.tethera.net with esmtp (Exim 4.72) + (envelope-from ) + id 1TdqL0-00073Z-Gw + for david@tethera.net; Wed, 28 Nov 2012 18:42:38 -0400 +Received: from localhost ([::1] helo=alioth.debian.org) + by wagner.debian.org with esmtp (Exim 4.72) + (envelope-from ) + id 1TdqKz-0007sR-PR + for david@tethera.net; Wed, 28 Nov 2012 22:42:37 +0000 +Received: from buxtehude.debian.org ([140.211.166.26]) + by wagner.debian.org with esmtp (Exim 4.72) + (envelope-from ) id 1TdqKU-0007SW-IG + for debian-science-maintainers@lists.alioth.debian.org; + Wed, 28 Nov 2012 22:42:07 +0000 +Received: from debbugs by buxtehude.debian.org with local (Exim 4.72) + (envelope-from ) + id 1TdqKR-0001dH-UL; Wed, 28 Nov 2012 22:42:03 +0000 +X-Loop: owner@bugs.debian.org +Resent-From: Anton Gladky +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Science Maintainers + +X-Loop: owner@bugs.debian.org +Resent-Date: Wed, 28 Nov 2012 22:42:02 +0000 +Resent-Message-ID: +X-Debian-PR-Message: followup 691896 +X-Debian-PR-Package: gmsh +X-Debian-PR-Keywords: pending +X-Debian-PR-Source: gmsh +Received: via spool by 691896-submit@bugs.debian.org id=B691896.13541424145158 + (code B ref 691896); Wed, 28 Nov 2012 22:42:02 +0000 +Received: (at 691896) by bugs.debian.org; 28 Nov 2012 22:40:14 +0000 +Received: from wagner.debian.org ([217.196.43.132]) + by buxtehude.debian.org with esmtps (TLS1.0:RSA_AES_256_CBC_SHA1:32) + (Exim 4.72) (envelope-from ) + id 1TdqIg-0001Kh-Ba; Wed, 28 Nov 2012 22:40:14 +0000 +Received: from vasks.debian.org ([217.196.43.140]) + by wagner.debian.org with esmtp (Exim 4.72) + (envelope-from ) + id 1TdqIc-0006jm-OC; Wed, 28 Nov 2012 22:40:11 +0000 +Received: from gladky-anton-guest by vasks.debian.org with local (Exim 4.72) + (envelope-from ) + id 1TdqIc-0003j1-DE; Wed, 28 Nov 2012 22:40:10 +0000 +Date: Wed, 28 Nov 2012 22:40:10 +0000 +From: Anton Gladky +To: 691896@bugs.debian.org, control@bugs.debian.org, + 691896-submitter@bugs.debian.org +Message-ID: +X-PTS-Approved: Yes +Resent-Sender: Debian BTS +X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on wagner.debian.org +X-Spam-Level: +X-Spam-Status: No, score=-4.2 required=5.0 tests=BAYES_00,FREEMAIL_FROM, + RCVD_IN_DNSWL_MED autolearn=ham version=3.3.1 +Subject: Bug#691896: [87ea161] Fix for Bug#691896 committed to git +x-debian-approved: yes +X-BeenThere: debian-science-maintainers@lists.alioth.debian.org +X-Mailman-Version: 2.1.13 +Precedence: list +Reply-To: Anton Gladky , 691896@bugs.debian.org +List-Id: Mailing list for maintainer discussions and BTS messages + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +MIME-Version: 1.0 +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org +Errors-To: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org +X-SA-Exim-Connect-IP: ::1 +X-SA-Exim-Mail-From: debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org +X-SA-Exim-Scanned: No (on wagner.debian.org); SAEximRunCond expanded to false +X-Spam-Score: 1.3 +X-Spam_bar: + + + +tags 691896 + pending +thanks + +Hello, + + The following change has been committed for this bug by + Anton Gladky on Wed, 31 Oct 2012 08:16:42 +0100. + The fix will be in the next upload. +==================================== +Minor fixes in README.Debian. (Closes: #691896) + + +==================================== + +You can check the diff of the fix at: + + ;a=commitdiff;h=87ea161 + +-- +debian-science-maintainers mailing list +debian-science-maintainers@lists.alioth.debian.org +http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-science-maintainers diff --git a/test/corpora/duplicate/msg-2-1:2, b/test/corpora/duplicate/msg-2-1:2, new file mode 100644 index 00000000..e118d784 --- /dev/null +++ b/test/corpora/duplicate/msg-2-1:2, @@ -0,0 +1,43 @@ +From: David Bremner +To: Samuel Bronson , 695159@bugs.debian.org, Debian Bug Tracking System +Subject: Re: Bug#695159: debian-el: Shouldn't put downloaded bugs loose in ~/ +In-Reply-To: <87vcch7hxy.fsf@naesten.dyndns.org> +References: <87vcch7hxy.fsf@naesten.dyndns.org> +Date: Thu, 25 Oct 2018 10:41:38 -0300 +Message-ID: <87r2geywh9.fsf@tethera.net> +MIME-Version: 1.0 +Content-Type: text/plain + + +Control: severity -1 minor +Control: tag -1 moreinfo + +Samuel Bronson writes: + +> Package: debian-el +> Version: 35.2+nmu1 +> Severity: normal +> File: /usr/share/emacs/site-lisp/debian-el/debian-bug.el +> +> Dear Maintainer, +> +> After being mildly annoyed with this for ages, it finally occurred to me +> to file a bug about it: +> +> It's rather rude of `getdebian-bug-get-bug-as-email' to default to +> sticking downloaded mbox files loose in ~/, isn't it? +> +> (And might it not make sense to try and use the same files as the bts(1) +> command from the devscripts package?) +> + +Hi Samuel + +There is already a variable "debian-bug-download-directory" which can be +customized. Is there an obviously better default? I guess we could put +things in /tmp by default, with a minor privacy leak on multiuser +systems. + +d + +# body 1 diff --git a/test/corpora/duplicate/msg-2-2:2, b/test/corpora/duplicate/msg-2-2:2, new file mode 100644 index 00000000..a7549c47 --- /dev/null +++ b/test/corpora/duplicate/msg-2-2:2, @@ -0,0 +1,140 @@ +Return-path: +Envelope-to: david@tethera.net +Delivery-date: Thu, 25 Oct 2018 09:45:10 -0400 +Received: from muffat.debian.org ([2607:f8f0:614:1::1274:33]) + by fethera.tethera.net with esmtp (Exim 4.89) + (envelope-from ) + id 1gFfwj-0004Y9-69 + for david@tethera.net; Thu, 25 Oct 2018 09:45:10 -0400 +Received: from ticharich.debian.org ([2001:41c8:1000:21::21:23]) + from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=ticharich.debian.org,EMAIL=hostmaster@ticharich.debian.org (verified) + by muffat.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1gFfwi-0004A1-J6 + for david@tethera.net; Thu, 25 Oct 2018 13:45:08 +0000 +Received: from localhost ([::1] helo=ticharich.debian.org) + by ticharich.debian.org with esmtp (Exim 4.89) + (envelope-from ) + id 1gFfwh-0002Ex-6w + for david@tethera.net; Thu, 25 Oct 2018 13:45:07 +0000 +Received: from mailly.debian.org ([2001:41b8:202:deb:6564:a62:52c3:4b72]) + from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=mailly.debian.org,EMAIL=hostmaster@mailly.debian.org (verified) + by ticharich.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1gFfwg-0002Em-NI + for dispatch+emacs-goodies-el@tracker.debian.org; Thu, 25 Oct 2018 13:45:06 +0000 +Received: from quantz.debian.org ([2001:41c8:1000:21::21:28]) + from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=quantz.debian.org,EMAIL=hostmaster@quantz.debian.org (verified) + by mailly.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1gFfwg-0004h1-AC + for dispatch+emacs-goodies-el@tracker.debian.org; Thu, 25 Oct 2018 13:45:06 +0000 +Received: from qa by quantz.debian.org with local (Exim 4.89) + (envelope-from ) + id 1gFfwf-0007Lg-TQ + for dispatch+emacs-goodies-el@tracker.debian.org; Thu, 25 Oct 2018 13:45:05 +0000 +Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39]) + from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=buxtehude.debian.org,EMAIL=hostmaster@buxtehude.debian.org (verified) + by quantz.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1gFfwf-0007J0-3r; Thu, 25 Oct 2018 13:45:05 +0000 +Received: from debbugs by buxtehude.debian.org with local (Exim 4.89) + (envelope-from ) + id 1gFfwc-0003jj-VU; Thu, 25 Oct 2018 13:45:02 +0000 +X-Loop: owner@bugs.debian.org +Subject: Bug#695159: debian-el: Shouldn't put downloaded bugs loose in ~/ +Reply-To: David Bremner , 695159@bugs.debian.org +Resent-From: David Bremner +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Emacsen team +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 25 Oct 2018 13:45:01 +0000 +Resent-Message-ID: +X-Debian-PR-Message: followup 695159 +X-Debian-PR-Package: debian-el +X-Debian-PR-Keywords: +References: <87vcch7hxy.fsf@naesten.dyndns.org> <87vcch7hxy.fsf@naesten.dyndns.org> +X-Debian-PR-Source: debian-el, emacs-goodies-el +Received: via spool by submit@bugs.debian.org id=B.154047490313415 + (code B); Thu, 25 Oct 2018 13:45:01 +0000 +Received: (at submit) by bugs.debian.org; 25 Oct 2018 13:41:43 +0000 +X-Spam-Checker-Version: SpamAssassin 3.4.1-bugs.debian.org_2005_01_02 + (2015-04-28) on buxtehude.debian.org +X-Spam-Level: +X-Spam-Status: No, score=-19.5 required=4.0 tests=BAYES_00,FROMDEVELOPER,GMAIL, + HAS_BUG_NUMBER,TXREP autolearn=ham autolearn_force=no + version=3.4.1-bugs.debian.org_2005_01_02 +X-Spam-Bayes: score:0.0000 Tokens: new, 16; hammy, 110; neutral, 41; spammy, + 2. spammytokens:0.971-+--privacy, 0.857-+--customized + hammytokens:0.000-+--UD:el, 0.000-+--H*F:U*bremner, 0.000-+--Maintainer, + 0.000-+--sitelisp, 0.000-+--site-lisp +Received: from fethera.tethera.net ([2607:5300:60:c5::1]) + by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1gFftO-0003U6-RV; Thu, 25 Oct 2018 13:41:42 +0000 +Received: from remotemail by fethera.tethera.net with local (Exim 4.89) + (envelope-from ) + id 1gFftL-0004VK-V8; Thu, 25 Oct 2018 09:41:39 -0400 +Received: (nullmailer pid 477 invoked by uid 1000); + Thu, 25 Oct 2018 13:41:38 -0000 +From: David Bremner +To: Samuel Bronson , 695159@bugs.debian.org, Debian Bug Tracking System +In-Reply-To: <87vcch7hxy.fsf@naesten.dyndns.org> +Date: Thu, 25 Oct 2018 10:41:38 -0300 +Message-ID: <87r2geywh9.fsf@tethera.net> +MIME-Version: 1.0 +Content-Type: text/plain +Delivered-To: submit@bugs.debian.org +Delivered-To: emacs-goodies-el@packages.qa.debian.org +Delivered-To: dispatch+emacs-goodies-el@tracker.debian.org +X-Loop: dispatch@tracker.debian.org +X-Distro-Tracker-Keyword: bts +X-Distro-Tracker-Package: emacs-goodies-el +List-Id: +X-Debian: tracker.debian.org +X-Debian-Package: emacs-goodies-el +X-PTS-Package: emacs-goodies-el +X-PTS-Keyword: bts +X-Distro-Tracker-Team: emacsen +X-Spam_score: -2.3 +X-Spam_score_int: -22 +X-Spam_bar: -- + + +Control: severity -1 minor +Control: tag -1 moreinfo + +Samuel Bronson writes: + +> Package: debian-el +> Version: 35.2+nmu1 +> Severity: normal +> File: /usr/share/emacs/site-lisp/debian-el/debian-bug.el +> +> Dear Maintainer, +> +> After being mildly annoyed with this for ages, it finally occurred to me +> to file a bug about it: +> +> It's rather rude of `getdebian-bug-get-bug-as-email' to default to +> sticking downloaded mbox files loose in ~/, isn't it? +> +> (And might it not make sense to try and use the same files as the bts(1) +> command from the devscripts package?) +> + +Hi Samuel + +There is already a variable "debian-bug-download-directory" which can be +customized. Is there an obviously better default? I guess we could put +things in /tmp by default, with a minor privacy leak on multiuser +systems. + +d + +# body 2 diff --git a/test/corpora/duplicate/msg-3-1:2, b/test/corpora/duplicate/msg-3-1:2, new file mode 100644 index 00000000..8bb6a8c2 --- /dev/null +++ b/test/corpora/duplicate/msg-3-1:2, @@ -0,0 +1,183 @@ +Return-path: +Envelope-to: david@tethera.net +Delivery-date: Thu, 20 Dec 2018 13:27:11 -0500 +Received: from muffat.debian.org ([2607:f8f0:614:1::1274:33]) + by fethera.tethera.net with esmtp (Exim 4.89) + (envelope-from ) + id 1ga32M-0005ae-Ko + for david@tethera.net; Thu, 20 Dec 2018 13:27:11 -0500 +Received: from alioth-lists-01.debian.net ([2001:ba8:0:2c77:0:4:0:1]) + by muffat.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1ga32K-0008PT-Nm + for david@tethera.net; Thu, 20 Dec 2018 18:27:08 +0000 +Received: from localhost ([::1] helo=alioth-lists-01.debian.net) + by alioth-lists-01.debian.net with esmtp (Exim 4.89) + (envelope-from ) + id 1ga32I-0003to-Kn + for bremner@debian.org; Thu, 20 Dec 2018 18:27:06 +0000 +Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39]) + by alioth-lists-01.debian.net with esmtps + (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89) + (envelope-from ) id 1ga32H-0003tO-29 + for pkg-emacsen-addons@lists.alioth.debian.org; Thu, 20 Dec 2018 18:27:05 +0000 +Received: from debbugs by buxtehude.debian.org with local (Exim 4.89) + (envelope-from ) + id 1ga32F-0005PP-9m; Thu, 20 Dec 2018 18:27:03 +0000 +X-Loop: owner@bugs.debian.org +Resent-From: Sean Whitton +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Emacs addons team + +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 20 Dec 2018 18:27:02 +0000 +Resent-Message-ID: +X-Debian-PR-Message: followup 916805 +X-Debian-PR-Package: src:assess-el +X-Debian-PR-Keywords: ftbfs +References: <87k1k6h43h.fsf@zephyr.silentflame.com> +X-Debian-PR-Source: assess-el +Received: via spool by 916805-submit@bugs.debian.org id=B916805.154533033319811 + (code B ref 916805); Thu, 20 Dec 2018 18:27:02 +0000 +Received: (at 916805) by bugs.debian.org; 20 Dec 2018 18:25:33 +0000 +X-Spam-Checker-Version: SpamAssassin 3.4.2-bugs.debian.org_2005_01_02 + (2018-09-13) on buxtehude.debian.org +X-Spam-Level: +X-Spam-Status: No, score=-8.8 required=4.0 tests=BAYES_00,DKIM_SIGNED, + DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,PGPSIGNATURE,RCVD_IN_DNSWL_LOW, + SORTED_RECIPS,SPF_HELO_PASS,SPF_PASS,SUSPICIOUS_RECIPS,TXREP + autolearn=ham autolearn_force=no + version=3.4.2-bugs.debian.org_2005_01_02 +X-Spam-Bayes: score:0.0000 Tokens: new, 32; hammy, 83; neutral, 22; spammy, 1. + spammytokens:0.902-+--emails + hammytokens:0.000-+--HX-ME-Sender:xms, + 0.000-+--H*RU:10.202.2.43, + 0.000-+--Hx-spam-relays-external:10.202.2.43, 0.000-+--H*F:U*spwhitton, + 0.000-+--H*F:D*spwhitton.name +Received: from wout2-smtp.messagingengine.com ([64.147.123.25]) + by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) (envelope-from ) + id 1ga30m-00059C-Kv; Thu, 20 Dec 2018 18:25:32 +0000 +Received: from compute3.internal (compute3.nyi.internal [10.202.2.43]) + by mailout.west.internal (Postfix) with ESMTP id C50B712F6; + Thu, 20 Dec 2018 13:25:30 -0500 (EST) +Received: from mailfrontend1 ([10.202.2.162]) + by compute3.internal (MEProxy); Thu, 20 Dec 2018 13:25:31 -0500 +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=spwhitton.name; + h=from:to:subject:date:message-id:mime-version:content-type; s= + fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lqnNZlsr/mb10zqE=; b=X9S4kI14 + zxdg9IKMIyALL2Eao3GTyyojuATJkgCqJvZWd8RWpty6RBStZfyeOqR1L3Gr5m3/ + EVeiHQyFNnPor2xjMDmblfPS/u09JxVlc0KMpT0XXRfNWsVQn+U40nNRX15kXzZ/ + D1rYhxpxzKRzU2tByUULCgbGlXAwJQtOXMDw3mpj1BxcoO13H/0H/KQTQ+AcpiOw + BV3JFKL/jA+mH8uAPIgNM2mUYZz5REO89eh3lPhLyc7tw745X+4ywZlo/Piqa0+6 + BCldY9/nDR3csAUKx1+3hkpJPdqFALBWvG3SelGt44BqcoLsOJLB8QH6trCro39o + fEnBaUBTAkTAHA== +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= + messagingengine.com; h=content-type:date:from:message-id + :mime-version:subject:to:x-me-proxy:x-me-proxy:x-me-sender + :x-me-sender:x-sasl-enc; s=fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lq + nNZlsr/mb10zqE=; b=QYqLjUBZtfbO0DUlagvXRP2BPnpTgkvIna9/uCewkdoIH + /LuPW6DZUNWQa55qiDquqKXcs0tTODTEzYgeIDgqC+DDBTHnFQvXdWyS1X3o4sLL + 8dTKk8lv7M1/zKFxyg/ycNvPJGS9m4ZucGbxjwdgAcozhg7W1Qztxt9eVhPVnenS + 5sdeJ9mjIE7lYkKX4QVsXPOi86j6QlfMNyi/OnBfX2+95QiA/xPE/wEq4MYlLNm7 + Av1P/8OrI4ImDKkOEivarktL+isYL7OXyGB4GfUTsydiy9dhP7RKPxrai1kJRu5S + b2470KXNatu2WkyMFrsdcwrSqyKIe096k5xPfVI2A== +X-ME-Sender: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod + ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu + fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg + gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi + thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh + hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr + ufhiiigvpedt +X-ME-Proxy: + + + +From: Sean Whitton +To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org, + 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org, + 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org, + 916876@bugs.debian.org +Date: Thu, 20 Dec 2018 18:25:26 +0000 +Message-ID: <87r2ecrr6x.fsf@zephyr.silentflame.com> +MIME-Version: 1.0 +Received-SPF: pass client-ip=2607:f8f0:614:1::1274:39; + envelope-from=debbugs@buxtehude.debian.org; helo=buxtehude.debian.org +x-debian-approved: yes +Subject: [Pkg-emacsen-addons] Bug#916805: Increase severity to 'serious' +X-BeenThere: pkg-emacsen-addons@alioth-lists.debian.net +X-Mailman-Version: 2.1.23 +Precedence: list +List-Id: Maintainers list for Emacs addon packages + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Reply-To: Sean Whitton , 916805@bugs.debian.org +Content-Type: multipart/mixed; boundary="===============5317466403067656157==" +Errors-To: pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net +Sender: "Pkg-emacsen-addons" + +X-Spam_score: 2.8 +X-Spam_score_int: 28 +X-Spam_bar: ++ + +--===============5317466403067656157== +Content-Type: multipart/signed; boundary="=-=-="; + micalg=pgp-sha512; protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain +Content-Transfer-Encoding: quoted-printable + +control: severity -1 serious + +Hello, + +Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for +all the e-mails). + +=2D-=20 +Sean Whitton + +--=-=-= +Content-Type: application/pgp-signature; name="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEm5FwB64DDjbk/CSLaVt65L8GYkAFAlwb3pYACgkQaVt65L8G +YkDLrhAAhORxZzZDE5vlXRm89JYA3jd9OyleioZvDDRCrpEd7CQ5AHiMMJizW1lU +gn6OBIoW4O04TZ5oOuUnDHK/rS0G4zgNCJUyNf06zVECmdvkzspNNpQ3J5aOi4t2 +lhjRIFOKA9ifGsEqYLwP2dork1xFuyHEqHkDH8zpCTvdzkWky1bwAD/Pj5dArd7t +FeQGsPm7/64H1/rHk8pSP2pQgRsMDX6rIdx3vuQ7r+NssdRq+II4e479l02TiCDi +FBOX+n3nPXxREPdZ9EKL4SauL/AnRqpeC9GX6fC9OOnQeQ1xVTzNWKa6ixrqkFoH +TI/vy51p16jFNgdkLkyLtZA8Tq72TIAKWbZC0GFzWJVNASWu7WDIoMn5pgoi454w +TgsvK9MOnEYeABiDUa1ppaoMiP4+3j5yT0eWttTMSkcKjk1Ap1o+RfUxlIGl0Rog +ShbG2y6Mv8FERtjzPVQ7VMLDN9zRIbtlSJFm7CboPNSAygzzzaA/RIN/e8MdbZoM +a8AT9KiAVHEEcw+nWFAatAew5VP9iRZVgrVdWBszuaWOolxnYvpAL45WanqG0eab +VMe66+rZ8momI0MsM9JcqBwXO+fOf8CrPSO9PL8VFEJXFLZQS7asFStJf2l8msWE +3IYhvk4B6Nf1R96XzpXLlkOnoGtcnPVAvotrGU/rDfk5i/WF810= +=mWfF +-----END PGP SIGNATURE----- +--=-=-=-- + + +--===============5317466403067656157== +Content-Type: text/plain; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 +Content-Disposition: inline + +X19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX18KUGtnLWVtYWNz +ZW4tYWRkb25zIG1haWxpbmcgbGlzdApQa2ctZW1hY3Nlbi1hZGRvbnNAYWxpb3RoLWxpc3RzLmRl +Ymlhbi5uZXQKaHR0cHM6Ly9hbGlvdGgtbGlzdHMuZGViaWFuLm5ldC9jZ2ktYmluL21haWxtYW4v +bGlzdGluZm8vcGtnLWVtYWNzZW4tYWRkb25zCg== + +--===============5317466403067656157==-- + diff --git a/test/corpora/duplicate/msg-3-2:2, b/test/corpora/duplicate/msg-3-2:2, new file mode 100644 index 00000000..292d515f --- /dev/null +++ b/test/corpora/duplicate/msg-3-2:2, @@ -0,0 +1,184 @@ +Return-path: +Envelope-to: david@tethera.net +Delivery-date: Thu, 20 Dec 2018 13:27:12 -0500 +Received: from mailly.debian.org ([2001:41b8:202:deb:6564:a62:52c3:4b72]) + by fethera.tethera.net with esmtp (Exim 4.89) + (envelope-from ) + id 1ga32O-0005ap-Hn + for david@tethera.net; Thu, 20 Dec 2018 13:27:12 -0500 +Received: from alioth-lists-01.debian.net ([2001:ba8:0:2c77:0:4:0:1]) + by mailly.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1ga32O-0004V7-4x + for david@tethera.net; Thu, 20 Dec 2018 18:27:12 +0000 +Received: from localhost ([::1] helo=alioth-lists-01.debian.net) + by alioth-lists-01.debian.net with esmtp (Exim 4.89) + (envelope-from ) + id 1ga32N-0003wA-Sc + for bremner@debian.org; Thu, 20 Dec 2018 18:27:11 +0000 +Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39]) + by alioth-lists-01.debian.net with esmtps + (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89) + (envelope-from ) id 1ga32M-0003vQ-Q9 + for pkg-emacsen-addons@lists.alioth.debian.org; Thu, 20 Dec 2018 18:27:10 +0000 +Received: from debbugs by buxtehude.debian.org with local (Exim 4.89) + (envelope-from ) + id 1ga32K-0005Sn-7B; Thu, 20 Dec 2018 18:27:08 +0000 +X-Loop: owner@bugs.debian.org +Resent-From: Sean Whitton +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Emacs addons team + +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 20 Dec 2018 18:27:06 +0000 +Resent-Message-ID: +X-Debian-PR-Message: followup 916808 +X-Debian-PR-Package: src:hydra-el +X-Debian-PR-Keywords: ftbfs +References: <87bm5ih3w5.fsf@zephyr.silentflame.com> +X-Debian-PR-Source: hydra-el +Received: via spool by 916808-submit@bugs.debian.org id=B916808.154533033319830 + (code B ref 916808); Thu, 20 Dec 2018 18:27:06 +0000 +Received: (at 916808) by bugs.debian.org; 20 Dec 2018 18:25:33 +0000 +X-Spam-Checker-Version: SpamAssassin 3.4.2-bugs.debian.org_2005_01_02 + (2018-09-13) on buxtehude.debian.org +X-Spam-Level: +X-Spam-Status: No, score=-11.3 required=4.0 tests=BAYES_00,DKIM_SIGNED, + DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,PGPSIGNATURE,RCVD_IN_DNSWL_LOW, + SORTED_RECIPS,SPF_HELO_PASS,SPF_PASS,SUSPICIOUS_RECIPS,TXREP + autolearn=unavailable autolearn_force=no + version=3.4.2-bugs.debian.org_2005_01_02 +X-Spam-Bayes: score:0.0000 Tokens: new, 0; hammy, 115; neutral, 22; spammy, 1. + spammytokens:0.902-+--emails + hammytokens:0.000-+--HX-ME-Sender:xms, + 0.000-+--H*RU:10.202.2.43, + 0.000-+--Hx-spam-relays-external:10.202.2.43, 0.000-+--H*F:U*spwhitton, + 0.000-+--H*F:D*spwhitton.name +Received: from wout2-smtp.messagingengine.com ([64.147.123.25]) + by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) (envelope-from ) + id 1ga30m-00059C-Kv; Thu, 20 Dec 2018 18:25:32 +0000 +Received: from compute3.internal (compute3.nyi.internal [10.202.2.43]) + by mailout.west.internal (Postfix) with ESMTP id C50B712F6; + Thu, 20 Dec 2018 13:25:30 -0500 (EST) +Received: from mailfrontend1 ([10.202.2.162]) + by compute3.internal (MEProxy); Thu, 20 Dec 2018 13:25:31 -0500 +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=spwhitton.name; + h=from:to:subject:date:message-id:mime-version:content-type; s= + fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lqnNZlsr/mb10zqE=; b=X9S4kI14 + zxdg9IKMIyALL2Eao3GTyyojuATJkgCqJvZWd8RWpty6RBStZfyeOqR1L3Gr5m3/ + EVeiHQyFNnPor2xjMDmblfPS/u09JxVlc0KMpT0XXRfNWsVQn+U40nNRX15kXzZ/ + D1rYhxpxzKRzU2tByUULCgbGlXAwJQtOXMDw3mpj1BxcoO13H/0H/KQTQ+AcpiOw + BV3JFKL/jA+mH8uAPIgNM2mUYZz5REO89eh3lPhLyc7tw745X+4ywZlo/Piqa0+6 + BCldY9/nDR3csAUKx1+3hkpJPdqFALBWvG3SelGt44BqcoLsOJLB8QH6trCro39o + fEnBaUBTAkTAHA== +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= + messagingengine.com; h=content-type:date:from:message-id + :mime-version:subject:to:x-me-proxy:x-me-proxy:x-me-sender + :x-me-sender:x-sasl-enc; s=fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lq + nNZlsr/mb10zqE=; b=QYqLjUBZtfbO0DUlagvXRP2BPnpTgkvIna9/uCewkdoIH + /LuPW6DZUNWQa55qiDquqKXcs0tTODTEzYgeIDgqC+DDBTHnFQvXdWyS1X3o4sLL + 8dTKk8lv7M1/zKFxyg/ycNvPJGS9m4ZucGbxjwdgAcozhg7W1Qztxt9eVhPVnenS + 5sdeJ9mjIE7lYkKX4QVsXPOi86j6QlfMNyi/OnBfX2+95QiA/xPE/wEq4MYlLNm7 + Av1P/8OrI4ImDKkOEivarktL+isYL7OXyGB4GfUTsydiy9dhP7RKPxrai1kJRu5S + b2470KXNatu2WkyMFrsdcwrSqyKIe096k5xPfVI2A== +X-ME-Sender: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod + ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu + fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg + gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi + thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh + hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr + ufhiiigvpedt +X-ME-Proxy: + + + +From: Sean Whitton +To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org, + 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org, + 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org, + 916876@bugs.debian.org +Date: Thu, 20 Dec 2018 18:25:26 +0000 +Message-ID: <87r2ecrr6x.fsf@zephyr.silentflame.com> +MIME-Version: 1.0 +X-CrossAssassin-Score: 3 +Received-SPF: pass client-ip=2607:f8f0:614:1::1274:39; + envelope-from=debbugs@buxtehude.debian.org; helo=buxtehude.debian.org +x-debian-approved: yes +Subject: [Pkg-emacsen-addons] Bug#916808: Increase severity to 'serious' +X-BeenThere: pkg-emacsen-addons@alioth-lists.debian.net +X-Mailman-Version: 2.1.23 +Precedence: list +List-Id: Maintainers list for Emacs addon packages + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Reply-To: Sean Whitton , 916808@bugs.debian.org +Content-Type: multipart/mixed; boundary="===============8231894308137086149==" +Errors-To: pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net +Sender: "Pkg-emacsen-addons" + +X-Spam_score: 2.8 +X-Spam_score_int: 28 +X-Spam_bar: ++ + +--===============8231894308137086149== +Content-Type: multipart/signed; boundary="=-=-="; + micalg=pgp-sha512; protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain +Content-Transfer-Encoding: quoted-printable + +control: severity -1 serious + +Hello, + +Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for +all the e-mails). + +=2D-=20 +Sean Whitton + +--=-=-= +Content-Type: application/pgp-signature; name="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEm5FwB64DDjbk/CSLaVt65L8GYkAFAlwb3pYACgkQaVt65L8G +YkDLrhAAhORxZzZDE5vlXRm89JYA3jd9OyleioZvDDRCrpEd7CQ5AHiMMJizW1lU +gn6OBIoW4O04TZ5oOuUnDHK/rS0G4zgNCJUyNf06zVECmdvkzspNNpQ3J5aOi4t2 +lhjRIFOKA9ifGsEqYLwP2dork1xFuyHEqHkDH8zpCTvdzkWky1bwAD/Pj5dArd7t +FeQGsPm7/64H1/rHk8pSP2pQgRsMDX6rIdx3vuQ7r+NssdRq+II4e479l02TiCDi +FBOX+n3nPXxREPdZ9EKL4SauL/AnRqpeC9GX6fC9OOnQeQ1xVTzNWKa6ixrqkFoH +TI/vy51p16jFNgdkLkyLtZA8Tq72TIAKWbZC0GFzWJVNASWu7WDIoMn5pgoi454w +TgsvK9MOnEYeABiDUa1ppaoMiP4+3j5yT0eWttTMSkcKjk1Ap1o+RfUxlIGl0Rog +ShbG2y6Mv8FERtjzPVQ7VMLDN9zRIbtlSJFm7CboPNSAygzzzaA/RIN/e8MdbZoM +a8AT9KiAVHEEcw+nWFAatAew5VP9iRZVgrVdWBszuaWOolxnYvpAL45WanqG0eab +VMe66+rZ8momI0MsM9JcqBwXO+fOf8CrPSO9PL8VFEJXFLZQS7asFStJf2l8msWE +3IYhvk4B6Nf1R96XzpXLlkOnoGtcnPVAvotrGU/rDfk5i/WF810= +=mWfF +-----END PGP SIGNATURE----- +--=-=-=-- + + +--===============8231894308137086149== +Content-Type: text/plain; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 +Content-Disposition: inline + +X19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX18KUGtnLWVtYWNz +ZW4tYWRkb25zIG1haWxpbmcgbGlzdApQa2ctZW1hY3Nlbi1hZGRvbnNAYWxpb3RoLWxpc3RzLmRl +Ymlhbi5uZXQKaHR0cHM6Ly9hbGlvdGgtbGlzdHMuZGViaWFuLm5ldC9jZ2ktYmluL21haWxtYW4v +bGlzdGluZm8vcGtnLWVtYWNzZW4tYWRkb25zCg== + +--===============8231894308137086149==-- + diff --git a/test/corpora/duplicate/msg-3-3:2, b/test/corpora/duplicate/msg-3-3:2, new file mode 100644 index 00000000..bff2473c --- /dev/null +++ b/test/corpora/duplicate/msg-3-3:2, @@ -0,0 +1,178 @@ +Return-path: +Envelope-to: david@tethera.net +Delivery-date: Thu, 20 Dec 2018 13:27:16 -0500 +Received: from muffat.debian.org ([2607:f8f0:614:1::1274:33]) + by fethera.tethera.net with esmtp (Exim 4.89) + (envelope-from ) + id 1ga32S-0005aw-5h + for david@tethera.net; Thu, 20 Dec 2018 13:27:16 -0500 +Received: from ticharich.debian.org ([2001:41c8:1000:21::21:23]) + from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=ticharich.debian.org,EMAIL=hostmaster@ticharich.debian.org (verified) + by muffat.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1ga32R-0008QQ-PL + for david@tethera.net; Thu, 20 Dec 2018 18:27:15 +0000 +Received: from localhost ([::1] helo=ticharich.debian.org) + by ticharich.debian.org with esmtp (Exim 4.89) + (envelope-from ) + id 1ga32Q-0008BB-Ad + for david@tethera.net; Thu, 20 Dec 2018 18:27:14 +0000 +Received: from muffat.debian.org ([2607:f8f0:614:1::1274:33]) + from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=muffat.debian.org,EMAIL=hostmaster@muffat.debian.org (verified) + by ticharich.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1ga32Q-0008Ai-2N + for dispatch+haskell-mode@tracker.debian.org; Thu, 20 Dec 2018 18:27:14 +0000 +Received: from quantz.debian.org ([2001:41c8:1000:21::21:28]) + from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=quantz.debian.org,EMAIL=hostmaster@quantz.debian.org (verified) + by muffat.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1ga32O-0008QE-Eo + for dispatch+haskell-mode@tracker.debian.org; Thu, 20 Dec 2018 18:27:12 +0000 +Received: from qa by quantz.debian.org with local (Exim 4.89) + (envelope-from ) + id 1ga32M-0006hb-Vn + for dispatch+haskell-mode@tracker.debian.org; Thu, 20 Dec 2018 18:27:10 +0000 +Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39]) + from C=NA,ST=NA,L=Ankh Morpork,O=Debian SMTP,OU=Debian SMTP CA,CN=buxtehude.debian.org,EMAIL=hostmaster@buxtehude.debian.org (verified) + by quantz.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1ga32J-0006gG-1x + for haskell-mode@packages.qa.debian.org; Thu, 20 Dec 2018 18:27:07 +0000 +Received: from debbugs by buxtehude.debian.org with local (Exim 4.89) + (envelope-from ) + id 1ga32H-0005SF-MF; Thu, 20 Dec 2018 18:27:05 +0000 +X-Loop: owner@bugs.debian.org +Subject: Bug#916807: Increase severity to 'serious' +Reply-To: Sean Whitton , 916807@bugs.debian.org +Resent-From: Sean Whitton +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Emacs addons team +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 20 Dec 2018 18:27:04 +0000 +Resent-Message-ID: +X-Debian-PR-Message: followup 916807 +X-Debian-PR-Package: src:haskell-mode +X-Debian-PR-Keywords: ftbfs +References: <87efaeh3zr.fsf@zephyr.silentflame.com> +X-Debian-PR-Source: haskell-mode +Received: via spool by 916807-submit@bugs.debian.org id=B916807.154533033319820 + (code B ref 916807); Thu, 20 Dec 2018 18:27:04 +0000 +Received: (at 916807) by bugs.debian.org; 20 Dec 2018 18:25:33 +0000 +X-Spam-Checker-Version: SpamAssassin 3.4.2-bugs.debian.org_2005_01_02 + (2018-09-13) on buxtehude.debian.org +X-Spam-Level: +X-Spam-Status: No, score=-11.3 required=4.0 tests=BAYES_00,DKIM_SIGNED, + DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,PGPSIGNATURE,RCVD_IN_DNSWL_LOW, + SORTED_RECIPS,SPF_HELO_PASS,SPF_PASS,SUSPICIOUS_RECIPS,TXREP + autolearn=unavailable autolearn_force=no + version=3.4.2-bugs.debian.org_2005_01_02 +X-Spam-Bayes: score:0.0000 Tokens: new, 0; hammy, 115; neutral, 22; spammy, 1. + spammytokens:0.902-+--emails hammytokens:0.000-+--HX-ME-Sender:xms, + 0.000-+--H*RU:10.202.2.43, + 0.000-+--Hx-spam-relays-external:10.202.2.43, + 0.000-+--H*F:D*spwhitton.name, 0.000-+--H*F:U*spwhitton +Received: from wout2-smtp.messagingengine.com ([64.147.123.25]) + by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1ga30m-00059C-Kv; Thu, 20 Dec 2018 18:25:32 +0000 +Received: from compute3.internal (compute3.nyi.internal [10.202.2.43]) + by mailout.west.internal (Postfix) with ESMTP id C50B712F6; + Thu, 20 Dec 2018 13:25:30 -0500 (EST) +Received: from mailfrontend1 ([10.202.2.162]) + by compute3.internal (MEProxy); Thu, 20 Dec 2018 13:25:31 -0500 +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=spwhitton.name; + h=from:to:subject:date:message-id:mime-version:content-type; s= + fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lqnNZlsr/mb10zqE=; b=X9S4kI14 + zxdg9IKMIyALL2Eao3GTyyojuATJkgCqJvZWd8RWpty6RBStZfyeOqR1L3Gr5m3/ + EVeiHQyFNnPor2xjMDmblfPS/u09JxVlc0KMpT0XXRfNWsVQn+U40nNRX15kXzZ/ + D1rYhxpxzKRzU2tByUULCgbGlXAwJQtOXMDw3mpj1BxcoO13H/0H/KQTQ+AcpiOw + BV3JFKL/jA+mH8uAPIgNM2mUYZz5REO89eh3lPhLyc7tw745X+4ywZlo/Piqa0+6 + BCldY9/nDR3csAUKx1+3hkpJPdqFALBWvG3SelGt44BqcoLsOJLB8QH6trCro39o + fEnBaUBTAkTAHA== +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= + messagingengine.com; h=content-type:date:from:message-id + :mime-version:subject:to:x-me-proxy:x-me-proxy:x-me-sender + :x-me-sender:x-sasl-enc; s=fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lq + nNZlsr/mb10zqE=; b=QYqLjUBZtfbO0DUlagvXRP2BPnpTgkvIna9/uCewkdoIH + /LuPW6DZUNWQa55qiDquqKXcs0tTODTEzYgeIDgqC+DDBTHnFQvXdWyS1X3o4sLL + 8dTKk8lv7M1/zKFxyg/ycNvPJGS9m4ZucGbxjwdgAcozhg7W1Qztxt9eVhPVnenS + 5sdeJ9mjIE7lYkKX4QVsXPOi86j6QlfMNyi/OnBfX2+95QiA/xPE/wEq4MYlLNm7 + Av1P/8OrI4ImDKkOEivarktL+isYL7OXyGB4GfUTsydiy9dhP7RKPxrai1kJRu5S + b2470KXNatu2WkyMFrsdcwrSqyKIe096k5xPfVI2A== +X-ME-Sender: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod + ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu + fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg + gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi + thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh + hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr + ufhiiigvpedt +X-ME-Proxy: + + + +From: Sean Whitton +To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org, 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org, 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org, 916876@bugs.debian.org +Date: Thu, 20 Dec 2018 18:25:26 +0000 +Message-ID: <87r2ecrr6x.fsf@zephyr.silentflame.com> +MIME-Version: 1.0 +Content-Type: multipart/signed; boundary="=-=-="; + micalg=pgp-sha512; protocol="application/pgp-signature" +X-CrossAssassin-Score: 2 +Delivered-To: haskell-mode@packages.qa.debian.org +Delivered-To: dispatch+haskell-mode@tracker.debian.org +X-Loop: dispatch@tracker.debian.org +X-Distro-Tracker-Keyword: bts +X-Distro-Tracker-Package: haskell-mode +List-Id: +X-Debian: tracker.debian.org +X-Debian-Package: haskell-mode +X-PTS-Package: haskell-mode +X-PTS-Keyword: bts +Precedence: list +List-Unsubscribe: +X-Spam_score: 2.8 +X-Spam_score_int: 28 +X-Spam_bar: ++ + +--=-=-= +Content-Type: text/plain +Content-Transfer-Encoding: quoted-printable + +control: severity -1 serious + +Hello, + +Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for +all the e-mails). + +=2D-=20 +Sean Whitton + +--=-=-= +Content-Type: application/pgp-signature; name="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEm5FwB64DDjbk/CSLaVt65L8GYkAFAlwb3pYACgkQaVt65L8G +YkDLrhAAhORxZzZDE5vlXRm89JYA3jd9OyleioZvDDRCrpEd7CQ5AHiMMJizW1lU +gn6OBIoW4O04TZ5oOuUnDHK/rS0G4zgNCJUyNf06zVECmdvkzspNNpQ3J5aOi4t2 +lhjRIFOKA9ifGsEqYLwP2dork1xFuyHEqHkDH8zpCTvdzkWky1bwAD/Pj5dArd7t +FeQGsPm7/64H1/rHk8pSP2pQgRsMDX6rIdx3vuQ7r+NssdRq+II4e479l02TiCDi +FBOX+n3nPXxREPdZ9EKL4SauL/AnRqpeC9GX6fC9OOnQeQ1xVTzNWKa6ixrqkFoH +TI/vy51p16jFNgdkLkyLtZA8Tq72TIAKWbZC0GFzWJVNASWu7WDIoMn5pgoi454w +TgsvK9MOnEYeABiDUa1ppaoMiP4+3j5yT0eWttTMSkcKjk1Ap1o+RfUxlIGl0Rog +ShbG2y6Mv8FERtjzPVQ7VMLDN9zRIbtlSJFm7CboPNSAygzzzaA/RIN/e8MdbZoM +a8AT9KiAVHEEcw+nWFAatAew5VP9iRZVgrVdWBszuaWOolxnYvpAL45WanqG0eab +VMe66+rZ8momI0MsM9JcqBwXO+fOf8CrPSO9PL8VFEJXFLZQS7asFStJf2l8msWE +3IYhvk4B6Nf1R96XzpXLlkOnoGtcnPVAvotrGU/rDfk5i/WF810= +=mWfF +-----END PGP SIGNATURE----- +--=-=-=-- + diff --git a/test/corpora/duplicate/msg-3-4:2, b/test/corpora/duplicate/msg-3-4:2, new file mode 100644 index 00000000..861719d1 --- /dev/null +++ b/test/corpora/duplicate/msg-3-4:2, @@ -0,0 +1,184 @@ +Return-path: +Envelope-to: david@tethera.net +Delivery-date: Thu, 20 Dec 2018 13:27:16 -0500 +Received: from mailly.debian.org ([2001:41b8:202:deb:6564:a62:52c3:4b72]) + by fethera.tethera.net with esmtp (Exim 4.89) + (envelope-from ) + id 1ga32S-0005ax-Kz + for david@tethera.net; Thu, 20 Dec 2018 13:27:16 -0500 +Received: from alioth-lists-01.debian.net ([2001:ba8:0:2c77:0:4:0:1]) + by mailly.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) + (envelope-from ) + id 1ga32S-0004W6-8i + for david@tethera.net; Thu, 20 Dec 2018 18:27:16 +0000 +Received: from localhost ([::1] helo=alioth-lists-01.debian.net) + by alioth-lists-01.debian.net with esmtp (Exim 4.89) + (envelope-from ) + id 1ga32S-0003xQ-0U + for bremner@debian.org; Thu, 20 Dec 2018 18:27:16 +0000 +Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39]) + by alioth-lists-01.debian.net with esmtps + (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89) + (envelope-from ) id 1ga32Q-0003wj-1x + for pkg-emacsen-addons@lists.alioth.debian.org; Thu, 20 Dec 2018 18:27:14 +0000 +Received: from debbugs by buxtehude.debian.org with local (Exim 4.89) + (envelope-from ) + id 1ga32O-0005Te-Sa; Thu, 20 Dec 2018 18:27:12 +0000 +X-Loop: owner@bugs.debian.org +Resent-From: Sean Whitton +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Emacs addons team + +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 20 Dec 2018 18:27:11 +0000 +Resent-Message-ID: +X-Debian-PR-Message: followup 916811 +X-Debian-PR-Package: src:weechat-el +X-Debian-PR-Keywords: ftbfs +References: <8736quh3la.fsf@zephyr.silentflame.com> +X-Debian-PR-Source: weechat-el +Received: via spool by 916811-submit@bugs.debian.org id=B916811.154533033319851 + (code B ref 916811); Thu, 20 Dec 2018 18:27:11 +0000 +Received: (at 916811) by bugs.debian.org; 20 Dec 2018 18:25:33 +0000 +X-Spam-Checker-Version: SpamAssassin 3.4.2-bugs.debian.org_2005_01_02 + (2018-09-13) on buxtehude.debian.org +X-Spam-Level: +X-Spam-Status: No, score=-15.2 required=4.0 tests=BAYES_00,DKIM_SIGNED, + DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,PGPSIGNATURE,RCVD_IN_DNSWL_LOW, + SORTED_RECIPS,SPF_HELO_PASS,SPF_PASS,SUSPICIOUS_RECIPS,TXREP + autolearn=unavailable autolearn_force=no + version=3.4.2-bugs.debian.org_2005_01_02 +X-Spam-Bayes: score:0.0000 Tokens: new, 0; hammy, 115; neutral, 22; spammy, 1. + spammytokens:0.902-+--emails + hammytokens:0.000-+--HX-ME-Sender:xms, + 0.000-+--H*RU:10.202.2.43, + 0.000-+--Hx-spam-relays-external:10.202.2.43, + 0.000-+--H*F:D*spwhitton.name, 0.000-+--H*F:U*spwhitton +Received: from wout2-smtp.messagingengine.com ([64.147.123.25]) + by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) (envelope-from ) + id 1ga30m-00059C-Kv; Thu, 20 Dec 2018 18:25:32 +0000 +Received: from compute3.internal (compute3.nyi.internal [10.202.2.43]) + by mailout.west.internal (Postfix) with ESMTP id C50B712F6; + Thu, 20 Dec 2018 13:25:30 -0500 (EST) +Received: from mailfrontend1 ([10.202.2.162]) + by compute3.internal (MEProxy); Thu, 20 Dec 2018 13:25:31 -0500 +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=spwhitton.name; + h=from:to:subject:date:message-id:mime-version:content-type; s= + fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lqnNZlsr/mb10zqE=; b=X9S4kI14 + zxdg9IKMIyALL2Eao3GTyyojuATJkgCqJvZWd8RWpty6RBStZfyeOqR1L3Gr5m3/ + EVeiHQyFNnPor2xjMDmblfPS/u09JxVlc0KMpT0XXRfNWsVQn+U40nNRX15kXzZ/ + D1rYhxpxzKRzU2tByUULCgbGlXAwJQtOXMDw3mpj1BxcoO13H/0H/KQTQ+AcpiOw + BV3JFKL/jA+mH8uAPIgNM2mUYZz5REO89eh3lPhLyc7tw745X+4ywZlo/Piqa0+6 + BCldY9/nDR3csAUKx1+3hkpJPdqFALBWvG3SelGt44BqcoLsOJLB8QH6trCro39o + fEnBaUBTAkTAHA== +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= + messagingengine.com; h=content-type:date:from:message-id + :mime-version:subject:to:x-me-proxy:x-me-proxy:x-me-sender + :x-me-sender:x-sasl-enc; s=fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lq + nNZlsr/mb10zqE=; b=QYqLjUBZtfbO0DUlagvXRP2BPnpTgkvIna9/uCewkdoIH + /LuPW6DZUNWQa55qiDquqKXcs0tTODTEzYgeIDgqC+DDBTHnFQvXdWyS1X3o4sLL + 8dTKk8lv7M1/zKFxyg/ycNvPJGS9m4ZucGbxjwdgAcozhg7W1Qztxt9eVhPVnenS + 5sdeJ9mjIE7lYkKX4QVsXPOi86j6QlfMNyi/OnBfX2+95QiA/xPE/wEq4MYlLNm7 + Av1P/8OrI4ImDKkOEivarktL+isYL7OXyGB4GfUTsydiy9dhP7RKPxrai1kJRu5S + b2470KXNatu2WkyMFrsdcwrSqyKIe096k5xPfVI2A== +X-ME-Sender: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod + ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu + fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg + gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi + thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh + hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr + ufhiiigvpedt +X-ME-Proxy: + + + +From: Sean Whitton +To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org, + 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org, + 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org, + 916876@bugs.debian.org +Date: Thu, 20 Dec 2018 18:25:26 +0000 +Message-ID: <87r2ecrr6x.fsf@zephyr.silentflame.com> +MIME-Version: 1.0 +X-CrossAssassin-Score: 5 +Received-SPF: pass client-ip=2607:f8f0:614:1::1274:39; + envelope-from=debbugs@buxtehude.debian.org; helo=buxtehude.debian.org +x-debian-approved: yes +Subject: [Pkg-emacsen-addons] Bug#916811: Increase severity to 'serious' +X-BeenThere: pkg-emacsen-addons@alioth-lists.debian.net +X-Mailman-Version: 2.1.23 +Precedence: list +List-Id: Maintainers list for Emacs addon packages + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Reply-To: Sean Whitton , 916811@bugs.debian.org +Content-Type: multipart/mixed; boundary="===============5359377007738902760==" +Errors-To: pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net +Sender: "Pkg-emacsen-addons" + +X-Spam_score: 2.8 +X-Spam_score_int: 28 +X-Spam_bar: ++ + +--===============5359377007738902760== +Content-Type: multipart/signed; boundary="=-=-="; + micalg=pgp-sha512; protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain +Content-Transfer-Encoding: quoted-printable + +control: severity -1 serious + +Hello, + +Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for +all the e-mails). + +=2D-=20 +Sean Whitton + +--=-=-= +Content-Type: application/pgp-signature; name="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEm5FwB64DDjbk/CSLaVt65L8GYkAFAlwb3pYACgkQaVt65L8G +YkDLrhAAhORxZzZDE5vlXRm89JYA3jd9OyleioZvDDRCrpEd7CQ5AHiMMJizW1lU +gn6OBIoW4O04TZ5oOuUnDHK/rS0G4zgNCJUyNf06zVECmdvkzspNNpQ3J5aOi4t2 +lhjRIFOKA9ifGsEqYLwP2dork1xFuyHEqHkDH8zpCTvdzkWky1bwAD/Pj5dArd7t +FeQGsPm7/64H1/rHk8pSP2pQgRsMDX6rIdx3vuQ7r+NssdRq+II4e479l02TiCDi +FBOX+n3nPXxREPdZ9EKL4SauL/AnRqpeC9GX6fC9OOnQeQ1xVTzNWKa6ixrqkFoH +TI/vy51p16jFNgdkLkyLtZA8Tq72TIAKWbZC0GFzWJVNASWu7WDIoMn5pgoi454w +TgsvK9MOnEYeABiDUa1ppaoMiP4+3j5yT0eWttTMSkcKjk1Ap1o+RfUxlIGl0Rog +ShbG2y6Mv8FERtjzPVQ7VMLDN9zRIbtlSJFm7CboPNSAygzzzaA/RIN/e8MdbZoM +a8AT9KiAVHEEcw+nWFAatAew5VP9iRZVgrVdWBszuaWOolxnYvpAL45WanqG0eab +VMe66+rZ8momI0MsM9JcqBwXO+fOf8CrPSO9PL8VFEJXFLZQS7asFStJf2l8msWE +3IYhvk4B6Nf1R96XzpXLlkOnoGtcnPVAvotrGU/rDfk5i/WF810= +=mWfF +-----END PGP SIGNATURE----- +--=-=-=-- + + +--===============5359377007738902760== +Content-Type: text/plain; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 +Content-Disposition: inline + +X19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX18KUGtnLWVtYWNz +ZW4tYWRkb25zIG1haWxpbmcgbGlzdApQa2ctZW1hY3Nlbi1hZGRvbnNAYWxpb3RoLWxpc3RzLmRl +Ymlhbi5uZXQKaHR0cHM6Ly9hbGlvdGgtbGlzdHMuZGViaWFuLm5ldC9jZ2ktYmluL21haWxtYW4v +bGlzdGluZm8vcGtnLWVtYWNzZW4tYWRkb25zCg== + +--===============5359377007738902760==-- + diff --git a/test/corpora/duplicate/msg-3-5:2, b/test/corpora/duplicate/msg-3-5:2, new file mode 100644 index 00000000..af26374d --- /dev/null +++ b/test/corpora/duplicate/msg-3-5:2, @@ -0,0 +1,179 @@ +Return-path: +Envelope-to: david@tethera.net +Delivery-date: Thu, 20 Dec 2018 13:27:21 -0500 +Received: from alioth-lists-01.debian.net ([2001:ba8:0:2c77:0:4:0:1]) + by fethera.tethera.net with esmtp (Exim 4.89) + (envelope-from ) + id 1ga32X-0005bA-Go + for david@tethera.net; Thu, 20 Dec 2018 13:27:21 -0500 +Received: from localhost ([::1] helo=alioth-lists-01.debian.net) + by alioth-lists-01.debian.net with esmtp (Exim 4.89) + (envelope-from ) + id 1ga32U-0003yc-Ai + for david@tethera.net; Thu, 20 Dec 2018 18:27:18 +0000 +Received: from buxtehude.debian.org ([2607:f8f0:614:1::1274:39]) + by alioth-lists-01.debian.net with esmtps + (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89) + (envelope-from ) id 1ga32S-0003xN-6J + for debian-science-maintainers@lists.alioth.debian.org; + Thu, 20 Dec 2018 18:27:16 +0000 +Received: from debbugs by buxtehude.debian.org with local (Exim 4.89) + (envelope-from ) + id 1ga32R-0005U9-0Z; Thu, 20 Dec 2018 18:27:15 +0000 +X-Loop: owner@bugs.debian.org +Subject: Bug#916867: Increase severity to 'serious' +Resent-From: Sean Whitton +Resent-To: debian-bugs-dist@lists.debian.org +Resent-CC: Debian Science Maintainers + +X-Loop: owner@bugs.debian.org +Resent-Date: Thu, 20 Dec 2018 18:27:13 +0000 +Resent-Message-ID: +X-Debian-PR-Message: followup 916867 +X-Debian-PR-Package: src:hkl +X-Debian-PR-Keywords: ftbfs +References: <87sgyt2xyg.fsf@zephyr.silentflame.com> +X-Debian-PR-Source: hkl +Received: via spool by 916867-submit@bugs.debian.org id=B916867.154533033319861 + (code B ref 916867); Thu, 20 Dec 2018 18:27:13 +0000 +Received: (at 916867) by bugs.debian.org; 20 Dec 2018 18:25:33 +0000 +X-Spam-Checker-Version: SpamAssassin 3.4.2-bugs.debian.org_2005_01_02 + (2018-09-13) on buxtehude.debian.org +X-Spam-Level: +X-Spam-Status: No, score=-13.0 required=4.0 tests=BAYES_00,DKIM_SIGNED, + DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,PGPSIGNATURE,RCVD_IN_DNSWL_LOW, + SORTED_RECIPS,SPF_HELO_PASS,SPF_PASS,SUSPICIOUS_RECIPS,TXREP + autolearn=unavailable autolearn_force=no + version=3.4.2-bugs.debian.org_2005_01_02 +X-Spam-Bayes: score:0.0000 Tokens: new, 0; hammy, 115; neutral, 22; spammy, 1. + spammytokens:0.902-+--emails + hammytokens:0.000-+--HX-ME-Sender:xms, + 0.000-+--H*RU:10.202.2.43, + 0.000-+--Hx-spam-relays-external:10.202.2.43, + 0.000-+--H*F:D*spwhitton.name, 0.000-+--H*F:U*spwhitton +Received: from wout2-smtp.messagingengine.com ([64.147.123.25]) + by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) + (Exim 4.89) (envelope-from ) + id 1ga30m-00059C-Kv; Thu, 20 Dec 2018 18:25:32 +0000 +Received: from compute3.internal (compute3.nyi.internal [10.202.2.43]) + by mailout.west.internal (Postfix) with ESMTP id C50B712F6; + Thu, 20 Dec 2018 13:25:30 -0500 (EST) +Received: from mailfrontend1 ([10.202.2.162]) + by compute3.internal (MEProxy); Thu, 20 Dec 2018 13:25:31 -0500 +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=spwhitton.name; + h=from:to:subject:date:message-id:mime-version:content-type; s= + fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lqnNZlsr/mb10zqE=; b=X9S4kI14 + zxdg9IKMIyALL2Eao3GTyyojuATJkgCqJvZWd8RWpty6RBStZfyeOqR1L3Gr5m3/ + EVeiHQyFNnPor2xjMDmblfPS/u09JxVlc0KMpT0XXRfNWsVQn+U40nNRX15kXzZ/ + D1rYhxpxzKRzU2tByUULCgbGlXAwJQtOXMDw3mpj1BxcoO13H/0H/KQTQ+AcpiOw + BV3JFKL/jA+mH8uAPIgNM2mUYZz5REO89eh3lPhLyc7tw745X+4ywZlo/Piqa0+6 + BCldY9/nDR3csAUKx1+3hkpJPdqFALBWvG3SelGt44BqcoLsOJLB8QH6trCro39o + fEnBaUBTAkTAHA== +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= + messagingengine.com; h=content-type:date:from:message-id + :mime-version:subject:to:x-me-proxy:x-me-proxy:x-me-sender + :x-me-sender:x-sasl-enc; s=fm1; bh=jAd0CK1DR6lBA7NSLH5FsHIYQv3lq + nNZlsr/mb10zqE=; b=QYqLjUBZtfbO0DUlagvXRP2BPnpTgkvIna9/uCewkdoIH + /LuPW6DZUNWQa55qiDquqKXcs0tTODTEzYgeIDgqC+DDBTHnFQvXdWyS1X3o4sLL + 8dTKk8lv7M1/zKFxyg/ycNvPJGS9m4ZucGbxjwdgAcozhg7W1Qztxt9eVhPVnenS + 5sdeJ9mjIE7lYkKX4QVsXPOi86j6QlfMNyi/OnBfX2+95QiA/xPE/wEq4MYlLNm7 + Av1P/8OrI4ImDKkOEivarktL+isYL7OXyGB4GfUTsydiy9dhP7RKPxrai1kJRu5S + b2470KXNatu2WkyMFrsdcwrSqyKIe096k5xPfVI2A== +X-ME-Sender: +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod + ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu + fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg + gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi + thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh + hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr + ufhiiigvpedt +X-ME-Proxy: + + + +From: Sean Whitton +To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org, + 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org, + 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org, + 916876@bugs.debian.org +Date: Thu, 20 Dec 2018 18:25:26 +0000 +Message-ID: <87r2ecrr6x.fsf@zephyr.silentflame.com> +MIME-Version: 1.0 +X-CrossAssassin-Score: 6 +Received-SPF: pass client-ip=2607:f8f0:614:1::1274:39; + envelope-from=debbugs@buxtehude.debian.org; helo=buxtehude.debian.org +x-debian-approved: yes +X-BeenThere: debian-science-maintainers@alioth-lists.debian.net +X-Mailman-Version: 2.1.23 +Precedence: list +List-Id: Mailing list for maintainer discussions and BTS messages + +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Reply-To: Sean Whitton , 916867@bugs.debian.org +Content-Type: multipart/mixed; boundary="===============7686561040995282884==" +Errors-To: debian-science-maintainers-bounces+david=tethera.net@alioth-lists.debian.net +Sender: "debian-science-maintainers" + +X-Spam_score: 2.8 +X-Spam_score_int: 28 +X-Spam_bar: ++ + +--===============7686561040995282884== +Content-Type: multipart/signed; boundary="=-=-="; + micalg=pgp-sha512; protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain +Content-Transfer-Encoding: quoted-printable + +control: severity -1 serious + +Hello, + +Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for +all the e-mails). + +=2D-=20 +Sean Whitton + +--=-=-= +Content-Type: application/pgp-signature; name="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEm5FwB64DDjbk/CSLaVt65L8GYkAFAlwb3pYACgkQaVt65L8G +YkDLrhAAhORxZzZDE5vlXRm89JYA3jd9OyleioZvDDRCrpEd7CQ5AHiMMJizW1lU +gn6OBIoW4O04TZ5oOuUnDHK/rS0G4zgNCJUyNf06zVECmdvkzspNNpQ3J5aOi4t2 +lhjRIFOKA9ifGsEqYLwP2dork1xFuyHEqHkDH8zpCTvdzkWky1bwAD/Pj5dArd7t +FeQGsPm7/64H1/rHk8pSP2pQgRsMDX6rIdx3vuQ7r+NssdRq+II4e479l02TiCDi +FBOX+n3nPXxREPdZ9EKL4SauL/AnRqpeC9GX6fC9OOnQeQ1xVTzNWKa6ixrqkFoH +TI/vy51p16jFNgdkLkyLtZA8Tq72TIAKWbZC0GFzWJVNASWu7WDIoMn5pgoi454w +TgsvK9MOnEYeABiDUa1ppaoMiP4+3j5yT0eWttTMSkcKjk1Ap1o+RfUxlIGl0Rog +ShbG2y6Mv8FERtjzPVQ7VMLDN9zRIbtlSJFm7CboPNSAygzzzaA/RIN/e8MdbZoM +a8AT9KiAVHEEcw+nWFAatAew5VP9iRZVgrVdWBszuaWOolxnYvpAL45WanqG0eab +VMe66+rZ8momI0MsM9JcqBwXO+fOf8CrPSO9PL8VFEJXFLZQS7asFStJf2l8msWE +3IYhvk4B6Nf1R96XzpXLlkOnoGtcnPVAvotrGU/rDfk5i/WF810= +=mWfF +-----END PGP SIGNATURE----- +--=-=-=-- + + +--===============7686561040995282884== +Content-Type: text/plain; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 +Content-Disposition: inline + +LS0gCmRlYmlhbi1zY2llbmNlLW1haW50YWluZXJzIG1haWxpbmcgbGlzdApkZWJpYW4tc2NpZW5j +ZS1tYWludGFpbmVyc0BhbGlvdGgtbGlzdHMuZGViaWFuLm5ldApodHRwczovL2FsaW90aC1saXN0 +cy5kZWJpYW4ubmV0L2NnaS1iaW4vbWFpbG1hbi9saXN0aW5mby9kZWJpYW4tc2NpZW5jZS1tYWlu +dGFpbmVycw== + +--===============7686561040995282884==-- + diff --git a/test/corpora/indexing/PATCH-1-2-system_data_types.7-srcfix.txt:2,S b/test/corpora/indexing/PATCH-1-2-system_data_types.7-srcfix.txt:2,S new file mode 100644 index 00000000..1361c6f2 --- /dev/null +++ b/test/corpora/indexing/PATCH-1-2-system_data_types.7-srcfix.txt:2,S @@ -0,0 +1,282 @@ +From mboxrd@z Thu Jan 1 00:00:00 1970 +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on + aws-us-west-2-korg-lkml-1.web.codeaurora.org +X-Spam-Level: +X-Spam-Status: No, score=-8.3 required=3.0 tests=BAYES_00,DKIM_SIGNED, + DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, + HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_PATCH,MAILING_LIST_MULTI,SPF_HELO_NONE, + SPF_PASS,URIBL_BLOCKED,USER_AGENT_SANE_1 autolearn=ham autolearn_force=no + version=3.4.0 +Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) + by smtp.lore.kernel.org (Postfix) with ESMTP id AFE3FC4727E + for ; Wed, 30 Sep 2020 10:12:21 +0000 (UTC) +Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) + by mail.kernel.org (Postfix) with ESMTP id 4E0D62074A + for ; Wed, 30 Sep 2020 10:12:21 +0000 (UTC) +Authentication-Results: mail.kernel.org; + dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="Osm9Pn67" +Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand + id S1725823AbgI3KMU (ORCPT ); + Wed, 30 Sep 2020 06:12:20 -0400 +Received: from lindbergh.monkeyblade.net ([23.128.96.19]:50038 "EHLO + lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org + with ESMTP id S1725779AbgI3KMU (ORCPT + ); Wed, 30 Sep 2020 06:12:20 -0400 +Received: from mail-pf1-x443.google.com (mail-pf1-x443.google.com [IPv6:2607:f8b0:4864:20::443]) + by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 5026DC061755 + for ; Wed, 30 Sep 2020 03:12:20 -0700 (PDT) +Received: by mail-pf1-x443.google.com with SMTP id b124so832681pfg.13 + for ; Wed, 30 Sep 2020 03:12:20 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=gmail.com; s=20161025; + h=date:from:to:cc:subject:message-id:references:mime-version + :content-disposition:in-reply-to:user-agent; + bh=qR1FJVXOhU6/g+m4SoSco3vMtV+CNvRvNyXS1xuG+T4=; + b=Osm9Pn67G380QiA1ORltntJShSHlKg/KZZfKV8ebvfEXJw9893EO0N6J6GDR+zkmHi + TOQuIe7x9y95Pipm54rWWEW33U3gwoXRHsPc2Kivm6L8Ixb+f0T0rMPKw/FOkL8OGo9t + WmmSvnlErAXHqBq9aRAJJsf2bSlDgdAyYY1Qe6PSq2hKi2rg+sOy1Vaj4RqZ6jTK/DWY + tX28Ql0XS3kKWp0Lc8MNsSP+SXlcdwHQYll5LeReAg1oi++hICgWphuMmo3OH+2B1WtO + hMH7VuUONqbuE1aLoZ6PyyUlCeN1soJd8bKY0cmY0TKCsw0Jvkuh/XzYDVNi6wOSM6Ez + okpA== +X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=1e100.net; s=20161025; + h=x-gm-message-state:date:from:to:cc:subject:message-id:references + :mime-version:content-disposition:in-reply-to:user-agent; + bh=qR1FJVXOhU6/g+m4SoSco3vMtV+CNvRvNyXS1xuG+T4=; + b=TJU+duGLhruSES/5sJy4y1wfcltfokDpA58edkSUJyasvsooUo67VNtOB3ZK49iHm5 + C/cjy0ExxTECB0aM6p+B1jcePdWoPUaVBY9bVd/Q5DNhm4KhTO8ON96gB43d2rLWLOiK + /Y1vCu+MwOpY0JQTojbC140s/JYccR/KPapTmbUkRzrpmeoYqw8CbBPV60rQxYCn9GUu + FeCXJY5q9OfaYW1viQZoBL5n1IMMpJDVa61Q8gZ33b3wRCvQv/x1eZCsVlYpjcqf7Umc + /Amx3i27cxvo8pSvvwiTzrlJHJv0Gkytz13i7s+zW+XKzZRyzy3yirtU2DFTGat6FeMn + H8Ig== +X-Gm-Message-State: AOAM530Yon7xNOW6kiuy6bVpbpwbzR/9pldRB49OtZaSAHAZg7Gyf7qE + JXgAH20rZzYlwqOZyeZCeAwtWh09PeI= +X-Google-Smtp-Source: ABdhPJxzyZAVDBtMwQ5+dUqVg37y/LgZByrSaTxvhS6wnx6sJuG8ROItw0CwDAg939XUVADeje/nZQ== +X-Received: by 2002:a63:c547:: with SMTP id g7mr1563654pgd.234.1601460739764; + Wed, 30 Sep 2020 03:12:19 -0700 (PDT) +Received: from localhost.localdomain ([1.129.172.177]) + by smtp.gmail.com with ESMTPSA id k14sm1804437pjd.45.2020.09.30.03.12.17 + (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); + Wed, 30 Sep 2020 03:12:19 -0700 (PDT) +Date: Wed, 30 Sep 2020 20:12:15 +1000 +From: "G. Branden Robinson" +To: "Michael Kerrisk (man-pages)" +Cc: Jakub Wilk , linux-man@vger.kernel.org +Subject: Re: [PATCH 1/2] system_data_types.7: srcfix +Message-ID: <20200930101213.2m2pt3jrspvcrxfx@localhost.localdomain> +References: <20200925080330.184303-1-colomar.6.4.3@gmail.com> + <20200927061015.4obt73pdhyh7wecu@localhost.localdomain> + <20200928132959.x4koforqnzohxh5u@jwilk.net> + <9b8303fe-969e-c9f0-e3cd-0590b342d5bf@gmail.com> +MIME-Version: 1.0 +Content-Type: multipart/signed; micalg=pgp-sha256; + protocol="application/pgp-signature"; boundary="jg2hlfugxpumieke" +Content-Disposition: inline +In-Reply-To: <9b8303fe-969e-c9f0-e3cd-0590b342d5bf@gmail.com> +User-Agent: NeoMutt/20180716 +Precedence: bulk +List-ID: +X-Mailing-List: linux-man@vger.kernel.org + + +--jg2hlfugxpumieke +Content-Type: multipart/mixed; boundary="wl6i3r6gpq7ibouc" +Content-Disposition: inline + + +--wl6i3r6gpq7ibouc +Content-Type: text/plain; charset=us-ascii +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable + +Hi Jakub and Michael, + +At 2020-09-29T14:13:26+0200, Michael Kerrisk (man-pages) wrote: +> On 9/28/20 3:29 PM, Jakub Wilk wrote: +> > Hi Branden! +> >=20 +> > In groff_man_style(7) you wrote: +> >> Unused macro arguments are more often simply omitted, or good style +> >> suggests that a more appropriate macro be chosen, that earlier +> >> arguments are more important than later ones, or that arguments +> >> have identical significance such that skipping any is superfluous. +> >=20 +> > After 15 minutes of gawking at this sentence, I still don't +> > understand what are you trying to say here. The sentence should be +> > either thoroughly rephrased or removed. +>=20 +> I must say that I too found it hard to parse. I presume, Branden, +> that you mean: +>=20 +> [[ +> Unused macro arguments are more often simply omitted, or good style=20 +> suggests +> EITHER (1)=20 +> that a more appropriate macro be chosen,=20 +> (2) +> that earlier arguments are more important than later ones, or +> (3) +> that arguments have=20 +> identical significance such that skipping any is superfluous. +> ]] + +You got it. But it was too much work. + +> But it takes a few scans to work that out. Perhaps break this into +> smaller pieces, or add some explicit structuring elements to the +> sentence? + +I was trying to be comprehensive with respect to several anti-patterns I +had in mind. However, using the anti-patterns concretely is premature +at that point in the page. So I both expanded and relocated the +material. + +I'm attaching what I've just committed to groff git. + +Further feedback is welcome, of course; revision of documentation is a +process that is never completed, only abandoned. And I haven't given up +yet. :) + +Thank you both for your reviews. + +Regards, +Branden + +--wl6i3r6gpq7ibouc +Content-Type: text/x-diff; charset=us-ascii +Content-Disposition: attachment; filename="excise_standardese.diff" +Content-Transfer-Encoding: quoted-printable + +commit dd2c4cf05a659ae7127e342924668ff0fa0deaa1 +Author: G. Branden Robinson +Date: Wed Sep 30 19:56:38 2020 +1000 + + groff_man_style(7): Clarify empty macro arguments. + =20 + Rewrite some ersatz standardese I had managed to concoct regarding why + empty macro arguments are usually not needed. Put an expanded + discussion, with anti-patterns and remedies, in section "Notes", with + forward reference from subsection "Macro reference preliminaries". + =20 + Thanks to Jakub Wilk and Michael Kerrisk for the critique. + +diff --git a/tmac/groff_man.7.man.in b/tmac/groff_man.7.man.in +index c62d97ba..b96cbaf4 100644 +--- a/tmac/groff_man.7.man.in ++++ b/tmac/groff_man.7.man.in +@@ -281,23 +281,8 @@ but the + package is designed such that this should seldom be necessary. + _ifstyle()dnl + . +-Unused macro arguments are more often simply omitted, +-.\" antipattern: '.TP ""' (just '.TP' will do) +-or good style suggests that a more appropriate macro be chosen, +-.\" antipattern: '.BI "" italic bold' (use '.IB' instead) +-that earlier arguments are more important than later ones, +-.\" antipattern: '.TH foo 1 "" "foo "1.2.3"' (don't skip the date!) +-.\" antipattern: '.IP "" 4n' (use .TP or .RS/.RE, depending on needs) +-or that arguments have identical significance such that skipping any is +-superfluous. +-.\" antipattern: '.B one two "" three' (pointless) +-.\" Technically, the above has a side-effect of additional space +-.\" between "two" and "three", but there are much more obvious ways of +-.\" getting it if desired. +-.\" .B "one two three" +-.\" .B one "two " three +-.\" .B one two " three" +-.\" .B one two\~ three ++See section \(lqNotes\(rq below for examples of cases where better ++alternatives to empty arguments in macro calls are available. + _endif()dnl + . + Most macro arguments are strings that will be output as text; +@@ -3235,6 +3220,63 @@ Some tips on troubleshooting your man pages follow. + . + . + .TP ++\(bu Do I ever need to use an empty macro argument ("")? ++Probably not. ++. ++When this seems necessary, ++often a shorter or clearer alternative is available. ++. ++.\" antipattern: '.TP ""' (just '.TP' will do) ++.\" antipattern: '.BI "" italic bold' (use '.IB' instead) ++.\" antipattern: '.TH foo 1 "" "foo 1.2.3"' (don't skip the date!) ++.\" antipattern: '.IP "" 4n' (use .TP or .RS/.RE, depending on needs) ++.\" antipattern: '.B one two "" three' (pointless) ++.\" Technically, the above has a side-effect of additional space ++.\" between "two" and "three", but there are much more obvious ways of ++.\" getting it if desired. ++.\" .B "one two three" ++.\" .B one "two " three ++.\" .B one two " three" ++.\" .B one two\~ three ++.TS ++c c ++lfCB lfCB. ++Instead of.\|.\. .\|.\|.do this. ++_ ++\&.TP \(dq\(dq .TP ++\&.BI \(dq\(dq italic-text bold-text .IB italic-text bold-text ++\&.TH foo 1 \(dq\(dq \(dqfoo 1.2.3\(dq .TH foo 1 \ ++\f(CIyyyy\fP-\f(CImm\fP-\f(CIdd\fP \(dqfoo 1.2.3\(dq ++\&.IP \(dq\(dq 4n .TP 4n ++\&.B one two \(dq\(dq three .B one two three ++.TE ++. ++. ++.IP ++In the title heading ++.RB ( .TH ), ++the date of the page's last revision is more important than packaging ++information; ++it should not be omitted. ++. ++Ideally, ++a page maintainer will keep both up to date. ++. ++. ++.IP ++In the last example, ++the empty argument does have a subtly different effect than its ++suggested replacement; ++the empty argument becomes an additional space character\(embut it is a ++regular breaking space, ++so it can be discarded at the end of an output line. ++. ++It is better not to be subtle, ++particularly with space, ++which can be overlooked in source and rendered forms. ++. ++. ++.TP + .RB \(bu " .RS" " doesn't indent relative to my indented paragraph" + The + .B .RS + +--wl6i3r6gpq7ibouc-- + +--jg2hlfugxpumieke +Content-Type: application/pgp-signature; name="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCAAdFiEEh3PWHWjjDgcrENwa0Z6cfXEmbc4FAl90WfUACgkQ0Z6cfXEm +bc5raQ/9GhXG/5U7McaEEu+aW1IgaTYTMbsMpew5u3tBlj3/IenGzsy8wDO912BD +aHPSedYoc485k1Vh/Kowyx569RhyIXiMtH7uINCEtheMSUNgITNFqXo8mhaqVMlU +3JoV12btQddOIqHnGX6c5V9Z38KXFmVctD6CxjLaWGLp/Bu9tSKwSaHOOmtUYyOv +fYpMzr0amd4z9f+O8PPnToqBhwUitEvis1ZHYU6gIj8VwOjD0gNsWjA9HR3uC3c9 +GK/R5przMANrNejzSgofm0/yAL6a61WhqhYEtzLUYu2NFnsyNJWzITNsNnoxzgQ5 +liKL0Onmw0YWjOo4Z9Zht9Iyd6JhJxW0uRwlpFhE6UlCkFHK8nbv3NbHT2xlx/po +rxY5jDC3Ap3+mdYHY8k5o8vFd4QOXc2bSTuDRZoWtFZQsjnl4Fpkqks1W54Txq4y +o3Vu9aOPx//Jfi8sDc/qD/mFnyUu+AMFWjIj8UxQN4HmbrbXg/DEczRfP68DjOiX +ssy/0Rmm/H1cu7oBMoSss63mpk/NvPTSzzCR+VhU4PHQ7rxSZYS105tzkBVfe37e +hSS00rQVWe2YnI1KkfJHFjzveHiPXf+IxC0Z4PpJuLhl+pIZ/FgxJ5yEkX0XVUIy +aYRzKI3JaJktYl6WvulKSBPzQxIyOgrqVkZW4lv/uTh64pE6E5w= +=oeam +-----END PGP SIGNATURE----- + +--jg2hlfugxpumieke-- + diff --git a/test/corpora/indexing/fake-pdf:2,S b/test/corpora/indexing/fake-pdf:2,S new file mode 100644 index 00000000..60a7a47f --- /dev/null +++ b/test/corpora/indexing/fake-pdf:2,S @@ -0,0 +1,11 @@ +From: David Bremner +To: example@example.com +Subject: attachment content type +Date: Thu, 05 Jan 2023 08:02:36 -0400 +Message-ID: <871qo9p4tf.fsf@tethera.net> +MIME-Version: 1.0 +Content-Type: application/pdf +Content-Disposition: attachment; filename=fake.pdf +Content-Transfer-Encoding: base64 + +dGhpcyBpcyBub3QgcmVhbGx5IFBERgo= \ No newline at end of file diff --git a/test/corpora/insert/mbox-attachment.eml b/test/corpora/insert/mbox-attachment.eml new file mode 100644 index 00000000..98a8fc91 --- /dev/null +++ b/test/corpora/insert/mbox-attachment.eml @@ -0,0 +1,83 @@ +From david@tethera.net Sat Feb 5 09:19:10 2022 +From: David Bremner +To: David Bremner +Subject: Re: [RFC PATCH v2 12/12] emacs: whitespace cleanup for keybindings +Date: Sat, 05 Feb 2022 10:19:09 -0400 +Message-ID: <87k0e9o0pu.fsf@tethera.net> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=-=-=" + +--=-=-= +Content-Type: text/plain +Content-Disposition: inline + + +I figured out the race condition in the tests. The previous test was +still running when the failing test started, the joys of using a shared +emacs for running all of the tests in one file. + +The attached diff is split into the the commits that introduce the tests +in question in my working series, but you should be able to just apply +it on top of the posted series if you want. + + +--=-=-= +Content-Type: text/x-diff +Content-Disposition: inline; filename=0001-test-fixups.patch + +From fc88cba7f1f37b9cf3b296eace2422dd0e173502 Mon Sep 17 00:00:00 2001 +From: David Bremner +Date: Thu, 3 Feb 2022 21:05:05 -0400 +Subject: [PATCH] test fixups + +--- + test/T315-emacs-tagging.sh | 9 ++++----- + 1 file changed, 4 insertions(+), 5 deletions(-) + +diff --git a/test/T315-emacs-tagging.sh b/test/T315-emacs-tagging.sh +index c9e3e53a..c26413ce 100755 +--- a/test/T315-emacs-tagging.sh ++++ b/test/T315-emacs-tagging.sh +@@ -119,7 +119,8 @@ for mode in search show tree unthreaded; do + (notmuch-$mode \"$os_x_darwin_thread\") + (notmuch-test-wait) + (execute-kbd-macro \"+tag-to-be-undone-$mode\") +- (notmuch-tag-undo))" ++ (notmuch-tag-undo) ++ (notmuch-test-wait))" + count=$(notmuch count "tag:tag-to-be-undone-$mode") + test_expect_equal "$count" "0" + +@@ -128,9 +129,7 @@ for mode in search show tree unthreaded; do + (notmuch-$mode \"$os_x_darwin_thread\") + (notmuch-test-wait) + (execute-kbd-macro \"+one-$mode\") +- (notmuch-test-wait) + (execute-kbd-macro \"+two-$mode\") +- (notmuch-test-wait) + (notmuch-tag-undo) + (notmuch-test-wait) + (execute-kbd-macro \"+three-$mode\"))" +@@ -143,7 +142,6 @@ for mode in search show tree unthreaded; do + (notmuch-$mode \"$os_x_darwin_thread\") + (notmuch-test-wait) + (execute-kbd-macro \"+one-$mode\") +- (notmuch-test-wait) + (execute-kbd-macro \"+two-$mode\") + (notmuch-tag-undo) + (notmuch-test-wait) +@@ -159,7 +157,8 @@ for mode in search show tree unthreaded; do + (notmuch-$mode \"$os_x_darwin_thread\") + (notmuch-test-wait) + (execute-kbd-macro \"+tag-to-be-undone-$mode\") +- (execute-kbd-macro (kbd \"C-x u\")))" ++ (execute-kbd-macro (kbd \"C-x u\")) ++ (notmuch-test-wait))" + count=$(notmuch count "tag:tag-to-be-undone-$mode") + test_expect_equal "$count" "0" + done +-- +2.30.2 + + +--=-=-=-- diff --git a/test/corpora/mangling/mixed-up.eml b/test/corpora/mangling/mixed-up.eml index a09f6191..d4702798 100644 --- a/test/corpora/mangling/mixed-up.eml +++ b/test/corpora/mangling/mixed-up.eml @@ -21,13 +21,9 @@ VmVyc2lvbjogMQ0K Content-Type: application/octet-stream Content-Transfer-Encoding: base64 -LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQoNCmhJd0R4RTAyM3ExVXF4WUJCQUNwNzBlN0tQ -eTlPWWFoZUlya0x6bWhxMWxScW15NTFhTDFqQkwwSy9xTjdyZksNCkJaRUcxY1I4amVMalRGZFBL -UExWS0pJODByN0ZnS0kweXd2V3ZsNlIxYUUxVHk1Qm5WWFQ5WHpDckVIN2ZxQ2wNClNLSzgyRXZv -bFhUb2hBWkhVcmg2SzY2ZVFRVFRJQUMxbjdCMEE4aEVyemtnYU00K3NlTjNMbHZlelQ2VExOS00N -CkFUcHFzRWJNMk1WckdndzBiM29Vc0dHQVBFdDJNbWpORVlzcmlLbnF3dDZkSkRaYy8vWHloamdN -UWF5aUQ4ZGENCk4xZ1Qzb3FndS9nS0NwQlpEWXpIZjlPdFZpMlVubEZEV3k2cnJNWkxqV0RuSXY0 -dmU5UG4vcW9sd0hWanpkSjENClpmak5DNXQwejNYQURLR3JqTjl3dXRyNHFtN1NUVzFySEFYSFA2 -OFRRVHhJMHFnSktqUFhOS1dFdzZnPQ0KPXBKRzQNCi0tLS0tRU5EIFBHUCBNRVNTQUdFLS0tLS0N -Cg== +wV4DHXHP849rSK8SAQdA4L/4ntrLU9OGEHy0JIryOhNPoVCN1MgZieW5E+1wDjMw +GZL3EGlGTYWPhAm6wmDXsMHWMtuBVawUN0lZDuUoQtWzXYZrn3HiKQZn2ahfIrrs +0oMBLz48JurPlrHCIHfsVa/YbfZOTun/TPa5zcjX/Vi0+FgOHEFCmHzK/VqlqsdW +PYgQPvn4rQL25GACLzzJGrDvvJKS4fEo2p3pf6SGDFtBeyKb0AdyXoBWfXelS+ST +rMTVqEDTT+71MToOcmYPX2QJRIMEVix8tCmvMkUXni8AjurehQ== --=-=-=-- diff --git a/test/corpora/protected-headers/double-wrapped-with-phony-protected-header.eml b/test/corpora/protected-headers/double-wrapped-with-phony-protected-header.eml index b05cb545..115019b0 100644 --- a/test/corpora/protected-headers/double-wrapped-with-phony-protected-header.eml +++ b/test/corpora/protected-headers/double-wrapped-with-phony-protected-header.eml @@ -17,22 +17,25 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBBACkvfKZEkuRUQ2ujdel8U2ufplGxE2oNOK+CI5S1O8cS9vE -DIkVIXAtpZcCc31pYBTRl0TwCrLKFT/siYfshbxyWjMZjX/Jc38Yjg9pDFTIZ312 -LoM5uH22f1X8O8020HgH+CQk9T4s9bBuvxTvJ6GQvK/ssnoYsGr9TGcjjh3uMdLp -AXkkF76a2iimkq2163ee/8X0vgI+2fx6EjJJvlcSIlDcUvhYHIt8kjnlADSBMpho -gaMa90baGlE1RAK9nSBC+4ty0fIlfsgcecRtFEifFRj6foYPFIFzkgwhRkXovouG -FyXi8QrDVS8cz61I03PMVsFHo4FtJw9cAfvTh45QFGl+inW2pSvZyRnyu6uHDe61 -NqUTJOVN4B+dFPbKafUKuJ4YGXLsDoQoE8VF0lwznA7AOATmqPQpp+Anq40C/4Su -Zf1hGaBTuYjlChSTMxX+wV22+PQwJmK3tl1NQRFGlR1pQZWdNcu6/6RGooiVZSg+ -VsmtZjgpZa8aaEEnrsIEVPfvbIZ4OQhmgNi4CYNB306UOjIh3/8m+8JmlkxPiGXW -gnzNUTuwKytlZnIgT1o9a7PAkz+ZiHhMLmk5nPN+dlwsVN7Ff1FHqLIMbKaZbeKK -txvhw7/NdaCALnjamqtDJTc4kL50F44DC0im0U9hcoy8X/HBrYkTGfHgRttCp5V/ -XisGT6/rzyUzTi2usZpRtl3WhHrE0Uj0w2Bm/Qqe64vNd3F8xwuJ5qMZ3QLVxoX0 -MPTajY1pLgfMViqLaLV8fR8hLmattxaO92sbVuxHiaba8er3jzO2HfmRLqesio7u -8FXZQnBgeqBkoRlrHhvScuZLJVU1I4UHd9s3mcR+IY5VvjxdPMcnxTNqcRB/He4H -MrrH26P0uSFe6WJYQVXEDt4OO73ROyFZE0+rSw1z+VnjmHVIzUVvvFqwJZo6Y/0v -1+3ab4TGMPJSkfQYHY8/O1RF67BNlA== -=gizc +hF4DHXHP849rSK8SAQdAhuTX3AYQs007gCtcGsYQmtZg5DrD3ev6Dm+1Fq9vfjQw +bHP9+UKDAmWL5XW2+zlFQVb1CcWn8QUXwjJBZvHEpwyxRWJNYqNlhsnXsX+5woGs +0ukB6Jq0OJydXSKubEnI5Bxs7UC7rekUfxUH2ij6ZXmz5Xyc/h5rF2rJO1uaPVVx +LuVOF+ZX0muBzegGMiUtunHmY0Akis7m3xDl2ZRVViRdW+LPoxb4oAGoPq5LHfHp +5adDtrno13K6oO0NriVEPsSl6xFCLXOqTj600F8658o16yA8fiZj4kY8G8NunmMD +lN4UNw/EQuWIe343dKaQo+iH52atihw90OG4ahmjvkhBC/iIiXZ3J5m10VoJ7Sxy +G6O84UW06pCAqvPmkjnkgpflGdpM3Qv66Uu9ss6jAnRLP2LVv+IVWrwle/Yp6B8V +1adoFUMphkp534vTR4v0xJ/6WaI1YzIwlT2zd2zCMoqwT5weuX509pTbae0iGLRG +Q+l0ImN4AxsUdgWIW+GtMn3GVWCXk2BDLcr1xkl5lJVlOIbIvbpTwqYFMVAfnDGc +s6JZS3Bwg9bfmxSkM4GhZlDiCdZE7f0IUBSnRRHPcMkaox0g7ibF8aG0himZRqhD +3z6gtPmWO+c5i1iV6pQbntB7MY80vprz7gzmvmadbGFS+A4uCaH2afWr1eBb6nGx +W6deJxHaCQrrxujz20y5osSMhdAk47WPkZhphU/8ERncZ1F8aYJhUprAbKFxNjt6 +I4p8lrfAe7iM4UxqtSfVx2rPV9/Nqm8rFVH5l4f/0P93qsBNdF7rVQl9Ry2kVqkk +86ct9ZK2QrOtsnuSYFzhlaDo9pOHkhlMOmQXAgSb12VP87z65NTYRGbElypmQl8F +MXMxrljqnCbPaQaPq/37GEHTRJ7rrUvCQ2L6Ljp+F0m555eYHNiZRaxYA17uABOf +RIr5gfB5ICmnH2EGIYiEkSeX3k8+8xHffrzTpOAqv/5oJBDlRcu6qyVSAumNMPYX +fKqlNDhKh9xmK7K88vWwTgeOBm/sOXcfuAkr9nXqDqdLHb+duLiH+xjvBdl+ewTx +gpzIkoTSsd0fgmNqmmfwmmU4oIQu4QX3/zh5HiQS70xogRxxU3przSFrrtNkiSkI +Oe5fIqQ3tlUSzutTEoaSxeU= +=4vL7 -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/encrypted-message-with-forwarded-attachment.eml b/test/corpora/protected-headers/encrypted-message-with-forwarded-attachment.eml index eea66a94..d43331bd 100644 --- a/test/corpora/protected-headers/encrypted-message-with-forwarded-attachment.eml +++ b/test/corpora/protected-headers/encrypted-message-with-forwarded-attachment.eml @@ -17,17 +17,22 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBBAC9RgjF0vsqVqHMB8fauhazs2XoTMKkANrDS6ECANm0wcvO -tU1huRepG8ezoow/OgZ0Yd9y/zw6w+Frrx1PhVEr01lQsUdRq7INq2FRia015Q6Q -eOgSv9Q8wg4Vcy9XD1wI2Un71nDvbNwqx+hiR9m8vhiWfXH1MvxVQUWcUocUMtLA -uAEB+fx5ag3Qr42VAgyymvNrHJKtuhdj7CvdT/a5oVbZV7ilflFlYms7Wq0jSex+ -Jrb+/CnNLow4LehrOpf+IfgPumo0nBbseB17rAM9vtjNy+tHEqPsB0YFIpVR9FOp -zJITbWeFyGbOd5vMk9xbEFbw58JR8PPqsYJK41RleU2QoPEO69hoV0tXzjby5JQZ -2G/SrH+m9tggi3rWxHx9XuNKJP4iK9wZnO4k5DFaUXq6PGCYkgDi/K1RuUcJjcv7 -ob6Yp/cTLxHMmIS9VNNjUnnoaD71ndzYsZoaI6MTMX7/4eu5roeE3887NU5af/wS -ep6POG8WFJzKwc4dvAPd0NBVojdrftJkYKONsYL5KN8TY8SqUPxiXReGwg2evQqb -aGEU02zdRGYtmNSneGl20dJ39cHoW7B66ek9OQkgilSHQq4adPleq07r3HSv87jk -xNYoQ7xH2fahqbosW8N5uI9L2sdGVmTBNZgejiNyZoUn47tFEt4Uocg= -=/ZB1 +hF4DHXHP849rSK8SAQdACh9CzQl7cqCVO0bpHfyqL0Kr/V2TtUO68yhOYuMMWXEw +v+aH1aLYervLMNDSC8Fj6XY9myqaybbOfHnLZIQg0XkR3JIKhWR3LpZtfpFGSr/H +0ukBgdihDN26YqD+UETkBN0lg/fd4+cvVyOJSwuU5DQFdsOk1znvEercz1UVpKxz +gg1lniZzRYCyPSl9NcZVtNXggz6z9XqxT0cVE3WCH2IsVrJPfgnFu/P63pAhp43X +IxJe5mB6y5eaX5Dcvj48Z2E+3tVVn1gKGwLzPH40WIy98KM4dv59I0ZghM8h1dVr +LUUgdiz17fo5oWphzdgn57ERk9B/pApkzujrB4Sum9qy9y/Q2DWFKdOARNJSZ/Li +KZVXZLVEyp1WeAU262blQRtqWXq30wU9PazFo3wsUUVoLCu5SWsRpircSIWrh3gH +iBE/YdPYQNJ6kJoz17AdIq0a4jH3ae881P6eBW22zMxdqD4zfkT43iVQAtuki4JS +iBJ2vHoMU3z+rJ59ea+P595QHCGNyfkgl50e0E8rh9j+9/CExyD6F4GwAZe9VA6o +hYFQ3U089Cp/Cd5jVojUWIAOWqwb6nB7w3SabPZIbhhJSBdQEGe4Xh3TN3k5cpLr +TxDJz4zf220EQ2pt0BJwDu8fdskMXC6ytQettvR46+UtFOOG9eClnAFkgeuqwKmv +k1YyYMFWQ3Bv/IHOFrcu3negBQpP4ln5px6zggNoeNLzaZ9PN9Y2lHnoQTkw/93L +qyjLD+xEnzgdPYZvVsNA1iI6LMrCandDwKySQyfZadj/G3/nLFcoThvn03NNUyCK +Sh0Vy2gJhxD04HF8nkR/E5MLFCQ3x0qpGbOoanctgs/glxCpnzWRcSZERlilQ+3Y +0r5UkO5XJU3Z3CP4uJnjykgB8Qjq/WzERgOtOQ/bVaWBWkKzxS/Pj1sJ47kKMtBJ +3r5LXymlkA70RA0cJIv1F/At +=vLN3 -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/encrypted-signed-not-masked.eml b/test/corpora/protected-headers/encrypted-signed-not-masked.eml index 8dfd7c39..9f0e51b9 100644 --- a/test/corpora/protected-headers/encrypted-signed-not-masked.eml +++ b/test/corpora/protected-headers/encrypted-signed-not-masked.eml @@ -17,18 +17,17 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBBADAJ03D4w48sefkQsBWXUc1spTljROjVN+y5a2yCKtYMt3M -wWMeQyem5hwLpLYRCfeIzXCrlBfpZffuOkA5okGGVEWFvJ5a1kZNZnH5Wg0ccBp7 -KBGnJY0gS/BlrKK2Sjmk9Z3ww7GAgDGPbc7mc3Csj9G38UvneBdrQgm6kZR3GNLA -6AGLN3KJETruI3Js6++aG+7tSkJ8Vo4WCVUR7oQROwF601X0QF/XghCoJCrx8B/1 -cw6Yb2wQj2nv3gw1rqWVsPVpAKsMc1yHx/2Vsee/VPtt4f67fSAMuJF3EJ6JkcK7 -tM761v69GoJGgvsie45pb1N2l/GfVMuwWU0wZhEsF7eXxqPzoE/kIGX1XIqleLaw -On2kPSM5RgqV6gLOcw4WaFPi0oMbDhltNs72SV9cV6ZhhuwEQRq+u/K76NKLwte2 -R1JutAiuPZVF0WanmmiN6RbIpWOB5XxQfWagfr4vcf/03TaLP4hJMnqUdFMk20HP -eI8TMQxkfryZK2Z6VxEBVdXhK05VEdkolmc4j9U+76A96Gd5zbYPApirkebmZatS -X3rKKAiBqwWrFXi/7LNDoCwhRRmqDuHXruh3vZEcz+xiPfJh0G31GJQgIpE15Sv6 -trf20u3CXAFjHg9zPpSFV7uAOsqv7bg+xtG9PgN4aLCiVbXHsT0z6PAz+6K+SiKw -QW8ZOtLikj5HyLAz/TDcsIShFaM3QHk2qq9RY10kmxlQVrf9Oyh3Wmc= -=om0O +hF4DHXHP849rSK8SAQdAhRhcdC3r33MMW+D1PgLAoIhstxnEWOjI0s5wyH2gKTIw +ceJ7nVvmnqLNWbEaoLp2tVH1+cI4guvgqzV60BoxNU5G5YMGqzMS4VQ47N4BENmn +0sDEAWcBT1+PGFlXAeotmryF5ErMXesEVJJHru/KHZxsP3wwRp3qZjrwIWVgVlVo +SpJJ+hwIRqCTbdsw0ejKY0+BJwQ/z+GPmYkWE+ARxkzwMzfA47PW+iNudrQNEJuu +ALnsbyVX0/JA6E8jhvIA2OHb6G2yqFA499I6kWiSkT3m2SikuyKGybwWbaUDZVJW +Y+JN2L8snDUHJKfl5JMhAgu2+fx/joE9PxM6fdx8rJJbpmTrPqrYf6vYyuq5BGYV +snGJozwiN/cqR+PruMT7i5/Dhs/EDl3Z9D8iH7FEDtIzpSJNvGFIHetXC4JXgO7d +C48g+0uuUL3SSuZviz+OgLJbyDu1Pc3tCrtBUs0zYJGio5ghJljU5tUnCNhbBwd7 +oWgYxOmSzbZrs73E5Lpvnq+juZpXSGNoYzaZHacp2FTpo0LlZ2k6o8kx6eYfOlHs +JN+43CnzTnR6eZYkfjIaBbjYKi0rMH2DRJjMXyeYRLjEi9ET2fFX3WWn487snIjw +om8EPhsOxQ== +=wKPm -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/encrypted-signed.eml b/test/corpora/protected-headers/encrypted-signed.eml index c97d8c3c..f64b99bf 100644 --- a/test/corpora/protected-headers/encrypted-signed.eml +++ b/test/corpora/protected-headers/encrypted-signed.eml @@ -17,18 +17,17 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBBADAJ03D4w48sefkQsBWXUc1spTljROjVN+y5a2yCKtYMt3M -wWMeQyem5hwLpLYRCfeIzXCrlBfpZffuOkA5okGGVEWFvJ5a1kZNZnH5Wg0ccBp7 -KBGnJY0gS/BlrKK2Sjmk9Z3ww7GAgDGPbc7mc3Csj9G38UvneBdrQgm6kZR3GNLA -6AGLN3KJETruI3Js6++aG+7tSkJ8Vo4WCVUR7oQROwF601X0QF/XghCoJCrx8B/1 -cw6Yb2wQj2nv3gw1rqWVsPVpAKsMc1yHx/2Vsee/VPtt4f67fSAMuJF3EJ6JkcK7 -tM761v69GoJGgvsie45pb1N2l/GfVMuwWU0wZhEsF7eXxqPzoE/kIGX1XIqleLaw -On2kPSM5RgqV6gLOcw4WaFPi0oMbDhltNs72SV9cV6ZhhuwEQRq+u/K76NKLwte2 -R1JutAiuPZVF0WanmmiN6RbIpWOB5XxQfWagfr4vcf/03TaLP4hJMnqUdFMk20HP -eI8TMQxkfryZK2Z6VxEBVdXhK05VEdkolmc4j9U+76A96Gd5zbYPApirkebmZatS -X3rKKAiBqwWrFXi/7LNDoCwhRRmqDuHXruh3vZEcz+xiPfJh0G31GJQgIpE15Sv6 -trf20u3CXAFjHg9zPpSFV7uAOsqv7bg+xtG9PgN4aLCiVbXHsT0z6PAz+6K+SiKw -QW8ZOtLikj5HyLAz/TDcsIShFaM3QHk2qq9RY10kmxlQVrf9Oyh3Wmc= -=om0O +hF4DHXHP849rSK8SAQdAliiUK1XTMeUUmjJ6n4yEEBGVPmQuVTD4wdJZx4r2J3Mw +uSVIq8qBJ7NrZfvDa63807usyKpk0Io3tdH83hn3wIKKmUMTroaRZMP1BdJ3aVdh +0sDEAf18AhE5POknHzzolNbTXmQkPNspdNpu0ANdKkukvUZjfWvssZ1WN3jB08Fc +Wr2Byez+UyLggj1bm+HuzbcZGPfyMOpoajFG0+yK4Ccqpf3XapNJn7v1ic2QhR/B +pmWpJR0cXDigNLYQ/RoNEiSur2+0hzeVkDiYQtDYD02Cv4rnhyJdCalyriRaLOZr +4Tau4xfK9PdOydahZZHEFllDU+yfEb9xtBZ+DEGh5AyR1/sG8ORnJc+5m/z54zmJ +W3MmZ7M+4O1q5l5MbDwHq1TrQF5R9JSXyeGTW2XscsIPdvY5bExMwyMoW/XY4E8+ +GJ12UVpC/EWt2Nisfx2LhIoxtwaZm2r0Xkt6hY5DSMkpHLZtGPOOe1QxtXF1qXH2 +owTZynRxlmaWslOXdhtFRHyfGiFK4pNoV6fVB0e2khKKfXynN9Zx99GJ+DoJsOFT +AYOadP1WlbIbSaimv/xSpaijE2XReKTLfN6aK1vSe90r96nCWr31fcd6x9ax0nVx +jDReMRJrHA== +=8mRD -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/misplaced-protected-header.eml b/test/corpora/protected-headers/misplaced-protected-header.eml index f1a72f0d..c93ce0f7 100644 --- a/test/corpora/protected-headers/misplaced-protected-header.eml +++ b/test/corpora/protected-headers/misplaced-protected-header.eml @@ -17,19 +17,27 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBBACwbgx3N72gYKIU63tNE6kf6UA5ed39VFXh3zdM6eDdA0bG -DWt5yROckkCeCvMoFaRswK8MiX8aGG0GdH6VKhyn7HjT/Dm84QLwoB0ccZs3MnwU -aJ9yTC9HbX3yfTVZYOu0w47NZho/LXX2Yd1pi8OUgrPg44fjgvx2kNRQ9EsNBdLA -/AGMhwwcTPHjyWQ4XYZoL6WeVJfq2C0m3hQ3bxrKuAzW53HrSa4tPCXzX3G8KEz5 -sSk3ZOmajSvLde0LG8bxwexgAHC/Wd07e2HgHtZ/H+Cw9oYLgwcgVyXg7sGVrMrs -IlwW0Njf93DJmJZuTD8P9XJc3h1VzKA+YhbtnofFZw4JexpHcC+R8Lcso16Mkp91 -7Ig0E8WTZ+K+judGS010b5ND2ETyc+TYY4/XJ2R90pbNrRLNTFG+P2HUob6PBCwE -rXot6TeBSgm+k4bvl9aMKyrBSplKktQey4WsdblbJnJUxSl/rMpW6xwglkyIgrCU -vbhffqgB8y1JLmK6Ow/A6Pzi3T6Zn95zu2GN8+yAOzDhGwlAfIV85TYnX6ybOkX/ -Amoh7qNS17pzc6ch/mif/RsSPYo+y2UQuVFhG+kOy9oGAQOOHeiCWZPa09o3R2Jn -myMg1FPgoDgsjE6QpD0mx9ORdPGC2e8jwrifS/W9eHJ2QG+mNkcKlAr5b8WiUTkq -hEZ+BaaVhbXN8EuHHTJT6YojusCIsXI0BMF1su1KupQw+dwQnys8wuy45Fr3H58x -zqHoU9KzdQGLbeJTgA== -=+EWE +hF4DHXHP849rSK8SAQdAlqPAqdaaB751ZhN8drQheW77zbyPrbofTZgTa/32UFow +ShGDg7SQxQD+LssHmGT0g4pUPo8JGor8BIvnvcZa4qRiZJI1i/VWUVoLc+TBT442 +0ukBxtUeKC0V35/zRgS1ZXrbkiBh8TGy0lee/eM9yvfT5FmFhotzoQkexDSEPxjb +g8Qr/IcH0v2XOEmHux3hQpngY/LRYoCv1c8Niu2Zfan1p7zefOZjAxRraeihHi6I +KyyA8WIdiIac97vO1CRyXXATJabjeijH/BaUDlPvQ39c9Mdie+HeaVYcaC3e9oW3 +sjn8fN+KjkjLXwPWhgMDpvP9f3ZD8oCHkN7Utq4OTwz8gb0IbUIkElF4G6O3AckV +UXwrASodKBL58IWYz6VWhyjYxoV2R2hLNtnQWWvKgYzru2Mdb/ONZBM/JMXMjFUt +oxDJs2pb2sun2q6n1sWKTrS9MXbxeFEcp2v3hMXrmP8nLeNiPJ+HlerFCSZasDOm +c/3X97zIneJUosm3ltqLunSSE4vp+FAGIRaibMjyE2u2niDReINxX58E7o9cfJL0 +9Af6olZA1gQ/8t5qMei5qTN3wi4a1ieqTN4yD4v7r8yZE5PmlQfyY0joXVepm8Tx +9/rDLM/gU/zogoo17enlU3ipvLZKsVDkLIlv2SdT1AF0ROnjLBGJ2iWzKst9Rg7/ +mXzEqv9CSVM2lXd3qUqra7reeaVD3vd00zmLq3yM/2sHeuLAc40R0MkIOSa89eRP +iXu4k9+/m4WU4bNPugaH8Szm7OuLQNoB7mAB6t9GJGq7Y8DKxMn5FSUwLtKkGkUQ +YdGCw2/salvqoQIDJVMteKpM4pWi9sFoVbxAU+djinJejSjSmY+iKHcMYeQwOpja +33+W2RM40JOUZ7yc+LzgWy/ahyD27cdS1TPaghlQxPeSjcEfekrjgGW+2wIMmCxy +xyMPWeN+Mf7e/fNh6YDbld9BKk6zY4fNVWlC+gR/aMVapomNgW1/FS7F7vk5qY0Q +pAf1GS1YR4pKpo3sSEaLHw5rhpI0pT5ZvB09+GYRhsBQzP3LvnBCsxcVvi7yERY2 +QVlHejpTWwuRcTQCXP9UXa7z9UhEOieHwy6xNRTi4HVWHbXMxVunvlUnBCnQdFIr +s4bcOjY94Q29FeRjaweYIZBUSabXDLJhLZNH1u6Z9Z8dBZ6Th1+ou77m0rEN6B3d +mf83pKPrOR8ASw88rRGq8O9yVpFR4mD7HiE7YNofsPBtk0Cz178WLCi/D8b9A80h +R/jVyxLmrCPHQUFfu46JgytBta+VBYwnYFHDynnYczuB0gydvuPJ4cJvfn3vPA== +=DmA/ -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/nested-rfc822-message.eml b/test/corpora/protected-headers/nested-rfc822-message.eml index 059783ce..4d5073bb 100644 --- a/test/corpora/protected-headers/nested-rfc822-message.eml +++ b/test/corpora/protected-headers/nested-rfc822-message.eml @@ -17,16 +17,17 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBBADCWqobSSS78XdrxhBh5W01OZbUMdnrwrYJsiG9fQoVfFHN -eALvOfviTcSBD97/jO2IRL2W8hyF7k1BVAYMwSuxe4qLbLdsxK1i4KBRIFRkm990 -ipBddgFXV16WNO2cTK7boEJ7Xfjp/zjoS2z2YUXsdGx3OSJciyHBVJki2UfkL9LA -egHa7dsw6BxoNbAkrD+ijVbsFrKHeeJIlWkNbSYOk/YLmqLAEy1CYvSvC8ZSBtQT -fVYc37fc3RB0vQC+Vu5k5d/I5Z1/Yz+McBJDMNvcn4yoFiXemY8YVFvj7iC0sbuq -lwitvgMYaljhb8RUQAa3Dy08Jju09DIBcCgRsx32U+3aqZ0MhU6CRgt8kc9oK1g4 -yBVppqpX6hCXjtt9LUArY3DIchRb+IWTXsb+eDR700GXDyNMk1G5WUl0eLuw75uz -EqU5Tjh36fP0ceMESjaxuxyhhw1jjE3ON7vqFQRVcs7UtazbxznWQH3Z73mDmY3G -q9JGMOOqVnnFdnEq8vDFF7m+Cp3N1ieyXUXjn3aLtvSRMmVV20Q5QXSFg8nP6juT -Yn1xZjqOodSeig1ITZZF58Whv+LHGtzDHwV8 -=cNYF +hF4DHXHP849rSK8SAQdAc+vNxgvGe1T23qYt8zZ2dtmU4+DzCiMlKBFrI64sICMw +foE2ym+RPLXzL2SbgDaUqgYbkiKmo6HrFSURtlDae0lFHrmLYZHToVWXF14DGsyu +0sDgAcsIt7i8j6XWpAI9slRqjDAEPBp+4EFJKL+BdIuEYa6z1ULrv5BUimUi6D1o +UE+k49xE6iFOpgSEghF1+ZneE3bj8rqJJwA+sAjth/Kp8vNccA42mCyn3Avxvk9q +aMw7GWvTRJ5oc+RGo6BZukQtApQTbLzOeI92w68XNmQaSq4+LKUA4+CTZpasR/WA +CR2/MWfwW3vmZilPbW2249Nj8CAawaDxTsIY9i5bHE0HjbfJhBBNffDPoNCh/+Pb +6wIZ9/6xLHAzxFtY+qvDhVO/nWOLrdd9ACZudoD/x4qITc9IFo4F8bWF5iKPs67q +wtw1qFTT8ODc2WWhOizDByOkk/D+Z3mrlsOC/x7ioho0IIeWldkfaET2ucc9FI9S +SPX+huu6vnPAGO21T4EMqevwDLNGWMQBolHSlU3SRnNyU1bqUNDF+/9RiOk/NFj9 +FTwMj8FAI6/q0kZLQUF34h4BPF8/v1TmVZKniaVXqQIE78MvFWXV4FXYvP4w+IdK +q634Ah+cC9NVEB+U6H4aSB9IojUurd+RvD+4x7rd+EtLFdc= +=HmgM -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/no-protected-header-attribute.eml b/test/corpora/protected-headers/no-protected-header-attribute.eml index 880f60e3..cf786c02 100644 --- a/test/corpora/protected-headers/no-protected-header-attribute.eml +++ b/test/corpora/protected-headers/no-protected-header-attribute.eml @@ -17,13 +17,13 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBA/9GY8NN4NDwpNttr/hTXpS701Z8TDr3hC89obZNnNpYxSct -p+YkS+FsPMLimIDfU1meG8R+YgtQOJIhmKPHW8CLQ1heBsX0Dcv2oLxXodqNGD7M -/szVRR6duVnALPgmV66vkcBHKbsiuv8EO86C7G1hAnXfs0H47WoeUz9dQ6RaHdKw -AVbxw7KWVbiP+S4SO1rvNsAL1xiRPA0FFmDRMyoFRC/618dGS6HitkD0UR708oVt -PooD4Rk22c8b549wvZ88flGk+WBCLhyXAuWYPHwag1DLzLjWH5r+XmK2O7JoQZeq -k7JM/M8QM+xetFaPmsWs52IynhXyWpXBBanm9NEsNEiIB59480D7tJ0oivo8T24d -izSAMGATP26ReatoXltCl9x8uUfUSAjWt8iJ1+n/3ds= -=hGDA +hF4DHXHP849rSK8SAQdAQd0btUvfUMmCgmDv3mG8d2tfmr3MYsOdE4TeSqk6sFsw +8Wsp5J4s2t9ua6ScvqCVtlUVUL4Z6BsACArwy0/XwGQ8JQ6BUuYH7JSWb6O6tzed +0r8BYDEY7/8+QCgXneo6k3wvPHrzTsvg9fxmCSTbvA+8JCrEzbvM5l/wQmSDf18x +difPsrCT75x3eYgOy7NNYIAg97teybVWZ4raPQ/SdJ9J0TmPTjQF07HPndrsYGQC +rEKvu6oq2/HL4NWOWMNNixs0u6ALRpPuUUXKgwSaZUG8+juOZ3yFe56bd1IvwkWK +ZkKwa9gBn7WA+eNpcJauux/ta6LuXiNvNHUGUnd9puVepi9GO3XAxYcOzqF6ly1V +iw== +=c3OU -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/phony-protected-header-bad-encryption.eml b/test/corpora/protected-headers/phony-protected-header-bad-encryption.eml index 15dc08ab..d0e11a10 100644 --- a/test/corpora/protected-headers/phony-protected-header-bad-encryption.eml +++ b/test/corpora/protected-headers/phony-protected-header-bad-encryption.eml @@ -18,13 +18,10 @@ Subject: this should not show up as a protected header -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBA/9ZaOuxGtLVWiA7KQfB+4td1AILd1uy039UDb+9YwlhmJTq -mNqVJu+ZkFniZPMliM0z1QRBkBeL2Q7MrHAdYxYBKrDHKVja4O7jwqeKjy5BzQCW -fnyT+sb2Mh+dz5P2voF3XJHgqzhFY1rtVEatXSZADwwIVU6oZqGZ8GOELNGSd9KX -ASNElH7WGZB/TQ5X+MktzOLExx5QWaRK9skogI2RRoOquS7KpMcjzb2FWaJDjr1s -hd8FCQVjWuUDrolMGH8cgeq9iUBlHMzfPY6/jeGHNrjk12wwhBNcq6O95uzXtIRS -BM2xnwCYec6wYJ46fHukTgv+286nSQcV0XT6a+qM5GMgV5DMHW2vSyl6kTszJ3EP -xvQBfPCItA== -=Gkxz +BADBADBAD49rSK8SAQdAeybb8KrIaEFV5+Ks5loaz651PudVdzS8ombK8EW7Mnsw +kEppTiE4jo6ZocHvhjSzPdEK4MPh0qmKvu1RrHa23dc1n7Cutg1FjOb6ZRloTisz +0jEB3YxBhFDBZyWSGeAeZx93JaNcV6CBjOfZ6GJhzkqmSs73VdwFUWQxcoV4q5sL +GYCW +=BADC -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/protected-header.eml b/test/corpora/protected-headers/protected-header.eml index dec822c2..a10612f9 100644 --- a/test/corpora/protected-headers/protected-header.eml +++ b/test/corpora/protected-headers/protected-header.eml @@ -18,13 +18,12 @@ Subject: this should not show up as a protected header -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBA/9ZaOuxGtLVWiA7KQfB+4td1AILd1uy039UDb+9YwlhmJTq -mNqVJu+ZkFniZPMliM0z1QRBkBeL2Q7MrHAdYxYBKrDHKVja4O7jwqeKjy5BzQCW -fnyT+sb2Mh+dz5P2voF3XJHgqzhFY1rtVEatXSZADwwIVU6oZqGZ8GOELNGSd9KX -ASNElH7WGZB/TQ5X+MktzOLExx5QWaRK9skogI2RRoOquS7KpMcjzb2FWaJDjr1s -RGboX7NG3xCvNUV2ByFTvLOeo7eO1GfUsabTUbMMvh3AE1UvHgCu8VJiRrMdmPln -BM2xnwCYec6wYJ46fHukTgv+286nSQcV0XT6a+qM5GMgV5DMHW2vSyl6kTszJ3EP -xvQBfPCItA== -=Gkxz +hF4DHXHP849rSK8SAQdA3xy46BwN9R4tHbyAqwTMeOSfrrNzqsvCT9hqcVfRIzQw +ThEINU6n9x5QgU2L/mtSaAMkw7ikOmzrJkvjEEE913dvUsw80+Q3QDYODETYzXBN +0qIBrEpD0pYlXiQECDAYqox9JBkOPi3K6c4TLdACG2q7oOtHzbApzHC636oOFAXB +uhARzvr/TN5/Fr4KDM2j4LqdsxSwE2mOJn4lM8EfPAm0jxbDmHKOpjpk/QnKR7ry +BbctXwYIwlkp6voRsAJ/zG3XcLpbO/w+a5U14P6qCp2RM+bNWKtZTT4Gl0yCBk3A +CYGvWb797ZA+5BhraXCWAQcscec= +=WKBy -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/protected-with-legacy-display.eml b/test/corpora/protected-headers/protected-with-legacy-display.eml index 8c5dd251..9a3c7644 100644 --- a/test/corpora/protected-headers/protected-with-legacy-display.eml +++ b/test/corpora/protected-headers/protected-with-legacy-display.eml @@ -17,24 +17,26 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBBACkgwtKOAP+UlKYDzYkZY+gDuMNKnHjWIvv2Cdnovy40QzL -5sbuib40y7orO+MqYMCWpoFtgBVsGiOUE3bZAg8n3Ji38/zVGwQveu6sh7SAy0Q9 -zFEvLhtajw17nPe+QH2UmIyfVikA57Mot13THq4i6C4ozVCyhyIltx+sNJkmw9Lp -AdQd+cgCMRSMbi++eRwIi4zgxKrfAoGOmdMiVzBrh3yZqnbI0rCxJIKu7gEWuQLT -7BuvN2bJUkPGLAUhUanFararVoD7WWOl67IlWFkyncES0PRskUf9coV68WZnYjsR -Y3LdLnha1sdMwUNeBKQ44XBd2e7mXbDSp1cSjTDf9euwB4m7uQFTLwoQ8Of+LmQD -KMHzjmucbkNAIpfAjcDusTA/oaaqUiEgGIgYYMDqG1CaaxdT55S7tMjW5yJryQmo -pg65jrUMgEn5XHZ+KI2OsCmwGdoBYNau8p1a2hsiKhHJmLUeEAu34gFI3hylIOC0 -0KC40d0zTSb0s7SZuTrD6vYgiXG9aFktHvAWFH0ATCts7qyiRN7k5jt7yWfRntE2 -UCexTGE3TH7aju+IqDPC1XsaKF4T3CVhdr8WmKCa+0VOaw7xHRGYnzq9y91GcaCx -8AcoZ3kYs+f2LIn+T667A0KKP4Z6OmLjCx3b1RvRUQYR9taruEMAQbIuAajiyTe9 -KfUrsUULZfInE50x+OneYvDhzoSgSJoHIK+18X/wo6YcyleJ9fZxCQ/vaXTDkAeF -ve7TFcbIqmJ4MHygXILHUuDwp7P4t/tIL7SZwja70P3digjsgoNZY29VTnU8uyIb -d6eOjgpeNVhRjDWxbUvhFD7i4rHCi/bbXFlW0cCXoiaVQBtYmiNysRoRZOv0h3TW -q/+/UmqkaQFnF3zp5sr87y+ValItgPWmb9Ds0lyAoSvQx35zVh8DFfH04m7hmsb7 -gcvemlPTAnQWkIMC3c/bZWgt8tNcG7tQeUMWd9n4281y/hApbm90x2NLzEqvVcRq -K0iIgVxbCHSKqGh4TtbIwpNhzSP+KHYkZ8h6+QUDRwGEV9QqZKg= -=2O0V +hF4DHXHP849rSK8SAQdAGiLZx0/sI1uoQ27OPpwunlzzlY7ba4E2YNU+AErRA1gw +YYKlhgfzSgO0LpbxHJfithVAkrGYvj5HNWGxriXEsOyfy+Ax0FtAJuzYr6DGKGx1 +0ukBxLoRfWuoo2C9gXwojieSI9lTh7V+FaVUoVmTXhd/WFjWLtHbkIVmvXI1WVyi +wM5NfDi3Ho995P+DMZxTKkdqtbdeYgUK7oCw3FHsYsNSTf3XFazjEvHg/TvOWd5u +m9SbZAKfOBrEGGCzNHfgyluaBKdzVX04GPfZ2GblHlnU9AnGSWys1i8kxmuQYAKp +UrXDlMcVB4V5bcGd1KMvGyhnKzICXWhXiJhEiNOc+uhl97jxXRujflfT6S1+8thv +o86332XixGu8o5svVAEWobN8LUXpHZZlnK9a0Zftf4v8ATHEzQLAa5vdx+BYN8ua +e4dmCtxA4XCfRD58FJ6EwjDqhv45KYnJP2W5eZujQ7Pl1m3HJXGwFQmtnOSB/9dw +M+y1Aif07VBYE3LmUUqmS0HLZoqmOEoh6rKldzyxFmtfZyn73n/zcUoQblEWTE7z +lxIqpCmo8jHPcs1tm9QD3sUmqQ/YXwmqZbD3pOn0PIXZKVY8/DaeggMWKQ/UhCWa +7Z8g2GVq17AjHsS9n3ShDhf6B/8qI+jjaZQqH0W6KLmDQixjf1BoPnTrXNjcloJk +uf0YAuol05fXNAiyPbFNO9zoFPxm8ZVEZG9nbcnNOz7ac/Aea6hqhxHnzNFPU09K +J92FZ08XXDlrt0jw11Z/i606U/7kX6Zy4vCtZjGB4h04msBiLQwI0POIcY28SJ0U +W1AqcReye6lQTz47AkOKAfVQl9hQP++G7nZXlxUQ+z0VRqBEqd/QJdHgoe6X4ctd +r8093odiz6/DXJNwDTHPkaV5IseghzSLYyjmbLR5DUjnfuxKw5zpG+mK3X2PDx1B +LtUNfBGmnLN3jBa8Q/i2WYxYpAuMZzJcCcocxW0H+yBf8+rZNpIvi/RsTklKkaap +EOgP9sZXlgJePUbBmdd4Wwx7WTsjna6ckNp/9WE8CuDy6x9Zkc97Rkd+Oxc/KKtF +1mQ/VdRZj3trlABnHmF0H/H4Qlrt//P/PCl3qRZpE5v34OHDlTT6UjLh5ahWZ3hf +pj3cSKy9uajnWPFf9tnI0/9cWYbllaCMhIMbDZXRM3F4H03bi2k= +=VKCz -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/signed-protected-header.eml b/test/corpora/protected-headers/signed-protected-header.eml index c3a21b85..f5efaff0 100644 --- a/test/corpora/protected-headers/signed-protected-header.eml +++ b/test/corpora/protected-headers/signed-protected-header.eml @@ -6,7 +6,7 @@ Message-ID: MIME-Version: 1.0 Content-Type: multipart/signed; boundary="=-=-="; protocol="application/pgp-signature"; - micalg=pgp-sha512 + micalg=pgp-sha256 --=-=-= Content-Type: text/plain; protected-headers="v1" @@ -20,10 +20,9 @@ Content-Type: application/pgp-signature -----BEGIN PGP SIGNATURE----- -iLMEAQEKAB0WIQRa6rEfXjPc6HXdt1ttkmEtlORjgQUCWusAfwAKCRBtkmEtlORj -geIJA/0WcyxlwDfXRMbiGE/crLBYhLpXK6ZMzjEn6HQDntMIk3Kr61rAwL8edKGx -gbxr1+XlMYRt+PJDhi8iI0odDI1YjiBjjc0bXUoDn60UcjL2MPGshI3426CA7cqB -cMaoRHajfdxYjSzzfh8duVgi0vmUnsyoePBhANRbDIVmCQS11g== -=c4cq +iHUEARYIAB0WIQSaOv5sYAZaFI/UtYp+ar6SRkXMYAUCYxiQlwAKCRB+ar6SRkXM +YIm6AP0UlyfUbhd7bG4Azs0rby3qPUXOC1DtbSpQegSuR7nGgAEAub3WeYgEVVOS +fsnuNE9Q/LnPTS5m85eMa1s1bS8fcAE= +=O+fm -----END PGP SIGNATURE----- --=-=-=-- diff --git a/test/corpora/protected-headers/simple-signed-mail.eml b/test/corpora/protected-headers/simple-signed-mail.eml index ebf4b786..cd1e9fc3 100644 --- a/test/corpora/protected-headers/simple-signed-mail.eml +++ b/test/corpora/protected-headers/simple-signed-mail.eml @@ -6,7 +6,7 @@ Message-ID: MIME-Version: 1.0 Content-Type: multipart/signed; boundary="=-=-="; protocol="application/pgp-signature"; - micalg=pgp-sha512 + micalg=pgp-sha256 --=-=-= Content-Type: text/plain @@ -19,10 +19,9 @@ Content-Type: application/pgp-signature -----BEGIN PGP SIGNATURE----- -iLMEAQEKAB0WIQRa6rEfXjPc6HXdt1ttkmEtlORjgQUCWu718wAKCRBtkmEtlORj -gUXaA/4/m6CPRgC9JODRKRWo3Szi5D3zg7uf29DIJu9m2vVRw5o0ZeHcxLb26UPe -qdjPq6GBclkXdeTH9Nv2TW5cToJmMA9UvESeRRzbe6ytvswNEYdSbiYAsv/k9t6K -KQO2ZSbsbVlkh8xVYC3ORiUS775YrPxVT6QlPkMKAXw3l3Zwcg== -=jnDO +iHUEARYIAB0WIQSaOv5sYAZaFI/UtYp+ar6SRkXMYAUCYxiQYgAKCRB+ar6SRkXM +YJkmAP9TEGDYF4GZcHaxWDZYf6EKHmNqu1RPYuwEN8QdVbUIxAEA7IiFYPQtKXgr +wyEYNcJ8aD1CYCGhR8pTA9oT/Vp16Qk= +=a+TS -----END PGP SIGNATURE----- --=-=-=-- diff --git a/test/corpora/protected-headers/subjectless-protected-header.eml b/test/corpora/protected-headers/subjectless-protected-header.eml index 7163b9ae..8c7fb3eb 100644 --- a/test/corpora/protected-headers/subjectless-protected-header.eml +++ b/test/corpora/protected-headers/subjectless-protected-header.eml @@ -17,13 +17,12 @@ Subject: this should not show up as a protected header -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBA/9ZaOuxGtLVWiA7KQfB+4td1AILd1uy039UDb+9YwlhmJTq -mNqVJu+ZkFniZPMliM0z1QRBkBeL2Q7MrHAdYxYBKrDHKVja4O7jwqeKjy5BzQCW -fnyT+sb2Mh+dz5P2voF3XJHgqzhFY1rtVEatXSZADwwIVU6oZqGZ8GOELNGSd9KX -ASNElH7WGZB/TQ5X+MktzOLExx5QWaRK9skogI2RRoOquS7KpMcjzb2FWaJDjr1s -RGboX7NG3xCvNUV2ByFTvLOeo7eO1GfUsabTUbMMvh3AE1UvHgCu8VJiRrMdmPln -BM2xnwCYec6wYJ46fHukTgv+286nSQcV0XT6a+qM5GMgV5DMHW2vSyl6kTszJ3EP -xvQBfPCItA== -=Gkxz +hF4DHXHP849rSK8SAQdAxAZy4nBUDdm2u4sgr1inLki0LMCVcsVlax6Pd0AiZAow +iYz940UtZwQNRRb640w1bB2pAvg5Nn8hJK5ye3qtyUWNW1VEvAa+GXndI/Qt0+7x +0qIBOXRBCrkOxB10iCvSDVoOMZPj8GgvQwpsnslATJbsp9jV74fU7eFCKE5VWKUw +FTos+VX1YFZyf2RsznHXdi0CrL2rkUNoLby4SEUa/urd6GKb3xuOjJlIYN4Fh9xz +e33+Dl3NHohNooytxoJuTNiXbNWe7kidfOwMGzvdYsegk/WMqMLg8DmZPvl0BcYI +o+4ZU3kuhbk5Pup5nOV6OLrs7A0= +=BF8X -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/corpora/protected-headers/wrapped-protected-header.eml b/test/corpora/protected-headers/wrapped-protected-header.eml index 9a3c1384..87f3f8da 100644 --- a/test/corpora/protected-headers/wrapped-protected-header.eml +++ b/test/corpora/protected-headers/wrapped-protected-header.eml @@ -20,14 +20,13 @@ Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- -hIwDxE023q1UqxYBA/9ZaOuxGtLVWiA7KQfB+4td1AILd1uy039UDb+9YwlhmJTq -mNqVJu+ZkFniZPMliM0z1QRBkBeL2Q7MrHAdYxYBKrDHKVja4O7jwqeKjy5BzQCW -fnyT+sb2Mh+dz5P2voF3XJHgqzhFY1rtVEatXSZADwwIVU6oZqGZ8GOELNGSd9KX -ASNElH7WGZB/TQ5X+MktzOLExx5QWaRK9skogI2RRoOquS7KpMcjzb2FWaJDjr1s -RGboX7NG3xCvNUV2ByFTvLOeo7eO1GfUsabTUbMMvh3AE1UvHgCu8VJiRrMdmPln -BM2xnwCYec6wYJ46fHukTgv+286nSQcV0XT6a+qM5GMgV5DMHW2vSyl6kTszJ3EP -xvQBfPCItA== -=Gkxz +hF4DHXHP849rSK8SAQdAbnAG8Oxige+8PXg1B/Ex8Nc/IcBW7R4Cmnq3rArI3g0w +rb1P2LngDMpqMxRGAkucBU/omYHuyDyLIoQZc84XYQy9N+M/u4HK187tyXaKx970 +0qIBnZhdiVm9RFn8CvQLG1hhw8E6UFm/YlURkMoaP66HIU9WLFAlmHrZPOnXJBr5 +2qOWnqSttuD/1Bjt1R2dguoltYqv1iBkwDlE2mWubSTkDp3Pf3QeJGz3Q727+bHV +MI3k/5sNfJyyx9lIB3nyjwa/+Ap5orrPBwe+Y8tRdLO9xtvIFO+U9l9L6yPTYPyz +4P+LVzDS+6tnWxPiLeEz/sRGmtA= +=pPju -----END PGP MESSAGE----- --=-=-=-- diff --git a/test/emacs-exclude.expected-output/notmuch-search-tag-inbox-with-excluded b/test/emacs-exclude.expected-output/notmuch-search-tag-inbox-with-excluded new file mode 100644 index 00000000..ce1d7118 --- /dev/null +++ b/test/emacs-exclude.expected-output/notmuch-search-tag-inbox-with-excluded @@ -0,0 +1,25 @@ + 2009-11-17 [5/5] Mikhail Gusarov, Carl Worth, Keith Packard [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 [7/7] Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth [notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 [2/2] Alex Botero-Lowry, Carl Worth [notmuch] preliminary FreeBSD support (attachment inbox unread) + 2009-11-17 [1/1] Mikhail Gusarov [notmuch] [PATCH] Handle rename of message file (inbox unread) + 2009-11-17 [2/2] Keith Packard, Carl Worth [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-17 [2/2] Jan Janak, Carl Worth [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + 2009-11-17 [3/3] Jan Janak, Carl Worth [notmuch] What a great idea! (inbox unread) + 2009-11-17 [3/3] Israel Herraiz, Keith Packard, Carl Worth [notmuch] New to the list (inbox unread) + 2009-11-17 [3/3] Adrian Perez de Castro, Keith Packard, Carl Worth [notmuch] Introducing myself (inbox signed unread) + 2009-11-17 [3/3] Aron Griffis, Keith Packard, Carl Worth [notmuch] archive (inbox unread) + 2009-11-17 [2/2] Ingmar Vanhassel, Carl Worth [notmuch] [PATCH] Typsos (inbox unread) + 2009-11-18 [2/2] Alex Botero-Lowry, Carl Worth [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-18 [2/2] Lars Kellogg-Stedman [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + 2009-11-18 [1/1] Stewart Smith [notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (deleted inbox unread) + 2009-11-18 [1/1] Stewart Smith [notmuch] [PATCH 2/2] Read mail directory in inode number order (deleted inbox unread) + 2009-11-18 [1/1] Stewart Smith [notmuch] [PATCH] count_files: sort directory in inode order before statting (deleted inbox unread) + 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 [1/1] Jan Janak [notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 [1/1] Rolland Santimano [notmuch] Link to mailing list archives ? (inbox unread) + 2009-11-18 [1/1] Alexander Botero-Lowry [notmuch] request for pull (inbox unread) + 2009-11-18 [2/2] Keith Packard, Alexander Botero-Lowry [notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 [1/1] Chris Wilson [notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (deleted inbox unread) + 2010-12-16 [1/1] Olivier Berger Essai accentué (inbox unread) + 2010-12-29 [1/1] François Boulogne [aur-general] Guidelines: cp, mkdir vs install (inbox unread) +End of search results. diff --git a/test/emacs-exclude.expected-output/notmuch-search-tag-inbox-without-excluded b/test/emacs-exclude.expected-output/notmuch-search-tag-inbox-without-excluded new file mode 100644 index 00000000..8a874320 --- /dev/null +++ b/test/emacs-exclude.expected-output/notmuch-search-tag-inbox-without-excluded @@ -0,0 +1,21 @@ + 2009-11-17 [5/5] Mikhail Gusarov, Carl Worth, Keith Packard [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 [7/7] Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth [notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 [2/2] Alex Botero-Lowry, Carl Worth [notmuch] preliminary FreeBSD support (attachment inbox unread) + 2009-11-17 [1/1] Mikhail Gusarov [notmuch] [PATCH] Handle rename of message file (inbox unread) + 2009-11-17 [2/2] Keith Packard, Carl Worth [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-17 [2/2] Jan Janak, Carl Worth [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + 2009-11-17 [3/3] Jan Janak, Carl Worth [notmuch] What a great idea! (inbox unread) + 2009-11-17 [3/3] Israel Herraiz, Keith Packard, Carl Worth [notmuch] New to the list (inbox unread) + 2009-11-17 [3/3] Adrian Perez de Castro, Keith Packard, Carl Worth [notmuch] Introducing myself (inbox signed unread) + 2009-11-17 [3/3] Aron Griffis, Keith Packard, Carl Worth [notmuch] archive (inbox unread) + 2009-11-17 [2/2] Ingmar Vanhassel, Carl Worth [notmuch] [PATCH] Typsos (inbox unread) + 2009-11-18 [2/2] Alex Botero-Lowry, Carl Worth [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-18 [2/2] Lars Kellogg-Stedman [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 [1/1] Jan Janak [notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 [1/1] Rolland Santimano [notmuch] Link to mailing list archives ? (inbox unread) + 2009-11-18 [1/1] Alexander Botero-Lowry [notmuch] request for pull (inbox unread) + 2009-11-18 [2/2] Keith Packard, Alexander Botero-Lowry [notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2010-12-16 [1/1] Olivier Berger Essai accentué (inbox unread) + 2010-12-29 [1/1] François Boulogne [aur-general] Guidelines: cp, mkdir vs install (inbox unread) +End of search results. diff --git a/test/emacs-exclude.expected-output/notmuch-tree-tag-inbox-with-excluded b/test/emacs-exclude.expected-output/notmuch-tree-tag-inbox-with-excluded new file mode 100644 index 00000000..0f640b3f --- /dev/null +++ b/test/emacs-exclude.expected-output/notmuch-tree-tag-inbox-with-excluded @@ -0,0 +1,53 @@ + 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) + 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Keith Packard ╰┬► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 Mikhail Gusarov ├┬► ... (inbox signed unread) + 2009-11-17 Lars Kellogg-Stedman │╰┬► ... (inbox signed unread) + 2009-11-17 Mikhail Gusarov │ ├─► ... (inbox unread) + 2009-11-17 Keith Packard │ ╰┬► ... (inbox unread) + 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... (inbox signed unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Alex Botero-Lowry ┬►[notmuch] preliminary FreeBSD support (attachment inbox unread) + 2009-11-17 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox unread) + 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Jan Janak ┬►[notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Jan Janak ┬►[notmuch] What a great idea! (inbox unread) + 2009-11-17 Jan Janak ├─► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Israel Herraiz ┬►[notmuch] New to the list (inbox unread) + 2009-11-18 Keith Packard ├─► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox signed unread) + 2009-11-18 Keith Packard ├─► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox unread) + 2009-11-18 Keith Packard ╰┬► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + 2009-11-18 Lars Kellogg-Stedman ╰─► ... (attachment inbox signed unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (deleted inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (deleted inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (deleted inbox unread) + 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Alexander Botero-Low ╰┬► ... (inbox unread) + 2009-11-18 Jjgod Jiang ╰┬► ... (inbox unread) + 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) + 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox unread) + 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox unread) + 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) + 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (deleted inbox unread) + 2010-12-16 Olivier Berger ─►Essai accentué (inbox unread) + 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox unread) +End of search results. diff --git a/test/emacs-exclude.expected-output/notmuch-tree-tag-inbox-without-excluded b/test/emacs-exclude.expected-output/notmuch-tree-tag-inbox-without-excluded new file mode 100644 index 00000000..34abae80 --- /dev/null +++ b/test/emacs-exclude.expected-output/notmuch-tree-tag-inbox-without-excluded @@ -0,0 +1,49 @@ + 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) + 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Keith Packard ╰┬► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 Mikhail Gusarov ├┬► ... (inbox signed unread) + 2009-11-17 Lars Kellogg-Stedman │╰┬► ... (inbox signed unread) + 2009-11-17 Mikhail Gusarov │ ├─► ... (inbox unread) + 2009-11-17 Keith Packard │ ╰┬► ... (inbox unread) + 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... (inbox signed unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Alex Botero-Lowry ┬►[notmuch] preliminary FreeBSD support (attachment inbox unread) + 2009-11-17 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox unread) + 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Jan Janak ┬►[notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Jan Janak ┬►[notmuch] What a great idea! (inbox unread) + 2009-11-17 Jan Janak ├─► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Israel Herraiz ┬►[notmuch] New to the list (inbox unread) + 2009-11-18 Keith Packard ├─► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox signed unread) + 2009-11-18 Keith Packard ├─► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox unread) + 2009-11-18 Keith Packard ╰┬► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + 2009-11-18 Lars Kellogg-Stedman ╰─► ... (attachment inbox signed unread) + 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Alexander Botero-Low ╰┬► ... (inbox unread) + 2009-11-18 Jjgod Jiang ╰┬► ... (inbox unread) + 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) + 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox unread) + 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox unread) + 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) + 2010-12-16 Olivier Berger ─►Essai accentué (inbox unread) + 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox unread) +End of search results. diff --git a/test/emacs-exclude.expected-output/notmuch-unthreaded-tag-inbox-with-excluded b/test/emacs-exclude.expected-output/notmuch-unthreaded-tag-inbox-with-excluded new file mode 100644 index 00000000..ea8bf3d2 --- /dev/null +++ b/test/emacs-exclude.expected-output/notmuch-unthreaded-tag-inbox-with-excluded @@ -0,0 +1,53 @@ + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) + 2009-11-17 Carl Worth [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Lars Kellogg-Stedman [notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 Mikhail Gusarov [notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 Alex Botero-Lowry [notmuch] preliminary FreeBSD support (attachment inbox unread) + 2009-11-17 Carl Worth [notmuch] preliminary FreeBSD support (inbox unread) + 2009-11-17 Lars Kellogg-Stedman [notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 Mikhail Gusarov [notmuch] Working with Maildir storage? (inbox unread) + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH] Handle rename of message file (inbox unread) + 2009-11-17 Keith Packard [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Keith Packard [notmuch] Working with Maildir storage? (inbox unread) + 2009-11-17 Keith Packard [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-17 Jan Janak [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + 2009-11-17 Jan Janak [notmuch] What a great idea! (inbox unread) + 2009-11-17 Jan Janak [notmuch] What a great idea! (inbox unread) + 2009-11-17 Israel Herraiz [notmuch] New to the list (inbox unread) + 2009-11-17 Adrian Perez de Cast [notmuch] Introducing myself (inbox signed unread) + 2009-11-17 Aron Griffis [notmuch] archive (inbox unread) + 2009-11-17 Ingmar Vanhassel [notmuch] [PATCH] Typsos (inbox unread) + 2009-11-18 Alex Botero-Lowry [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-18 Lars Kellogg-Stedman [notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-18 Lars Kellogg-Stedman [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + 2009-11-18 Lars Kellogg-Stedman [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + 2009-11-18 Stewart Smith [notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (deleted inbox unread) + 2009-11-18 Stewart Smith [notmuch] [PATCH 2/2] Read mail directory in inode number order (deleted inbox unread) + 2009-11-18 Keith Packard [notmuch] New to the list (inbox unread) + 2009-11-18 Keith Packard [notmuch] Introducing myself (inbox unread) + 2009-11-18 Keith Packard [notmuch] archive (inbox unread) + 2009-11-18 Stewart Smith [notmuch] [PATCH] count_files: sort directory in inode order before statting (deleted inbox unread) + 2009-11-18 Jjgod Jiang [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Jan Janak [notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 Rolland Santimano [notmuch] Link to mailing list archives ? (inbox unread) + 2009-11-18 Alexander Botero-Low [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Jjgod Jiang [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Alexander Botero-Low [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Alexander Botero-Low [notmuch] request for pull (inbox unread) + 2009-11-18 Keith Packard [notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Alexander Botero-Low [notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Carl Worth [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-18 Carl Worth [notmuch] Working with Maildir storage? (inbox unread) + 2009-11-18 Carl Worth [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-18 Carl Worth [notmuch] archive (inbox unread) + 2009-11-18 Carl Worth [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + 2009-11-18 Carl Worth [notmuch] What a great idea! (inbox unread) + 2009-11-18 Carl Worth [notmuch] New to the list (inbox unread) + 2009-11-18 Carl Worth [notmuch] Introducing myself (inbox unread) + 2009-11-18 Carl Worth [notmuch] [PATCH] Typsos (inbox unread) + 2009-11-18 Carl Worth [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox unread) + 2009-11-18 Chris Wilson [notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (deleted inbox unread) + 2010-12-16 Olivier Berger Essai accentué (inbox unread) + 2010-12-29 François Boulogne [aur-general] Guidelines: cp, mkdir vs install (inbox unread) +End of search results. diff --git a/test/emacs-exclude.expected-output/notmuch-unthreaded-tag-inbox-without-excluded b/test/emacs-exclude.expected-output/notmuch-unthreaded-tag-inbox-without-excluded new file mode 100644 index 00000000..fabb43e4 --- /dev/null +++ b/test/emacs-exclude.expected-output/notmuch-unthreaded-tag-inbox-without-excluded @@ -0,0 +1,49 @@ + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) + 2009-11-17 Carl Worth [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Lars Kellogg-Stedman [notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 Mikhail Gusarov [notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 Alex Botero-Lowry [notmuch] preliminary FreeBSD support (attachment inbox unread) + 2009-11-17 Carl Worth [notmuch] preliminary FreeBSD support (inbox unread) + 2009-11-17 Lars Kellogg-Stedman [notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 Mikhail Gusarov [notmuch] Working with Maildir storage? (inbox unread) + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH] Handle rename of message file (inbox unread) + 2009-11-17 Keith Packard [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Keith Packard [notmuch] Working with Maildir storage? (inbox unread) + 2009-11-17 Keith Packard [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-17 Jan Janak [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + 2009-11-17 Jan Janak [notmuch] What a great idea! (inbox unread) + 2009-11-17 Jan Janak [notmuch] What a great idea! (inbox unread) + 2009-11-17 Israel Herraiz [notmuch] New to the list (inbox unread) + 2009-11-17 Adrian Perez de Cast [notmuch] Introducing myself (inbox signed unread) + 2009-11-17 Aron Griffis [notmuch] archive (inbox unread) + 2009-11-17 Ingmar Vanhassel [notmuch] [PATCH] Typsos (inbox unread) + 2009-11-18 Alex Botero-Lowry [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-18 Lars Kellogg-Stedman [notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-18 Lars Kellogg-Stedman [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + 2009-11-18 Lars Kellogg-Stedman [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + 2009-11-18 Keith Packard [notmuch] New to the list (inbox unread) + 2009-11-18 Keith Packard [notmuch] Introducing myself (inbox unread) + 2009-11-18 Keith Packard [notmuch] archive (inbox unread) + 2009-11-18 Jjgod Jiang [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Jan Janak [notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 Rolland Santimano [notmuch] Link to mailing list archives ? (inbox unread) + 2009-11-18 Alexander Botero-Low [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Jjgod Jiang [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Alexander Botero-Low [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Alexander Botero-Low [notmuch] request for pull (inbox unread) + 2009-11-18 Keith Packard [notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Alexander Botero-Low [notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Carl Worth [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-18 Carl Worth [notmuch] Working with Maildir storage? (inbox unread) + 2009-11-18 Carl Worth [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-18 Carl Worth [notmuch] archive (inbox unread) + 2009-11-18 Carl Worth [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + 2009-11-18 Carl Worth [notmuch] What a great idea! (inbox unread) + 2009-11-18 Carl Worth [notmuch] New to the list (inbox unread) + 2009-11-18 Carl Worth [notmuch] Introducing myself (inbox unread) + 2009-11-18 Carl Worth [notmuch] [PATCH] Typsos (inbox unread) + 2009-11-18 Carl Worth [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox unread) + 2010-12-16 Olivier Berger Essai accentué (inbox unread) + 2010-12-29 François Boulogne [aur-general] Guidelines: cp, mkdir vs install (inbox unread) +End of search results. diff --git a/test/emacs-reply.expected-output/notmuch-reply-duplicate-4 b/test/emacs-reply.expected-output/notmuch-reply-duplicate-4 new file mode 100644 index 00000000..836f77b1 --- /dev/null +++ b/test/emacs-reply.expected-output/notmuch-reply-duplicate-4 @@ -0,0 +1,21 @@ +From: Notmuch Test Suite +To: Sean Whitton , 916811@bugs.debian.org, 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org, 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org, 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org, 916876@bugs.debian.org +Subject: Re: [Pkg-emacsen-addons] Bug#916811: Increase severity to 'serious' +In-Reply-To: <87r2ecrr6x.fsf@zephyr.silentflame.com> +Fcc: MAIL_DIR/sent +--text follows this line-- +Sean Whitton writes: + +> control: severity -1 serious +> +> Hello, +> +> Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for +> all the e-mails). +> +> -- +> Sean Whitton +> _______________________________________________ +> Pkg-emacsen-addons mailing list +> Pkg-emacsen-addons@alioth-lists.debian.net +> https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/pkg-emacsen-addons diff --git a/test/emacs-show.expected-output/notmuch-show-depth b/test/emacs-show.expected-output/notmuch-show-depth new file mode 100644 index 00000000..8299519c --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-depth @@ -0,0 +1,44 @@ +Lars Kellogg-Stedman (2009-11-17) (inbox signed) +Subject: [notmuch] Working with Maildir storage? +To: notmuch@notmuchmail.org +Date: Tue, 17 Nov 2009 14:00:54 -0500 + +[ multipart/mixed (hidden) ] + Mikhail Gusarov (2009-11-17) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 01:02:38 +0600 + + [ multipart/mixed (hidden) ] + Lars Kellogg-Stedman (2009-11-17) (inbox signed) + Subject: Re: [notmuch] Working with Maildir storage? + To: Mikhail Gusarov + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 15:33:01 -0500 + + [ multipart/mixed (hidden) ] + Mikhail Gusarov (2009-11-17) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 02:50:48 +0600 + + [ text/plain (hidden) ] + Keith Packard (2009-11-17) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 13:24:13 -0800 + + [ text/plain (hidden) ] + Lars Kellogg-Stedman (2009-11-18) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: Keith Packard + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 19:50:40 -0500 + + [ multipart/mixed (hidden) ] + Carl Worth (2009-11-18) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 02:08:10 -0800 + + [ text/plain (hidden) ] diff --git a/test/emacs-show.expected-output/notmuch-show-depth-1 b/test/emacs-show.expected-output/notmuch-show-depth-1 new file mode 100644 index 00000000..e7c376bb --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-depth-1 @@ -0,0 +1,119 @@ +Lars Kellogg-Stedman (2009-11-17) (inbox signed) +Subject: [notmuch] Working with Maildir storage? +To: notmuch@notmuchmail.org +Date: Tue, 17 Nov 2009 14:00:54 -0500 + +[ multipart/mixed ] +[ multipart/signed ] +[ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ] +[ text/plain ] +I saw the LWN article and decided to take a look at notmuch. I'm +currently using mutt and mairix to index and read a collection of +Maildir mail folders (around 40,000 messages total). + +notmuch indexed the messages without complaint, but my attempt at +searching bombed out. Running, for example: + + notmuch search storage + +Resulted in 4604 lines of errors along the lines of: + + Error opening + /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S: + Too many open files + +I'm curious if this is expected behavior (i.e., notmuch does not work +with Maildir) or if something else is going on. + +Cheers, + +[ 4-line signature. Click/Enter to show. ] +[ application/pgp-signature ] +[ text/plain ] +[ 4-line signature. Click/Enter to show. ] + Mikhail Gusarov (2009-11-17) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 01:02:38 +0600 + + [ multipart/mixed ] + [ multipart/signed ] + [ Unknown key ID 0x9D20F6503E338888 or unsupported algorithm ] + [ text/plain ] + + Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did + gyre and gimble: + + LK> Resulted in 4604 lines of errors along the lines of: + + LK> Error opening + LK> + /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S: + LK> Too many open files + + See the patch just posted here. + + [ 2-line signature. Click/Enter to show. ] + [ application/pgp-signature ] + [ text/plain ] + [ 4-line signature. Click/Enter to show. ] + Lars Kellogg-Stedman (2009-11-17) (inbox signed) + Subject: Re: [notmuch] Working with Maildir storage? + To: Mikhail Gusarov + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 15:33:01 -0500 + + [ multipart/mixed (hidden) ] + Mikhail Gusarov (2009-11-17) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 02:50:48 +0600 + + [ text/plain (hidden) ] + Keith Packard (2009-11-17) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 13:24:13 -0800 + + [ text/plain (hidden) ] + Lars Kellogg-Stedman (2009-11-18) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: Keith Packard + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 19:50:40 -0500 + + [ multipart/mixed (hidden) ] + Carl Worth (2009-11-18) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 02:08:10 -0800 + + On Tue, 17 Nov 2009 14:00:54 -0500, Lars Kellogg-Stedman wrote: + > I saw the LWN article and decided to take a look at notmuch. I'm + > currently using mutt and mairix to index and read a collection of + > Maildir mail folders (around 40,000 messages total). + + Welcome, Lars! + + I hadn't even seen that Keith's blog post had been picked up by lwn.net. + That's very interesting. So, thanks for coming and trying out notmuch. + + > Error opening + > /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S: + > Too many open files + + Sadly, the lwn article coincided with me having just introduced this + bug, and then getting on a Trans-Atlantic flight. So I fixed the bug + fairly quickly, but there was quite a bit of latency before I could push + the fix out. It should be fixed now. + + > I'm curious if this is expected behavior (i.e., notmuch does not work + > with Maildir) or if something else is going on. + + Notmuch works just fine with maildir---it's one of the things that it + likes the best. + + Happy hacking, + + -Carl diff --git a/test/emacs-show.expected-output/notmuch-show-duplicate-4 b/test/emacs-show.expected-output/notmuch-show-duplicate-4 new file mode 100644 index 00000000..6bf49d81 --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-duplicate-4 @@ -0,0 +1,20 @@ +Sean Whitton (2018-12-20) (inbox signed) 4/5 +Subject: [Pkg-emacsen-addons] Bug#916811: Increase severity to 'serious' +To: 916805@bugs.debian.org, 916807@bugs.debian.org, 916808@bugs.debian.org, 916809@bugs.debian.org, 916811@bugs.debian.org, 916867@bugs.debian.org, 916869@bugs.debian.org, 916872@bugs.debian.org, 916875@bugs.debian.org, 916876@bugs.debian.org +Date: Thu, 20 Dec 2018 18:25:26 +0000 + +[ multipart/mixed ] +[ multipart/signed ] +[ Unknown key ID 0x695B7AE4BF066240 or unsupported algorithm ] +[ text/plain ] +control: severity -1 serious + +Hello, + +Emacs 26.1 has reached Debian unstable (sooner than expected; sorry for +all the e-mails). + +[ 2-line signature. Click/Enter to show. ] +[ signature.asc: application/pgp-signature ] +[ text/plain ] +[ 4-line signature. Click/Enter to show. ] diff --git a/test/emacs-show.expected-output/notmuch-show-height-0 b/test/emacs-show.expected-output/notmuch-show-height-0 new file mode 100644 index 00000000..d646353e --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-height-0 @@ -0,0 +1,97 @@ +Lars Kellogg-Stedman (2009-11-17) (inbox signed) +Subject: [notmuch] Working with Maildir storage? +To: notmuch@notmuchmail.org +Date: Tue, 17 Nov 2009 14:00:54 -0500 + +[ multipart/mixed (hidden) ] + Mikhail Gusarov (2009-11-17) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 01:02:38 +0600 + + [ multipart/mixed (hidden) ] + Lars Kellogg-Stedman (2009-11-17) (inbox signed) + Subject: Re: [notmuch] Working with Maildir storage? + To: Mikhail Gusarov + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 15:33:01 -0500 + + [ multipart/mixed (hidden) ] + Mikhail Gusarov (2009-11-17) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 02:50:48 +0600 + + Twas brillig at 15:33:01 17.11.2009 UTC-05 when lars at seas.harvard.edu + did gyre and gimble: + + LK> Is the list archived anywhere? The obvious archives + LK> (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I + LK> think I subscribed too late to get the patch (I only just saw the + LK> discussion about it). + + LK> It doesn't look like the patch is in git yet. + + Just has been pushed + + [ 10-line signature. Click/Enter to show. ] + Keith Packard (2009-11-17) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 13:24:13 -0800 + + [ text/plain (hidden) ] + Lars Kellogg-Stedman (2009-11-18) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: Keith Packard + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 19:50:40 -0500 + + [ multipart/mixed ] + [ multipart/signed ] + [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ] + [ text/plain ] + > I've also pushed a slightly more complicated (and complete) fix to my + > private notmuch repository + + The version of lib/messages.cc in your repo doesn't build because it's + missing "#include " (for the uint32_t on line 466). + + [ 4-line signature. Click/Enter to show. ] + [ application/pgp-signature ] + [ text/plain ] + [ 4-line signature. Click/Enter to show. ] + Carl Worth (2009-11-18) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 02:08:10 -0800 + + On Tue, 17 Nov 2009 14:00:54 -0500, Lars Kellogg-Stedman wrote: + > I saw the LWN article and decided to take a look at notmuch. I'm + > currently using mutt and mairix to index and read a collection of + > Maildir mail folders (around 40,000 messages total). + + Welcome, Lars! + + I hadn't even seen that Keith's blog post had been picked up by lwn.net. + That's very interesting. So, thanks for coming and trying out notmuch. + + > Error opening + > /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S: + > Too many open files + + Sadly, the lwn article coincided with me having just introduced this + bug, and then getting on a Trans-Atlantic flight. So I fixed the bug + fairly quickly, but there was quite a bit of latency before I could push + the fix out. It should be fixed now. + + > I'm curious if this is expected behavior (i.e., notmuch does not work + > with Maildir) or if something else is going on. + + Notmuch works just fine with maildir---it's one of the things that it + likes the best. + + Happy hacking, + + -Carl diff --git a/test/emacs-show.expected-output/notmuch-show-multipart-alternative b/test/emacs-show.expected-output/notmuch-show-multipart-alternative new file mode 100644 index 00000000..e2951d2b --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-multipart-alternative @@ -0,0 +1,62 @@ +Alex Botero-Lowry (2009-11-17) (attachment inbox) +Subject: [notmuch] preliminary FreeBSD support +To: notmuch@notmuchmail.org +Date: Tue, 17 Nov 2009 11:36:14 -0800 + +[ multipart/mixed ] +[ multipart/alternative ] +[ text/plain ] +I saw the announcement this morning, and was very excited, as I had been +hoping sup would be turned into a library, +since I like the concept more than the UI (I'd rather an emacs interface). + +I did a preliminary compile which worked out fine, but +sysconf(_SC_SC_GETPW_R_SIZE_MAX) returns -1 on +FreeBSD, so notmuch_config_open segfaulted. + +Attached is a patch that supplies a default buffer size of 64 in cases where +-1 is returned. + +http://www.opengroup.org/austin/docs/austin_328.txt - seems to indicate this +is acceptable behavior, +and +http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg016808.htmlspecifically +uses 64 as the +buffer size. +[ text/html (hidden) ] +[ 0001-Deal-with-situation-where-sysconf-_SC_GETPW_R_SIZE_M.patch: text/x-diff ] +From e3bc4bbd7b9d0d086816ab5f8f2d6ffea1dd3ea4 Mon Sep 17 00:00:00 2001 +From: Alexander Botero-Lowry +Date: Tue, 17 Nov 2009 11:30:39 -0800 +Subject: [PATCH] Deal with situation where sysconf(_SC_GETPW_R_SIZE_MAX) returns -1 + +--- + notmuch-config.c | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/notmuch-config.c b/notmuch-config.c +index 248149c..e7220d8 100644 +--- a/notmuch-config.c ++++ b/notmuch-config.c +@@ -77,6 +77,7 @@ static char * + get_name_from_passwd_file (void *ctx) + { + long pw_buf_size = sysconf(_SC_GETPW_R_SIZE_MAX); ++ if (pw_buf_size == -1) pw_buf_size = 64; + char *pw_buf = talloc_zero_size (ctx, pw_buf_size); + struct passwd passwd, *ignored; + char *name; +@@ -101,6 +102,7 @@ static char * + get_username_from_passwd_file (void *ctx) + { + long pw_buf_size = sysconf(_SC_GETPW_R_SIZE_MAX); ++ if (pw_buf_size == -1) pw_buf_size = 64; + char *pw_buf = talloc_zero_size (ctx, pw_buf_size); + struct passwd passwd, *ignored; + char *name; +-- +1.6.5.2 + +[ text/plain ] +[ 4-line signature. Click/Enter to show. ] + Carl Worth (2009-11-17) (inbox unread) diff --git a/test/emacs-show.expected-output/notmuch-show-size b/test/emacs-show.expected-output/notmuch-show-size new file mode 100644 index 00000000..cdde467e --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-size @@ -0,0 +1,64 @@ +Lars Kellogg-Stedman (2009-11-17) (inbox signed) +Subject: [notmuch] Working with Maildir storage? +To: notmuch@notmuchmail.org +Date: Tue, 17 Nov 2009 14:00:54 -0500 + +[ multipart/mixed ] +[ multipart/signed ] +[ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ] +[ text/plain (hidden) ] +[ application/pgp-signature ] +[ text/plain (hidden) ] + Mikhail Gusarov (2009-11-17) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 01:02:38 +0600 + + [ multipart/mixed ] + [ multipart/signed ] + [ Unknown key ID 0x9D20F6503E338888 or unsupported algorithm ] + [ text/plain (hidden) ] + [ application/pgp-signature ] + [ text/plain (hidden) ] + Lars Kellogg-Stedman (2009-11-17) (inbox signed) + Subject: Re: [notmuch] Working with Maildir storage? + To: Mikhail Gusarov + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 15:33:01 -0500 + + [ multipart/mixed ] + [ multipart/signed ] + [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ] + [ text/plain (hidden) ] + [ application/pgp-signature ] + [ text/plain (hidden) ] + Mikhail Gusarov (2009-11-17) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 02:50:48 +0600 + + [ text/plain (hidden) ] + Keith Packard (2009-11-17) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 13:24:13 -0800 + + [ text/plain (hidden) ] + Lars Kellogg-Stedman (2009-11-18) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: Keith Packard + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 19:50:40 -0500 + + [ multipart/mixed ] + [ multipart/signed ] + [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ] + [ text/plain (hidden) ] + [ application/pgp-signature ] + [ text/plain (hidden) ] + Carl Worth (2009-11-18) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 02:08:10 -0800 + + [ text/plain (hidden) ] diff --git a/test/emacs-show.expected-output/notmuch-show-size-450 b/test/emacs-show.expected-output/notmuch-show-size-450 new file mode 100644 index 00000000..ec34612e --- /dev/null +++ b/test/emacs-show.expected-output/notmuch-show-size-450 @@ -0,0 +1,89 @@ +Lars Kellogg-Stedman (2009-11-17) (inbox signed) +Subject: [notmuch] Working with Maildir storage? +To: notmuch@notmuchmail.org +Date: Tue, 17 Nov 2009 14:00:54 -0500 + +[ multipart/mixed ] +[ multipart/signed ] +[ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ] +[ text/plain (hidden) ] +[ application/pgp-signature ] +[ text/plain ] +[ 4-line signature. Click/Enter to show. ] + Mikhail Gusarov (2009-11-17) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 01:02:38 +0600 + + [ multipart/mixed ] + [ multipart/signed ] + [ Unknown key ID 0x9D20F6503E338888 or unsupported algorithm ] + [ text/plain ] + + Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did + gyre and gimble: + + LK> Resulted in 4604 lines of errors along the lines of: + + LK> Error opening + LK> + /home/lars/Mail/read-messages.2008/cur/1246413773.24928_27334.hostname,U=3026:2,S: + LK> Too many open files + + See the patch just posted here. + + [ 2-line signature. Click/Enter to show. ] + [ application/pgp-signature ] + [ text/plain ] + [ 4-line signature. Click/Enter to show. ] + Lars Kellogg-Stedman (2009-11-17) (inbox signed) + Subject: Re: [notmuch] Working with Maildir storage? + To: Mikhail Gusarov + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 15:33:01 -0500 + + [ multipart/mixed ] + [ multipart/signed ] + [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ] + [ text/plain (hidden) ] + [ application/pgp-signature ] + [ text/plain ] + [ 4-line signature. Click/Enter to show. ] + Mikhail Gusarov (2009-11-17) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 02:50:48 +0600 + + [ text/plain (hidden) ] + Keith Packard (2009-11-17) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 13:24:13 -0800 + + [ text/plain (hidden) ] + Lars Kellogg-Stedman (2009-11-18) (inbox signed unread) + Subject: Re: [notmuch] Working with Maildir storage? + To: Keith Packard + Cc: notmuch@notmuchmail.org + Date: Tue, 17 Nov 2009 19:50:40 -0500 + + [ multipart/mixed ] + [ multipart/signed ] + [ Unknown key ID 0xD74695063141ACD8 or unsupported algorithm ] + [ text/plain ] + > I've also pushed a slightly more complicated (and complete) fix to my + > private notmuch repository + + The version of lib/messages.cc in your repo doesn't build because it's + missing "#include " (for the uint32_t on line 466). + + [ 4-line signature. Click/Enter to show. ] + [ application/pgp-signature ] + [ text/plain ] + [ 4-line signature. Click/Enter to show. ] + Carl Worth (2009-11-18) (inbox unread) + Subject: [notmuch] Working with Maildir storage? + To: notmuch@notmuchmail.org + Date: Wed, 18 Nov 2009 02:08:10 -0800 + + [ text/plain (hidden) ] diff --git a/test/emacs-subject-warnings.el b/test/emacs-subject-warnings.el new file mode 100644 index 00000000..1e547429 --- /dev/null +++ b/test/emacs-subject-warnings.el @@ -0,0 +1,48 @@ +(require 'cl-lib) +(require 'notmuch-mua) + +(defun subject-check-test (&optional fn) + "Test `notmuch-mua-subject-check'. +Optionally, evaluate FN before doing the test. + +Return t if the message would be sent, and nil otherwise." + (notmuch-mua-mail) + (message-goto-subject) + (when fn + (funcall fn)) + (prog1 + (condition-case nil + ;; Force `y-or-n-p' to always return `nil', as if the user + ;; pressed "n". + (cl-letf (((symbol-function 'y-or-n-p) + (lambda (&rest args) nil))) + (notmuch-mua-subject-check) + t) + ('error nil)) + (set-buffer-modified-p nil) + (kill-buffer (current-buffer)))) + +(defvar subject-check-tests + '(;; These are okay. + (t . (lambda () (insert "something"))) + ;; These should not be okay. + (nil) + (nil . (lambda () (insert " "))) + (nil . (lambda () (insert " "))) + (nil . (lambda () (insert " "))) ; NON-BREAKING SPACE + )) + +(defun notmuch-test-subject-warning-1 () + (let (output expected) + (dolist (test subject-check-tests) + (let* ((expect (car test)) + (body (cdr test)) + (result (subject-check-test body))) + (push expect expected) + (push (if (eq result expect) + result + ;; In the case of a failure, include the test + ;; details to make it simpler to debug. + (format "%S <-- %S" result body)) + output))) + (notmuch-test-expect-equal output expected))) diff --git a/test/emacs-tree.expected-output/inbox-outline b/test/emacs-tree.expected-output/inbox-outline new file mode 100644 index 00000000..4acb62a9 --- /dev/null +++ b/test/emacs-tree.expected-output/inbox-outline @@ -0,0 +1,25 @@ + 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox unread) + 2010-12-16 Olivier Berger ─►Essai accentué (inbox unread) + 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread) + 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox unread) + 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox signed unread) + 2009-11-17 Israel Herraiz ┬►[notmuch] New to the list (inbox unread) + 2009-11-17 Jan Janak ┬►[notmuch] What a great idea! (inbox unread) + 2009-11-17 Jan Janak ┬►[notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox unread) + 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox unread) + 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox unread) + 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread) + 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox unread) + 2009-11-17 Alex Botero-Lowry ┬►[notmuch] preliminary FreeBSD support (attachment inbox unread) +End of search results. diff --git a/test/emacs-tree.expected-output/notmuch-tree-single-thread b/test/emacs-tree.expected-output/notmuch-tree-single-thread index 2285d10e..3da7628d 100644 --- a/test/emacs-tree.expected-output/notmuch-tree-single-thread +++ b/test/emacs-tree.expected-output/notmuch-tree-single-thread @@ -1,5 +1,5 @@ - 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox) - 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) + 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox) + 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) 2009-11-17 Keith Packard ╰┬► ... (inbox unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) diff --git a/test/emacs-tree.expected-output/notmuch-tree-tag-inbox b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox index f28d4856..3970e8e8 100644 --- a/test/emacs-tree.expected-output/notmuch-tree-tag-inbox +++ b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox @@ -1,8 +1,8 @@ 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox unread) 2010-12-16 Olivier Berger ─►Essai accentué (inbox unread) 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread) - 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) - 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox unread) + 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox signed unread) @@ -19,8 +19,8 @@ 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox unread) 2009-11-18 Keith Packard ╰┬► ... (inbox unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) - 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) - 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox signed unread) 2009-11-17 Mikhail Gusarov ├┬► ... (inbox signed unread) 2009-11-17 Lars Kellogg-Stedman │╰┬► ... (inbox signed unread) @@ -28,23 +28,23 @@ 2009-11-17 Keith Packard │ ╰┬► ... (inbox unread) 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... (inbox signed unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) - 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) - 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) + 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) 2009-11-17 Keith Packard ╰┬► ... (inbox unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) - 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) - 2009-11-18 Alexander Botero-Low ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox unread) 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox unread) 2009-11-18 Alexander Botero-Low ╰┬► ... (inbox unread) 2009-11-18 Jjgod Jiang ╰┬► ... (inbox unread) 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox unread) - 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) - 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread) + 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread) 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox unread) - 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread) 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) 2009-11-18 Lars Kellogg-Stedman ╰─► ... (attachment inbox signed unread) 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox unread) diff --git a/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-oldest-first b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-oldest-first new file mode 100644 index 00000000..4277eda5 --- /dev/null +++ b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-oldest-first @@ -0,0 +1,53 @@ + 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) + 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Keith Packard ╰┬► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox signed unread) + 2009-11-17 Mikhail Gusarov ├┬► ... (inbox signed unread) + 2009-11-17 Lars Kellogg-Stedman │╰┬► ... (inbox signed unread) + 2009-11-17 Mikhail Gusarov │ ├─► ... (inbox unread) + 2009-11-17 Keith Packard │ ╰┬► ... (inbox unread) + 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... (inbox signed unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Alex Botero-Lowry ┬►[notmuch] preliminary FreeBSD support (attachment inbox unread) + 2009-11-17 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox unread) + 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Jan Janak ┬►[notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Jan Janak ┬►[notmuch] What a great idea! (inbox unread) + 2009-11-17 Jan Janak ├─► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Israel Herraiz ┬►[notmuch] New to the list (inbox unread) + 2009-11-18 Keith Packard ├─► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox signed unread) + 2009-11-18 Keith Packard ├─► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox unread) + 2009-11-18 Keith Packard ╰┬► ... (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) + 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + 2009-11-18 Lars Kellogg-Stedman ╰─► ... (attachment inbox signed unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread) + 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + 2009-11-18 Alexander Botero-Low ╰┬► ... (inbox unread) + 2009-11-18 Jjgod Jiang ╰┬► ... (inbox unread) + 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) + 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox unread) + 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox unread) + 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) + 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread) + 2010-12-16 Olivier Berger ─►Essai accentué (inbox unread) + 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox unread) +End of search results. diff --git a/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-tagged b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-tagged index 428c0ae8..eb62e144 100644 --- a/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-tagged +++ b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-tagged @@ -1,8 +1,8 @@ 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox unread) 2010-12-16 Olivier Berger ─►Essai accentué (inbox test_tag unread) 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread) - 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) - 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox unread) + 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox signed unread) @@ -19,8 +19,8 @@ 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox unread) 2009-11-18 Keith Packard ╰┬► ... (inbox unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) - 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) - 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox signed unread) 2009-11-17 Mikhail Gusarov ├┬► ... (inbox signed unread) 2009-11-17 Lars Kellogg-Stedman │╰┬► ... (inbox signed unread) @@ -28,23 +28,23 @@ 2009-11-17 Keith Packard │ ╰┬► ... (inbox unread) 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... (inbox signed unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) - 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) - 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) + 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) 2009-11-17 Keith Packard ╰┬► ... (inbox unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) - 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) - 2009-11-18 Alexander Botero-Low ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox unread) 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox unread) 2009-11-18 Alexander Botero-Low ╰┬► ... (inbox unread) 2009-11-18 Jjgod Jiang ╰┬► ... (inbox unread) 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox unread) - 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) - 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread) + 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread) 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox unread) - 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread) 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) 2009-11-18 Lars Kellogg-Stedman ╰─► ... (attachment inbox signed unread) 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox unread) diff --git a/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-thread-tagged b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-thread-tagged index 828c5251..c90d726e 100644 --- a/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-thread-tagged +++ b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-thread-tagged @@ -1,8 +1,8 @@ 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install (inbox unread) 2010-12-16 Olivier Berger ─►Essai accentué (inbox unread) 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread) - 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) - 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox unread) + 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos (inbox unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself (inbox signed unread) @@ -19,8 +19,8 @@ 2009-11-17 Aron Griffis ┬►[notmuch] archive (inbox unread) 2009-11-18 Keith Packard ╰┬► ... (inbox unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) - 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) - 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + 2009-11-18 Carl Worth ╰─► ... (inbox unread) 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? (inbox signed test_thread_tag unread) 2009-11-17 Mikhail Gusarov ├┬► ... (inbox signed test_thread_tag unread) 2009-11-17 Lars Kellogg-Stedman │╰┬► ... (inbox signed test_thread_tag unread) @@ -28,23 +28,23 @@ 2009-11-17 Keith Packard │ ╰┬► ... (inbox test_thread_tag unread) 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... (inbox signed test_thread_tag unread) 2009-11-18 Carl Worth ╰─► ... (inbox test_thread_tag unread) - 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) - 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) + 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) 2009-11-17 Keith Packard ╰┬► ... (inbox unread) 2009-11-18 Carl Worth ╰─► ... (inbox unread) - 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) - 2009-11-18 Alexander Botero-Low ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull (inbox unread) 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues (inbox unread) 2009-11-18 Alexander Botero-Low ╰┬► ... (inbox unread) 2009-11-18 Jjgod Jiang ╰┬► ... (inbox unread) 2009-11-18 Alexander Botero-Low ╰─► ... (inbox unread) 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? (inbox unread) - 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) - 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread) + 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread) 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox unread) - 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread) 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) 2009-11-18 Lars Kellogg-Stedman ╰─► ... (attachment inbox signed unread) 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file (inbox unread) diff --git a/test/emacs-tree.expected-output/result-format-function b/test/emacs-tree.expected-output/result-format-function index 7eb24696..42285f0e 100644 --- a/test/emacs-tree.expected-output/result-format-function +++ b/test/emacs-tree.expected-output/result-format-function @@ -1,8 +1,8 @@ 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install ( ui) 2010-12-16 Olivier Berger ─►Essai accentué ( ui) 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once ( ui) - 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (& ui) - 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop ( ui) + 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (& ui) + 2009-11-18 Carl Worth ╰─► ... ( ui) 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos ( ui) 2009-11-18 Carl Worth ╰─► ... ( ui) 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself ( =ui) @@ -19,8 +19,8 @@ 2009-11-17 Aron Griffis ┬►[notmuch] archive ( ui) 2009-11-18 Keith Packard ╰┬► ... ( ui) 2009-11-18 Carl Worth ╰─► ... ( ui) - 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags ( ui) - 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags ( ui) + 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags ( ui) + 2009-11-18 Carl Worth ╰─► ... ( ui) 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? ( = i) 2009-11-17 Mikhail Gusarov ├┬► ... ( =ui) 2009-11-17 Lars Kellogg-Stedman │╰┬► ... ( =ui) @@ -28,23 +28,23 @@ 2009-11-17 Keith Packard │ ╰┬► ... ( ui) 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... ( =ui) 2009-11-18 Carl Worth ╰─► ... ( ui) - 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers ( i) - 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 ( ui) + 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers ( i) + 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 ( ui) 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers ( ui) 2009-11-17 Keith Packard ╰┬► ... ( ui) 2009-11-18 Carl Worth ╰─► ... ( ui) - 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap ( ui) - 2009-11-18 Alexander Botero-Low ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap ( ui) + 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap ( ui) + 2009-11-18 Alexander Botero-Low ╰─► ... ( ui) 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull ( ui) 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues ( ui) 2009-11-18 Alexander Botero-Low ╰┬► ... ( ui) 2009-11-18 Jjgod Jiang ╰┬► ... ( ui) 2009-11-18 Alexander Botero-Low ╰─► ... ( ui) 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? ( ui) - 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags ( ui) - 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting ( ui) + 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags ( ui) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting ( ui) 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order ( ui) - 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. ( ui) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. ( ui) 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (&=ui) 2009-11-18 Lars Kellogg-Stedman ╰─► ... (&=ui) 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file ( ui) diff --git a/test/emacs-unthreaded.expected-output/result-format-function b/test/emacs-unthreaded.expected-output/result-format-function index bcb10b96..91bbf694 100644 --- a/test/emacs-unthreaded.expected-output/result-format-function +++ b/test/emacs-unthreaded.expected-output/result-format-function @@ -12,24 +12,24 @@ 2009-11-18 Carl Worth [notmuch] Working with Maildir storage? ( ui) 2009-11-18 Carl Worth [notmuch] [PATCH 1/2] Close message file after parsing message headers( ui) 2009-11-18 Alexander Botero-Low[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap( ui) - 2009-11-18 Keith Packard [notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap( ui) + 2009-11-18 Keith Packard [notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap( ui) 2009-11-18 Alexander Botero-Low[notmuch] request for pull ( ui) 2009-11-18 Alexander Botero-Low[notmuch] Mac OS X/Darwin compatibility issues ( ui) 2009-11-18 Jjgod Jiang [notmuch] Mac OS X/Darwin compatibility issues ( ui) 2009-11-18 Alexander Botero-Low[notmuch] Mac OS X/Darwin compatibility issues ( ui) 2009-11-18 Rolland Santimano [notmuch] Link to mailing list archives ? ( ui) - 2009-11-18 Jan Janak [notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags( ui) + 2009-11-18 Jan Janak [notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags( ui) 2009-11-18 Jjgod Jiang [notmuch] Mac OS X/Darwin compatibility issues ( ui) - 2009-11-18 Stewart Smith [notmuch] [PATCH] count_files: sort directory in inode order before statting( ui) + 2009-11-18 Stewart Smith [notmuch] [PATCH] count_files: sort directory in inode order before statting( ui) 2009-11-18 Keith Packard [notmuch] archive ( ui) 2009-11-18 Keith Packard [notmuch] Introducing myself ( ui) 2009-11-18 Keith Packard [notmuch] New to the list ( ui) 2009-11-18 Stewart Smith [notmuch] [PATCH 2/2] Read mail directory in inode number order( ui) - 2009-11-18 Stewart Smith [notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs.( ui) + 2009-11-18 Stewart Smith [notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs.( ui) 2009-11-18 Lars Kellogg-Stedman[notmuch] "notmuch help" outputs to stderr? (&=ui) 2009-11-18 Lars Kellogg-Stedman[notmuch] "notmuch help" outputs to stderr? (&=ui) 2009-11-18 Lars Kellogg-Stedman[notmuch] Working with Maildir storage? ( =ui) - 2009-11-18 Alex Botero-Lowry [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop(& ui) + 2009-11-18 Alex Botero-Lowry [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop(& ui) 2009-11-17 Ingmar Vanhassel [notmuch] [PATCH] Typsos ( ui) 2009-11-17 Aron Griffis [notmuch] archive ( ui) 2009-11-17 Adrian Perez de Cast[notmuch] Introducing myself ( =ui) @@ -37,7 +37,7 @@ 2009-11-17 Jan Janak [notmuch] What a great idea! ( ui) 2009-11-17 Jan Janak [notmuch] What a great idea! ( ui) 2009-11-17 Jan Janak [notmuch] [PATCH] Older versions of install do not support -C.( ui) - 2009-11-17 Keith Packard [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags( ui) + 2009-11-17 Keith Packard [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags( ui) 2009-11-17 Keith Packard [notmuch] Working with Maildir storage? ( ui) 2009-11-17 Keith Packard [notmuch] [PATCH 1/2] Close message file after parsing message headers( ui) 2009-11-17 Mikhail Gusarov [notmuch] [PATCH] Handle rename of message file ( ui) @@ -48,6 +48,6 @@ 2009-11-17 Mikhail Gusarov [notmuch] Working with Maildir storage? ( =ui) 2009-11-17 Lars Kellogg-Stedman[notmuch] Working with Maildir storage? ( =ui) 2009-11-17 Carl Worth [notmuch] [PATCH 1/2] Close message file after parsing message headers( ui) - 2009-11-17 Mikhail Gusarov [notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4( ui) - 2009-11-17 Mikhail Gusarov [notmuch] [PATCH 1/2] Close message file after parsing message headers( ui) + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4( ui) + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH 1/2] Close message file after parsing message headers( ui) End of search results. diff --git a/test/emacs.expected-output/notmuch-hello-all-tags b/test/emacs.expected-output/notmuch-hello-all-tags new file mode 100644 index 00000000..65e479fa --- /dev/null +++ b/test/emacs.expected-output/notmuch-hello-all-tags @@ -0,0 +1,11 @@ + Welcome to notmuch. You have 52 messages. + +Search: . + +All tags: [hide] + + 4 attachment 52 inbox 52 unread + 52 exclude_me 7 signed + + Hit `?' for context-sensitive help in any Notmuch screen. + Customize Notmuch or this page. diff --git a/test/emacs.expected-output/raw-message-cf0c4d-52ad0a b/test/emacs.expected-output/raw-message-cf0c4d-52ad0a index 75b05fa4..ce174b52 100644 --- a/test/emacs.expected-output/raw-message-cf0c4d-52ad0a +++ b/test/emacs.expected-output/raw-message-cf0c4d-52ad0a @@ -63,7 +63,7 @@ and &2 exit 1 fi diff --git a/test/gnupg-secret-key.NOTE b/test/gnupg-secret-key.NOTE deleted file mode 100644 index 604496c5..00000000 --- a/test/gnupg-secret-key.NOTE +++ /dev/null @@ -1,9 +0,0 @@ -How the crypto test gnupg secret was generated: - -GNUPGHOME=gnupghome gpg --quick-random --gen-key -kind: 1 (RSA/RSA) -size: 1024 -expire: 0 -name: Notmuch Test Suite -email: test_suite@notmuchmail.org -(no passphrase) diff --git a/test/gnupg-secret-key.asc b/test/gnupg-secret-key.asc deleted file mode 100644 index 6431b56c..00000000 --- a/test/gnupg-secret-key.asc +++ /dev/null @@ -1,34 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -lQHYBE1Mm18BBADlMsMlUeO6usp/XuulgimqlCSphHcYZvH6+Sy7u7W4TpJzid7e -jEOCrk3UZi2XMPW9+snDMhV9e28HeRz61zAO9G/gedn4N+mKOyTaELEmj9SP2IG2 -ZTvdUvn30vWIHyfRIww3qEiSzNULKn6zTDfcg6BIY6ZDQ6GFSfH5EioxuQARAQAB -AAP8CM2/sS9JZWLHZHJrmsU6fygxlaarlxmyhxwLG9WZ+qUJ+xDQqWZkhssrMigP -7ZQehwLwZ7mvbvfOy/qwTPJMZjQMMuTGEzclwBTOTttSxEDS+kgYmZ05CBjIgXbo -8+k+L347l+kVRBFsi1cqOkCr+VZQwhOnbeNb8uJsUx27aAECAPD7jsBP73LRgoXQ -x650D2fzjjuomGVsIxSAPjkDRYmtorsRftaEy7DkvX3Ihu5WN6YRRjJavoL+f8ar -4escR40CAPN7NOFOGmiFZYzQcfJYQI2m7YDk4B51JxORFvLrvGT+LJnVwhtFsdGS -QnMyO4eNpKH0qeEkT5Zqha2oyAc0Yd0B/3f962YCmYlbDAvWjcbMvhV7G4DbazVp -2TNR0BhhEMiOlHuwmTO59s2iukuE5ifaVbwrj+NgpipTsaffKnhALlGjV7Q7Tm90 -bXVjaCBUZXN0IFN1aXRlIDx0ZXN0X3N1aXRlQG5vdG11Y2htYWlsLm9yZz4gKElO -U0VDVVJFISmIuAQTAQIAIgUCTUybXwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgEC -F4AACgkQbZJhLZTkY4GJFAP9E0mOw+RUGdmqbxSbd2rm0/inUSYOC0Pvt/D05pUY -xzXDAMZwsy1DWhfS7bSgdD3YTM/22b/LJ2FmbLUF1cU6cNslmdPdfHDZ5+C4qpa1 -uW11y7djlBFAwxc3NBypT6Bmh/iIixrx413cw8CEU0lSZbSXUvbxZ7Rg4JYm2K6f -Y7SdAdgETUybXwEEAM74QJJWzPavquSF0IkKDFjEvI44WC1HGNsJF3JVuKv9G00P -RaHavNNcHEG8MorbfaWk7pipaEJ3+zbPKgp2vRCSJnLL6z813JIQqXJTZzu1ip63 -s4icfOfXkxFJ5AaFd/pVdi+wjmEwvv+YMtJT9DyXANo6b2eQu+0bMtP4Xuv/ABEB -AAEAA/wJArUJw450070K6eoXeg22wT0iq/O0aCExSzoI5Kmywytj6KnnAmp9TftL -WVgrkQntVjrhzPsYoB40JEMrGKd7QL/6LPTNWq3eFW38PSpCiG83T0rtmKCKqHB1 -Uo0B78AHfYYX7MUOEuCq2AhKTAdZukesoCpmVxcEFtjDEbOB8QIA3cvXrPJN/J2S -W61mdMT7KlaXZZD8Phs/TY2ZLAiMKUAP1dVYNDvRSDjZLvQrqKQjEAN5jM81cWAV -pvOIavLhOwIA7uMVIiaQ3vIy10C7ltiLT6YuJL/O6XDnXY/PDuXOatQahd/gmI0q -dGQLSaHIxYILPZPaW6t0orx+dduQ0ES0DQIA21nEKX0MZpYOY1eIt6OlKemsjL2a -UTdFhq/OgwVv+QRVHNdYQXmKpKDeW30lN/+BI3zyDTZjtehwKMMxNTu4AJu/iJ8E -GAECAAkFAk1Mm18CGwwACgkQbZJhLZTkY4H8kgQA4vHsTt8dlJdWJAu2SKZGOPRs -bIPu5XtRXe3RYbW5H7PqbAnrKIzlIKpkPNTwLL4wVXaF+R/aHa8ZKX3paohrPL74 -qpbffwtHXyVEwyWlw3m9mgti0de1dy1YvVasCe/UQ8Frc6uNmOwtlQE20k4R4cLI -SWXT1JrwPoKh9xe++90= -=rvTR ------END PGP PRIVATE KEY BLOCK----- diff --git a/test/notmuch-test b/test/notmuch-test index 14747bdb..5d27e4d1 100755 --- a/test/notmuch-test +++ b/test/notmuch-test @@ -20,6 +20,14 @@ fi set -eu +# Where to run the tests +# XXX FIXME this code is duplicated with test-lib.sh +if [[ -n "${NOTMUCH_BUILDDIR}" ]]; then + TEST_DIRECTORY=$NOTMUCH_BUILDDIR/test +else + TEST_DIRECTORY=$NOTMUCH_SRCDIR/test +fi + TESTS= for test in ${NOTMUCH_TESTS-}; do TESTS="$TESTS $NOTMUCH_SRCDIR/test/$test" @@ -80,7 +88,7 @@ for file in $TESTS do file=${file##*/} # drop leading path components file=${file%.sh} # drop trailing '.sh' - RESULT_FILES="$RESULT_FILES $NOTMUCH_BUILDDIR/test/test-results/$file" + RESULT_FILES="$RESULT_FILES $TEST_DIRECTORY/test-results/$file" done echo @@ -94,6 +102,6 @@ if [ -n "$META_FAILURE" ]; then fi # Clean up -rm -rf $NOTMUCH_BUILDDIR/test/test-results +rm -rf $TEST_DIRECTORY/test-results exit $ev diff --git a/test/notmuch-test.h b/test/notmuch-test.h index 34dbb8e0..ed713099 100644 --- a/test/notmuch-test.h +++ b/test/notmuch-test.h @@ -1,7 +1,21 @@ #ifndef _NOTMUCH_TEST_H #define _NOTMUCH_TEST_H +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include + #include inline static void @@ -14,4 +28,23 @@ expect0 (int line, notmuch_status_t ret) } #define EXPECT0(v) expect0 (__LINE__, v); + +inline static void * +dlsym_next (const char *symbol) +{ + void *sym = dlsym (RTLD_NEXT, symbol); + char *str = dlerror (); + + if (str != NULL) { + fprintf (stderr, "finding symbol '%s' failed: %s", symbol, str); + exit (77); + } + return sym; +} + +#define WRAP_DLFUNC(_rtype, _func, _args) \ + _rtype _func _args; \ + _rtype _func _args { \ + static _rtype (*_func##_orig) _args = NULL; \ + if (! _func##_orig ) *(void **) (&_func##_orig) = dlsym_next (#_func); #endif diff --git a/test/openpgp4-secret-key.asc b/test/openpgp4-secret-key.asc new file mode 100644 index 00000000..182a8249 --- /dev/null +++ b/test/openpgp4-secret-key.asc @@ -0,0 +1,15 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lFgEYxhtlxYJKwYBBAHaRw8BAQdA0PoNKr90DaQV1dIK77wbWm4RT+JQzqBkwIjA +HQM9RHYAAQDQ5wSfkOGXvKYroALWgibztISzXS5b8boGXykcHERo6w/ctDtOb3Rt +dWNoIFRlc3QgU3VpdGUgKElOU0VDVVJFISkgPHRlc3Rfc3VpdGVAbm90bXVjaG1h +aWwub3JnPoiQBBMWCAA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEEmjr+ +bGAGWhSP1LWKfmq+kkZFzGAFAmMYbZwACgkQfmq+kkZFzGDtrwEAjQRn3xhEomah +wICjQjfi4RKNbvnRViZgosijDBANUAgA/28GrK1tPnQsXWqmuZxQ1Cd5ry4NAnj/ +4jsxD3cTbnEHnF0EYxhtlxIKKwYBBAGXVQEFAQEHQEOd3EyCD5qo4+QuHz0lruCG +VM6n6RI4dtAh3cX9uHwiAwEIBwAA/1oe+p5jNjNE5lEj4yTpYjCxCeC98MolbiAy +0yY7526wECqIeAQYFggAIBYhBJo6/mxgBloUj9S1in5qvpJGRcxgBQJjGG2XAhsM +AAoJEH5qvpJGRcxgBdsA/R9ZECoxai5QhOitDIAUZVCRr59Pm1VMPiJOOIla2N1p +AQCNESwJ9IJOdO/06q+bR2GG4WyEkB4VoVBiA3hFx/zZAA== +=uGTo +-----END PGP PRIVATE KEY BLOCK----- diff --git a/test/openpgp4-secret-key.asc.NOTE b/test/openpgp4-secret-key.asc.NOTE new file mode 100644 index 00000000..4693768e --- /dev/null +++ b/test/openpgp4-secret-key.asc.NOTE @@ -0,0 +1,5 @@ +The OpenPGPv4 secret key for the crypto tests was generated using: + +$ gpg --quick-generate-key \ + 'Notmuch Test Suite (INSECURE!) ' \ + future-default default never diff --git a/test/setup.expected-output/config-with-comments b/test/setup.expected-output/config-with-comments index 56c628e5..5d96be7f 100644 --- a/test/setup.expected-output/config-with-comments +++ b/test/setup.expected-output/config-with-comments @@ -1,18 +1,18 @@ # .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". +# Supported values are 'mail_root' and 'path'. The recommended option +# is to set 'mail_root' to the directory where your mail currently exists +# and to where mail will be delivered in the future. Files should be +# individual email messages. By default notmuch will store its database +# in $XDG_DATA_HOME/notmuch; you can override this by setting 'path'. +# If only 'path' is set, this directory is for 'mail_root' and for +# the database location (in a subdirectory called ".notmuch"). # [database] path=/path/to/maildir - # User configuration # # Here is where you can let notmuch know how you would like to be @@ -31,8 +31,7 @@ path=/path/to/maildir [user] name=Test Suite primary_email=test.suite@example.com -other_email=another.suite@example.com; - +other_email=another.suite@example.com # Configuration for "notmuch new" # # The following options are supported here: @@ -49,7 +48,6 @@ other_email=another.suite@example.com; # [new] tags=foo;bar; - # Search configuration # # The following option is supported here: @@ -60,8 +58,7 @@ tags=foo;bar; # query will override that exclusion. # [search] -exclude_tags=baz; - +exclude_tags=baz # Maildir compatibility configuration # # The following option is supported here: diff --git a/test/symbol-test.cc b/test/symbol-test.cc index 9d73a571..9e956ddf 100644 --- a/test/symbol-test.cc +++ b/test/symbol-test.cc @@ -12,8 +12,10 @@ main (int argc, char **argv) if (argc != 3) return 1; - if (notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, - ¬much, &message)) { + if (notmuch_database_open_with_config (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, + "", + NULL, + ¬much, &message)) { if (message) { fputs (message, stderr); free (message); diff --git a/test/test-lib-common.sh b/test/test-lib-common.sh index ebbf4cdf..f5d72e12 100644 --- a/test/test-lib-common.sh +++ b/test/test-lib-common.sh @@ -24,11 +24,25 @@ # type die >/dev/null 2>&1 || die () { echo "$@" >&2; exit 1; } -if [[ -z "$NOTMUCH_SRCDIR" ]] || [[ -z "$NOTMUCH_BUILDDIR" ]]; then +if [[ -z "$NOTMUCH_SRCDIR" ]] || [ -z "${NOTMUCH_TEST_INSTALLED-}" -a -z "$NOTMUCH_BUILDDIR" ]; then echo "internal: srcdir or builddir not set" >&2 exit 1 fi +# Explicitly require external prerequisite. Useful when binary is +# called indirectly (e.g. from emacs). +# Returns success if dependency is available, failure otherwise. +test_require_external_prereq () { + local binary + binary="$1" + if [[ ${test_missing_external_prereq_["${binary}"]} == t ]]; then + # dependency is missing, call the replacement function to note it + eval "$binary" + else + true + fi +} + backup_database () { test_name=$(basename $0 .sh) rm -rf $TMP_DIRECTORY/notmuch-dir-backup."$test_name" @@ -47,7 +61,9 @@ LD_LIBRARY_PATH=${TEST_DIRECTORY%/*}/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} export LD_LIBRARY_PATH # configure output -. "$NOTMUCH_BUILDDIR/sh.config" || exit 1 +if [ -z "${NOTMUCH_TEST_INSTALLED-}" ]; then + . "$NOTMUCH_BUILDDIR/sh.config" || exit 1 +fi # load OS specifics if [[ -e "$NOTMUCH_SRCDIR/test/test-lib-$PLATFORM.sh" ]]; then @@ -301,7 +317,12 @@ export PATH MANPATH # Test repository test="tmp.$(basename "$0" .sh)" -TMP_DIRECTORY="$TEST_DIRECTORY/$test" +if [ -z "${NOTMUCH_TEST_INSTALLED-}" ]; then + TMP_DIRECTORY="$TEST_DIRECTORY/$test" +else + TMP_DIRECTORY=$(mktemp -d "${TMPDIR:-/tmp}/notmuch-$test.XXXXXX") +fi + test ! -z "$debug" || remove_tmp=$TMP_DIRECTORY rm -rf "$TMP_DIRECTORY" || { GIT_EXIT_OK=t @@ -309,6 +330,10 @@ rm -rf "$TMP_DIRECTORY" || { exit 1 } +# Provide a guess at a usable Python, to support running tests without +# running configure first. +NOTMUCH_PYTHON=${NOTMUCH_PYTHON-python3} + # A temporary home directory is needed by at least: # - emacs/"Sending a message via (fake) SMTP" # - emacs/"Reply within emacs" diff --git a/test/test-lib-emacs.sh b/test/test-lib-emacs.sh index dde32177..0ab58fc2 100644 --- a/test/test-lib-emacs.sh +++ b/test/test-lib-emacs.sh @@ -30,6 +30,7 @@ test_require_emacs () { # to the message and encrypting/signing. emacs_deliver_message () { local subject body smtp_dummy_pid smtp_dummy_port + test_subtest_broken_for_installed subject="$1" body="$2" shift 2 @@ -54,8 +55,9 @@ emacs_deliver_message () { (message-goto-body) (insert \"${body}\") $* - (notmuch-mua-send-and-exit))" - + (let ((mml-secure-smime-sign-with-sender t) + (mml-secure-openpgp-sign-with-sender t)) + (notmuch-mua-send-and-exit)))" # In case message was sent properly, client waits for confirmation # before exiting and resuming control here; therefore making sure # that server exits by sending (KILL) signal to it is safe. @@ -143,6 +145,13 @@ emacs_generate_script () { # Construct a little test script here for the benefit of the user, # (who can easily run "run_emacs" to get the same emacs environment # for investigating any failures). + if [ -z "${NOTMUCH_TEST_INSTALLED-}" ]; then + find_notmuch_el='--directory "$NOTMUCH_BUILDDIR/emacs"' + else + ### XXX FIXME: this should really use the installed emacs lisp files + find_notmuch_el='--directory "$NOTMUCH_SRCDIR/emacs"' + fi + cat <"$TMP_DIRECTORY/run_emacs" #!/bin/sh export PATH=$PATH @@ -157,8 +166,8 @@ export NOTMUCH_CONFIG=$NOTMUCH_CONFIG # # --load Force loading of notmuch.el and test-lib.el -exec ${TEST_EMACS} --quick \ - --directory "$NOTMUCH_BUILDDIR/emacs" --load notmuch.el \ +exec ${TEST_EMACS} ${find_notmuch_el} --quick \ + ${EXTRA_DIR} --load notmuch.el \ --directory "$NOTMUCH_SRCDIR/test" --load test-lib.el \ "\$@" EOF @@ -206,4 +215,12 @@ test_emacs () { ${TEST_EMACSCLIENT} --socket-name="$EMACS_SERVER" --eval "(notmuch-test-progn $*)" } +time_emacs () { + rm -f MESSAGES + printf "%s" "$1" + shift + test_emacs "(test-time $*)" > emacs.out + tail -n 1 MESSAGES +} + emacs_generate_script diff --git a/test/test-lib.el b/test/test-lib.el index 6831b46f..bf1fab66 100644 --- a/test/test-lib.el +++ b/test/test-lib.el @@ -22,8 +22,20 @@ ;;; Code: +;; minimize impact of native compilation on the test suite. +;; These are the Emacs 29.1 version of the variables. +;; Leave trampolines enabled per Emacs upstream recommendations. +;; It is important to set these variables before loading any +;; .elc files. +(setq native-comp-jit-compilation nil) +(setq native-comp-speed -1) +(setq native-comp-async-jobs-number 1) + (require 'cl-lib) +;; Use old pretty print algorithm, so tests don't break with Emacs 30 +(setq-default pp-default-function 'pp-28) + ;; Ensure that the dynamic variables that are defined by this library ;; are defined by the time that we let-bind them. This is needed ;; because starting with Emacs 27 undeclared variables in evaluated @@ -186,6 +198,11 @@ running, quit if it terminated." (t (message "%s" err))) (with-current-buffer "*Messages*" (test-output "MESSAGES")))) +(defmacro test-time (&rest body) + `(let ((results (mapcar (lambda (x) (/ x 5.0)) (benchmark-run 5 ,@body)))) + (message "\t\t%0.2f\t%0.2f\t%0.2f" (nth 0 results) (nth 1 results) (nth 2 results)) + (with-current-buffer "*Messages*" (test-output "MESSAGES")))) + ;; For historical reasons, we hide deleted tags by default in the test ;; suite (setq notmuch-tag-deleted-formats @@ -200,3 +217,8 @@ running, quit if it terminated." ;; environments (setq mm-text-html-renderer 'html2text) + +;; Set our own default for message-hidden-headers, to avoid tests +;; breaking when the Emacs default changes. +(setq message-hidden-headers + '("^References:" "^Face:" "^X-Face:" "^X-Draft-From:")) diff --git a/test/test-lib.sh b/test/test-lib.sh index 32f710a5..5b3cd454 100644 --- a/test/test-lib.sh +++ b/test/test-lib.sh @@ -29,8 +29,8 @@ shopt -u xpg_echo # Ensure NOTMUCH_SRCDIR and NOTMUCH_BUILDDIR are set. . $(dirname "$0")/export-dirs.sh || exit 1 -# It appears that people try to run tests without building... -if [[ ! -x "$NOTMUCH_BUILDDIR/notmuch" ]]; then +# We need either a built tree, or a promise of an installed notmuch +if [ -z "${NOTMUCH_TEST_INSTALLED-}" -a ! -x "$NOTMUCH_BUILDDIR/notmuch" ]; then echo >&2 'You do not seem to have built notmuch yet.' exit 1 fi @@ -64,62 +64,14 @@ exec 6>&1 7>&2 BASH_XTRACEFD=7 export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' -# Keep the original TERM for say_color and test_emacs -ORIGINAL_TERM=$TERM - -# Set SMART_TERM to vt100 for known dumb/unknown terminal. -# Otherwise use whatever TERM is currently used so that -# users' actual TERM environments are being used in tests. -case ${TERM-} in - '' | dumb | unknown ) - SMART_TERM=vt100 ;; - *) - SMART_TERM=$TERM ;; -esac - -# For repeatability, reset the environment to known value. -LANG=C -LC_ALL=C -PAGER=cat -TZ=UTC -TERM=dumb -export LANG LC_ALL PAGER TERM TZ -GIT_TEST_CMP=${GIT_TEST_CMP:-diff -u} -if [[ ( -n "$TEST_EMACS" && -z "$TEST_EMACSCLIENT" ) || \ - ( -z "$TEST_EMACS" && -n "$TEST_EMACSCLIENT" ) ]]; then - echo "error: must specify both or neither of TEST_EMACS and TEST_EMACSCLIENT" >&2 - exit 1 -fi -TEST_EMACS=${TEST_EMACS:-${EMACS:-emacs}} -TEST_EMACSCLIENT=${TEST_EMACSCLIENT:-emacsclient} -TEST_GDB=${TEST_GDB:-gdb} -TEST_CC=${TEST_CC:-cc} -TEST_CFLAGS=${TEST_CFLAGS:-"-g -O0"} -TEST_SHIM_CFLAGS=${TEST_SHIM_CFLAGS:-"-fpic -shared"} -TEST_SHIM_LDFLAGS=${TEST_SHIM_LDFLAGS:-"-ldl"} - -# Protect ourselves from common misconfiguration to export -# CDPATH into the environment -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 -unset NAME +. "$NOTMUCH_SRCDIR/test/test-vars.sh" || exit 1 add_gnupg_home () { [ -e "${GNUPGHOME}/gpg.conf" ] && return _gnupg_exit () { gpgconf --kill all 2>/dev/null || true; } at_exit_function _gnupg_exit mkdir -p -m 0700 "$GNUPGHOME" - gpg --no-tty --import <$NOTMUCH_SRCDIR/test/gnupg-secret-key.asc >"$GNUPGHOME"/import.log 2>&1 + gpg --no-tty --import <$NOTMUCH_SRCDIR/test/openpgp4-secret-key.asc >"$GNUPGHOME"/import.log 2>&1 test_debug "cat $GNUPGHOME/import.log" if (gpg --quick-random --version >/dev/null 2>&1) ; then echo quick-random >> "$GNUPGHOME"/gpg.conf @@ -129,8 +81,8 @@ add_gnupg_home () { echo no-emit-version >> "$GNUPGHOME"/gpg.conf # Change this if we ship a new test key - FINGERPRINT="5AEAB11F5E33DCE875DDB75B6D92612D94E46381" - SELF_USERID="Notmuch Test Suite (INSECURE!)" + FINGERPRINT="9A3AFE6C60065A148FD4B58A7E6ABE924645CC60" + SELF_USERID="Notmuch Test Suite (INSECURE!) " SELF_EMAIL="test_suite@notmuchmail.org" printf '%s:6:\n' "$FINGERPRINT" | gpg --quiet --batch --no-tty --import-ownertrust } @@ -145,7 +97,7 @@ add_gpgsm_home () { mkdir -p -m 0700 "$GNUPGHOME" gpgsm --batch --no-tty --no-common-certs-import --pinentry-mode=loopback --passphrase-fd 3 \ --disable-dirmngr --import >"$GNUPGHOME"/import.log 2>&1 3<<<'' <$NOTMUCH_SRCDIR/test/smime/0xE0972A47.p12 - fpr=$(gpgsm --batch --list-key test_suite@notmuchmail.org | sed -n 's/.*fingerprint: //p') + fpr=$(gpgsm --batch --with-colons --list-key test_suite@notmuchmail.org | awk -F: '/^fpr/ {print $10}') echo "$fpr S relax" >> "$GNUPGHOME/trustlist.txt" gpgsm --quiet --batch --no-tty --no-common-certs-import --disable-dirmngr --import < $NOTMUCH_SRCDIR/test/smime/ca.crt echo "4D:E0:FF:63:C0:E9:EC:01:29:11:C8:7A:EE:DA:3A:9A:7F:6E:C1:0D S" >> "$GNUPGHOME/trustlist.txt" @@ -330,11 +282,6 @@ die () { exit 1 } -GIT_EXIT_OK= -# Note: TEST_TMPDIR *NOT* exported! -TEST_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/notmuch-test-$$.XXXXXX") -# Put GNUPGHOME in TMPDIR to avoid problems with long paths. -export GNUPGHOME="${TEST_TMPDIR}/gnupg" trap 'trap_exit' EXIT trap 'trap_signal' HUP INT TERM @@ -432,6 +379,28 @@ test_expect_equal_file () { test_diff_file_ "$1" "$2" } +# Like test_expect_equal_file, but compare the part of the two files after the first blank line +test_expect_equal_message_body () { + exec 1>&6 2>&7 # Restore stdout and stderr + if [ -z "$inside_subtest" ]; then + error "bug in the test script: test_expect_equal_file without test_begin_subtest" + fi + test "$#" = 2 || + error "bug in the test script: not 2 parameters to test_expect_equal_file" + + for file in "$1" "$2"; do + if [ ! -s "$file" ]; then + test_failure_ "Missing or zero length file: $file" + inside_subtest= + return 1 + fi + done + + expected=$(sed '1,/^$/d' "$1") + output=$(sed '1,/^$/d' "$2") + test_expect_equal "$expected" "$output" +} + # Like test_expect_equal, but takes two filenames. Fails if either is empty test_expect_equal_file_nonempty () { exec 1>&6 2>&7 # Restore stdout and stderr @@ -529,7 +498,7 @@ notmuch_debug_sanitize () { } notmuch_exception_sanitize () { - perl -pe 's/(A Xapian exception occurred at .*[.]cc?):([0-9]*)/\1:XXX/' + perl -pe 's,(A Xapian exception occurred at) .*?([^/]*[.]cc?):([0-9]*),\1 \2:XXX,' } notmuch_search_sanitize () { @@ -537,7 +506,7 @@ notmuch_search_sanitize () { } notmuch_search_files_sanitize () { - notmuch_dir_sanitize + notmuch_dir_sanitize | sed 's/msg-[0-9][0-9][0-9]/msg-XXX/' } notmuch_dir_sanitize () { @@ -561,11 +530,27 @@ notmuch_json_show_sanitize () { -e 's|"id": "[^"]*",|"id": "XXXXX",|g' \ -e 's|"Date": "Fri, 05 Jan 2001 [^"]*0000"|"Date": "GENERATED_DATE"|g' \ -e 's|"filename": "signature.asc",||g' \ + -e 's|"duplicate": 1,||g' \ -e 's|"filename": \["/[^"]*"\],|"filename": \["YYYYY"\],|g' \ -e 's|"timestamp": 97.......|"timestamp": 42|g' \ -e 's|"content-length": [1-9][0-9]*|"content-length": "NONZERO"|g' } +notmuch_sexp_show_sanitize () { + sed \ + -e 's|:id "[^"]*"|:id "XXXXX"|g' \ + -e 's|:Date "Sat, 01 Jan 2000 [^"]*0000"|:Date "GENERATED_DATE"|g' \ + -e 's|:filename "signature.asc"||g' \ + -e 's|:duplicate 1 ||g' \ + -e 's|:filename ("/[^"]*")|:filename ("YYYYY")|g' \ + -e 's|:timestamp 9........|:timestamp 42|g' \ + -e 's|:content-length [1-9][0-9]*|:content-length "NONZERO"|g' +} + +notmuch_sexp_search_sanitize () { + sed -e 's|:thread "[^"]*"|:thread "XXX"|' +} + notmuch_emacs_error_sanitize () { local command command=$1 @@ -584,6 +569,10 @@ notmuch_date_sanitize () { -e 's/^Date: Fri, 05 Jan 2001 .*0000/Date: GENERATED_DATE/' } +# remove redundant parts of notmuch-git internal paths +notmuch_git_sanitize () { + sed -e 's,tags/\([0-9a-f]\{2\}/\)\{2\},,' -e '/FORMAT/d' +} notmuch_uuid_sanitize () { sed 's/[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}/UUID/g' } @@ -642,20 +631,6 @@ $binary () { fi } -# Explicitly require external prerequisite. Useful when binary is -# called indirectly (e.g. from emacs). -# Returns success if dependency is available, failure otherwise. -test_require_external_prereq () { - local binary - binary="$1" - if [[ ${test_missing_external_prereq_["${binary}"]} == t ]]; then - # dependency is missing, call the replacement function to note it - eval "$binary" - else - true - fi -} - # You are not expected to call test_ok_ and test_failure_ directly, use # the text_expect_* functions instead. @@ -773,6 +748,18 @@ test_subtest_known_broken () { test_subtest_known_broken_=t } +test_subtest_broken_for_installed () { + if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + test_subtest_known_broken_=t + fi +} + +test_subtest_broken_for_root () { + if [ "$EUID" = "0" ]; then + test_subtest_known_broken_=t + fi +} + test_expect_success () { exec 1>&6 2>&7 # Restore stdout and stderr if [ -z "$inside_subtest" ]; then @@ -909,7 +896,7 @@ test_done () { test_python () { # Note: if there is need to print debug information from python program, # use stdout = os.fdopen(6, 'w') or stderr = os.fdopen(7, 'w') - PYTHONPATH="$NOTMUCH_SRCDIR/bindings/python${PYTHONPATH:+:$PYTHONPATH}" \ + PYTHONPATH="$NOTMUCH_BUILDDIR/bindings/python-cffi/build/stage${PYTHONPATH:+:$PYTHONPATH}" \ $NOTMUCH_PYTHON -B - > OUTPUT } @@ -925,6 +912,19 @@ test_C () { notmuch_dir_sanitize OUTPUT.stdout OUTPUT.stderr | notmuch_exception_sanitize | notmuch_debug_sanitize > OUTPUT } +test_private_C () { + local exec_file test_file + exec_file="test${test_count}" + test_file="${exec_file}.c" + echo '#include ' > ${test_file} + cat >> ${test_file} + ${TEST_CC} ${TEST_CFLAGS} -I${NOTMUCH_SRCDIR}/test -I${NOTMUCH_SRCDIR}/lib -I${NOTMUCH_SRCDIR}/util -I${NOTMUCH_SRCDIR}/compat ${NOTMUCH_GMIME_CFLAGS} -o ${exec_file} ${test_file} ${NOTMUCH_BUILDDIR}/lib/libnotmuch.a ${NOTMUCH_GMIME_LDFLAGS} ${NOTMUCH_XAPIAN_LDFLAGS} ${NOTMUCH_BUILDDIR}/util/libnotmuch_util.a ${NOTMUCH_SFSEXP_LDFLAGS} ${NOTMUCH_BUILDDIR}/parse-time-string/libparse-time-string.a -ltalloc -lstdc++ + echo "== stdout ==" > OUTPUT.stdout + echo "== stderr ==" > OUTPUT.stderr + ./${exec_file} "$@" 1>>OUTPUT.stdout 2>>OUTPUT.stderr + notmuch_dir_sanitize OUTPUT.stdout OUTPUT.stderr | notmuch_exception_sanitize | notmuch_debug_sanitize > OUTPUT +} + make_shim () { local base_name test_file shim_file base_name="$1" @@ -935,11 +935,16 @@ make_shim () { } notmuch_with_shim () { - local base_name shim_file - base_name="$1" + local base_name shim_file notmuch_cmd + if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then + notmuch_cmd="notmuch" + else + notmuch_cmd="notmuch-shared" + fi + base_name=$1 shift shim_file="${base_name}.so" - LD_PRELOAD=${LD_PRELOAD:+:$LD_PRELOAD}:./${shim_file} notmuch-shared "$@" + LD_PRELOAD=${LD_PRELOAD:+:$LD_PRELOAD}:./${shim_file} $notmuch_cmd "$@" } # Creates a script that counts how much time it is executed and calls @@ -991,7 +996,11 @@ test_init_ () { # Where to run the tests -TEST_DIRECTORY=$NOTMUCH_BUILDDIR/test +if [[ -n "${NOTMUCH_BUILDDIR}" ]]; then + TEST_DIRECTORY=$NOTMUCH_BUILDDIR/test +else + TEST_DIRECTORY=$NOTMUCH_SRCDIR/test +fi . "$NOTMUCH_SRCDIR/test/test-lib-common.sh" || exit 1 diff --git a/test/test-vars.sh b/test/test-vars.sh new file mode 100644 index 00000000..02d60f89 --- /dev/null +++ b/test/test-vars.sh @@ -0,0 +1,62 @@ +# Common variable settings for (correctness) tests and performance +# tests. + +# Keep the original TERM for say_color and test_emacs +ORIGINAL_TERM=$TERM + +# Set SMART_TERM to vt100 for known dumb/unknown terminal. +# Otherwise use whatever TERM is currently used so that +# users' actual TERM environments are being used in tests. +case ${TERM-} in + '' | dumb | unknown ) + SMART_TERM=vt100 ;; + *) + SMART_TERM=$TERM ;; +esac + +# For repeatability, reset the environment to known value. +LANG=C +LC_ALL=C +PAGER=cat +TZ=UTC +TERM=dumb +export LANG LC_ALL PAGER TERM TZ +GIT_TEST_CMP=${GIT_TEST_CMP:-diff -u} +if [[ ( -n "$TEST_EMACS" && -z "$TEST_EMACSCLIENT" ) || \ + ( -z "$TEST_EMACS" && -n "$TEST_EMACSCLIENT" ) ]]; then + echo "error: must specify both or neither of TEST_EMACS and TEST_EMACSCLIENT" >&2 + exit 1 +fi +TEST_EMACS=${TEST_EMACS:-${EMACS:-emacs}} +TEST_EMACSCLIENT=${TEST_EMACSCLIENT:-emacsclient} +TEST_GDB=${TEST_GDB:-gdb} +TEST_CC=${TEST_CC:-cc} +TEST_CFLAGS=${TEST_CFLAGS:-"-g -O0"} +TEST_SHIM_CFLAGS=${TEST_SHIM_CFLAGS:-"-fpic -shared"} +TEST_SHIM_LDFLAGS=${TEST_SHIM_LDFLAGS:-"-ldl"} + +# Protect ourselves from common misconfiguration to export +# CDPATH into the environment +unset CDPATH + +unset GREP_OPTIONS + +# For lib/open.cc:_load_key_file +unset XDG_CONFIG_HOME + +# for lib/open.cc:_choose_database_path +unset XDG_DATA_HOME +unset MAILDIR + +# For emacsclient +unset ALTERNATE_EDITOR + +# for reproducibility +unset EMAIL +unset NAME + +GIT_EXIT_OK= +# Note: TEST_TMPDIR *NOT* exported! +TEST_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/notmuch-test-$$.XXXXXX") +# Put GNUPGHOME in TMPDIR to avoid problems with long paths. +export GNUPGHOME="${TEST_TMPDIR}/gnupg" diff --git a/util/hex-escape.h b/util/hex-escape.h index 8703334c..83a4c6f1 100644 --- a/util/hex-escape.h +++ b/util/hex-escape.h @@ -5,7 +5,7 @@ extern "C" { #endif -typedef enum hex_status { +typedef enum { HEX_SUCCESS = 0, HEX_SYNTAX_ERROR, HEX_OUT_OF_MEMORY diff --git a/util/string-util.c b/util/string-util.c index 9c46a81a..03d7648d 100644 --- a/util/string-util.c +++ b/util/string-util.c @@ -42,13 +42,15 @@ const char * strsplit_len (const char *s, char delim, size_t *len) { bool escaping = false; - size_t count = 0; + size_t count = 0, last_nonspace = 0; - /* Skip initial unescaped delimiters */ - while (*s && *s == delim) + /* Skip initial unescaped delimiters and whitespace */ + while (*s && (*s == delim || isspace (*s))) s++; while (s[count] && (escaping || s[count] != delim)) { + if (! isspace (s[count])) + last_nonspace = count; escaping = (s[count] == '\\'); count++; } @@ -56,7 +58,7 @@ strsplit_len (const char *s, char delim, size_t *len) if (count == 0) return NULL; - *len = count; + *len = last_nonspace + 1; return s; } diff --git a/util/xapian-extra.h b/util/xapian-extra.h new file mode 100644 index 00000000..39c7f48f --- /dev/null +++ b/util/xapian-extra.h @@ -0,0 +1,15 @@ +#ifndef _XAPIAN_EXTRA_H +#define _XAPIAN_EXTRA_H + +#include +#include + +inline Xapian::Query +xapian_query_match_all (void) +{ + // Xapian::Query::MatchAll isn't thread safe (a static object with reference + // counting) so instead reconstruct the equivalent on demand. + return Xapian::Query (std::string ()); +} + +#endif diff --git a/version.txt b/version.txt index 61d2f357..751b1ea0 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.34 +0.39 diff --git a/vim/README b/vim/README index c137bacd..777c20c0 100644 --- a/vim/README +++ b/vim/README @@ -53,10 +53,10 @@ Enjoy ;) As an example to configure a key mapping to add the tag 'to-do' and archive, this is what I use: -let g:notmuch_rb_custom_search_maps = { +let g:notmuch_custom_search_maps = { \ 't': 'search_tag("+to-do -inbox")', \ } -let g:notmuch_rb_custom_show_maps = { +let g:notmuch_custom_show_maps = { \ 't': 'show_tag("+to-do -inbox")', \ } diff --git a/vim/notmuch.txt b/vim/notmuch.txt index 43741022..c98f2b53 100644 --- a/vim/notmuch.txt +++ b/vim/notmuch.txt @@ -89,7 +89,7 @@ s Send CONFIGURATION *notmuch-config* You can add the following configurations to your `.vimrc`, or -`~/.vim/plugin/notmuch.vim`. +`~/.vim/after/plugin/notmuch.vim`. *g:notmuch_folders* @@ -138,13 +138,13 @@ You can do the same for the thread view: If you want to count the threads instead of the messages in the folder view: > - let g:notmuch_folders_count_threads = 0 + let g:notmuch_folders_count_threads = 1 < *g:notmuch_reader* *g:notmuch_sendmail* -You can also configure your externail mail reader and sendemail program: +You can also configure your external mail reader and sendmail program: > let g:notmuch_reader = 'mutt -f %s' let g:notmuch_sendmail = 'sendmail'