]> git.notmuchmail.org Git - notmuch/commitdiff
emacs: Add new option notmuch-search-hide-excluded master
authorMohsin Kaleem <mohkale@kisara.moe>
Sun, 10 Mar 2024 18:57:41 +0000 (18:57 +0000)
committerDavid Bremner <david@tethera.net>
Sat, 6 Apr 2024 18:03:45 +0000 (15:03 -0300)
The new notmuch-search-hide-excluded option allows users to configure whether
to show or hide excluded messages (as determined by search.exclude_tags
in the local notmuch config file). It defaults to true for now to maintain
backwards-compatibility with how notmuch-{search,tree} already worked.

New commands notmuch-search-toggle-hide-excluded and
notmuch-tree-toggle-exclude have also been added. They toggle the value
of notmuch-search-hide-excluded for the search in the current search or
tree buffer. It's bound to "i" in the respective keymaps for these
modes.

Lastly I've amended some calls to notmuch-tree and notmuch-unthreaded
which didn't pass through the buffer local value of
notmuch-search-oldest-first (and now notmuch-search-exclude).
Examples of where I've done this include:
  + notmuch-jump-search
  + notmuch-tree-from-search-current-query
  + notmuch-unthreaded-from-search-current-query
  + notmuch-tree-from-search-thread

A new test file for Emacs has been added which covers the usage of the
new `notmuch-search-hide-excluded' option and interactively hiding or
showing mail with excluded tags. These test cover the basic usage of
the `notmuch-search-toggle-hide-excluded' command in notmuch-search,
notmuch-tree and notmuch-unthreaded searches. These tests also cover
the persistence of the current value of the hide-excluded mail option
as a user switches from between these different search commands.

[1]: id:87ilxlxsng.fsf@kisara.moe

Amended-by: db, fix indentation in T461-emacs-search-exclude.sh
369 files changed:
.gitignore
.travis.yml
INSTALL
Makefile.global
Makefile.local
NEWS
README
bindings/Makefile.local
bindings/python-cffi/notmuch2/_build.py
bindings/python-cffi/notmuch2/_config.py
bindings/python-cffi/notmuch2/_database.py
bindings/python-cffi/notmuch2/_errors.py
bindings/python-cffi/notmuch2/_message.py
bindings/python-cffi/setup.py
bindings/python-cffi/tests/conftest.py
bindings/python-cffi/tests/test_config.py
bindings/python-cffi/tests/test_database.py
bindings/python-cffi/tests/test_errors.py [new file with mode: 0644]
bindings/python-cffi/tests/test_message.py
bindings/python-cffi/tests/test_tags.py
bindings/python-cffi/tests/test_thread.py
bindings/python-cffi/version.txt [deleted file]
bindings/python/notmuch/compat.py
bindings/python/notmuch/version.py
bindings/ruby/database.c
bindings/ruby/defs.h
bindings/ruby/directory.c
bindings/ruby/extconf.rb
bindings/ruby/filenames.c
bindings/ruby/init.c
bindings/ruby/message.c
bindings/ruby/messages.c
bindings/ruby/query.c
bindings/ruby/tags.c
bindings/ruby/thread.c
bindings/ruby/threads.c
compat/Makefile.local
compat/canonicalize_file_name.c [deleted file]
compat/compat.h
compat/have_strcasestr.c
completion/notmuch-completion.bash
completion/zsh/_notmuch
configure
contrib/notmuch-mutt/README
contrib/notmuch-mutt/notmuch-mutt
debian/changelog
debian/control
debian/elpa-notmuch.elpa
debian/libnotmuch5.install [deleted file]
debian/libnotmuch5.symbols [deleted file]
debian/libnotmuch5t64.install [new file with mode: 0644]
debian/libnotmuch5t64.lintian-overrides [new file with mode: 0644]
debian/libnotmuch5t64.symbols [new file with mode: 0644]
debian/notmuch-doc.install [new file with mode: 0644]
debian/notmuch-emacs.maintscript
debian/notmuch-git.install [new file with mode: 0644]
debian/notmuch-git.manpages [new file with mode: 0644]
debian/notmuch.maintscript
debian/notmuch.manpages
debian/rules
debian/tests/control [new file with mode: 0644]
devel/author-scan.sh
devel/check-notmuch-commit [new file with mode: 0755]
devel/emacs-keybindings.org
devel/nmbug/nmbug [deleted file]
devel/notmuch-web/nmgunicorn.py [new file with mode: 0644]
devel/notmuch-web/nmweb.py [new file with mode: 0755]
devel/notmuch-web/static/css/jquery-ui.css [new symlink]
devel/notmuch-web/static/css/notmuch-0.1.css [new file with mode: 0644]
devel/notmuch-web/static/js/jquery-ui.js [new symlink]
devel/notmuch-web/static/js/jquery.js [new symlink]
devel/notmuch-web/static/js/notmuch-0.1.js [new file with mode: 0644]
devel/notmuch-web/templates/base.html [new file with mode: 0644]
devel/notmuch-web/templates/index.html [new file with mode: 0644]
devel/notmuch-web/templates/search.html [new file with mode: 0644]
devel/notmuch-web/templates/show.html [new file with mode: 0644]
devel/notmuch-web/todo [new file with mode: 0644]
devel/release-checks.sh
devel/schemata
doc/Makefile.local
doc/command-line.rst [new file with mode: 0644]
doc/conf.py
doc/elisp.py [new file with mode: 0644]
doc/index.rst
doc/man1/notmuch-address.rst
doc/man1/notmuch-compact.rst
doc/man1/notmuch-config.rst
doc/man1/notmuch-count.rst
doc/man1/notmuch-dump.rst
doc/man1/notmuch-emacs-mua.rst
doc/man1/notmuch-git.rst [new file with mode: 0644]
doc/man1/notmuch-insert.rst
doc/man1/notmuch-new.rst
doc/man1/notmuch-reindex.rst
doc/man1/notmuch-reply.rst
doc/man1/notmuch-restore.rst
doc/man1/notmuch-search.rst
doc/man1/notmuch-show.rst
doc/man1/notmuch-tag.rst
doc/man1/notmuch.rst
doc/man5/notmuch-hooks.rst
doc/man7/notmuch-properties.rst
doc/man7/notmuch-search-terms.rst
doc/man7/notmuch-sexp-queries.rst [new file with mode: 0644]
doc/notmuch-emacs.rst
doc/queries.rst [new file with mode: 0644]
emacs/Makefile.local
emacs/coolj.el
emacs/notmuch-address.el
emacs/notmuch-company.el
emacs/notmuch-compat.el
emacs/notmuch-crypto.el
emacs/notmuch-draft.el
emacs/notmuch-emacs-mua
emacs/notmuch-hello.el
emacs/notmuch-jump.el
emacs/notmuch-lib.el
emacs/notmuch-logo.png [deleted file]
emacs/notmuch-logo.svg [new file with mode: 0644]
emacs/notmuch-maildir-fcc.el
emacs/notmuch-mua.el
emacs/notmuch-parser.el
emacs/notmuch-print.el
emacs/notmuch-query.el
emacs/notmuch-show.el
emacs/notmuch-tag.el
emacs/notmuch-tree.el
emacs/notmuch-wash.el
emacs/notmuch.el
emacs/rstdoc.el
gmime-filter-reply.c
gmime-filter-reply.h
hooks.c
lib/Makefile.local
lib/add-message.cc
lib/built-with.c
lib/config.cc
lib/database-private.h
lib/database.cc
lib/index.cc
lib/indexopts.c
lib/init.cc [new file with mode: 0644]
lib/lastmod-fp.cc [new file with mode: 0644]
lib/lastmod-fp.h [new file with mode: 0644]
lib/message-file.c
lib/message-property.cc
lib/message.cc
lib/notmuch-private.h
lib/notmuch.h
lib/open.cc
lib/parse-sexp.cc [new file with mode: 0644]
lib/parse-time-vrp.cc
lib/prefix.cc
lib/query-fp.cc
lib/query.cc
lib/regexp-fields.cc
lib/regexp-fields.h
lib/sexp-fp.cc [new file with mode: 0644]
lib/sexp-fp.h [new file with mode: 0644]
lib/string-map.c
lib/tags.c
lib/thread-fp.cc
lib/thread.cc
mime-node.c
notmuch-client-init.c [new file with mode: 0644]
notmuch-client.h
notmuch-compact.c
notmuch-config.c
notmuch-count.c
notmuch-dump.c
notmuch-git.py [new file with mode: 0644]
notmuch-insert.c
notmuch-new.c
notmuch-reindex.c
notmuch-reply.c
notmuch-restore.c
notmuch-search.c
notmuch-setup.c
notmuch-show.c
notmuch-tag.c
notmuch.c
performance-test/README
performance-test/T00-new.sh
performance-test/T02-tag.sh
performance-test/T03-reindex.sh
performance-test/T05-ruby.sh [new file with mode: 0755]
performance-test/T06-emacs.sh [new file with mode: 0755]
performance-test/T07-git.sh [new file with mode: 0755]
performance-test/download/notmuch-email-corpus-0.5.tar.xz.asc [new file with mode: 0644]
performance-test/perf-test-lib.sh
performance-test/version.sh
sprinter-json.c
sprinter-sexp.c
sprinter-text.c
sprinter.h
test/README
test/T000-basic.sh
test/T010-help-test.sh
test/T030-config.sh
test/T040-setup.sh
test/T050-new.sh
test/T051-new-renames.sh [new file with mode: 0755]
test/T055-path-config.sh
test/T060-count.sh
test/T070-insert.sh
test/T080-search.sh
test/T081-sexpr-search.sh [new file with mode: 0755]
test/T090-search-output.sh
test/T095-address.sh
test/T100-search-by-folder.sh
test/T131-show-limiting.sh [new file with mode: 0755]
test/T140-excludes.sh
test/T150-tagging.sh
test/T160-json.sh
test/T170-sexp.sh
test/T190-multipart.sh
test/T210-raw.sh
test/T220-reply.sh
test/T240-dump-restore.sh
test/T300-encoding.sh
test/T310-emacs.sh
test/T315-emacs-tagging.sh [new file with mode: 0755]
test/T320-emacs-large-search-buffer.sh
test/T330-emacs-subject-to-filename.sh
test/T350-crypto.sh
test/T355-smime.sh
test/T356-protected-headers.sh
test/T357-index-decryption.sh
test/T358-emacs-protected-headers.sh
test/T360-symbol-hiding.sh
test/T370-search-folder-coherence.sh
test/T380-atomicity.sh
test/T385-transactions.sh [new file with mode: 0755]
test/T390-python.sh
test/T391-python-cffi.sh
test/T392-python-cffi-notmuch.sh [new file with mode: 0755]
test/T395-ruby.sh
test/T400-hooks.sh
test/T405-external.sh [new file with mode: 0755]
test/T410-argument-parsing.sh
test/T420-emacs-test-functions.sh
test/T430-emacs-address-cleaning.sh
test/T440-emacs-hello.sh
test/T450-emacs-show.sh
test/T453-emacs-reply.sh [new file with mode: 0755]
test/T454-emacs-dont-reply-names.sh [new file with mode: 0755]
test/T455-emacs-charsets.sh
test/T460-emacs-tree.sh
test/T461-emacs-search-exclude.sh [new file with mode: 0755]
test/T465-emacs-unthreaded.sh [new file with mode: 0755]
test/T480-hex-escaping.sh
test/T490-parse-time-string.sh
test/T510-thread-replies.sh
test/T520-show.sh
test/T530-upgrade.sh
test/T550-db-features.sh
test/T560-lib-error.sh
test/T562-lib-database.sh
test/T563-lib-directory.sh
test/T564-lib-query.sh
test/T565-lib-tags.sh [new file with mode: 0755]
test/T566-lib-message.sh
test/T568-lib-thread.sh
test/T570-revision-tracking.sh
test/T590-libconfig.sh
test/T590-thread-breakage.sh [deleted file]
test/T592-thread-breakage.sh [new file with mode: 0755]
test/T595-reopen.sh
test/T610-message-property.sh
test/T620-lock.sh
test/T630-emacs-draft.sh
test/T640-database-modified.sh
test/T650-regexp-query.sh
test/T670-duplicate-mid.sh
test/T700-reindex.sh
test/T710-message-id.sh
test/T720-emacs-attachment-warnings.sh
test/T720-lib-lifetime.sh
test/T730-emacs-forwarding.sh
test/T750-gzip.sh
test/T750-user-header.sh
test/T760-as-text.sh [new file with mode: 0755]
test/T800-asan.sh [new file with mode: 0755]
test/T810-tsan.sh [new file with mode: 0755]
test/T810-tsan.suppressions [new file with mode: 0644]
test/T850-git.sh [new file with mode: 0755]
test/aggregate-results.sh
test/corpora/attachment/x-gtar-compressed.eml [new file with mode: 0644]
test/corpora/crypto/basic-encrypted.eml
test/corpora/crypto/encrypted-rfc822-attachment [new file with mode: 0644]
test/corpora/crypto/encrypted-signed.eml
test/corpora/default/bar/baz/05:2,
test/corpora/duplicate/msg-1-1:2, [new file with mode: 0644]
test/corpora/duplicate/msg-1-2:2, [new file with mode: 0644]
test/corpora/duplicate/msg-2-1:2, [new file with mode: 0644]
test/corpora/duplicate/msg-2-2:2, [new file with mode: 0644]
test/corpora/duplicate/msg-3-1:2, [new file with mode: 0644]
test/corpora/duplicate/msg-3-2:2, [new file with mode: 0644]
test/corpora/duplicate/msg-3-3:2, [new file with mode: 0644]
test/corpora/duplicate/msg-3-4:2, [new file with mode: 0644]
test/corpora/duplicate/msg-3-5:2, [new file with mode: 0644]
test/corpora/indexing/PATCH-1-2-system_data_types.7-srcfix.txt:2,S [new file with mode: 0644]
test/corpora/indexing/fake-pdf:2,S [new file with mode: 0644]
test/corpora/insert/mbox-attachment.eml [new file with mode: 0644]
test/corpora/mangling/mixed-up.eml
test/corpora/protected-headers/double-wrapped-with-phony-protected-header.eml
test/corpora/protected-headers/encrypted-message-with-forwarded-attachment.eml
test/corpora/protected-headers/encrypted-signed-not-masked.eml
test/corpora/protected-headers/encrypted-signed.eml
test/corpora/protected-headers/misplaced-protected-header.eml
test/corpora/protected-headers/nested-rfc822-message.eml
test/corpora/protected-headers/no-protected-header-attribute.eml
test/corpora/protected-headers/phony-protected-header-bad-encryption.eml
test/corpora/protected-headers/protected-header.eml
test/corpora/protected-headers/protected-with-legacy-display.eml
test/corpora/protected-headers/signed-protected-header.eml
test/corpora/protected-headers/simple-signed-mail.eml
test/corpora/protected-headers/subjectless-protected-header.eml
test/corpora/protected-headers/wrapped-protected-header.eml
test/emacs-exclude.expected-output/notmuch-search-tag-inbox-with-excluded [new file with mode: 0644]
test/emacs-exclude.expected-output/notmuch-search-tag-inbox-without-excluded [new file with mode: 0644]
test/emacs-exclude.expected-output/notmuch-tree-tag-inbox-with-excluded [new file with mode: 0644]
test/emacs-exclude.expected-output/notmuch-tree-tag-inbox-without-excluded [new file with mode: 0644]
test/emacs-exclude.expected-output/notmuch-unthreaded-tag-inbox-with-excluded [new file with mode: 0644]
test/emacs-exclude.expected-output/notmuch-unthreaded-tag-inbox-without-excluded [new file with mode: 0644]
test/emacs-reply.expected-output/notmuch-reply-duplicate-4 [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-depth [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-depth-1 [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-duplicate-4 [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-height-0 [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-multipart-alternative [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-size [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-size-450 [new file with mode: 0644]
test/emacs-tree.expected-output/inbox-outline [new file with mode: 0644]
test/emacs-tree.expected-output/notmuch-tree-tag-inbox-oldest-first [new file with mode: 0644]
test/emacs-tree.expected-output/result-format-function [new file with mode: 0644]
test/emacs-unthreaded.expected-output/result-format-function [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-all-tags [new file with mode: 0644]
test/emacs.expected-output/raw-message-cf0c4d-52ad0a
test/emacs.expected-output/search-result-format-function [new file with mode: 0644]
test/export-dirs.sh
test/gnupg-secret-key.NOTE [deleted file]
test/gnupg-secret-key.asc [deleted file]
test/notmuch-test
test/notmuch-test.h
test/openpgp4-secret-key.asc [new file with mode: 0644]
test/openpgp4-secret-key.asc.NOTE [new file with mode: 0644]
test/random-corpus.c
test/setup.expected-output/config-with-comments
test/smime/0xE0972A47.p12 [new file with mode: 0644]
test/symbol-test.cc
test/test-lib-common.sh
test/test-lib-emacs.sh [new file with mode: 0644]
test/test-lib.el
test/test-lib.sh
test/test-vars.sh [new file with mode: 0644]
util/Makefile.local
util/gmime-extra.c
util/gmime-extra.h
util/hex-escape.h
util/path-util.c [new file with mode: 0644]
util/path-util.h [new file with mode: 0644]
util/string-util.c
util/unicode-util.h
util/xapian-extra.h [new file with mode: 0644]
version.txt
vim/README
vim/notmuch.txt
vim/notmuch.vim

index 3edd1768ed85aac3dc6bfa94a91107a3df75c1cb..eda6d9cff7473613aca1e36d54fb4dd5e8872a2c 100644 (file)
 /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__
index 9dcec1ffc63a90aa34a9b11c2ee209aac70da994..5bb03de632941b82738f9c5f2d4c8a828f2199b9 100644 (file)
@@ -25,5 +25,5 @@ script:
 notifications:
   irc:
     channels:
-      - "chat.freenode.net#notmuch"
+      - "irc.libera.chat#notmuch"
     on_success: change
diff --git a/INSTALL b/INSTALL
index 40ea377af97d6b72325016044a8db37433671d3e..8054fafa1d34a05637323058e316b035bcd7fdff 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -32,13 +32,6 @@ Talloc, and zlib which are each described below:
 
        Xapian is available from https://xapian.org
 
-       Note: Notmuch will work best with Xapian 1.0.18 (or later) or
-       Xapian 1.1.4 (or later). Previous versions of Xapian (whether
-       1.0 or 1.1) had a performance bug that made notmuch very slow
-       when modifying tags. This would cause distracting pauses when
-       reading mail while notmuch would wait for Xapian when removing
-       the "inbox" and "unread" tags from messages in a thread.
-
        GMime
        -----
        GMime provides decoding of MIME email messages for Notmuch.
@@ -48,6 +41,15 @@ Talloc, and zlib which are each described below:
 
        GMime is available from https://github.com/jstedfast/gmime
 
+        Sfsexp
+        ------
+
+        sfsexp is the "small fast s-expression" library. Notmuch
+        optionally use it to provide a second query parser.
+
+        sfsexp is available from https://github.com/mjsottile/sfsexp.
+        In Debian Bookworm and later, install libsexp-dev.
+
        Talloc
        ------
        Talloc is a memory-pool allocator used by Notmuch.
index fe79121d018e0026716bf327e23536583c3e06a9..7a7a3c6d09ab687e6899cadd8055f83e6dbba07a 100644 (file)
@@ -50,9 +50,9 @@ 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
index bbb8f0b61a3fab43819e906ad3a2a9b3195b65f2..7699c208bb8eca696a35ae7ea0951a6fd8c44487 100644 (file)
@@ -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,6 +45,15 @@ $(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)
 
@@ -54,7 +63,6 @@ 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
 
 # We invoke make recursively only to force ordering of our phony
 # targets in the case of parallel invocation of make (-j).
@@ -74,7 +82,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 +101,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 +133,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 +181,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." ;; \
@@ -231,6 +242,7 @@ notmuch_client_srcs =               \
        gmime-filter-reply.c    \
        hooks.c                 \
        notmuch.c               \
+       notmuch-client-init.c   \
        notmuch-compact.c       \
        notmuch-config.c        \
        notmuch-count.c         \
diff --git a/NEWS b/NEWS
index 6e88ebd9f703fd72508a8a05b989c84566dd8225..cf8107f26362431549dc410cb8379de5c10221f1 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,627 @@
+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)
+=========================
+
+General
+-------
+
+An optional new s-expression based query parser is available if
+notmuch is built with the `sfsexp` library. See
+notmuch-sexp-queries(7) for syntax, and use `notmuch config get
+built_with.sexpr_query` to check if notmuch is compiled with
+s-expression query support.
+
+CLI
+---
+
+Support multiple `Delivered-To` headers in notmuch-reply(1).
+
+Emacs
+-----
+
+Functions are now allowed in `notmuch-search-result-format`.
+
+Improvements to unthreaded view on large threads.
+
+Tolerate bad/missing working directory for most commands.
+
+Allow customization of tree drawing symbols in notmuch-tree mode.
+
+Notmuch 0.33.2 (2021-09-30)
+===========================
+
+Tests
+-----
+
+Improve reliability of T355-smime by changing gpgsm initialization.
+
+Notmuch 0.33.1 (2021-09-10)
+===========================
+
+General
+-------
+
+Replace the fully-qualified-domain-name of the host with "localhost"
+in the default email address.  This should fix two flaky subtests in
+T590-libconfig.
+
+Notmuch 0.33 (2021-09-03)
+=========================
+
+Library
+-------
+
+Correct documentation about transactions.
+
+Add a configurable automatic commit of transactions. See
+`database.autocommit` in notmuch-config(1).
+
+Document the algorithm used to find a database.
+
+CLI
+---
+
+Define format version 5, which supports sorting the output of
+notmuch-show.
+
+Emacs
+-----
+
+`notmuch` no longer sets `mail-user-agent` on load. To restore the
+previous behaviour of using notmuch to send mail by default, customize
+`mail-user-agent` to `notmuch-user-agent`.
+
+`notmuch-company` now works in `org-msg`.
+
+Improve the display of messages from long threads in unthreaded mode.
+
+Prefer email addresses over User ID when showing valid signatures.
+
+Define a new face `notmuch-jump-key`.
+
+New commands in notmuch-tree view: `notmuch-tree-filter` and `notmuch-tree-filter-by-tag`.
+
+Honour `notmuch-show-text/html-blocked-images` when using `w3m` to
+render html.
+
+Support toggling sort order in notmuch-tree mode.
+
+Ruby
+----
+
+Memory management of allocated notmuch objects (database, messages,
+etc...) is now done via the Ruby GC. This removes all constraints on
+the order of object destruction.  Database close and destroy are
+split, following an old library API change.
+
+Vim
+---
+
+Respect excluded tags when showing a thread.
+
+Documentation
+-------------
+
+Fix doc build for Sphinx 4.0.
+
+Improve the markup and linking of the documentation.
+
+Notmuch 0.32.3 (2021-08-17)
+===========================
+
+Library
+-------
+
+Restore location of database via `MAILDIR` environment variable, which
+was broken in 0.32.
+
+Bump libnotmuch minor version to match the documentation in
+`notmuch.h`.
+
+Correct documentation for deprecated database opening functions to
+point out that they (still) do not load configuration information.
+
+CLI
+---
+
+Restore "notmuch config get built_with.*", which was broken in 0.32.
+
+Notmuch 0.32.2 (2021-06-27)
+===========================
+
+General
+-------
+
+Fix a bug from 2017 that can add duplicate thread-id terms to message
+documents.
+
+CLI
+---
+
+Fix small memory leak in notmuch new.
+
+Emacs
+-----
+
+Add `(require 'seq)` for `seq-some`.
+
+Documentation
+-------------
+
+Fix man page build for Sphinx 4.x. Fix variable name in emacs docs.
+
+Tests
+-----
+
+Fix backup creation in `perf-test/T00-new`.  Check openssl
+prerequisite in `add_gpgsm_home`.
+
+Notmuch 0.32.1 (2021-05-15)
+===========================
+
+General
+-------
+
+Restore handling of relative values for `database.path` that was
+broken by 0.32. Extend this handling to `database.mail_root`,
+`database.backup_dir`, and `database.hook_dir`.
+
+Reload certain metadata from Xapian database in
+notmuch_database_reopen. This fixes a bug when adding messages to the
+database in a pre-new hook.
+
+Fix default of `$HOME/mail` for `database.path`. In release 0.32, this
+default worked only in "notmuch config".
+
+Emacs
+-----
+
+Restore the dynamically bound variables `tag-changes` and `query` in
+in `notmuch-before-tag-hook` and `notmuch-after-tag-hook`.
+
+Add `notmuch-jump-key` face to fontify keys in `notmuch-jump` and
+related functions.  To ensure backward compatibility, the new face
+inherits from `minibuffer-prompt`.
+
+Notmuch 0.32 (2021-05-02)
+=========================
+
+General
+-------
+
+This release includes a significant overhaul of the configuration
+management facilities for notmuch.  The previous distinction between
+configuration items that can be modified via plain text configuration
+files and those that must be set in the database via the "notmuch
+config" subcommand is gone, and all configuration items can be set in
+both ways.  The external configuration file overrides configuration
+items in the database. The location of database, hooks, and
+configuration files is now more flexible, with several new
+configuration variables. In particular XDG locations are now supported
+as fallbacks for database, configuration and hooks. For more
+information see `notmuch-config(1)`.
+
+Library
+-------
+
+To support the new configuration facilities, several functions and
+constants have been added to the notmuch API. Most notably:
+
+- `notmuch_database_create_with_config`
+- `notmuch_database_open_with_config`
+- `notmuch_database_load_config`
+- `notmuch_config_get`
+
+A previously requested API change is that `notmuch_database_reopen` is
+now exposed (and generalized).
+
+The previously severe slowdowns from large numbers calls to
+notmuch_database_remove_message or notmuch_message_delete in one
+session has been fixed.
+
+As always, the canonical source of API documentation is
+`lib/notmuch.h`, or the doxygen formatted documentation in `notmuch(3)`.
+
+CLI
+---
+
+The `notmuch config set` subcommand gained a `--database` argument to
+specify that the database should be updated, rather than a config file.
+
+The speed of `notmuch new` and `notmuch reindex` in dealing with large
+numbers of mail file deletions is significantly improved.
+
+Emacs
+-----
+
+Completion related updates include: de-duplicating tags offered for
+completion, use the actual initial input in address completion, allow
+users to opt out of notmuch address completion, and do not force Ido
+when prompting for senders.
+
+Some keymaps used to contain bindings for unnamed commands.  These
+lambda expressions have been replaced by named commands (symbols), to
+ease customization.
+
+Lexical binding is now used in all notmuch-emacs libraries.
+
+Fix bug in calling `notmuch-mua-mail` with a non-nil RETURN-ACTION.
+
+Removed, inlined or renamed functions and variables:
+    `notmuch-address-locate-command`,
+    `notmuch-documentation-first-line`, `notmuch-folder`,
+    `notmuch-hello-trim', `notmuch-hello-versions` => `notmuch-version`,
+    `notmuch-remove-if-not`, `notmuch-search-disjunctive-regexp`,
+    `notmuch-sexp-eof`, `notmuch-split-content-type`, and
+    `notmuch-tree-button-activate`.
+
+Keymaps are no longer fset, which means they need to be referred to in
+define-key directly (without quotes).  If your Emacs configuration has a
+keybinding like:
+   (define-key 'notmuch-show-mode-map "7" 'foo)
+you should change it to:
+   (define-key notmuch-show-mode-map "7" 'foo)
+
 Notmuch 0.31.4 (2021-02-18)
 ===========================
 
@@ -22,12 +646,12 @@ Fix for exclude tags in notmuch2 bindings.
 Build
 -----
 
-Portability update for T360-symbol-hiding
+Portability update for T360-symbol-hiding.
 
 Library
 -------
 
-Fix for memory error in notmuch_database_get_config_list
+Fix for memory error in notmuch_database_get_config_list.
 
 Notmuch 0.31.2 (2020-11-08)
 ===========================
@@ -35,7 +659,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)
@@ -242,7 +866,7 @@ Command Line Interface
 ----------------------
 
 `notmuch show` now supports --body=false and --include-html with
---format=text
+--format=text.
 
 Fix several performance problems with `notmuch reindex`.
 
diff --git a/README b/README
index 0aa9a080d075a350b6d3610ddea94fda3119a49f..03bbb57f577938801367fab4841a6da23862bedf 100644 (file)
--- a/README
+++ b/README
@@ -73,5 +73,5 @@ information.
 There is also an IRC channel dedicated to talk about using and
 developing notmuch:
 
-       IRC server:     irc.freenode.net
+       IRC server:     irc.libera.chat
        Channel:        #notmuch
index bc960bbceabe592bb75522c25c20bbc2c9c70e10..9875123ab106cc6a94f5d62ba4e29ea60f24e742 100644 (file)
@@ -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)" \
                LIBNOTMUCH="../../lib/$(LINKER_NAME)" \
                NOTMUCH_SRCDIR='$(NOTMUCH_SRCDIR)' \
                $(RUBY) extconf.rb --vendor
-       $(MAKE) -C $(dir)/ruby
+       $(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
index f712b6c5e4243b00f95267d5b582ec3bb964cac6..65d7dcb6024cb576ffa0c887591b8d35a773d667 100644 (file)
@@ -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(
@@ -53,6 +53,9 @@ ffibuilder.cdef(
         NOTMUCH_STATUS_NO_CONFIG,
         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 {
@@ -95,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
@@ -325,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);
     """
 )
 
index 29de6495ecd7f0402de55ca4b551bddce33866e0..603fdcbf02c6b544e6224085df20f96c51ef00ee 100644 (file)
@@ -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 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)
index 868f4408cb18f2b00196d6314e1d4f3fe34ade5d..d7485b4d983d8f55efd92b8a8581dac86e415c3a 100644 (file)
@@ -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()
index 9301073ed7668836f74c68f1cc0f3503b772af46..17c3ad9cc39af7cf565f66aae9570bcba22a8f9c 100644 (file)
@@ -56,6 +56,8 @@ class NotmuchError(Exception):
                 NoDatabaseError,
             capi.lib.NOTMUCH_STATUS_DATABASE_EXISTS:
                 DatabaseExistsError,
+            capi.lib.NOTMUCH_STATUS_BAD_QUERY_SYNTAX:
+                QuerySyntaxError,
         }
         return types[status]
 
@@ -81,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'
 
@@ -103,6 +106,7 @@ class IllegalArgumentError(NotmuchError): pass
 class NoConfigError(NotmuchError): pass
 class NoDatabaseError(NotmuchError): pass
 class DatabaseExistsError(NotmuchError): pass
+class QuerySyntaxError(NotmuchError): pass
 
 class ObjectDestroyedError(NotmuchError):
     """The object has already been destroyed and it's memory freed.
index 2f2320766ce9805ca88e0ea81c34c36050263a2c..d4b34e91e2f88ff367a73cfa5b311d11cbfa6b9a 100644 (file)
@@ -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)
@@ -205,6 +205,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.
@@ -357,14 +371,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)
index cda5233840bc0e0183181b8464c750ed3dd75866..55fb2d24f832b49e2730db879309b441a58a0a78 100644 (file)
@@ -1,6 +1,7 @@
 import setuptools
+from _notmuch_config import *
 
-with open('version.txt') as fp:
+with open(NOTMUCH_VERSION_FILE) as fp:
     VERSION = fp.read().strip()
 
 setuptools.setup(
index 6835fd30f723509cd6235540d35deadfc838cfa7..fe90c7879d8ad7ef0babb85386d7c87c2706a47c 100644 (file)
@@ -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
index 1b2695f51707a9d447c0b3160a1f269d025f0f17..2a7f42f0e17d0868382a9b0d5276ca2ce6912e87 100644 (file)
@@ -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'
index 9b3219c00e634a9e4d91fa6d3c7c329dd8e59aa2..f1d12ea6c2f046186377a7ce48b104d3a5855873 100644 (file)
@@ -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):
diff --git a/bindings/python-cffi/tests/test_errors.py b/bindings/python-cffi/tests/test_errors.py
new file mode 100644 (file)
index 0000000..c2519f8
--- /dev/null
@@ -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'
index 532bf92159dd86cb23c5cd50133fa5444c4418dd..56701d056de5ff6d1cda5e7fc0c53b1cdd8a6de5 100644 (file)
@@ -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.
index faf3947b6090f901a63f5828c962fc64e395591f..f2c6209df437f8fcb0d0d906d781b048de9cc8e9 100644 (file)
@@ -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
index 1f44b35d2c9e1a798ddeefe597643fb52c4eb30a..619d2aacfaa255ebfa0a6e775d2bab2575c5c3a3 100644 (file)
@@ -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 (file)
index a8a0217..0000000
+++ /dev/null
@@ -1 +0,0 @@
-0.31.4
index c931329e96972237d5f380ba2270a9dcc2d28d56..4a94e05c805d1a9fc28d035fbde09f3b404ff202 100644 (file)
@@ -47,7 +47,10 @@ if sys.version_info[0] == 2:
 
         return value
 else:
-    from configparser import SafeConfigParser
+    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):
index 6f6372ab1f4e6d25742733c97a14c84940108cfc..fd4152eef9ca772fff115b70860f8fb506c21ab0 100644 (file)
@@ -1,3 +1,3 @@
 # this file should be kept in sync with ../../../version
-__VERSION__ = '0.31.4'
+__VERSION__ = '0.38.3'
 SOVERSION = '5'
index 416eb709f19d231bfdf6460c37a8e2262fad24af..ed224ef713bf1f1ed2319678038be5182563d38b 100644 (file)
 VALUE
 notmuch_rb_database_alloc (VALUE klass)
 {
-    return Data_Wrap_Struct (klass, NULL, NULL, NULL);
+    return Data_Wrap_Notmuch_Object (klass, &notmuch_rb_database_type, NULL);
+}
+
+/*
+ * call-seq: DB.destroy => nil
+ *
+ * Destroys the database, freeing all resources allocated for it.
+ */
+VALUE
+notmuch_rb_database_destroy (VALUE self)
+{
+    notmuch_rb_object_destroy (self, &notmuch_rb_database_type);
+
+    return Qnil;
 }
 
 /*
@@ -45,43 +58,59 @@ 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);
     }
 
-    Check_Type (self, T_DATA);
+    rb_check_typeddata (self, &notmuch_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) = database;
+    DATA_PTR (self) = notmuch_rb_object_create (database, "notmuch_rb_database");
 
     return self;
 }
@@ -113,12 +142,12 @@ notmuch_rb_database_open (int argc, VALUE *argv, VALUE klass)
 VALUE
 notmuch_rb_database_close (VALUE self)
 {
-    notmuch_status_t ret;
     notmuch_database_t *db;
+    notmuch_status_t ret;
 
     Data_Get_Notmuch_Database (self, db);
-    ret = notmuch_database_destroy (db);
-    DATA_PTR (self) = NULL;
+
+    ret = notmuch_database_close (db);
     notmuch_rb_status_raise (ret);
 
     return Qnil;
@@ -266,7 +295,7 @@ notmuch_rb_database_get_directory (VALUE self, VALUE pathv)
     ret = notmuch_database_get_directory (db, path, &dir);
     notmuch_rb_status_raise (ret);
     if (dir)
-       return Data_Wrap_Struct (notmuch_rb_cDirectory, NULL, NULL, dir);
+       return Data_Wrap_Notmuch_Object (notmuch_rb_cDirectory, &notmuch_rb_directory_type, dir);
     return Qnil;
 }
 
@@ -293,7 +322,7 @@ notmuch_rb_database_add_message (VALUE self, VALUE pathv)
 
     ret = notmuch_database_index_file (db, path, NULL, &message);
     notmuch_rb_status_raise (ret);
-    return rb_assoc_new (Data_Wrap_Struct (notmuch_rb_cMessage, NULL, NULL, message),
+    return rb_assoc_new (Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message),
         (ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) ? Qtrue : Qfalse);
 }
 
@@ -344,7 +373,7 @@ notmuch_rb_database_find_message (VALUE self, VALUE idv)
     notmuch_rb_status_raise (ret);
 
     if (message)
-        return Data_Wrap_Struct (notmuch_rb_cMessage, NULL, NULL, message);
+       return Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message);
     return Qnil;
 }
 
@@ -370,7 +399,7 @@ notmuch_rb_database_find_message_by_filename (VALUE self, VALUE pathv)
     notmuch_rb_status_raise (ret);
 
     if (message)
-        return Data_Wrap_Struct (notmuch_rb_cMessage, NULL, NULL, message);
+       return Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message);
     return Qnil;
 }
 
@@ -395,21 +424,28 @@ notmuch_rb_database_get_all_tags (VALUE self)
 
        rb_raise (notmuch_rb_eBaseError, "%s", msg);
     }
-    return Data_Wrap_Struct (notmuch_rb_cTags, NULL, NULL, tags);
+    return notmuch_rb_tags_get (tags);
 }
 
 /*
- * call-seq: DB.query(query) => QUERY
+ * call-seq:
+ *   DB.query(query) => QUERY
+ *   DB.query(query, sort:, excluded_tags:, omit_excluded:) => QUERY
  *
- * Retrieve a query object for the query string 'query'
+ * Retrieve a query object for the query string 'query'. When using keyword
+ * arguments they are passwed to the query object.
  */
 VALUE
-notmuch_rb_database_query_create (VALUE self, VALUE qstrv)
+notmuch_rb_database_query_create (int argc, VALUE *argv, VALUE self)
 {
+    VALUE qstrv;
+    VALUE opts;
     const char *qstr;
     notmuch_query_t *query;
     notmuch_database_t *db;
 
+    rb_scan_args (argc, argv, "1:", &qstrv, &opts);
+
     Data_Get_Notmuch_Database (self, db);
 
     SafeStringValue (qstrv);
@@ -419,5 +455,39 @@ notmuch_rb_database_query_create (VALUE self, VALUE qstrv)
     if (!query)
         rb_raise (notmuch_rb_eMemoryError, "Out of memory");
 
-    return Data_Wrap_Struct (notmuch_rb_cQuery, NULL, NULL, query);
+    if (!NIL_P (opts)) {
+       VALUE sort, exclude_tags, omit_excluded;
+       VALUE kwargs[3];
+       static ID keyword_ids[3];
+
+       if (!keyword_ids[0]) {
+           keyword_ids[0] = rb_intern_const ("sort");
+           keyword_ids[1] = rb_intern_const ("exclude_tags");
+           keyword_ids[2] = rb_intern_const ("omit_excluded");
+       }
+
+       rb_get_kwargs (opts, keyword_ids, 0, 3, kwargs);
+
+       sort = kwargs[0];
+       exclude_tags = kwargs[1];
+       omit_excluded = kwargs[2];
+
+       if (sort != Qundef)
+           notmuch_query_set_sort (query, FIX2UINT (sort));
+
+       if (exclude_tags != Qundef) {
+           for (int i = 0; i < RARRAY_LEN (exclude_tags); i++) {
+               VALUE e = RARRAY_AREF (exclude_tags, i);
+               notmuch_query_add_tag_exclude (query, RSTRING_PTR (e));
+           }
+       }
+
+       if (omit_excluded != Qundef) {
+           notmuch_exclude_t omit;
+           omit = FIXNUM_P (omit_excluded) ? FIX2UINT (omit_excluded) : RTEST(omit_excluded);
+           notmuch_query_set_omit_excluded (query, omit);
+       }
+    }
+
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cQuery, &notmuch_rb_query_type, query);
 }
index 48544ca2a4acbe57a48630957e9dd059de504f69..a2cb38c8a74425ed193144c2607b752210f603f5 100644 (file)
@@ -23,6 +23,7 @@
 
 #include <notmuch.h>
 #include <ruby.h>
+#include <talloc.h>
 
 extern VALUE notmuch_rb_cDatabase;
 extern VALUE notmuch_rb_cDirectory;
@@ -32,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;
@@ -47,85 +47,100 @@ 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)
 # define RSTRING_PTR(v) (RSTRING((v))->ptr)
 #endif /* !defined (RSTRING_PTR) */
 
-#define Data_Get_Notmuch_Database(obj, ptr)                    \
-    do {                                                       \
-       Check_Type ((obj), T_DATA);                             \
-       if (DATA_PTR ((obj)) == NULL)                           \
-       rb_raise (rb_eRuntimeError, "database closed");         \
-       Data_Get_Struct ((obj), notmuch_database_t, (ptr));     \
+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_query_type;
+extern const rb_data_type_t notmuch_rb_threads_type;
+extern const rb_data_type_t notmuch_rb_thread_type;
+extern const rb_data_type_t notmuch_rb_messages_type;
+extern const rb_data_type_t notmuch_rb_message_type;
+extern const rb_data_type_t notmuch_rb_tags_type;
+
+#define Data_Get_Notmuch_Rb_Object(obj, type, ptr)                                 \
+    do {                                                                           \
+       (ptr) = rb_check_typeddata ((obj), (type));                                 \
+       if (RB_UNLIKELY (!(ptr))) {                                                 \
+           VALUE cname = rb_class_name (CLASS_OF ((obj)));                         \
+           rb_raise (rb_eRuntimeError, "%"PRIsVALUE" object destroyed", cname);    \
+       }                                                                           \
     } while (0)
 
-#define Data_Get_Notmuch_Directory(obj, ptr)                   \
+#define Data_Get_Notmuch_Object(obj, type, ptr)                        \
     do {                                                       \
-       Check_Type ((obj), T_DATA);                             \
-       if (DATA_PTR ((obj)) == NULL)                           \
-       rb_raise (rb_eRuntimeError, "directory destroyed");     \
-       Data_Get_Struct ((obj), notmuch_directory_t, (ptr));    \
+       notmuch_rb_object_t *rb_wrapper;                        \
+       Data_Get_Notmuch_Rb_Object ((obj), (type), rb_wrapper); \
+       (ptr) = rb_wrapper->nm_object;                          \
     } while (0)
 
-#define Data_Get_Notmuch_FileNames(obj, ptr)                   \
-    do {                                                       \
-       Check_Type ((obj), T_DATA);                             \
-       if (DATA_PTR ((obj)) == NULL)                           \
-       rb_raise (rb_eRuntimeError, "filenames destroyed");     \
-       Data_Get_Struct ((obj), notmuch_filenames_t, (ptr));    \
-    } while (0)
+#define Data_Wrap_Notmuch_Object(klass, type, ptr) \
+    TypedData_Wrap_Struct ((klass), (type), notmuch_rb_object_create ((ptr), "notmuch_rb_object: " __location__))
 
-#define Data_Get_Notmuch_Query(obj, ptr)                       \
-    do {                                                       \
-       Check_Type ((obj), T_DATA);                             \
-       if (DATA_PTR ((obj)) == NULL)                           \
-       rb_raise (rb_eRuntimeError, "query destroyed");         \
-       Data_Get_Struct ((obj), notmuch_query_t, (ptr));        \
-    } while (0)
+#define Data_Get_Notmuch_Database(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_database_type, (ptr))
 
-#define Data_Get_Notmuch_Threads(obj, ptr)                     \
-    do {                                                       \
-       Check_Type ((obj), T_DATA);                             \
-       if (DATA_PTR ((obj)) == NULL)                           \
-       rb_raise (rb_eRuntimeError, "threads destroyed");       \
-       Data_Get_Struct ((obj), notmuch_threads_t, (ptr));      \
-    } while (0)
+#define Data_Get_Notmuch_Directory(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_directory_type, (ptr))
 
-#define Data_Get_Notmuch_Messages(obj, ptr)                    \
-    do {                                                       \
-       Check_Type ((obj), T_DATA);                             \
-       if (DATA_PTR ((obj)) == NULL)                           \
-       rb_raise (rb_eRuntimeError, "messages destroyed");      \
-       Data_Get_Struct ((obj), notmuch_messages_t, (ptr));     \
-    } while (0)
+#define Data_Get_Notmuch_Query(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_query_type, (ptr))
 
-#define Data_Get_Notmuch_Thread(obj, ptr)                      \
-    do {                                                       \
-       Check_Type ((obj), T_DATA);                             \
-       if (DATA_PTR ((obj)) == NULL)                           \
-       rb_raise (rb_eRuntimeError, "thread destroyed");        \
-       Data_Get_Struct ((obj), notmuch_thread_t, (ptr));       \
-    } while (0)
+#define Data_Get_Notmuch_Threads(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_threads_type, (ptr))
 
-#define Data_Get_Notmuch_Message(obj, ptr)                     \
-    do {                                                       \
-       Check_Type ((obj), T_DATA);                             \
-       if (DATA_PTR ((obj)) == NULL)                           \
-       rb_raise (rb_eRuntimeError, "message destroyed");       \
-       Data_Get_Struct ((obj), notmuch_message_t, (ptr));      \
-    } while (0)
+#define Data_Get_Notmuch_Messages(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_messages_type, (ptr))
 
-#define Data_Get_Notmuch_Tags(obj, ptr)                        \
-    do {                                               \
-       Check_Type ((obj), T_DATA);                     \
-       if (DATA_PTR ((obj)) == NULL)                   \
-       rb_raise (rb_eRuntimeError, "tags destroyed");  \
-       Data_Get_Struct ((obj), notmuch_tags_t, (ptr)); \
-    } while (0)
+#define Data_Get_Notmuch_Thread(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_thread_type, (ptr))
+
+#define Data_Get_Notmuch_Message(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_message_type, (ptr))
+
+#define Data_Get_Notmuch_Tags(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_tags_type, (ptr))
+
+typedef struct {
+    void *nm_object;
+} notmuch_rb_object_t;
+
+static inline void *
+notmuch_rb_object_create (void *nm_object, const char *name)
+{
+    notmuch_rb_object_t *rb_wrapper = talloc_named_const (NULL, sizeof (*rb_wrapper), name);
+
+    if (RB_UNLIKELY (!rb_wrapper))
+       return NULL;
+
+    rb_wrapper->nm_object = nm_object;
+    talloc_steal (rb_wrapper, nm_object);
+    return rb_wrapper;
+}
+
+static inline void
+notmuch_rb_object_free (void *rb_wrapper)
+{
+    talloc_free (rb_wrapper);
+}
+
+static inline void
+notmuch_rb_object_destroy (VALUE rb_object, const rb_data_type_t *type)
+{
+    notmuch_rb_object_t *rb_wrapper;
+
+    Data_Get_Notmuch_Rb_Object (rb_object, type, rb_wrapper);
+
+    /* Call the corresponding notmuch_*_destroy function */
+    ((void (*)(void *)) type->data) (rb_wrapper->nm_object);
+    notmuch_rb_object_free (rb_wrapper);
+    DATA_PTR (rb_object) = NULL;
+}
 
 /* status.c */
 void
@@ -135,6 +150,9 @@ notmuch_rb_status_raise (notmuch_status_t status);
 VALUE
 notmuch_rb_database_alloc (VALUE klass);
 
+VALUE
+notmuch_rb_database_destroy (VALUE self);
+
 VALUE
 notmuch_rb_database_initialize (int argc, VALUE *argv, VALUE klass);
 
@@ -181,7 +199,7 @@ VALUE
 notmuch_rb_database_get_all_tags (VALUE self);
 
 VALUE
-notmuch_rb_database_query_create (VALUE self, VALUE qstrv);
+notmuch_rb_database_query_create (int argc, VALUE *argv, VALUE self);
 
 /* directory.c */
 VALUE
@@ -201,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
@@ -345,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
index 0f37b3910042426219ae7cdf7137ec1b017cfb11..f267d82f16976ee7fec41779c11d332ab7ac35ad 100644 (file)
 VALUE
 notmuch_rb_directory_destroy (VALUE self)
 {
-    notmuch_directory_t *dir;
-
-    Data_Get_Struct (self, notmuch_directory_t, dir);
-
-    notmuch_directory_destroy (dir);
-    DATA_PTR (self) = NULL;
+    notmuch_rb_object_destroy (self, &notmuch_rb_directory_type);
 
     return Qnil;
 }
@@ -92,7 +87,7 @@ notmuch_rb_directory_get_child_files (VALUE self)
 
     fnames = notmuch_directory_get_child_files (dir);
 
-    return Data_Wrap_Struct (notmuch_rb_cFileNames, NULL, NULL, fnames);
+    return notmuch_rb_filenames_get (fnames);
 }
 
 /*
@@ -111,5 +106,5 @@ notmuch_rb_directory_get_child_directories (VALUE self)
 
     fnames = notmuch_directory_get_child_directories (dir);
 
-    return Data_Wrap_Struct (notmuch_rb_cFileNames, NULL, NULL, fnames);
+    return notmuch_rb_filenames_get (fnames);
 }
index 161de5a2c7c52bb6d5130ee6db3616c36acdba1d..d914537cff1bbb502e8dd85c1f9e764eed2d1b55 100644 (file)
@@ -19,6 +19,7 @@ if not ENV['LIBNOTMUCH']
 end
 
 $LOCAL_LIBS += ENV['LIBNOTMUCH']
+$LIBS += " -ltalloc"
 
 # Create Makefile
 dir_config('notmuch')
index 656c58e6f40dfdc87bcbc86b6dd7b2eb2680ecab..60c3fb8b3b412e121d5cea76b46b11af15b25191 100644 (file)
@@ -1,58 +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 <alip@exherbo.org>
- */
-
 #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_filenames_t *fnames;
-
-    Data_Get_Notmuch_FileNames (self, fnames);
-
-    notmuch_filenames_destroy (fnames);
-    DATA_PTR (self) = NULL;
-
-    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;
 }
index 5556b43efde1b8f847d3ce3fde76bb72d1d44a36..2d1994af22740c6640d91367b8d81b40283fe717 100644 (file)
 
 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,31 @@ 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",
+    .function = {
+       .dfree = notmuch_rb_object_free,
+    },
+};
+
+#define define_type(id) \
+    const rb_data_type_t notmuch_rb_ ## id ## _type = { \
+       .wrap_struct_name = "notmuch_" #id, \
+       .parent = &notmuch_rb_object_type, \
+       .data = &notmuch_ ## id ## _destroy, \
+       .function = { \
+           .dfree = notmuch_rb_object_free, \
+       }, \
+    }
+
+define_type (database);
+define_type (directory);
+define_type (query);
+define_type (threads);
+define_type (thread);
+define_type (messages);
+define_type (message);
 
 /*
  * Document-module: Notmuch
@@ -59,13 +80,11 @@ ID ID_db_mode;
  * the user:
  *
  * - Notmuch::Database
- * - Notmuch::FileNames
  * - Notmuch::Query
  * - Notmuch::Threads
  * - Notmuch::Messages
  * - Notmuch::Thread
  * - Notmuch::Message
- * - Notmuch::Tags
  */
 
 void
@@ -74,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");
 
@@ -133,6 +150,30 @@ Init_notmuch (void)
      * Maximum allowed length of a tag
      */
     rb_define_const (mod, "TAG_MAX", INT2FIX (NOTMUCH_TAG_MAX));
+    /*
+     * Document-const: Notmuch::EXCLUDE_FLAG
+     *
+     * Only flag excluded results
+     */
+    rb_define_const (mod, "EXCLUDE_FLAG", INT2FIX (NOTMUCH_EXCLUDE_FLAG));
+    /*
+     * Document-const: Notmuch::EXCLUDE_TRUE
+     *
+     * Exclude messages from the results
+     */
+    rb_define_const (mod, "EXCLUDE_TRUE", INT2FIX (NOTMUCH_EXCLUDE_TRUE));
+    /*
+     * Document-const: Notmuch::EXCLUDE_FALSE
+     *
+     * Don't exclude anything
+     */
+    rb_define_const (mod, "EXCLUDE_FALSE", INT2FIX (NOTMUCH_EXCLUDE_FALSE));
+    /*
+     * Document-const: Notmuch::EXCLUDE_ALL
+     *
+     * Exclude all results
+     */
+    rb_define_const (mod, "EXCLUDE_ALL", INT2FIX (NOTMUCH_EXCLUDE_ALL));
 
     /*
      * Document-class: Notmuch::BaseError
@@ -211,10 +252,11 @@ Init_notmuch (void)
      *
      * Notmuch database interaction
      */
-    notmuch_rb_cDatabase = rb_define_class_under (mod, "Database", rb_cData);
+    notmuch_rb_cDatabase = rb_define_class_under (mod, "Database", rb_cObject);
     rb_define_alloc_func (notmuch_rb_cDatabase, notmuch_rb_database_alloc);
     rb_define_singleton_method (notmuch_rb_cDatabase, "open", notmuch_rb_database_open, -1); /* in database.c */
     rb_define_method (notmuch_rb_cDatabase, "initialize", notmuch_rb_database_initialize, -1); /* in database.c */
+    rb_define_method (notmuch_rb_cDatabase, "destroy!", notmuch_rb_database_destroy, 0); /* in database.c */
     rb_define_method (notmuch_rb_cDatabase, "close", notmuch_rb_database_close, 0); /* in database.c */
     rb_define_method (notmuch_rb_cDatabase, "path", notmuch_rb_database_path, 0); /* in database.c */
     rb_define_method (notmuch_rb_cDatabase, "version", notmuch_rb_database_version, 0); /* in database.c */
@@ -230,14 +272,14 @@ Init_notmuch (void)
     rb_define_method (notmuch_rb_cDatabase, "find_message_by_filename",
                      notmuch_rb_database_find_message_by_filename, 1); /* in database.c */
     rb_define_method (notmuch_rb_cDatabase, "all_tags", notmuch_rb_database_get_all_tags, 0); /* in database.c */
-    rb_define_method (notmuch_rb_cDatabase, "query", notmuch_rb_database_query_create, 1); /* in database.c */
+    rb_define_method (notmuch_rb_cDatabase, "query", notmuch_rb_database_query_create, -1); /* in database.c */
 
     /*
      * Document-class: Notmuch::Directory
      *
      * Notmuch directory
      */
-    notmuch_rb_cDirectory = rb_define_class_under (mod, "Directory", rb_cData);
+    notmuch_rb_cDirectory = rb_define_class_under (mod, "Directory", rb_cObject);
     rb_undef_method (notmuch_rb_cDirectory, "initialize");
     rb_define_method (notmuch_rb_cDirectory, "destroy!", notmuch_rb_directory_destroy, 0); /* in directory.c */
     rb_define_method (notmuch_rb_cDirectory, "mtime", notmuch_rb_directory_get_mtime, 0); /* in directory.c */
@@ -245,23 +287,12 @@ 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_cData);
-    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_cData);
+    notmuch_rb_cQuery = rb_define_class_under (mod, "Query", rb_cObject);
     rb_undef_method (notmuch_rb_cQuery, "initialize");
     rb_define_method (notmuch_rb_cQuery, "destroy!", notmuch_rb_query_destroy, 0); /* in query.c */
     rb_define_method (notmuch_rb_cQuery, "sort", notmuch_rb_query_get_sort, 0); /* in query.c */
@@ -279,7 +310,7 @@ Init_notmuch (void)
      *
      * Notmuch threads
      */
-    notmuch_rb_cThreads = rb_define_class_under (mod, "Threads", rb_cData);
+    notmuch_rb_cThreads = rb_define_class_under (mod, "Threads", rb_cObject);
     rb_undef_method (notmuch_rb_cThreads, "initialize");
     rb_define_method (notmuch_rb_cThreads, "destroy!", notmuch_rb_threads_destroy, 0); /* in threads.c */
     rb_define_method (notmuch_rb_cThreads, "each", notmuch_rb_threads_each, 0); /* in threads.c */
@@ -290,7 +321,7 @@ Init_notmuch (void)
      *
      * Notmuch messages
      */
-    notmuch_rb_cMessages = rb_define_class_under (mod, "Messages", rb_cData);
+    notmuch_rb_cMessages = rb_define_class_under (mod, "Messages", rb_cObject);
     rb_undef_method (notmuch_rb_cMessages, "initialize");
     rb_define_method (notmuch_rb_cMessages, "destroy!", notmuch_rb_messages_destroy, 0); /* in messages.c */
     rb_define_method (notmuch_rb_cMessages, "each", notmuch_rb_messages_each, 0); /* in messages.c */
@@ -302,7 +333,7 @@ Init_notmuch (void)
      *
      * Notmuch thread
      */
-    notmuch_rb_cThread = rb_define_class_under (mod, "Thread", rb_cData);
+    notmuch_rb_cThread = rb_define_class_under (mod, "Thread", rb_cObject);
     rb_undef_method (notmuch_rb_cThread, "initialize");
     rb_define_method (notmuch_rb_cThread, "destroy!", notmuch_rb_thread_destroy, 0); /* in thread.c */
     rb_define_method (notmuch_rb_cThread, "thread_id", notmuch_rb_thread_get_thread_id, 0); /* in thread.c */
@@ -321,7 +352,7 @@ Init_notmuch (void)
      *
      * Notmuch message
      */
-    notmuch_rb_cMessage = rb_define_class_under (mod, "Message", rb_cData);
+    notmuch_rb_cMessage = rb_define_class_under (mod, "Message", rb_cObject);
     rb_undef_method (notmuch_rb_cMessage, "initialize");
     rb_define_method (notmuch_rb_cMessage, "destroy!", notmuch_rb_message_destroy, 0); /* in message.c */
     rb_define_method (notmuch_rb_cMessage, "message_id", notmuch_rb_message_get_message_id, 0); /* in message.c */
@@ -343,15 +374,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_cData);
-    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);
 }
index 6ea82afa20c5071086883b437054d80613e7a45d..13c182f6549481b0866fb1789e25e8d874ac8b69 100644 (file)
 VALUE
 notmuch_rb_message_destroy (VALUE self)
 {
-    notmuch_message_t *message;
-
-    Data_Get_Notmuch_Message (self, message);
-
-    notmuch_message_destroy (message);
-    DATA_PTR (self) = NULL;
+    notmuch_rb_object_destroy (self, &notmuch_rb_message_type);
 
     return Qnil;
 }
@@ -89,7 +84,7 @@ notmuch_rb_message_get_replies (VALUE self)
 
     messages = notmuch_message_get_replies (message);
 
-    return Data_Wrap_Struct (notmuch_rb_cMessages, NULL, NULL, messages);
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
 }
 
 /*
@@ -125,7 +120,7 @@ notmuch_rb_message_get_filenames (VALUE self)
 
     fnames = notmuch_message_get_filenames (message);
 
-    return Data_Wrap_Struct (notmuch_rb_cFileNames, NULL, NULL, fnames);
+    return notmuch_rb_filenames_get (fnames);
 }
 
 /*
@@ -226,7 +221,7 @@ notmuch_rb_message_get_tags (VALUE self)
     if (!tags)
        rb_raise (notmuch_rb_eMemoryError, "Out of memory");
 
-    return Data_Wrap_Struct (notmuch_rb_cTags, NULL, NULL, tags);
+    return notmuch_rb_tags_get (tags);
 }
 
 /*
index a337feeb499fff177e7f3c6659373eaf0c004a00..6369d0523008b6dadda38bc58cde3c8c908badf5 100644 (file)
 VALUE
 notmuch_rb_messages_destroy (VALUE self)
 {
-    notmuch_messages_t *messages;
-
-    Data_Get_Notmuch_Messages (self, messages);
-
-    notmuch_messages_destroy (messages);
-    DATA_PTR (self) = NULL;
+    notmuch_rb_object_destroy (self, &notmuch_rb_messages_type);
 
     return Qnil;
 }
@@ -53,7 +48,7 @@ notmuch_rb_messages_each (VALUE self)
 
     for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) {
        message = notmuch_messages_get (messages);
-       rb_yield (Data_Wrap_Struct (notmuch_rb_cMessage, NULL, NULL, message));
+       rb_yield (Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message));
     }
 
     return self;
@@ -76,5 +71,5 @@ notmuch_rb_messages_collect_tags (VALUE self)
     if (!tags)
        rb_raise (notmuch_rb_eMemoryError, "Out of memory");
 
-    return Data_Wrap_Struct (notmuch_rb_cTags, NULL, NULL, tags);
+    return notmuch_rb_tags_get (tags);
 }
index 8b46d700bdab01554c8944ec7903ea9ce8a3707e..077def02f63021336de66d2c8c8e68edf1b4e6c4 100644 (file)
 VALUE
 notmuch_rb_query_destroy (VALUE self)
 {
-    notmuch_query_t *query;
-
-    Data_Get_Notmuch_Query (self, query);
-
-    notmuch_query_destroy (query);
-    DATA_PTR (self) = NULL;
+    notmuch_rb_object_destroy (self, &notmuch_rb_query_type);
 
     return Qnil;
 }
@@ -50,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));
 }
 
 /*
@@ -107,19 +102,21 @@ notmuch_rb_query_add_tag_exclude (VALUE self, VALUE tagv)
 }
 
 /*
- * call-seq: QUERY.omit_excluded=(boolean) => nil
+ * call-seq: QUERY.omit_excluded=(fixnum) => nil
  *
  * Specify whether to omit excluded results or simply flag them.
- * By default, this is set to +true+.
+ * By default, this is set to +Notmuch::EXCLUDE_TRUE+.
  */
 VALUE
 notmuch_rb_query_set_omit_excluded (VALUE self, VALUE omitv)
 {
     notmuch_query_t *query;
+    notmuch_exclude_t value;
 
     Data_Get_Notmuch_Query (self, query);
 
-    notmuch_query_set_omit_excluded (query, RTEST (omitv));
+    value = FIXNUM_P (omitv) ? FIX2UINT (omitv) : RTEST(omitv);
+    notmuch_query_set_omit_excluded (query, value);
 
     return Qnil;
 }
@@ -142,7 +139,7 @@ notmuch_rb_query_search_threads (VALUE self)
     if (status)
        notmuch_rb_status_raise (status);
 
-    return Data_Wrap_Struct (notmuch_rb_cThreads, NULL, NULL, threads);
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cThreads, &notmuch_rb_threads_type, threads);
 }
 
 /*
@@ -163,7 +160,7 @@ notmuch_rb_query_search_messages (VALUE self)
     if (status)
        notmuch_rb_status_raise (status);
 
-    return Data_Wrap_Struct (notmuch_rb_cMessages, NULL, NULL, messages);
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
 }
 
 /*
index db8b4cfc86f0b3eaa818ce0d7876862158826862..b64874d1d4f441aabe8851338633871e8018886d 100644 (file)
@@ -1,61 +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 <alip@exherbo.org>
- */
-
 #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_tags_t *tags;
-
-    Data_Get_Notmuch_Tags (self, tags);
-
-    notmuch_tags_destroy (tags);
-    DATA_PTR (self) = NULL;
-
-    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;
 }
index 9b295981801779e1d970972d595b60eec1c4d5c6..b20ed89376f802626d5cb4b0d847b020c18de196 100644 (file)
 VALUE
 notmuch_rb_thread_destroy (VALUE self)
 {
-    notmuch_thread_t *thread;
-
-    Data_Get_Notmuch_Thread (self, thread);
-
-    notmuch_thread_destroy (thread);
-    DATA_PTR (self) = NULL;
+    notmuch_rb_object_destroy (self, &notmuch_rb_thread_type);
 
     return Qnil;
 }
@@ -88,7 +83,7 @@ notmuch_rb_thread_get_toplevel_messages (VALUE self)
     if (!messages)
        rb_raise (notmuch_rb_eMemoryError, "Out of memory");
 
-    return Data_Wrap_Struct (notmuch_rb_cMessages, NULL, NULL, messages);
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
 }
 
 /*
@@ -108,7 +103,7 @@ notmuch_rb_thread_get_messages (VALUE self)
     if (!messages)
        rb_raise (notmuch_rb_eMemoryError, "Out of memory");
 
-    return Data_Wrap_Struct (notmuch_rb_cMessages, NULL, NULL, messages);
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
 }
 
 /*
@@ -209,5 +204,5 @@ notmuch_rb_thread_get_tags (VALUE self)
     if (!tags)
        rb_raise (notmuch_rb_eMemoryError, "Out of memory");
 
-    return Data_Wrap_Struct (notmuch_rb_cTags, NULL, NULL, tags);
+    return notmuch_rb_tags_get (tags);
 }
index ed403a8f1f3db30e88d8fd030991ae1b62b1c9e6..50280260a4d3aa9576f4d09c7d9528ed6be6539c 100644 (file)
 VALUE
 notmuch_rb_threads_destroy (VALUE self)
 {
-    notmuch_threads_t *threads;
-
-    Data_Get_Struct (self, notmuch_threads_t, threads);
-
-    notmuch_threads_destroy (threads);
-    DATA_PTR (self) = NULL;
+    notmuch_rb_object_destroy (self, &notmuch_rb_threads_type);
 
     return Qnil;
 }
@@ -53,7 +48,7 @@ notmuch_rb_threads_each (VALUE self)
 
     for (; notmuch_threads_valid (threads); notmuch_threads_move_to_next (threads)) {
        thread = notmuch_threads_get (threads);
-       rb_yield (Data_Wrap_Struct (notmuch_rb_cThread, NULL, NULL, thread));
+       rb_yield (Data_Wrap_Notmuch_Object (notmuch_rb_cThread, &notmuch_rb_thread_type, thread));
     }
 
     return self;
index 2ee1b3996f23f17ca980affdf1bf8cbae763029e..c58ca746c5dd8f954d62b652c14df9c65d14078c 100644 (file)
@@ -5,10 +5,6 @@ extra_cflags += -I$(srcdir)/$(dir)
 
 notmuch_compat_srcs :=
 
-ifneq ($(HAVE_CANONICALIZE_FILE_NAME),1)
-notmuch_compat_srcs += $(dir)/canonicalize_file_name.c
-endif
-
 ifneq ($(HAVE_GETLINE),1)
 notmuch_compat_srcs += $(dir)/getline.c $(dir)/getdelim.c
 endif
diff --git a/compat/canonicalize_file_name.c b/compat/canonicalize_file_name.c
deleted file mode 100644 (file)
index 000f9e7..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-#include "compat.h"
-#include <limits.h>
-#undef _GNU_SOURCE
-#include <stdlib.h>
-
-char *
-canonicalize_file_name (const char *path)
-{
-#ifdef PATH_MAX
-    char *resolved_path =  malloc (PATH_MAX + 1);
-    if (resolved_path == NULL)
-       return NULL;
-
-    return realpath (path, resolved_path);
-#else
-#error undefined PATH_MAX _and_ missing canonicalize_file_name not supported
-#endif
-}
index 8f15e585fda82221eaeed22bfc0834b4ccd9fa67..59e9161830667c229ac936994f4a2bfb60c95dc2 100644 (file)
@@ -37,14 +37,6 @@ extern "C" {
 #define _POSIX_PTHREAD_SEMANTICS 1
 #endif
 
-#if ! HAVE_CANONICALIZE_FILE_NAME
-/* we only call this function from C, and this makes testing easier */
-#ifndef __cplusplus
-char *
-canonicalize_file_name (const char *path);
-#endif
-#endif
-
 #if ! HAVE_GETLINE
 #include <stdio.h>
 #include <unistd.h>
index 3cd1838d950bd30c6239053a09d2a8f401f8b537..8e004572dbf04865f538ba8d051b1745ae2fd495 100644 (file)
@@ -1,5 +1,6 @@
 #define _GNU_SOURCE
-#include <strings.h>
+#include <strings.h> /* strcasecmp() in POSIX */
+#include <string.h> /* strcasecmp() in *BSD */
 
 int
 main ()
index 15425697a215290e79a4d58e7cdf4884f3a5f2bc..3748846edf8392ef80403bf352e0dd9318a80854 100644 (file)
@@ -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}) )
            ;;
index e920f10b2363dedba6e712bec26f75ebbcfce600..0bdd7f772a7ae9be4f85a2b31bb1dc65d91a6864 100644 (file)
@@ -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'
 }
 
index cfa9c09bd3dc4c186c163c6b223b56598789ba3e..7afd08c7dac7822b74cf3315b5ba6adacbdb1de7 100755 (executable)
--- 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 <<EOF
 #include <stdio.h>
@@ -496,16 +534,16 @@ int main () {
 }
 EOF
     if ! TEMP_GPG=$(mktemp -d "${TMPDIR:-/tmp}/notmuch.XXXXXX"); then
-        printf 'No.\nCould not make tempdir for testing session-key support.\n'
-        errors=$((errors + 1))
+       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 \
-           && SESSION_KEY=$(GNUPGHOME=${TEMP_GPG} ./_check_session_keys) \
-           && [ $SESSION_KEY = 9:0BACD64099D1468AB07C796F0C0AC4851948A658A15B34E803865E9FC635F2F5 ]
+          && GNUPGHOME=${TEMP_GPG} gpg --batch --quiet --import < "$srcdir"/test/openpgp4-secret-key.asc \
+          && SESSION_KEY=$(GNUPGHOME=${TEMP_GPG} ./_check_session_keys) \
+          && [ $SESSION_KEY = 9:496A0B6D15A5E7BA762FB8E5FE6DEE421D4D9BBFCEAD1CDD0CCF636D07ADE621 ]
     then
-        printf "OK.\n"
+       printf "OK.\n"
     else
-        cat <<EOF
+       cat <<EOF
 No.
 *** Error: Could not extract session keys from encrypted message.
 
@@ -515,22 +553,18 @@ 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)"
-        else
-            printf 'You do not have the GPGME development libraries installed.\n'
-        fi
-        errors=$((errors + 1))
+       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
+       errors=$((errors + 1))
     fi
     if [ -n "$TEMP_GPG" -a -d "$TEMP_GPG" ]; then
-        rm -rf "$TEMP_GPG"
+       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 <<EOF
+    cat > _check_gmime_cert.c <<EOF
 #include <stdio.h>
 #include <gmime/gmime.h>
 
@@ -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,43 +596,63 @@ 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 \
-            && 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
+       printf 'No.\nCould not make tempdir for testing X.509 certificate validity support.\n'
+       errors=$((errors + 1))
+    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
     then
-        if GNUPGHOME=${TEMP_GPG} ./_check_x509_validity; then
-            gmime_x509_cert_validity=1
-            printf "Yes.\n"
-        else
-            gmime_x509_cert_validity=0
-            printf "No.\n"
-            if pkg-config --exists "gmime-3.0 >= 3.2.7"; then
-                cat <<EOF
+       if GNUPGHOME=${TEMP_GPG} ./_check_x509_validity; then
+           gmime_x509_cert_validity=1
+           printf "Yes.\n"
+       else
+           gmime_x509_cert_validity=0
+           printf "No.\n"
+           if pkg-config --exists "gmime-3.0 >= 3.2.7"; then
+               cat <<EOF
 *** Error: GMime fails to calculate X.509 certificate validity, and
 is later than 3.2.7, which should have fixed this issue.
 
 Please follow up on https://github.com/jstedfast/gmime/pull/90 with
 more details.
 EOF
-                errors=$((errors + 1))
-            fi
-        fi
+               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))
+       printf 'No.\nFailed to set up gpgsm for testing X.509 certificate validity support.\n'
+       errors=$((errors + 1))
     fi
     if [ -n "$TEMP_GPG" -a -d "$TEMP_GPG" ]; then
-        rm -rf "$TEMP_GPG"
+       rm -rf "$TEMP_GPG"
     fi
 
     # see https://dev.gnupg.org/T3464
@@ -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);
@@ -647,31 +700,31 @@ int main () {
 }
 EOF
     if ! TEMP_GPG=$(mktemp -d "${TMPDIR:-/tmp}/notmuch.XXXXXX"); then
-        printf 'No.\nCould not make tempdir for testing signature verification when decrypting with session keys.\n'
-        errors=$((errors + 1))
+       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 \
-            && rm -f ${TEMP_GPG}/private-keys-v1.d/*.key
+           && 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
-            gmime_verify_with_session_key=1
-            printf "Yes.\n"
-        else
-            gmime_verify_with_session_key=0
-            printf "No.\n"
-            cat <<EOF
+       if GNUPGHOME=${TEMP_GPG} ./_verify_sig_with_session_key; then
+           gmime_verify_with_session_key=1
+           printf "Yes.\n"
+       else
+           gmime_verify_with_session_key=0
+           printf "No.\n"
+           cat <<EOF
 *** Error: GMime fails to verify signatures when decrypting with a session key.
 
 This is most likely due to a buggy version of GPGME, which should be fixed in 1.13.2 or later.
 See https://dev.gnupg.org/T3464 for more details.
 EOF
-        fi
+       fi
     else
-        printf 'No.\nFailed to set up gpg for testing signature verification while decrypting with a session key.\n'
-        errors=$((errors + 1))
+       printf 'No.\nFailed to set up gpg for testing signature verification while decrypting with a session key.\n'
+       errors=$((errors + 1))
     fi
     if [ -n "$TEMP_GPG" -a -d "$TEMP_GPG" ]; then
-        rm -rf "$TEMP_GPG"
+       rm -rf "$TEMP_GPG"
     fi
 else
     have_gmime=0
@@ -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
 
@@ -768,24 +823,40 @@ have_python3=0
 if [ $have_python -eq 1 ]; then
     printf "Checking for python3 (>= 3.5)..."
     if "$python" -c 'import sys, sysconfig; assert sys.version_info >= (3,5)'; >/dev/null 2>&1; then
-        printf "Yes.\n"
-        have_python3=1
+       printf "Yes.\n"
+       have_python3=1
     else
-        printf "No (will not install CFFI-based python bindings).\n"
+       printf "No (will not install CFFI-based python bindings).\n"
+    fi
+fi
+
+have_python3_dev=0
+if [ $have_python3 -eq 1 ]; then
+    printf "Checking for python3 version ..."
+    python3_version=$("$python" -c 'import sysconfig; print(sysconfig.get_python_version());')
+    printf "(%s)\n" $python3_version
+
+    printf "Checking for python $python3_version development files..."
+    if pkg-config --exists "python-$python3_version"; then
+       have_python3_dev=1
+       printf "Yes.\n"
+    else
+       have_python3_dev=0
+       printf "No (will not install CFFI-based python bindings).\n"
     fi
 fi
 
 have_python3_cffi=0
 have_python3_pytest=0
-if [ $have_python3 -eq 1 ]; then
+if [ $have_python3_dev -eq 1 ]; then
     printf "Checking for python3 cffi and setuptools... "
     if "$python" -c 'import cffi,setuptools; cffi.FFI().verify()' >/dev/null 2>&1; then
-        printf "Yes.\n"
-        have_python3_cffi=1
-        WITH_PYTHON_DOCS=1
+       printf "Yes.\n"
+       have_python3_cffi=1
+       WITH_PYTHON_DOCS=1
     else
-        WITH_PYTHON_DOCS=0
-        printf "No (will not install CFFI-based python bindings).\n"
+       WITH_PYTHON_DOCS=0
+       printf "No (will not install CFFI-based python bindings).\n"
     fi
     rm -rf __pycache__  # cffi.FFI().verify() uses this space
 
@@ -793,10 +864,10 @@ if [ $have_python3 -eq 1 ]; then
     conf=$(mktemp)
     printf "[pytest]\nminversion=3.0\n" > $conf
     if "$python" -m pytest -c $conf --version >/dev/null 2>&1; then
-        printf "Yes.\n"
-        have_python3_pytest=1
+       printf "Yes.\n"
+       have_python3_pytest=1
     else
-        printf "No (will not test CFFI-based python bindings).\n"
+       printf "No (will not test CFFI-based python bindings).\n"
     fi
     rm -f $conf
 fi
@@ -820,6 +891,19 @@ else
     WITH_BASH=0
 fi
 
+printf "Checking for sfsexp... "
+if pkg-config --exists sfsexp; then
+    printf "Yes.\n"
+    have_sfsexp=1
+    sfsexp_cflags=$(pkg-config --cflags sfsexp)
+    sfsexp_ldflags=$(pkg-config --libs sfsexp)
+else
+    printf "No (will not enable s-expression queries).\n"
+    have_sfsexp=0
+    sfsexp_cflags=
+    sfsexp_ldflags=
+fi
+
 if [ -z "${EMACSLISPDIR-}" ]; then
     EMACSLISPDIR="\$(prefix)/share/emacs/site-lisp"
 fi
@@ -831,10 +915,10 @@ fi
 if [ $WITH_EMACS = "1" ]; then
     printf "Checking if emacs (>= 25) is available... "
     if emacs --quick --batch --eval '(if (< emacs-major-version 25) (kill-emacs 1))' > /dev/null 2>&1; then
-        printf "Yes.\n"
+       printf "Yes.\n"
     else
-        printf "No (disabling emacs related parts of build)\n"
-        WITH_EMACS=0
+       printf "No (disabling emacs related parts of build)\n"
+       WITH_EMACS=0
     fi
 fi
 
@@ -1199,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
@@ -1216,6 +1300,7 @@ cat > Makefile.config <<EOF
 # directory (the current directory at the time configure was run).
 srcdir = ${srcdir}
 NOTMUCH_SRCDIR = ${NOTMUCH_SRCDIR}
+NOTMUCH_BUILDDIR = ${NOTMUCH_BUILDDIR}
 
 # subdirectories to build
 subdirs = ${subdirs}
@@ -1443,6 +1528,13 @@ HAVE_VALGRIND = ${have_valgrind}
 # And if so, flags needed at compile time for valgrind macros
 VALGRIND_CFLAGS = ${valgrind_cflags}
 
+# Whether the sfsexp library is available
+HAVE_SFSEXP = ${have_sfsexp}
+
+# And if so, flags needed at compile/link time for sfsexp
+SFSEXP_CFLAGS = ${sfsexp_cflags}
+SFSEXP_LDFLAGS = ${sfsexp_ldflags}
+
 # Support for emacs
 WITH_EMACS = ${WITH_EMACS}
 
@@ -1459,6 +1551,7 @@ WITH_ZSH = ${WITH_ZSH}
 COMMON_CONFIGURE_CFLAGS = \\
        \$(GMIME_CFLAGS) \$(TALLOC_CFLAGS) \$(ZLIB_CFLAGS)      \\
        -DHAVE_VALGRIND=\$(HAVE_VALGRIND) \$(VALGRIND_CFLAGS)   \\
+       -DHAVE_SFSEXP=\$(HAVE_SFSEXP) \$(SFSEXP_CFLAGS)         \\
        -DHAVE_GETLINE=\$(HAVE_GETLINE)                         \\
        -DWITH_EMACS=\$(WITH_EMACS)                             \\
        -DHAVE_CANONICALIZE_FILE_NAME=\$(HAVE_CANONICALIZE_FILE_NAME) \\
@@ -1475,7 +1568,7 @@ CONFIGURE_CFLAGS = \$(COMMON_CONFIGURE_CFLAGS)
 
 CONFIGURE_CXXFLAGS = \$(COMMON_CONFIGURE_CFLAGS) \$(XAPIAN_CXXFLAGS)
 
-CONFIGURE_LDFLAGS = \$(GMIME_LDFLAGS) \$(TALLOC_LDFLAGS) \$(ZLIB_LDFLAGS) \$(XAPIAN_LDFLAGS)
+CONFIGURE_LDFLAGS = \$(GMIME_LDFLAGS) \$(TALLOC_LDFLAGS) \$(ZLIB_LDFLAGS) \$(XAPIAN_LDFLAGS) \$(SFSEXP_LDFLAGS)
 EOF
 
 # construct the sh.config
@@ -1485,15 +1578,34 @@ cat > sh.config <<EOF
 
 NOTMUCH_SRCDIR='${NOTMUCH_SRCDIR}'
 
+# Flags needed to compile and link against Xapian
+NOTMUCH_XAPIAN_CXXFLAGS="${xapian_cxxflags}"
+NOTMUCH_XAPIAN_LDFLAGS="${xapian_ldflags}"
+
 # Whether to have Xapian retry lock
 NOTMUCH_HAVE_XAPIAN_DB_RETRY_LOCK=${WITH_RETRY_LOCK}
 
+# Flags needed to compile and link against GMime
+NOTMUCH_GMIME_CFLAGS="${gmime_cflags}"
+NOTMUCH_GMIME_LDFLAGS="${gmime_ldflags}"
+
 # Whether GMime can verify X.509 certificate validity
 NOTMUCH_GMIME_X509_CERT_VALIDITY=${gmime_x509_cert_validity}
 
+# Whether GMime emits addresses with angle brackets (with <>)
+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))
 
@@ -1524,6 +1636,13 @@ NOTMUCH_HAVE_PYTHON3_CFFI=${have_python3_cffi}
 # Is the python pytest package available?
 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
@@ -1531,14 +1650,22 @@ EOF
 {
     echo "# Generated by configure, run from doc/conf.py"
     if [ $WITH_EMACS = "1" ]; then
-        echo "tags.add('WITH_EMACS')"
+       echo "tags.add('WITH_EMACS')"
     fi
     if [ $WITH_PYTHON_DOCS = "1" ]; then
-        echo "tags.add('WITH_PYTHON')"
+       echo "tags.add('WITH_PYTHON')"
     fi
     printf "rsti_dir = '%s'\n" "$(cd emacs && pwd -P)"
 } > sphinx.config
 
+cat > bindings/python-cffi/_notmuch_config.py <<EOF
+# _notmuch_config.py was automatically generated by the configure
+# script in the root of the notmuch source tree.
+NOTMUCH_VERSION_FILE='${NOTMUCH_SRCDIR}/version.txt'
+NOTMUCH_INCLUDE_DIR='${NOTMUCH_SRCDIR}/lib'
+NOTMUCH_LIB_DIR='${NOTMUCH_SRCDIR}/lib'
+EOF
+
 # Finally, after everything configured, inform the user how to continue.
 cat <<EOF
 
index 26996c4ab2ccd1136b56e71e0c9d8c86c21a295a..c7520228782b2cacaf48e3b661c593013741e1ba 100644 (file)
@@ -39,8 +39,6 @@ To *run* notmuch-mutt you will need Perl with the following libraries:
   (Debian package: libmail-box-perl)
 - Mail::Header <https://metacpan.org/pod/Mail::Header>
   (Debian package: libmailtools-perl)
-- String::ShellQuote <https://metacpan.org/pod/String::ShellQuote>
-  (Debian package: libstring-shellquote-perl)
 - Term::ReadLine::Gnu <https://metacpan.org/pod/Term::ReadLine::Gnu>
   (Debian package: libterm-readline-gnu-perl)
 
index d1e2c084b4208f093f97a47e81169da28a65d9ad..b81252c809a8e3ed2fae56fd8e2bb14a6e56ed09 100755 (executable)
@@ -2,7 +2,7 @@
 #
 # notmuch-mutt - notmuch (of a) helper for Mutt
 #
-# Copyright: © 2011-2015 Stefano Zacchiroli <zack@upsilon.cc>
+# Copyright: Â© 2011-2015 Stefano Zacchiroli <zack@upsilon.cc>
 # 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() {
index ec3de63d27df2c2aeeee82be0489e6940f7b6988..1177f085e9b067446e718b04455c19932b14072d 100644 (file)
@@ -1,3 +1,280 @@
+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 <bremner@debian.org>  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 <bdrung@debian.org>  Wed, 28 Feb 2024 23:56:48 +0000
+
+notmuch (0.38.2-1) unstable; urgency=medium
+
+  * New upstream bugfix release
+
+ -- David Bremner <bremner@debian.org>  Fri, 01 Dec 2023 07:51:09 -0400
+
+notmuch (0.38.1-1) unstable; urgency=medium
+
+  * New upstream bugfix release
+
+ -- David Bremner <bremner@debian.org>  Thu, 26 Oct 2023 19:58:42 -0300
+
+notmuch (0.38.1~rc1-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Thu, 12 Oct 2023 19:53:10 -0300
+
+notmuch (0.38.1~pre0-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  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 <bremner@debian.org>  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 <bremner@debian.org>  Tue, 12 Sep 2023 08:33:24 -0300
+
+notmuch (0.38~rc2-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  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 <bremner@debian.org>  Sat, 26 Aug 2023 08:31:21 -0300
+
+notmuch (0.38~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  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 <bremner@debian.org>  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 <bremner@debian.org>  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 <bremner@debian.org>  Sun, 14 Aug 2022 10:55:24 -0300
+
+notmuch (0.37~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Sun, 14 Aug 2022 07:28:22 -0300
+
+notmuch (0.36-1) unstable; urgency=medium
+
+  * New upstream release
+
+ -- David Bremner <bremner@debian.org>  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 <bremner@debian.org>  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 <bremner@debian.org>  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 <bremner@debian.org>  Mon, 28 Mar 2022 11:45:11 -0600
+
+notmuch (0.35-1) unstable; urgency=medium
+
+  * New upstream release
+
+ -- David Bremner <bremner@debian.org>  Sun, 06 Feb 2022 12:15:19 -0400
+
+notmuch (0.35~rc0-2) experimental; urgency=medium
+
+  * Reupload with binaries
+
+ -- David Bremner <bremner@debian.org>  Sat, 29 Jan 2022 21:53:29 -0400
+
+notmuch (0.35~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  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 <bremner@debian.org>  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 <bremner@debian.org>  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 <bremner@debian.org>  Wed, 03 Nov 2021 10:20:33 -0300
+
+notmuch (0.34-1) unstable; urgency=medium
+
+  * New upstream release
+  * Adds s-expression based query parser (man notmuch-sexp-queries).
+  * Bug fix: "notmuch breaks on directory removal", thanks to Joerg
+    Jaspert (Closes: #922536).
+  * Respect notmuch-show-text/html-blocked-images for renderer w3m
+    (Closes: #934082).
+  * Bug fix: "add an option to change the database path", thanks to
+    Michael Gold (Closes: #887041) (actually fixed in 0.32)
+
+ -- David Bremner <bremner@debian.org>  Wed, 20 Oct 2021 11:15:23 -0300
+
+notmuch (0.34~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Fri, 15 Oct 2021 08:50:57 -0300
+
+notmuch (0.33.2-1) unstable; urgency=medium
+
+  * Upstream fix for flaky/hanging tests in T355-smime
+
+ -- David Bremner <bremner@debian.org>  Thu, 30 Sep 2021 08:27:10 -0300
+
+notmuch (0.33.1-1) unstable; urgency=medium
+
+  * Upstream fix for flaky tests in T590-libconfig
+
+ -- David Bremner <bremner@debian.org>  Fri, 10 Sep 2021 08:28:48 -0300
+
+notmuch (0.33-2) unstable; urgency=medium
+
+  * Disable two flaky tests in T590-libconfig.
+
+ -- David Bremner <bremner@debian.org>  Sat, 04 Sep 2021 11:29:44 -0700
+
+notmuch (0.33-1) unstable; urgency=medium
+
+  * New upstream release
+  * See /usr/share/doc/notmuch/NEWS.gz for user visible changes.
+
+ -- David Bremner <bremner@debian.org>  Fri, 03 Sep 2021 12:24:41 -0700
+
+notmuch (0.33~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Thu, 26 Aug 2021 08:27:42 -0700
+
+notmuch (0.32.3-1) unstable; urgency=medium
+
+  * new upstream bugfix release
+  * fixes for a few configuration related bugs introduced in 0.32
+  * bump libnotmuch minor version to match documentation.
+
+ -- David Bremner <bremner@debian.org>  Tue, 17 Aug 2021 17:16:09 -0700
+
+notmuch (0.32.2-1) experimental; urgency=medium
+
+  * New upstream bugfix release
+  * Fix for memory leak in "notmuch new" introduced in 0.32
+  * Fix for bug from 2017 that can add duplicate thread-ids to messages.
+
+ -- David Bremner <bremner@debian.org>  Sat, 26 Jun 2021 22:33:56 -0300
+
+notmuch (0.32.1-1) experimental; urgency=medium
+
+  * New upstream bugfix release
+  * Configuration bug fixes (see /usr/share/doc/notmuch/NEWS.gz)
+  * Bug fix for {pre,after}-tag hooks in emacs, related to lexical scope
+    transition.
+
+ -- David Bremner <bremner@debian.org>  Sat, 15 May 2021 09:01:27 -0300
+
+notmuch (0.32-1) experimental; urgency=medium
+
+  * New upstream release
+  * Speedup for handling deleted message files
+  * New configuration features (see /usr/share/doc/notmuch/NEWS.gz) 
+  * Emacs interface codebase cleanup
+
+ -- David Bremner <bremner@debian.org>  Sun, 02 May 2021 07:05:15 -0300
+
+notmuch (0.32~rc2-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Wed, 28 Apr 2021 07:05:22 -0300
+
+notmuch (0.32~rc1-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Sat, 24 Apr 2021 12:46:10 -0300
+
+notmuch (0.31.4-2) unstable; urgency=medium
+
+  * Cherry pick upstream commit 3f4de98e7c8, which fixes a bug where
+    duplicate message-ids can cause multiple thread-ids for some message
+    documents.
+  * Add build-dependency on xapian-tools, for new test
+
+ -- David Bremner <bremner@debian.org>  Mon, 28 Jun 2021 22:48:02 -0300
+
 notmuch (0.31.4-1) unstable; urgency=medium
 
   * New upstream bugfix release
index 8b34d9edb86894998ce4670f47718fcc6ce35181..4fded909a03ca5d4910c2cc8f1fee4f03795ebbe 100644 (file)
@@ -9,7 +9,7 @@ Build-Conflicts:
  gdb [ia64 mips mips64el hppa],
  gdb-minimal,
  ruby1.8,
-Build-Depends:
+Build-Depends: dpkg-dev (>= 1.22.5),
  bash-completion (>=1.9.0~),
  debhelper-compat (= 13),
  dh-elpa (>= 1.3),
@@ -19,16 +19,19 @@ Build-Depends:
  dpkg-dev (>= 1.17.14),
  dtach (>= 0.8) <!nocheck>,
  emacs-nox | emacs-gtk | emacs-lucid | emacs25-nox | emacs25 (>=25~) | emacs25-lucid (>=25~) | emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~),
+ emacs-el,
  gdb [!ia64 !mips !mips64el !kfreebsd-any !alpha !hppa] <!nocheck>,
+ git <!nocheck>,
  gnupg <!nocheck>,
  gpgsm <!nocheck>,
  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~),
+ python3,
  python3-cffi,
  python3-pytest,
  python3-pytest-cov,
@@ -37,6 +40,7 @@ Build-Depends:
  ruby,
  ruby-dev (>>1:1.9.3~),
  texinfo,
+ xapian-tools <!nocheck>,
 Standards-Version: 4.4.1
 Homepage: https://notmuchmail.org/
 Vcs-Git: https://git.notmuchmail.org/git/notmuch -b release
@@ -46,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,
+ 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
@@ -63,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:
@@ -84,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
@@ -99,7 +138,7 @@ Package: python3-notmuch
 Architecture: all
 Section: python
 Depends:
- libnotmuch5 (>= ${source:Version}),
+ libnotmuch5t64 (>= ${source:Version}),
  ${misc:Depends},
  ${python3:Depends},
 Description: Python 3 legacy interface to the notmuch mail search and index library
@@ -117,7 +156,7 @@ Package: python3-notmuch2
 Architecture: any
 Section: python
 Depends:
- libnotmuch5 (>= ${source:Version}),
+ libnotmuch5t64 (>= ${source:Version}),
  ${misc:Depends},
  ${python3:Depends},
  ${shlibs:Depends},
@@ -191,7 +230,6 @@ Architecture: all
 Depends:
  libmail-box-perl,
  libmailtools-perl,
- libstring-shellquote-perl,
  libterm-readline-gnu-perl,
  notmuch (>= 0.4),
  ${misc:Depends},
index 4712b73ff76e8ad31fc54c51999ef862d863dd9e..7b3ce0fa483fab5e2d87d78135ad14749f10011f 100644 (file)
@@ -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 (file)
index a513b47..0000000
+++ /dev/null
@@ -1 +0,0 @@
-usr/lib/*/libnotmuch.so.*
diff --git a/debian/libnotmuch5.symbols b/debian/libnotmuch5.symbols
deleted file mode 100644 (file)
index ec01593..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-libnotmuch.so.5 libnotmuch5 #MINVER#
-* Build-Depends-Package: libnotmuch-dev
- notmuch_built_with@Base 0.23~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_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_create@Base 0.3
- notmuch_database_create_verbose@Base 0.20~rc1
- 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_needs_upgrade@Base 0.3
- notmuch_database_open@Base 0.3
- notmuch_database_open_verbose@Base 0.20~rc1
- notmuch_database_remove_message@Base 0.3
- 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_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++|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++|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 (file)
index 0000000..a513b47
--- /dev/null
@@ -0,0 +1 @@
+usr/lib/*/libnotmuch.so.*
diff --git a/debian/libnotmuch5t64.lintian-overrides b/debian/libnotmuch5t64.lintian-overrides
new file mode 100644 (file)
index 0000000..affb63b
--- /dev/null
@@ -0,0 +1 @@
+libnotmuch5t64: package-name-doesnt-match-sonames libnotmuch5
diff --git a/debian/libnotmuch5t64.symbols b/debian/libnotmuch5t64.symbols
new file mode 100644 (file)
index 0000000..5715dec
--- /dev/null
@@ -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 (file)
index 0000000..fa902fe
--- /dev/null
@@ -0,0 +1 @@
+doc/_build/html usr/share/doc/notmuch
index 6f93feb7b3a299e49eb1f0d1994874ef5a0884f9..8e3004ecc2440267b2dd0c082b27672fff9fc1a7 100644 (file)
@@ -1 +1 @@
-rm_conffile /etc/emacs/site-start.d/50notmuch.el
+rm_conffile /etc/emacs/site-start.d/50notmuch.el 0.33-1~
diff --git a/debian/notmuch-git.install b/debian/notmuch-git.install
new file mode 100644 (file)
index 0000000..2be0827
--- /dev/null
@@ -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 (file)
index 0000000..e0895c8
--- /dev/null
@@ -0,0 +1,2 @@
+usr/share/man/man1/notmuch-git.1.gz
+usr/share/man/man1/nmbug.1.gz
index 6f93feb7b3a299e49eb1f0d1994874ef5a0884f9..8e3004ecc2440267b2dd0c082b27672fff9fc1a7 100644 (file)
@@ -1 +1 @@
-rm_conffile /etc/emacs/site-start.d/50notmuch.el
+rm_conffile /etc/emacs/site-start.d/50notmuch.el 0.33-1~
index 3ac2ff5b45d51cf6801de20f5158406c153829bf..d7e8cf590ebdc1d4d6827050e282ca5211538d6a 100644 (file)
@@ -17,3 +17,4 @@ usr/share/man/man1/notmuch.1.gz
 usr/share/man/man5/notmuch-hooks.5.gz
 usr/share/man/man7/notmuch-properties.7.gz
 usr/share/man/man7/notmuch-search-terms.7.gz
+usr/share/man/man7/notmuch-sexp-queries.7.gz
index fa0551a951906e686860d08a78c615f24af6e3b0..a77ffa152daefd3ca8a611121cc3509a9e5c341d 100755 (executable)
@@ -1,13 +1,18 @@
 #!/usr/bin/make -f
+include /usr/share/dpkg/architecture.mk
+
+ifeq ($(DEB_HOST_ARCH),ppc64el)
+       export NOTMUCH_SKIP_TESTS = T810-tsan
+endif
 
 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,11 +20,8 @@ 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
+       dh_auto_build -- V=1 all sphinx-html
        PYBUILD_NAME=notmuch dh_auto_build --buildsystem=pybuild --sourcedirectory bindings/python
        PYBUILD_NAME=notmuch2 dh_auto_build --buildsystem=pybuild --sourcedirectory bindings/python-cffi
        $(MAKE) -C contrib/notmuch-mutt
@@ -27,7 +29,6 @@ override_dh_auto_build:
 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
 
diff --git a/debian/tests/control b/debian/tests/control
new file mode 100644 (file)
index 0000000..80be1de
--- /dev/null
@@ -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
+    ./test/notmuch-test
+Restrictions: allow-stderr
+Architecture: amd64, arm64
+Depends: @,
+ build-essential,
+ dtach,
+ emacs-nox,
+ gdb,
+ git,
+ gnupg,
+ gpgsm,
+ libtalloc-dev,
+ man,
+ xapian-tools
index 2d9c4af8deb34f844013321bb9ad957ba619c48c..23854f391a1f76fefce19c579de00d059c4d0b3c 100644 (file)
@@ -5,7 +5,7 @@ AUTHOR_EXCLUDE='uncrustify'
 # based on the FSF guideline, for want of a better idea.
 THRESHOLD=15
 
-git ls-files | grep -v -e "$FILE_EXCLUDE" | xargs -n 1 -d \\n \
+git ls-files | grep -v -e "$FILE_EXCLUDE" | tr '\n' '\0' | xargs -0 -n 1 \
                                                   git blame -w --line-porcelain -- | \
     sed -n "/$AUTHOR_EXCLUDE/d; s/^[aA][uU][tT][hH][Oo][rR] //p" | \
     sort -fd | uniq -ic | awk "\$1 >= $THRESHOLD" | sort -nr
diff --git a/devel/check-notmuch-commit b/devel/check-notmuch-commit
new file mode 100755 (executable)
index 0000000..eca5fb9
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+# Usage suggestion:
+#   git rebase -i --exec devel/check-notmuch-commit origin/master
+
+set -e
+
+quick=0
+case "$1" in
+    -q|-Q|--quick)
+       quick=1
+       ;;
+esac
+
+if [ $quick = 0 ]; then
+    make test
+fi
+
+unset uconf
+for file in $(git diff --name-only --diff-filter=AM HEAD^); do
+    case $file in
+       *.c|*.h|*.cc|*.hh)
+           uncrustify --replace -c "${uconf=$(dirname "$0")/uncrustify.cfg}" "$file"
+           ;;
+       *.el)
+           emacs -Q --batch "$file" --eval '(indent-region (point-min) (point-max) nil)' -f save-buffer
+           ;;
+    esac
+done
+
+git diff --quiet
index 65dfe0eb78215ec15f5057db1dd7a748cd8d16d6..218677c2960dc6477c350de7066dd8844e3c815c 100644 (file)
@@ -1,58 +1,60 @@
-|-----------+----------------------------------------+-------------------------------------------------------+-----------------------------------------|
-| 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                            |                                         |
-| 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            |                                                       |                                         |
-| 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-search-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                |                                         |
-| <DEL>     | notmuch-search-scroll-down             | notmuch-show-rewind                                   | notmuch-tree-scroll-message-window-back |
-| <RET>     | notmuch-search-show-thread             | notmuch-show-toggle-message                           | notmuch-tree-show-message               |
-| <SPC>     | notmuch-search-scroll-up               | notmuch-show-advance                                  | notmuch-tree-scroll-or-next             |
-| <TAB>     |                                        | notmuch-show-next-button                              | notmuch-show-next-button                |
-| <backtab> |                                        | 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-tree-button-activate)          |
+| 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            |                                        |                                                       |                                         |
+| 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-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                |                                         |
+| <DEL>        | notmuch-search-scroll-down             | notmuch-show-rewind                                   | notmuch-tree-scroll-message-window-back |
+| <RET>        | notmuch-search-show-thread             | notmuch-show-toggle-message                           | notmuch-tree-show-message               |
+| <SPC>        | notmuch-search-scroll-up               | notmuch-show-advance                                  | notmuch-tree-scroll-or-next             |
+| <TAB>        |                                        | notmuch-show-next-button                              | notmuch-show-next-button                |
+| <backtab>    |                                        | 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 (executable)
index 043c186..0000000
+++ /dev/null
@@ -1,852 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (c) 2011-2014 David Bremner <david@tethera.net>
-#                         W. Trevor King <wking@tremily.us>
-#
-# 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<id>[^/]*)/(?P<tag>[^/]*)')
-
-# 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.<name>.repository, likely 'origin', and
-    branch.<name>.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 ... <command> --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 <tree-ish>, 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.<name>.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 <refspec> 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 <refspec> 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 (file)
index 0000000..e71ba12
--- /dev/null
@@ -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 (executable)
index 0000000..e0e87b4
--- /dev/null
@@ -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(['<a href="mailto:%s">%s</a>' % ((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 = '<a href="%s/show/%s">%s</a>' % (prefix, lnk, subj)
+  return out
+env.globals['link_msg'] = link_msg
+
+def show_msgs(msgs):
+  r = '<ul>'
+  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 += '<li><span style="%s">%s&mdash;%s</span> [%s] %s</li>' % (red, frm, lnk, tags, rs)
+  r += '</ul>'
+  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 "<hr><ul>"
+  if prv: yield "<li>Previous message (by thread): %s</li>" % link_msg(prv)
+  if nxt: yield "<li>Next message (by thread): %s</li>" % link_msg(nxt)
+  yield "</ul><h3>Thread:</h3>"
+  # 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 '</div>'
+    elif part.get_content_maintype() == 'multipart':
+      yield '<div class="multipart-%s" id="%s">' % \
+          (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 '<ul>'
+        for subpart in part.get_payload():
+          yield ('<li><a href="#%s">%s</a></li>' %
+                 (css_part_id(subpart.get_content_type(), parts),
+                  subpart.get_content_type()))
+        yield '</ul>'
+    elif part.get_content_type() == 'message/rfc822':
+      # FIXME extract subject, date, to/cc/from into a separate template and use it here
+      yield '<div class="message-rfc822">'
+    elif part.get_content_maintype() == 'text':
+      if part.get_content_subtype() == 'plain':
+        yield '<div id="%s">' % css_part_id(part.get_content_type(), parts)
+        yield '<pre>'
+        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 '</pre></div>'
+      elif part.get_content_subtype() == 'html':
+        yield '<div id="%s">' % 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 '<iframe class="embedded-html" src="%s"></iframe>' % \
+            os.path.join(prefix, cachedir, mid, filename)
+        yield '</div>'
+      else:
+        yield '<div id="%s">' % css_part_id(part.get_content_type(), parts)
+        (filename, cid) = link_to_cached_file(part, mid, counter)
+        counter += 1
+        yield '<a href="%s">%s (%s)</a>' % (os.path.join(prefix,
+                                                         cachedir,
+                                                         mid,
+                                                         filename),
+                                            filename,
+                                            part.get_content_type())
+        yield '</div>'
+    elif part.get_content_maintype() == 'image':
+      (filename, cid) = link_to_cached_file(part, mid, counter)
+      if cid not in cid_refd:
+        counter += 1
+        yield '<img src="%s" alt="%s">' % (os.path.join(prefix,
+                                                        cachedir,
+                                                        mid,
+                                                        filename),
+                                           filename)
+    else:
+      (filename, cid) = link_to_cached_file(part, mid, counter)
+      counter += 1
+      yield '<a href="%s">%s (%s)</a>' % (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 (symlink)
index 0000000..eba7c76
--- /dev/null
@@ -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 (file)
index 0000000..0f08564
--- /dev/null
@@ -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 (symlink)
index 0000000..5c053ba
--- /dev/null
@@ -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 (symlink)
index 0000000..7fff887
--- /dev/null
@@ -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 (file)
index 0000000..ed6e9f4
--- /dev/null
@@ -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 (file)
index 0000000..90d9293
--- /dev/null
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<html lang="en">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"
+  />
+       <meta name="viewport" content="width=device-width, initial-scale=1"> 
+<link type="text/css" href="{{sprefix}}/css/jquery-ui.css" rel="stylesheet" />
+<link type="text/css" href="{{sprefix}}/css/notmuch-0.1.css" rel="stylesheet" />
+<script type="text/javascript" src="{{sprefix}}/js/jquery.js"></script>
+<script type="text/javascript" src="{{sprefix}}/js/jquery-ui.js"></script>
+<script type="text/javascript" src="{{sprefix}}/js/notmuch-0.1.js"></script>
+<title>{{title}}</title>
+</head><body>
+<div data-role="page">
+<div data-role="header">
+{% block searchform %}
+<form action="{{prefix}}/search/" method="GET" data-ajax="false">
+<label for="terms">Terms</label><input id="terms" name="terms">
+<label for="after">After</label><input id="after"
+name="after"><input type="hidden" id="afters" name="afters">
+<label for="before">Before</label><input id="before"
+name="before"><input id="befores" type="hidden" name="befores">
+<input type="submit" name="submit" id="submit" value="Search">
+</form>
+{% endblock searchform %}
+<h2>{{title}}</h2>
+</div>
+<div data-role="content">
+{% block content %}
+<h2>Common tags</h2>
+<ul>
+{% for tag in tags %}
+  <li><a href="search/tag:{{ tag|url }}">{{ tag|e }}</a></li>
+{% endfor %}
+</ul>
+</div>
+{% endblock content %}
+</div>
+</body></html>
diff --git a/devel/notmuch-web/templates/index.html b/devel/notmuch-web/templates/index.html
new file mode 100644 (file)
index 0000000..0eb3fd3
--- /dev/null
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+{% block content %}
+<h2>Common tags</h2>
+<ul>
+{% for tag in tags %}
+  <li><a href="search/tag:{{ tag|url }}">{{ tag|e }}</a></li>
+{% endfor %}
+</ul>
+{% endblock content %}
diff --git a/devel/notmuch-web/templates/search.html b/devel/notmuch-web/templates/search.html
new file mode 100644 (file)
index 0000000..6719c35
--- /dev/null
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+<h1>{{ terms|e }}</h1>
+{% block content %}
+{% for t in ts %}
+  <h2>{{ t.subject|e }}</h2>
+  <p><i>{{ t.authors|e }}</i></p>
+  <p><b>{{ format_time_range(t.first,t.last)|e }}</b></p>
+  {{ 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 (file)
index 0000000..690f546
--- /dev/null
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+{% block content %}
+{% set headers = ['Subject', 'Date'] %}
+{% set addr_headers = ['To', 'Cc', 'From'] %}
+{% for header in headers: %}
+<p><b>{{header}}:</b> {{m.header(header)|e}}</p>
+{% endfor %}
+{% for header in addr_headers: %}
+<p><b>{{header}}:</b> {{mailto_addrs(m,header)|safe}}</p>
+{% endfor %}
+<hr>
+{% for part in format_message(m,mid): %}{{ part|safe }}{% endfor %}
+{% for b in thread_nav(m): %}{{b|safe}}{% endfor %}
+<hr>
+{% endblock content %}
diff --git a/devel/notmuch-web/todo b/devel/notmuch-web/todo
new file mode 100644 (file)
index 0000000..3c885bd
--- /dev/null
@@ -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?
index 23c29eaafcdba5b37098cd4058fe53262be7b8da..c0accf78bf1fa3ae86ac2586c122dab9c1663499 100755 (executable)
@@ -59,7 +59,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 +77,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 +88,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 +98,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,7 +108,7 @@ else
        append_emsg "Version '$deb_version' is not '($VERSION-1)' in debian/changelog"
 fi
 
-echo -n "Checking that python bindings version is $VERSION... "
+printf %s "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
@@ -118,7 +118,7 @@ else
        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 +132,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 +142,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 +154,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 +176,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
index 28332c6bd1d9b0de96db2be77b695183e6a896df..4e05cdacdf0419718f94065a7c58169ca9138afc 100644 (file)
@@ -14,7 +14,7 @@ are interleaved. Keys are printed as keywords (symbols preceded by a
 colon), e.g. (:id "123" :time 54321 :from "foobar"). Null is printed as
 nil, true as t and false as nil.
 
-This is version 4 of the structured output format.
+This is version 5 of the structured output format.
 
 Version history
 ---------------
@@ -36,6 +36,10 @@ v4
 - (notmuch 0.29) added message.crypto to identify overall message
   cryptographic state
 
+v5
+- sorting support for notmuch show (no change to actual schema,
+  just new command line argument)
+
 Common non-terminals
 --------------------
 
@@ -72,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
@@ -79,6 +84,7 @@ message = {
 
     headers:        headers,
     crypto:         crypto,
+    duplicate:      integer,
     body?:          [part]    # omitted if --body=false
 }
 
@@ -141,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"}]
 
@@ -158,6 +166,7 @@ signature = {
     created?:       unix_time,
     expires?:       unix_time,
     userid?:        string
+    email?:         string
     # if status is not "good":
     keyid?:         string
     errors?:       sig_errors
index f476d1da46c990e3b2a6bc45a4cb3e5097b9aa09..aafa77a0fe229e7dc2f8435e6e7791c5dc8619ae 100644 (file)
@@ -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 (file)
index 0000000..543a5f9
--- /dev/null
@@ -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 <man1/notmuch-git>
+   notmuch-setup <man1/notmuch>
+   
index 1a5c217bb91e5299fbb2bc83f4e8d19bff466490..ee1b336af2ad61d36336c5891b821cf201915560 100644 (file)
@@ -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-2024, 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'):
+    for file in ('notmuch.rsti', 'notmuch-lib.rsti', 'notmuch-hello.rsti', 'notmuch-show.rsti', 'notmuch-tag.rsti', 'notmuch-tree.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,
@@ -80,6 +84,11 @@ htmlhelp_basename = 'notmuchdoc'
 # Despite the name, this actually affects manual pages as well.
 html_use_smartypants = False
 
+# See:
+# - https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-manpages_url
+# - https://manpages.debian.org/
+manpages_url = 'https://manpages.debian.org/{page}.{section}.html'
+
 # -- Options for manual page output ---------------------------------------
 
 # One entry per manual page. List of tuples
@@ -87,6 +96,8 @@ html_use_smartypants = False
 
 notmuch_authors = u'Carl Worth and many others'
 
+man_make_section_directory = False
+
 man_pages = [
     ('man1/notmuch', 'notmuch',
      u'thread-based email index, search, and tagging',
@@ -116,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),
@@ -152,6 +171,14 @@ 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),
+
     ('man1/notmuch-show', 'notmuch-show',
      u'show messages matching the given search terms',
      [notmuch_authors], 1),
@@ -189,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 (file)
index 0000000..1b0392e
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+
+
+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}
index a3bf348084b8c2f8140515328a107436ffdb732f..fec3e6c2d63f048e54c0aa55bd7c7a9d246af37d 100644 (file)
@@ -2,35 +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
-   man1/notmuch-show
-   man1/notmuch-tag
    python-bindings
-
-Indices and tables
-==================
+   
+Index and search
+-----------------
 
 * :ref:`genindex`
-* :ref:`modindex`
 * :ref:`search`
index 2a7df6f0d80f21133fee3f58902f907e83d7a34b..c70dde35d87f4836f7df889ef99bfd7ba211e02f 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-address(1):
+
 ===============
 notmuch-address
 ===============
@@ -13,94 +15,102 @@ DESCRIPTION
 Search for messages matching the given search terms, and display the
 addresses from them. Duplicate addresses are filtered out.
 
-See **notmuch-search-terms(7)** for details of the supported syntax for
+See :any:`notmuch-search-terms(7)` for details of the supported syntax for
 <search-terms>.
 
 Supported options for **address** include
 
-``--format=``\ (**json**\ \|\ **sexp**\ \|\ **text**\ \|\ **text0**)
-    Presents the results in either JSON, S-Expressions, newline
-    character separated plain-text (default), or null character
-    separated plain-text (compatible with **xargs(1)** -0 option where
-    available).
-
-``--format-version=N``
-    Use the specified structured output format version. This is
-    intended for programs that invoke **notmuch(1)** internally. If
-    omitted, the latest supported version will be used.
-
-``--output=(sender|recipients|count|address)``
-    Controls which information appears in the output. This option can
-    be given multiple times to combine different outputs.  When
-    neither ``--output=sender`` nor ``--output=recipients`` is
-    given, ``--output=sender`` is implied.
-
-    **sender**
-        Output all addresses from the *From* header.
-
-        Note: Searching for **sender** should be much faster than
-        searching for **recipients**, because sender addresses are
-        cached directly in the database whereas other addresses need
-        to be fetched from message files.
-
-    **recipients**
-        Output all addresses from the *To*, *Cc* and *Bcc* headers.
-
-    **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**
-        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.
-
-``--deduplicate=(no|mailbox|address)``
-    Control the deduplication of results.
-
-    **no**
-        Output all occurrences of addresses in the matching
-        messages. This is not applicable with ``--output=count``.
-
-    **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**
-        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
-        matching messages. If ``--output=count`` is specified, include all
-        variants in the count.
-
-``--sort=``\ (**newest-first**\ \|\ **oldest-first**)
-    This option can be used to present results in either chronological
-    order (**oldest-first**) or reverse chronological order
-    (**newest-first**).
-
-    By default, results will be displayed in reverse chronological
-    order, (that is, the newest results will be displayed first).
-
-    However, if either ``--output=count`` or ``--deduplicate=address`` is
-    specified, this option is ignored and the order of the results is
-    unspecified.
-
-``--exclude=(true|false)``
-    A message is called "excluded" if it matches at least one tag in
-    search.exclude\_tags that does not appear explicitly in the search
-    terms. This option specifies whether to omit excluded messages in
-    the search process.
-
-    The default value, **true**, prevents excluded messages from
-    matching the search terms.
-
-    **false** allows excluded messages to match search terms and
-    appear in displayed results.
+.. program:: address
+
+.. option:: --format=(json|sexp|text|text0)
+
+   Presents the results in either JSON, S-Expressions, newline
+   character separated plain-text (default), or null character
+   separated plain-text (compatible with :manpage:`xargs(1)` -0
+   option where available).
+
+.. option:: --format-version=N
+
+   Use the specified structured output format version. This is
+   intended for programs that invoke :any:`notmuch(1)` internally. If
+   omitted, the latest supported version will be used.
+
+.. option:: --output=(sender|recipients|count|address)
+
+   Controls which information appears in the output. This option can
+   be given multiple times to combine different outputs.  When
+   neither ``--output=sender`` nor ``--output=recipients`` is
+   given, ``--output=sender`` is implied.
+
+   sender
+     Output all addresses from the *From* header.
+
+     Note: Searching for **sender** should be much faster than
+     searching for **recipients**, because sender addresses are
+     cached directly in the database whereas other addresses need
+     to be fetched from message files.
+
+   recipients
+     Output all addresses from the *To*, *Cc* and *Bcc* headers.
+
+   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
+     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.
+
+.. option:: --deduplicate=(no|mailbox|address)
+
+   Control the deduplication of results.
+
+   no
+     Output all occurrences of addresses in the matching
+     messages. This is not applicable with ``--output=count``.
+
+   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
+     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
+     matching messages. If ``--output=count`` is specified, include all
+     variants in the count.
+
+.. option:: --sort=(newest-first|oldest-first)
+
+   This option can be used to present results in either chronological
+   order (**oldest-first**) or reverse chronological order
+   (**newest-first**).
+
+   By default, results will be displayed in reverse chronological
+   order, (that is, the newest results will be displayed first).
+
+   However, if either ``--output=count`` or ``--deduplicate=address`` is
+   specified, this option is ignored and the order of the results is
+   unspecified.
+
+.. option:: --exclude=(true|false)
+
+   A message is called "excluded" if it matches at least one tag in
+   search.exclude\_tags that does not appear explicitly in the search
+   terms. This option specifies whether to omit excluded messages in
+   the search process.
+
+   The default value, **true**, prevents excluded messages from
+   matching the search terms.
+
+   **false** allows excluded messages to match search terms and
+   appear in displayed results.
 
 EXIT STATUS
 ===========
@@ -116,16 +126,16 @@ This command supports the following special exit status codes
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**,
-**notmuch-search(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index b05593ec22134181e9a31d642cfe9c8b4f5dba23..cb1c858b1e4a85af2698d0c8e0b6441e90b4563f 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-compact(1):
+
 ===============
 notmuch-compact
 ===============
@@ -24,37 +26,31 @@ process (which may be quite long) to protect data integrity.
 
 Supported options for **compact** include
 
-``--backup=``\ <directory>
-    Save the current database to the given directory before replacing
-    it with the compacted database. The backup directory must not
-    exist and it must reside on the same mounted filesystem as the
-    current database.
+.. program:: compact
 
-``--quiet``
-    Do not report database compaction progress to stdout.
+.. option:: --backup=<directory>
 
-ENVIRONMENT
-===========
+   Save the current database to the given directory before replacing
+   it with the compacted database. The backup directory must not
+   exist and it must reside on the same mounted filesystem as the
+   current database.
 
-The following environment variables can be used to control the behavior
-of notmuch.
+.. option:: --quiet
 
-**NOTMUCH\_CONFIG**
-    Specifies the location of the notmuch configuration file. Notmuch
-    will use ${HOME}/.notmuch-config if this variable is not set.
+   Do not report database compaction progress to stdout.
 
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index 32290a38a3e60605230e24e0dbab651b97effc5d..bd34afa40ee6975a7f1aa316bd4e22e3f1605070 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-config(1):
+
 ==============
 notmuch-config
 ==============
@@ -17,138 +19,121 @@ DESCRIPTION
 The **config** command can be used to get or set settings in the notmuch
 configuration file and corresponding database.
 
-**get**
-    The value of the specified configuration item is printed to
-    stdout. If the item has multiple values (it is a list), each value
-    is separated by a newline character.
+.. program:: config
 
-**set**
-    The specified configuration item is set to the given value. To
-    specify a multiple-value item (a list), provide each value as a
-    separate command-line argument.
+.. option:: get
 
-    If no values are provided, the specified configuration item will
-    be removed from the configuration file.
+   The value of the specified configuration item is printed to
+   stdout. If the item has multiple values (it is a list), each value
+   is separated by a newline character.
 
-    With the `--database` option, updates configuration metadata
-    stored in the database, rather than the default (text)
-    configuration file.
+.. option:: set
 
-**list**
-    Every configuration item is printed to stdout, each on a separate
-    line of the form::
+   The specified configuration item is set to the given value. To
+   specify a multiple-value item (a list), provide each value as a
+   separate command-line argument.
 
-        section.item=value
+   If no values are provided, the specified configuration item will
+   be removed from the configuration file.
 
-    No additional whitespace surrounds the dot or equals sign
-    characters. In a multiple-value item (a list), the values are
-    separated by semicolon characters.
+   With the `--database` option, updates configuration metadata
+   stored in the database, rather than the default (text)
+   configuration file.
 
-The available configuration items are described below.
+.. option:: list
 
-**database.path**
-    Notmuch will store its database here, (in
-    sub-directory named ``.notmuch`` if **database.mail\_root**
-    is unset).
+   Every configuration item is printed to stdout, each on a separate
+   line of the form::
 
-    Default: ``$MAILDIR`` variable if set, otherwise ``$HOME/mail``.
+     section.item=value
 
-**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.
+   No additional whitespace surrounds the dot or equals sign
+   characters. In a multiple-value item (a list), the values are
+   separated by semicolon characters.
+
+The available configuration items are described below. Non-absolute
+paths are presumed relative to `$HOME` for items in section
+**database**.
+
+.. nmconfig:: built_with.<name>
+
+    Compile time feature <name>. Current possibilities include
+    "retry_lock" (configure option, included by default).
+    (since notmuch 0.30, "compact" and "field_processor" are
+    always included.)
+
+.. nmconfig:: 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.
+
+.. nmconfig:: database.backup_dir
+
+    Directory to store tag dumps when upgrading database.
 
     History: this configuration value was introduced in notmuch 0.32.
 
-    Default: For compatibility with older configurations, the value of
-    database.path is used if **database.mail\_root** is unset.
+    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
-    **notmuch-hooks(5)**.
+    :any:`notmuch-hooks(5)`.
 
-**user.name**
-    Your full name.
+    History: this configuration value was introduced in notmuch 0.32.
 
-    Default: ``$NAME`` variable if set, otherwise read from
-    ``/etc/passwd``.
+    Default: See HOOKS, below.
 
-**user.primary\_email**
-    Your primary email address.
+.. nmconfig:: database.mail_root
 
-    Default: ``$EMAIL`` variable if set, otherwise constructed from
-    the username and hostname of the current machine.
+    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.
 
-**user.other\_email**
-    A list of other email addresses at which you receive email.
+    History: this configuration value was introduced in notmuch 0.32.
 
-    Default: not set.
+    Default: For compatibility with older configurations, the value of
+    database.path is used if :nmconfig:`database.mail_root` is unset.
 
-**new.tags**
-    A list of tags that will be added to all messages incorporated by
-    **notmuch new**.
+.. nmconfig:: database.path
 
-    Default: ``unread;inbox``.
+    Notmuch will store its database here, (in
+    sub-directory named ``.notmuch`` if :nmconfig:`database.mail_root`
+    is unset).
 
-**new.ignore**
-    A list to specify files and directories that will not be searched
-    for messages by **notmuch new**. Each entry in the list is either:
+    Default: see :ref:`database`
 
-    A file or a directory name, without path, that will be ignored,
-    regardless of the location in the mail store directory hierarchy.
+.. nmconfig:: git.path
 
-    Or:
+    Default location for git repository for :any:`notmuch-git`.
 
-    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".
+.. nmconfig:: git.safe_fraction
 
-    Default: empty list.
+   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".
 
-**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.
+.. nmconfig:: git.tag_prefix
 
-    Default: empty list. Note that **notmuch-setup(1)** puts
-    ``deleted;spam`` here when creating new configuration file.
+    Default tag prefix (filter) for :any:`notmuch-git`.
 
-**maildir.synchronize\_flags**
-    If true, then the following maildir flags (in message filenames)
-    will be synchronized with the corresponding notmuch tags:
+.. nmconfig:: index.as_text
 
-    +--------+-----------------------------------------------+
-    | Flag   | Tag                                           |
-    +========+===============================================+
-    | D      | draft                                         |
-    +--------+-----------------------------------------------+
-    | F      | flagged                                       |
-    +--------+-----------------------------------------------+
-    | P      | passed                                        |
-    +--------+-----------------------------------------------+
-    | R      | replied                                       |
-    +--------+-----------------------------------------------+
-    | S      | unread (added when 'S' flag is not present)   |
-    +--------+-----------------------------------------------+
-
-    The **notmuch new** command will notice flag changes in filenames
-    and update tags, while the **notmuch tag** and **notmuch restore**
-    commands will notice tag changes and update flags in filenames.
+   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 **notmuch new** before **notmuch tag** or
-    **notmuch restore** 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``.
 
@@ -164,8 +149,8 @@ The available configuration items are described below.
     ``nostash`` is the same as ``true`` except that it will not stash
     newly-discovered session keys in the database.
 
-    From the command line (i.e. during **notmuch-new(1)**,
-    **notmuch-insert(1)**, or **notmuch-reindex(1)**), the user can
+    From the command line (i.e. during :any:`notmuch-new(1)`,
+    :any:`notmuch-insert(1)`, or :any:`notmuch-reindex(1)`), the user can
     override the database's stored decryption policy with the
     ``--decrypt=`` option.
 
@@ -189,7 +174,7 @@ The available configuration items are described below.
 
     Stashed session keys are kept in the database as properties
     associated with the message.  See ``session-key`` in
-    **notmuch-properties(7)** for more details about how they can be
+    :any:`notmuch-properties(7)` for more details about how they can be
     useful.
 
     Be aware that the notmuch index is likely sufficient (and a
@@ -201,73 +186,200 @@ The available configuration items are described below.
 
     Default: ``auto``.
 
-**index.header.<prefix>**
+.. _index.header:
+
+.. nmconfig:: index.header.<prefix>
+
     Define the query prefix <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
     defined prefixes must not start with 'a'...'z'; in particular
     adding a prefix with same name as a predefined prefix is not
-    supported. See **notmuch-search-terms(7)** for a list of existing
+    supported. See :any:`notmuch-search-terms(7)` for a list of existing
     prefixes, and an explanation of probabilistic prefixes.
 
-**built_with.<name>**
-    Compile time feature <name>. 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.<name>
 
-**query.<name>**
     Expansion for named query called <name>. See
-    **notmuch-search-terms(7)** for more information about named
+    :any:`notmuch-search-terms(7)` for more information about named
     queries.
 
-ENVIRONMENT
-===========
+.. nmconfig:: search.exclude_tags
 
-The following environment variables can be used to control the behavior
-of notmuch.
+    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:: show.extra_headers
 
-**NOTMUCH\_CONFIG**
-    Specifies the location of the notmuch configuration file.
+    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.
 
-**NOTMUCH_PROFILE**
-    Selects among notmuch configurations.
+    History: This configuration value was introduced in notmuch 0.35.
+
+    Default: empty list.
+
+.. nmconfig:: squery.<name>
+
+    Expansion for named query called <name>, 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
 -------------
 
-If ``NOTMUCH_CONFIG`` is unset, notmuch tries (in order)
+Notmuch configuration file search order:
+
+1. File specified by :option:`notmuch --config` global option; see
+   :any:`notmuch(1)`.
+
+2. File specified by :envvar:`NOTMUCH_CONFIG` environment variable.
+
+3. ``$XDG_CONFIG_HOME/notmuch/<profile>/config`` where ``<profile>``
+   is defined by :envvar:`NOTMUCH_PROFILE` environment variable if
+   set, ``$XDG_CONFIG_HOME/notmuch/default/config`` otherwise.
 
-- ``$XDG_CONFIG_HOME/notmuch/<profile>/config`` where ``<profile>`` is
-  defined by ``$NOTMUCH_PROFILE`` or "default"
-- ``${HOME}/.notmuch-config<profile>`` where ``<profile>`` is
-  ``.$NOTMUCH_PROFILE`` or ""
+4. ``$HOME/.notmuch-config.<profile>`` where ``<profile>`` is defined
+   by :envvar:`NOTMUCH_PROFILE` environment variable if set,
+   ``$HOME/.notmuch-config`` otherwise.
 
-Hooks
+.. _database:
+
+DATABASE LOCATION
+-----------------
+
+Notmuch database search order:
+
+1. Directory specified by :envvar:`NOTMUCH_DATABASE` environment variable.
+
+2. Directory specified by config key ``database.path``.
+
+3. ``$XDG_DATA_HOME/notmuch/<profile>`` where ``<profile>``
+   is defined by :envvar:`NOTMUCH_PROFILE` environment variable if
+   set, ``$XDG_DATA_HOME/notmuch/default`` otherwise.
+
+4. Directory specified by :envvar:`MAILDIR` environment variable.
+
+5. ``$HOME/mail``
+
+HOOKS
 -----
 
-If ``database.hook_dir`` is unset, notmuch tries (in order)
+Notmuch hook directory search order:
+
+1. Directory specified by ``database.hook_dir`` configuration option.
+
+2. ``$XDG_CONFIG_HOME/notmuch/<profile>/hooks`` where ``<profile>``
+   is defined by :envvar:`NOTMUCH_PROFILE` environment variable if
+   set, ``$XDG_CONFIG_HOME/notmuch/default/hooks`` otherwise.
 
-- ``$XDG_CONFIG_HOME/notmuch/<profile>/hooks`` where ``<profile>`` is
-  defined by ``$NOTMUCH_PROFILE`` or "default"
-- ``<database.path>/.notmuch/hooks``
+3. ``<database.path>/.notmuch/hooks``
 
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-properties(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-properties(7)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index 0eac5dbef4ba09242795cf14abc3de2affe96444..4c9c9a1cb4d78ef47c1c03bff273d46481e8bf45 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-count(1):
+
 =============
 notmuch-count
 =============
@@ -17,56 +19,63 @@ The number of matching messages (or threads) is output to stdout.
 With no search terms, a count of all messages (or threads) in the
 database will be displayed.
 
-See **notmuch-search-terms(7)** for details of the supported syntax for
+See :any:`notmuch-search-terms(7)` for details of the supported syntax for
 <search-terms>.
 
 Supported options for **count** include
 
-``--output=(messages|threads|files)``
-    **messages**
-        Output the number of matching messages. This is the default.
+.. program:: count
+
+.. option:: --output=(messages|threads|files)
+
+   messages
+     Output the number of matching messages. This is the default.
+
+   threads
+     Output the number of matching threads.
+
+   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
+     same message-id).
+
+.. option:: --exclude=(true|false)
+
+   Specify whether to omit messages matching search.exclude\_tags from
+   the count (the default) or not.
 
-    **threads**
-        Output the number of matching threads.
+.. option:: --batch
 
-    **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
-        same message-id).
+   Read queries from a file (stdin by default), one per line, and
+   output the number of matching messages (or threads) to stdout, one
+   per line. On an empty input line the count of all messages (or
+   threads) in the database will be output. This option is not
+   compatible with specifying search terms on the command line.
 
-``--exclude=(true|false)``
-    Specify whether to omit messages matching search.exclude\_tags from
-    the count (the default) or not.
+.. option:: --lastmod
 
-``--batch``
-    Read queries from a file (stdin by default), one per line, and
-    output the number of matching messages (or threads) to stdout, one
-    per line. On an empty input line the count of all messages (or
-    threads) in the database will be output. This option is not
-    compatible with specifying search terms on the command line.
+   Append lastmod (counter for number of database updates) and UUID
+   to the output. lastmod values are only comparable between
+   databases with the same UUID.
 
-``--lastmod``
-    Append lastmod (counter for number of database updates) and UUID
-    to the output. lastmod values are only comparable between
-    databases with the same UUID.
+.. option:: --input=<filename>
 
-``--input=``\ <filename>
-    Read input from given file, instead of from stdin. Implies
-    ``--batch``.
+   Read input from given file, instead of from stdin. Implies
+   ``--batch``.
 
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index ec6335b2febc97099249c01c02002cf6044192f6..a7ca39d008215aff883a1ba6d58ead49646b642f 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-dump(1):
+
 ============
 notmuch-dump
 ============
@@ -19,97 +21,103 @@ recreated from the messages themselves. The output of notmuch dump is
 therefore the only critical thing to backup (and much more friendly to
 incremental backup than the native database files.)
 
-See **notmuch-search-terms(7)** for details of the supported syntax
+See :any:`notmuch-search-terms(7)` for details of the supported syntax
 for <search-terms>. With no search terms, a dump of all messages in
 the database will be generated. A ``--`` argument instructs notmuch that
 the remaining arguments are search terms.
 
 Supported options for **dump** include
 
-``--gzip``
-    Compress the output in a format compatible with **gzip(1)**.
-
-``--format=(sup|batch-tag)``
-    Notmuch restore supports two plain text dump formats, both with
-    one message-id per line, followed by a list of tags.
-
-    **batch-tag**
-        The default **batch-tag** dump format is intended to more
-        robust against malformed message-ids and tags containing
-        whitespace or non-\ **ascii(7)** characters. Each line has the
-        form::
-
-         +<*encoded-tag*\ > +<*encoded-tag*\ > ... -- id:<*quoted-message-id*\ >
-
-        Tags are hex-encoded by replacing every byte not matching the
-        regex **[A-Za-z0-9@=.,\_+-]** with **%nn** where nn is the two
-        digit hex encoding. The message ID is a valid Xapian query,
-        quoted using Xapian boolean term quoting rules: if the ID
-        contains whitespace or a close paren or starts with a double
-        quote, it must be enclosed in double quotes and double quotes
-        inside the ID must be doubled. The astute reader will notice
-        this is a special case of the batch input format for
-        **notmuch-tag(1)**; note that the single message-id query is
-        mandatory for **notmuch-restore(1)**.
-
-    **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). Each line has the
-        following form::
-
-          <*message-id*\ > **(** <*tag*\ > ... **)**
-
-        with zero or more tags are separated by spaces. Note that
-        (malformed) message-ids may contain arbitrary non-null
-        characters. Note also that tags with spaces will not be
-        correctly restored with this format.
-
-``--include=(config|properties|tags)``
-    Control what kind of metadata is included in the output.
-
-    **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**
-        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 **notmuch-properties(7)** for more details.
-
-    **tags**
-        Output per-message boolean metadata, namely tags. See *format* above
-        for description of the output.
-
-    The default is to include all available types of data.  The option
-    can be specified multiple times to select some subset. As of
-    version 3 of the dump format, there is a header line of the
-    following form::
-
-      #notmuch-dump <*format*>:<*version*> <*included*>
-
-    where <*included*> is a comma separated list of the above options.
-
-``--output=``\ <filename>
-    Write output to given file instead of stdout.
+.. program:: dump
+
+.. option:: --gzip
+
+   Compress the output in a format compatible with :manpage:`gzip(1)`.
+
+.. option:: --format=(sup|batch-tag)
+
+   Notmuch restore supports two plain text dump formats, both with
+   one message-id per line, followed by a list of tags.
+
+   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
+     has the form::
+
+       +<*encoded-tag*\ > +<*encoded-tag*\ > ... -- id:<*quoted-message-id*\ >
+
+     Tags are hex-encoded by replacing every byte not matching the
+     regex **[A-Za-z0-9@=.,\_+-]** with **%nn** where nn is the two
+     digit hex encoding. The message ID is a valid Xapian query,
+     quoted using Xapian boolean term quoting rules: if the ID
+     contains whitespace or a close paren or starts with a double
+     quote, it must be enclosed in double quotes and double quotes
+     inside the ID must be doubled. The astute reader will notice
+     this is a special case of the batch input format for
+     :any:`notmuch-tag(1)`; note that the single message-id query is
+     mandatory for :any:`notmuch-restore(1)`.
+
+   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
+     for mail, then the :any:`notmuch-restore(1)` command provides
+     you a way to import all of your tags (or labels as sup calls
+     them). Each line has the following form::
+
+       <*message-id*\ > **(** <*tag*\ > ... **)**
+
+     with zero or more tags are separated by spaces. Note that
+     (malformed) message-ids may contain arbitrary non-null
+     characters. Note also that tags with spaces will not be
+     correctly restored with this format.
+
+.. option:: --include=(config|properties|tags)
+
+   Control what kind of metadata is included in the output.
+
+   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
+     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
+     Output per-message boolean metadata, namely tags. See *format* above
+     for description of the output.
+
+   The default is to include all available types of data.  The option
+   can be specified multiple times to select some subset. As of
+   version 3 of the dump format, there is a header line of the
+   following form::
+
+     #notmuch-dump <*format*>:<*version*> <*included*>
+
+   where <*included*> is a comma separated list of the above options.
+
+.. option:: --output=<filename>
+
+   Write output to given file instead of stdout.
 
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-properties(7)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-properties(7)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index a0476136f50340f9869d858a079b29e755e2df6a..d8af08bda47a530dac1827a28c55fa976fe96e0a 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-emacs-mua(1):
+
 =================
 notmuch-emacs-mua
 =================
@@ -15,67 +17,86 @@ subject, recipients, and message body, or mailto: URL.
 
 Supported options for **emacs-mua** include
 
-``-h, --help``
-    Display help.
+.. program:: emacs-mua
+
+.. option:: -h, --help
+
+   Display help.
+
+.. option:: -s, --subject=<subject>
+
+   Specify the subject of the message.
+
+.. option:: --to=<to-address>
+
+   Specify a recipient (To).
+
+.. option:: -c, --cc=<cc-address>
 
-``-s, --subject=``\ <subject>
-    Specify the subject of the message.
+   Specify a carbon-copy (Cc) recipient.
 
-``--to=``\ <to-address>
-    Specify a recipient (To).
+.. option:: -b, --bcc=<bcc-address>
 
-``-c, --cc=``\ <cc-address>
-    Specify a carbon-copy (Cc) recipient.
+   Specify a blind-carbon-copy (Bcc) recipient.
 
-``-b, --bcc=``\ <bcc-address>
-    Specify a blind-carbon-copy (Bcc) recipient.
+.. option:: -i, --body=<file>
 
-``-i, --body=``\ <file>
-    Specify a file to include into the body of the message.
+   Specify a file to include into the body of the message.
 
-``--hello``
-    Go to the Notmuch hello screen instead of the message composition
-    window if no message composition parameters are given.
+.. option:: --hello
 
-``--no-window-system``
-    Even if a window system is available, use the current terminal.
+   Go to the Notmuch hello screen instead of the message composition
+   window if no message composition parameters are given.
 
-``--client``
-    Use **emacsclient**, rather than **emacs**. For **emacsclient** to
-    work, you need an already running Emacs with a server, or use
-    ``--auto-daemon``.
+.. option:: --no-window-system
 
-``--auto-daemon``
-    Automatically start Emacs in daemon mode, if the Emacs server is
-    not running. Applicable with ``--client``. Implies
-    ``--create-frame``.
+   Even if a window system is available, use the current terminal.
 
-``--create-frame``
-    Create a new frame instead of trying to use the current Emacs
-    frame. Applicable with ``--client``. This will be required when
-    Emacs is running (or automatically started with ``--auto-daemon``)
-    in daemon mode.
+.. option:: --client
 
-``--print``
-    Output the resulting elisp to stdout instead of evaluating it.
+   Use :manpage:`emacsclient(1)`, rather than
+   :manpage:`emacs(1)`. For :manpage:`emacsclient(1)` to work, you
+   need an already running Emacs with a server, or use
+   ``--auto-daemon``.
+
+.. option:: --auto-daemon
+
+   Automatically start Emacs in daemon mode, if the Emacs server is
+   not running. Applicable with ``--client``. Implies
+   ``--create-frame``.
+
+.. option:: --create-frame
+
+   Create a new frame instead of trying to use the current Emacs
+   frame. Applicable with ``--client``. This will be required when
+   Emacs is running (or automatically started with ``--auto-daemon``)
+   in daemon mode.
+
+.. option:: --print
+
+   Output the resulting elisp to stdout instead of evaluating it.
 
 The supported positional parameters and short options are a compatible
-subset of the **mutt** MUA command-line options. The options and
-positional parameters modifying the message can't be combined with the
-mailto: URL.
+subset of the :manpage:`mutt(1)` MUA command-line options. The options
+and positional parameters modifying the message can't be combined with
+the mailto: URL.
 
 Options may be specified multiple times.
 
 ENVIRONMENT VARIABLES
 =====================
 
-**EMACS**
-    Name of emacs command to invoke. Defaults to "emacs".
+.. envvar:: EMACS
+
+   Name of emacs command to invoke. Defaults to "emacs".
+
+.. envvar:: EMACSCLIENT
 
-**EMACSCLIENT**
-    Name of emacsclient command to invoke. Defaults to "emacsclient".
+   Name of emacsclient command to invoke. Defaults to "emacsclient".
 
 SEE ALSO
 ========
 
-**notmuch(1)**, **emacsclient(1)**, **mutt(1)**
+:any:`notmuch(1)`,
+:manpage:`emacsclient(1)`,
+:manpage:`mutt(1)`
diff --git a/doc/man1/notmuch-git.rst b/doc/man1/notmuch-git.rst
new file mode 100644 (file)
index 0000000..33a46f8
--- /dev/null
@@ -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 <repo>, --git-dir <repo>
+
+   Operate on git repository *repo*. See :ref:`repo_location` for
+   defaults.
+
+.. option:: -p <prefix>, --tag-prefix <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 <level>, --log-level <level>
+
+   Log verbosity, one of: `critical`, `error`, `warning`, `info`,
+   `debug`. Defaults to `warning`.
+
+SUBCOMMANDS
+-----------
+
+For help on a particular subcommand, run: 'notmuch-git ... <command> --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 <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.
+
+    .. 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.<name>.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.<name>.repository`, likely `origin`, and `branch.<name>.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 nmbug
+   (equivalently, tag has been deleted in nmbug repo, e.g. by a
+   pull, but not restored to notmuch database).
+
+   .. describe:: D
+
+   Tag is present in nmbug 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 <format_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 <format_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)`
index 86e2f567348ba5920c972a967b7e936c35e34d82..e05bd0b5730507bd1bed22ee30016713ed36d569 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-insert(1):
+
 ==============
 notmuch-insert
 ==============
@@ -12,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 **notmuch new** afterwards.
+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.
 
@@ -25,58 +27,75 @@ If the new message is a duplicate of an existing message in the database
 (it has same Message-ID), it will be added to the maildir folder and
 notmuch database, but the tags will not be changed.
 
-The **insert** command supports hooks. See **notmuch-hooks(5)** for
+The **insert** command supports hooks. See :any:`notmuch-hooks(5)` for
 more details on hooks.
 
 Option arguments must appear before any tag operation arguments.
 Supported options for **insert** include
 
-``--folder=<``\ folder\ **>**
-    Deliver the message to the specified folder, relative to the
-    top-level directory given by the value of **database.path**. The
-    default is the empty string, which means delivering to the
-    top-level directory.
-
-``--create-folder``
-    Try to create the folder named by the ``--folder`` option, if it
-    does not exist. Otherwise the folder must already exist for mail
-    delivery to succeed.
-
-``--keep``
-    Keep the message file if indexing fails, and keep the message
-    indexed if applying tags or maildir flag synchronization
-    fails. Ignore these errors and return exit status 0 to indicate
-    successful mail delivery.
-
-``--no-hooks``
-    Prevent hooks from being run.
-
-``--world-readable``
-    When writing mail to the mailbox, allow it to be read by users
-    other than the current user.  Note that this does not override
-    umask.  By default, delivered mail is only readable by the current
-    user.
-
-``--decrypt=(true|nostash|auto|false)``
-    If ``true`` and the message is encrypted, try to decrypt the
-    message while indexing, stashing any session keys discovered.  If
-    ``auto``, and notmuch already knows about a session key for the
-    message, it will try decrypting using that session key but will
-    not try to access the user's secret keys.  If decryption is
-    successful, index the cleartext itself.  Either way, the message
-    is always stored to disk in its original form (ciphertext).
-
-    ``nostash`` is the same as ``true`` except that it will not stash
-    newly-discovered session keys in the database.
-
-    Be aware that the index is likely sufficient (and a stashed
-    session key is certainly sufficient) to reconstruct the cleartext
-    of the message itself, so please ensure that the notmuch message
-    index is adequately protected. DO NOT USE ``--decrypt=true`` or
-    ``--decrypt=nostash`` without considering the security of your
-    index.
-
-    See also ``index.decrypt`` in **notmuch-config(1)**.
+.. program:: insert
+
+.. option:: --folder=<folder>
+
+   Deliver the message to the specified folder, relative to 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.
+
+.. option:: --create-folder
+
+   Try to create the folder named by the ``--folder`` option, if it
+   does not exist. Otherwise the folder must already exist for mail
+   delivery to succeed.
+
+.. option:: --keep
+
+   Keep the message file if indexing fails, and keep the message
+   indexed if applying tags or maildir flag synchronization
+   fails. Ignore these errors and return exit status 0 to indicate
+   successful mail delivery.
+
+.. option:: --no-hooks
+
+   Prevent hooks from being run.
+
+.. option:: --world-readable
+
+   When writing mail to the mailbox, allow it to be read by users
+   other than the current user.  Note that this does not override
+   umask.  By default, delivered mail is only readable by the current
+   user.
+
+.. option:: --decrypt=(true|nostash|auto|false)
+
+   If ``true`` and the message is encrypted, try to decrypt the
+   message while indexing, stashing any session keys discovered.  If
+   ``auto``, and notmuch already knows about a session key for the
+   message, it will try decrypting using that session key but will
+   not try to access the user's secret keys.  If decryption is
+   successful, index the cleartext itself.  Either way, the message
+   is always stored to disk in its original form (ciphertext).
+
+   ``nostash`` is the same as ``true`` except that it will not stash
+   newly-discovered session keys in the database.
+
+   Be aware that the index is likely sufficient (and a stashed
+   session key is certainly sufficient) to reconstruct the cleartext
+   of the message itself, so please ensure that the notmuch message
+   index is adequately protected. DO NOT USE ``--decrypt=true`` or
+   ``--decrypt=nostash`` without considering the security of your
+   index.
+
+   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.\<prefix\>`.  Tagging
+is controlled by options :nmconfig:`new.tags` and
+:nmconfig:`maildir.synchronize_flags`.  See
+:any:`notmuch-config(1)` for details.
 
 EXIT STATUS
 ===========
@@ -101,14 +120,14 @@ status of the **insert** command.
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index 20a1103b3363a85e78d9ec07845ad4ca068eb81d..f0ed8eb836ffa6bfab11c77849947063692a6ce7 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-new(1):
+
 ===========
 notmuch-new
 ===========
@@ -17,56 +19,74 @@ performing full-text indexing on new messages that are found. Each new
 message will automatically be tagged with both the **inbox** and
 **unread** tags.
 
-You should run **notmuch new** once after first running **notmuch
-setup** to create the initial database. The first run may take a long
-time if you have a significant amount of mail (several hundred thousand
-messages or more). Subsequently, you should run **notmuch new** whenever
-new mail is delivered and you wish to incorporate it into the database.
-These subsequent runs will be much quicker than the initial run.
+You should run **notmuch new** once after first running
+:any:`notmuch-setup(1)` to create the initial database. The first run
+may take a long time if you have a significant amount of mail (several
+hundred thousand messages or more). Subsequently, you should run
+**notmuch new** whenever new mail is delivered and you wish to
+incorporate it into the database.  These subsequent runs will be much
+quicker than the initial run.
 
 Invoking ``notmuch`` with no command argument will run **new** if
-**notmuch setup** has previously been completed, but **notmuch new** has
-not previously been run.
+:any:`notmuch-setup(1)` has previously been completed, but **notmuch
+new** has not previously been run.
 
 **notmuch new** updates tags according to maildir flag changes if the
 **maildir.synchronize\_flags** configuration option is enabled. See
-**notmuch-config(1)** for details.
+:any:`notmuch-config(1)` for details.
 
-The **new** command supports hooks. See **notmuch-hooks(5)** for more
+The **new** command supports hooks. See :any:`notmuch-hooks(5)` for more
 details on hooks.
 
 Supported options for **new** include
 
-``--no-hooks``
-    Prevents hooks from being run.
+.. program:: new
+
+.. option:: --no-hooks
+
+   Prevents hooks from being run.
+
+.. option:: --quiet
+
+   Do not print progress or results.
+
+.. option:: --verbose
+
+   Print file names being processed. Ignored when combined with
+   ``--quiet``.
+
+.. option:: --decrypt=(true|nostash|auto|false)
+
+   If ``true``, when encountering an encrypted message, try to
+   decrypt it while indexing, and stash any discovered session keys.
+   If ``auto``, try to use any session key already known to belong to
+   this message, but do not attempt to use the user's secret keys.
+   If decryption is successful, index the cleartext of the message.
 
-``--quiet``
-    Do not print progress or results.
+   Be aware that the index is likely sufficient (and the session key
+   is certainly sufficient) to reconstruct the cleartext of the
+   message itself, so please ensure that the notmuch message index is
+   adequately protected.  DO NOT USE ``--decrypt=true`` or
+   ``--decrypt=nostash`` without considering the security of your
+   index.
 
-``--verbose``
-    Print file names being processed. Ignored when combined with
-    ``--quiet``.
+   See also ``index.decrypt`` in :any:`notmuch-config(1)`.
 
-``--decrypt=(true|nostash|auto|false)``
-    If ``true``, when encountering an encrypted message, try to
-    decrypt it while indexing, and stash any discovered session keys.
-    If ``auto``, try to use any session key already known to belong to
-    this message, but do not attempt to use the user's secret keys.
-    If decryption is successful, index the cleartext of the message.
+.. option:: --full-scan
 
-    Be aware that the index is likely sufficient (and the session key
-    is certainly sufficient) to reconstruct the cleartext of the
-    message itself, so please ensure that the notmuch message index is
-    adequately protected.  DO NOT USE ``--decrypt=true`` or
-    ``--decrypt=nostash`` without considering the security of your
-    index.
+   By default notmuch-new uses directory modification times (mtimes)
+   to optimize the scanning of directories for new mail. This option turns
+   that optimization off.
 
-    See also ``index.decrypt`` in **notmuch-config(1)**.
+CONFIGURATION
+=============
 
-``--full-scan``
-    By default notmuch-new uses directory modification times (mtimes)
-    to optimize the scanning of directories for new mail. This option turns
-    that optimization off.
+Indexing is influenced by the configuration options
+:nmconfig:`index.decrypt`, :nmconfig:`index.header.\<prefix\>`
+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
 ===========
@@ -79,15 +99,15 @@ This command supports the following special exit status code
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index cd7c91a008ace0009b06b44ad4d67161d7a27bf1..85dad249b5825f8c24e629010a99fbcdcf7c2b96 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-reindex(1):
+
 ===============
 notmuch-reindex
 ===============
@@ -12,7 +14,7 @@ DESCRIPTION
 
 Re-index all messages matching the search terms.
 
-See **notmuch-search-terms(7)** for details of the supported syntax for
+See :any:`notmuch-search-terms(7)` for details of the supported syntax for
 <*search-term*\ >.
 
 The **reindex** command searches for all messages matching the
@@ -21,28 +23,31 @@ messages using the supplied options.
 
 Supported options for **reindex** include
 
-``--decrypt=(true|nostash|auto|false)``
-    If ``true``, when encountering an encrypted message, try to
-    decrypt it while reindexing, stashing any session keys discovered.
-    If ``auto``, and notmuch already knows about a session key for the
-    message, it will try decrypting using that session key but will
-    not try to access the user's secret keys.  If decryption is
-    successful, index the cleartext itself.
+.. program:: reindex
+
+.. option:: --decrypt=(true|nostash|auto|false)
+
+   If ``true``, when encountering an encrypted message, try to
+   decrypt it while reindexing, stashing any session keys discovered.
+   If ``auto``, and notmuch already knows about a session key for the
+   message, it will try decrypting using that session key but will
+   not try to access the user's secret keys.  If decryption is
+   successful, index the cleartext itself.
 
-    ``nostash`` is the same as ``true`` except that it will not stash
-    newly-discovered session keys in the database.
+   ``nostash`` is the same as ``true`` except that it will not stash
+   newly-discovered session keys in the database.
 
-    If ``false``, notmuch reindex will also delete any stashed session
-    keys for all messages matching the search terms.
+   If ``false``, notmuch reindex will also delete any stashed session
+   keys for all messages matching the search terms.
 
-    Be aware that the index is likely sufficient (and a stashed
-    session key is certainly sufficient) to reconstruct the cleartext
-    of the message itself, so please ensure that the notmuch message
-    index is adequately protected. DO NOT USE ``--decrypt=true`` or
-    ``--decrypt=nostash`` without considering the security of your
-    index.
+   Be aware that the index is likely sufficient (and a stashed
+   session key is certainly sufficient) to reconstruct the cleartext
+   of the message itself, so please ensure that the notmuch message
+   index is adequately protected. DO NOT USE ``--decrypt=true`` or
+   ``--decrypt=nostash`` without considering the security of your
+   index.
 
-    See also ``index.decrypt`` in **notmuch-config(1)**.
+   See also ``index.decrypt`` in :any:`notmuch-config(1)`.
 
 EXAMPLES
 ========
@@ -84,17 +89,17 @@ https://trac.xapian.org/ticket/742:
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-compact(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-compact(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index 5c64c4a63b106ca5ec60c099de5ca013c0be22fd..4f39a9597e8c13e118e95b34901709e2861be1c7 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-reply(1):
+
 =============
 notmuch-reply
 =============
@@ -34,64 +36,75 @@ The resulting message template is output to stdout.
 
 Supported options for **reply** include
 
-``--format=``\ (**default**\ \|\ **json**\ \|\ **sexp**\ \|\ **headers-only**)
-    **default**
-        Includes subject and quoted message body as an RFC 2822
-        message.
+.. 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 <search --output>`.
+
+.. option:: --format=(default|json|sexp|headers-only)
+
+   default
+     Includes subject and quoted message body as an RFC 2822
+     message.
+
+   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
+     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.
 
-    **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.
+   headers-only
+     Only produces In-Reply-To, References, To, Cc, and Bcc
+     headers.
 
-    **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.
+.. option:: --format-version=N
 
-    **headers-only**
-        Only produces In-Reply-To, References, To, Cc, and Bcc
-        headers.
+   Use the specified structured output format version. This is
+   intended for programs that invoke :any:`notmuch(1)` internally. If
+   omitted, the latest supported version will be used.
 
-``--format-version=N``
-    Use the specified structured output format version. This is
-    intended for programs that invoke **notmuch(1)** internally. If
-    omitted, the latest supported version will be used.
+.. option:: --reply-to=(all|sender)
 
-``--reply-to=``\ (**all**\ \|\ **sender**)
-    **all** (default)
-        Replies to all addresses.
+   all (default)
+     Replies to all addresses.
 
-    **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
-        order, and copy values from the first that contains something
-        other than only the user's addresses.
+   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
+     order, and copy values from the first that contains something
+     other than only the user's addresses.
 
-``--decrypt=(false|auto|true)``
+.. option:: --decrypt=(false|auto|true)
 
-    If ``true``, decrypt any MIME encrypted parts found in the
-    selected content (i.e., "multipart/encrypted" parts). Status
-    of the decryption will be reported (currently only supported
-    with ``--format=json`` and ``--format=sexp``), and on successful
-    decryption the multipart/encrypted part will be replaced by
-    the decrypted content.
+   If ``true``, decrypt any MIME encrypted parts found in the
+   selected content (i.e., "multipart/encrypted" parts). Status
+   of the decryption will be reported (currently only supported
+   with ``--format=json`` and ``--format=sexp``), and on successful
+   decryption the multipart/encrypted part will be replaced by
+   the decrypted content.
 
-    If ``auto``, and a session key is already known for the
-    message, then it will be decrypted, but notmuch will not try
-    to access the user's secret keys.
+   If ``auto``, and a session key is already known for the
+   message, then it will be decrypted, but notmuch will not try
+   to access the user's secret keys.
 
-    Use ``false`` to avoid even automatic decryption.
+   Use ``false`` to avoid even automatic decryption.
 
-    Non-automatic decryption expects a functioning
-    **gpg-agent(1)** to provide any needed credentials. Without
-    one, the decryption will likely fail.
+   Non-automatic decryption expects a functioning
+   :manpage:`gpg-agent(1)` to provide any needed credentials. Without
+   one, the decryption will likely fail.
 
-    Default: ``auto``
+   Default: ``auto``
 
-See **notmuch-search-terms(7)** for details of the supported syntax for
+See :any:`notmuch-search-terms(7)` for details of the supported syntax for
 <search-terms>.
 
 Note: It is most common to use **notmuch reply** with a search string
@@ -116,15 +129,15 @@ This command supports the following special exit status codes
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index c0f47f261372fdf6f9afb2b7fa6ab5b9ba7b0d23..ac6b4245e94dbfa2afae92f44ef2824b779c24bc 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-restore(1):
+
 ===============
 notmuch-restore
 ===============
@@ -10,90 +12,96 @@ SYNOPSIS
 DESCRIPTION
 ===========
 
-Restores the tags from the given file (see **notmuch dump**).
+Restores the tags from the given file (see :any:`notmuch-dump(1)`).
 
 The input is read from the given filename, if any, or from stdin.
 
 Supported options for **restore** include
 
-``--accumulate``
-    The union of the existing and new tags is applied, instead of
-    replacing each message's tags as they are read in from the dump
-    file.
-
-``--format=(sup|batch-tag|auto)``
-    Notmuch restore supports two plain text dump formats, with each
-    line specifying a message-id and a set of tags. For details of the
-    actual formats, see **notmuch-dump(1)**.
-
-    **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**
-        The **batch-tag** dump format is intended to more robust
-        against malformed message-ids and tags containing whitespace
-        or non-\ **ascii(7)** characters. See **notmuch-dump(1)** for
-        details on this format.
-
-        **notmuch restore** updates the maildir flags according to tag
-        changes if the **maildir.synchronize\_flags** configuration
-        option is enabled. See **notmuch-config(1)** for details.
-
-    **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
-        no parentheses, should be accurate.
-
-``--include=(config|properties|tags)``
-    Control what kind of metadata is restored.
-
-    **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**
-        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 **notmuch-properties(7)** for more details.
-
-    **tags**
-        Restore per-message metadata, namely tags. See *format* above
-        for more details.
-
-    The default is to restore all available types of data. The option
-    can be specified multiple times to select some subset.
-
-``--input=``\ <filename>
-    Read input from given file instead of stdin.
+.. program:: restore
+
+.. option:: --accumulate
+
+   The union of the existing and new tags is applied, instead of
+   replacing each message's tags as they are read in from the dump
+   file.
+
+.. option:: --format=(sup|batch-tag|auto)
+
+   Notmuch restore supports two plain text dump formats, with each
+   line specifying a message-id and a set of tags. For details of the
+   actual formats, see :any:`notmuch-dump(1)`.
+
+   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
+     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
+     details on this format.
+
+     **notmuch restore** updates the maildir flags according to tag
+     changes if the **maildir.synchronize\_flags** configuration
+     option is enabled. See :any:`notmuch-config(1)` for details.
+
+   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
+     no parentheses, should be accurate.
+
+.. option:: --include=(config|properties|tags)
+
+   Control what kind of metadata is restored.
+
+   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
+     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
+     Restore per-message metadata, namely tags. See *format* above
+     for more details.
+
+   The default is to restore all available types of data. The option
+   can be specified multiple times to select some subset.
+
+.. option:: --input=<filename>
+
+   Read input from given file instead of stdin.
 
 GZIPPED INPUT
 =============
 
 \ **notmuch restore** will detect if the input is compressed in
-**gzip(1)** format and automatically decompress it while reading. This
-detection does not depend on file naming and in particular works for
-standard input.
+:manpage:`gzip(1)` format and automatically decompress it while
+reading. This detection does not depend on file naming and in
+particular works for standard input.
 
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-properties(7)**,
-**notmuch-reply(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-properties(7)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index ed9ff4e5b96522751b056192bb7eabc5b0da75dd..b87737ea3bd0ae4b2ba0ec10a0a7014cc971ff2b 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-search(1):
+
 ==============
 notmuch-search
 ==============
@@ -19,123 +21,133 @@ in the thread, the number of matched messages and total messages in the
 thread, the names of all participants in the thread, and the subject of
 the newest (or oldest) message.
 
-See **notmuch-search-terms(7)** for details of the supported syntax for
+See :any:`notmuch-search-terms(7)` for details of the supported syntax for
 <search-terms>.
 
 Supported options for **search** include
 
-``--format=``\ (**json**\ \|\ **sexp**\ \|\ **text**\ \|\ **text0**)
-    Presents the results in either JSON, S-Expressions, newline
-    character separated plain-text (default), or null character
-    separated plain-text (compatible with **xargs(1)** -0 option where
-    available).
-
-``--format-version=N``
-    Use the specified structured output format version. This is
-    intended for programs that invoke **notmuch(1)** internally. If
-    omitted, the latest supported version will be used.
-
-``--output=(summary|threads|messages|files|tags)``
-    **summary**
-        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
-        the total number), the authors of the thread and the
-        subject. In the case where a thread contains multiple files
-        for some messages, the total number of files is printed in
-        parentheses (see below for an example).
-
-    **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**
-        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**
-        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``),
-        or as an S-Expression list (``--format=sexp``).
-
-        Note that each message may have multiple filenames associated
-        with it. All of them are included in the output (unless
-        limited with the ``--duplicate=N`` option). This may be
-        particularly confusing for **folder:** or **path:** searches
-        in a specified directory, as the messages may have duplicates
-        in other directories that are included in the output, although
-        these files alone would not match the search.
-
-    **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``),
-        or as an S-Expression list (``--format=sexp``).
-
-``--sort=``\ (**newest-first**\ \|\ **oldest-first**)
-    This option can be used to present results in either chronological
-    order (**oldest-first**) or reverse chronological order
-    (**newest-first**).
-
-    Note: The thread order will be distinct between these two options
-    (beyond being simply reversed). When sorting by **oldest-first**
-    the threads will be sorted by the oldest message in each thread,
-    but when sorting by **newest-first** the threads will be sorted by
-    the newest message in each thread.
-
-    By default, results will be displayed in reverse chronological
-    order, (that is, the newest results will be displayed first).
-
-``--offset=[-]N``
-    Skip displaying the first N results. With the leading '-', start
-    at the Nth result from the end.
-
-``--limit=N``
-    Limit the number of displayed results to N.
-
-``--exclude=(true|false|all|flag)``
-    A message is called "excluded" if it matches at least one tag in
-    search.exclude\_tags that does not appear explicitly in the search
-    terms. This option specifies whether to omit excluded messages in
-    the search process.
-
-    **true** (default)
-        Prevent excluded messages from matching the search terms.
-
-    **all**
-        Additionally prevent excluded messages from appearing in
-        displayed results, in effect behaving as though the excluded
-        messages do not exist.
-
-    **false**
-        Allow excluded messages to match search terms and appear in
-        displayed results. Excluded messages are still marked in the
-        relevant outputs.
-
-    **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
-        than the number of matching messages.
-
-``--duplicate=N``
-    For ``--output=files``, output the Nth filename associated with
-    each message matching the query (N is 1-based). If N is greater
-    than the number of files associated with the message, don't print
-    anything.
-
-    For ``--output=messages``, only output message IDs of messages
-    matching the search terms that have at least N filenames
-    associated with them.
-
-    Note that this option is orthogonal with the **folder:** search
-    prefix. The prefix matches messages based on filenames. This
-    option filters filenames of the matching messages.
+.. program:: search
+
+.. option:: --format=(json|sexp|text|text0)
+
+   Presents the results in either JSON, S-Expressions, newline
+   character separated plain-text (default), or null character
+   separated plain-text (compatible with :manpage:`xargs(1)` -0
+   option where available).
+
+.. option:: --format-version=N
+
+   Use the specified structured output format version. This is
+   intended for programs that invoke :any:`notmuch(1)` internally. If
+   omitted, the latest supported version will be used.
+
+.. option:: --output=(summary|threads|messages|files|tags)
+
+   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
+     the total number), the authors of the thread and the
+     subject. In the case where a thread contains multiple files
+     for some messages, the total number of files is printed in
+     parentheses (see below for an example).
+
+   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
+     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
+     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``),
+     or as an S-Expression list (``--format=sexp``).
+
+     Note that each message may have multiple filenames associated
+     with it. All of them are included in the output (unless
+     limited with the ``--duplicate=N`` option). This may be
+     particularly confusing for **folder:** or **path:** searches
+     in a specified directory, as the messages may have duplicates
+     in other directories that are included in the output, although
+     these files alone would not match the search.
+
+   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``),
+     or as an S-Expression list (``--format=sexp``).
+
+.. option:: --sort=(newest-first|oldest-first)
+
+   This option can be used to present results in either chronological
+   order (**oldest-first**) or reverse chronological order
+   (**newest-first**).
+
+   Note: The thread order will be distinct between these two options
+   (beyond being simply reversed). When sorting by **oldest-first**
+   the threads will be sorted by the oldest message in each thread,
+   but when sorting by **newest-first** the threads will be sorted by
+   the newest message in each thread.
+
+   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:: --exclude=(true|false|all|flag)
+
+   A message is called "excluded" if it matches at least one tag in
+   search.exclude\_tags that does not appear explicitly in the search
+   terms. This option specifies whether to omit excluded messages in
+   the search process.
+
+   true (default)
+     Prevent excluded messages from matching the search terms.
+
+   all
+     Additionally prevent excluded messages from appearing in
+     displayed results, in effect behaving as though the excluded
+     messages do not exist.
+
+   false
+     Allow excluded messages to match search terms and appear in
+     displayed results. Excluded messages are still marked in the
+     relevant outputs.
+
+   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
+     than the number of matching messages.
+
+.. option:: --duplicate=N
+
+   For ``--output=files``, output the Nth filename associated with
+   each message matching the query (N is 1-based). If N is greater
+   than the number of files associated with the message, don't print
+   anything.
+
+   For ``--output=messages``, only output message IDs of messages
+   matching the search terms that have at least N filenames
+   associated with them.
+
+   Note that this option is orthogonal with the **folder:** search
+   prefix. The prefix matches messages based on filenames. This
+   option filters filenames of the matching messages.
 
 EXAMPLE
 =======
@@ -164,16 +176,16 @@ This command supports the following special exit status codes
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
-**notmuch-address(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-address(1)`
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index becd3e79929023b089a9e5542c15380961aae58f..c13d94de024400fa2ca9642424ab6ec03284ad56 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-show(1):
+
 ============
 notmuch-show
 ============
@@ -12,7 +14,7 @@ DESCRIPTION
 
 Shows all messages matching the search terms.
 
-See **notmuch-search-terms(7)** for details of the supported syntax for
+See :any:`notmuch-search-terms(7)` for details of the supported syntax for
 <search-terms>.
 
 The messages will be grouped and sorted based on the threading (all
@@ -23,176 +25,223 @@ post-processor (such as the emacs interface to notmuch).
 
 Supported options for **show** include
 
-``--entire-thread=(true|false)``
-    If true, **notmuch show** outputs all messages in the thread of
-    any message matching the search terms; if false, it outputs only
-    the matching messages. For ``--format=json`` and ``--format=sexp``
-    this defaults to true. For other formats, this defaults to false.
-
-``--format=(text|json|sexp|mbox|raw)``
-    **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
-        be delimited by easily-parsed markers. Each marker consists of
-        a Control-L character (ASCII decimal 12), the name of the
-        marker, and then either an opening or closing brace, ('{' or
-        '}'), to either open or close the component. For a multipart
-        MIME message, these parts will be nested.
-
-    **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
-        messages is reflected in nested JSON output. By default JSON
-        output includes all messages in a matching thread; that is, by
-        default, ``--format=json`` sets ``--entire-thread``. The
-        caller can disable this behaviour by setting
-        ``--entire-thread=false``.  The JSON output is always encoded
-        as UTF-8 and any message content included in the output will
-        be charset-converted to UTF-8.
-
-    **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
-        colon). True is formatted as ``t`` and both false and null are
-        formatted as ``nil``. As for JSON, the s-expression output is
-        always encoded as UTF-8.
-
-    **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
-        in the message content beginning with "From " (preceded by
-        zero or more '>' characters) have an additional '>' character
-        added. This reversible escaping is termed "mboxrd" format and
-        described in detail here:
-
-            http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/mail-mbox-formats.html
-
-    **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.
-
-        If the specified part is a leaf part, this outputs the body of
-        the part after performing content transfer decoding (but no
-        charset conversion). This is suitable for saving attachments,
-        for example.
-
-        For a multipart or message part, the output includes the part
-        headers as well as the body (including all child parts). No
-        decoding is performed because multipart and message parts
-        cannot have non-trivial content transfer encoding. Consumers
-        of this may need to implement MIME decoding and similar
-        functions.
-
-``--format-version=N``
-    Use the specified structured output format version. This is
-    intended for programs that invoke **notmuch(1)** internally. If
-    omitted, the latest supported version will be used.
-
-``--part=N``
-    Output the single decoded MIME part N of a single message. The
-    search terms must match only a single message. Message parts are
-    numbered in a depth-first walk of the message MIME structure, and
-    are identified in the 'json', 'sexp' or 'text' output formats.
-
-    Note that even a message with no MIME structure or a single body
-    part still has two MIME parts: part 0 is the whole message
-    (headers and body) and part 1 is just the body.
-
-``--verify``
-    Compute and report the validity of any MIME cryptographic
-    signatures found in the selected content (e.g., "multipart/signed"
-    parts). Status of the signature will be reported (currently only
-    supported with ``--format=json`` and ``--format=sexp``), and the
-    multipart/signed part will be replaced by the signed data.
-
-``--decrypt=(false|auto|true|stash)``
-    If ``true``, decrypt any MIME encrypted parts found in the
-    selected content (e.g., "multipart/encrypted" parts). Status of
-    the decryption will be reported (currently only supported
-    with ``--format=json`` and ``--format=sexp``) and on successful
-    decryption the multipart/encrypted part will be replaced by
-    the decrypted content.
-
-    ``stash`` behaves like ``true``, but upon successful decryption it
-    will also stash the message's session key in the database, and
-    index the cleartext of the message, enabling automatic decryption
-    in the future.
-
-    If ``auto``, and a session key is already known for the
-    message, then it will be decrypted, but notmuch will not try
-    to access the user's keys.
-
-    Use ``false`` to avoid even automatic decryption.
-
-    Non-automatic decryption (``stash`` or ``true``, in the absence of
-    a stashed session key) expects a functioning **gpg-agent(1)** to
-    provide any needed credentials. Without one, the decryption will
-    fail.
-
-    Note: setting either ``true`` or ``stash`` here implies
-    ``--verify``.
-
-    Here is a table that summarizes each of these policies:
-
-    +------------------------+-------+------+------+-------+
-    |                        | false | auto | true | stash |
-    +========================+=======+======+======+=======+
-    | Show cleartext if      |       |  X   |  X   |   X   |
-    | session key is         |       |      |      |       |
-    | already known          |       |      |      |       |
-    +------------------------+-------+------+------+-------+
-    | Use secret keys to     |       |      |  X   |   X   |
-    | show cleartext         |       |      |      |       |
-    +------------------------+-------+------+------+-------+
-    | Stash any newly        |       |      |      |   X   |
-    | recovered session keys,|       |      |      |       |
-    | reindexing message if  |       |      |      |       |
-    | found                  |       |      |      |       |
-    +------------------------+-------+------+------+-------+
-
-    Note: ``--decrypt=stash`` requires write access to the database.
-    Otherwise, ``notmuch show`` operates entirely in read-only mode.
-
-    Default: ``auto``
-
-``--exclude=(true|false)``
-    Specify whether to omit threads only matching search.exclude\_tags
-    from the search results (the default) or not. In either case the
-    excluded message will be marked with the exclude flag (except when
-    output=mbox when there is nowhere to put the flag).
-
-    If ``--entire-thread`` is specified then complete threads are returned
-    regardless (with the excluded flag being set when appropriate) but
-    threads that only match in an excluded message are not returned
-    when ``--exclude=true.``
-
-    The default is ``--exclude=true.``
-
-``--body=(true|false)``
-    If true (the default) **notmuch show** includes the bodies of the
-    messages in the output; if false, bodies are omitted.
-    ``--body=false`` is only implemented for the text, json and sexp
-    formats and it is incompatible with ``--part > 0.``
-
-    This is useful if the caller only needs the headers as body-less
-    output is much faster and substantially smaller.
-
-``--include-html``
-    Include "text/html" parts as part of the output (currently
-    only supported with ``--format=text``, ``--format=json`` and
-    ``--format=sexp``). By default, unless ``--part=N`` is used to
-    select a specific part or ``--include-html`` is used to include all
-    "text/html" parts, no part with content type "text/html" is included
-    in the output.
-
-A common use of **notmuch show** is to display a single thread of email
-messages. For this, use a search term of "thread:<thread-id>" as can be
-seen in the first column of output from the **notmuch search** command.
+.. 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 <search --output>`
+
+.. option:: --entire-thread=(true|false)
+
+   If true, **notmuch show** outputs all messages in the thread of
+   any message matching the search terms; if false, it outputs only
+   the matching messages. For ``--format=json`` and ``--format=sexp``
+   this defaults to true. For other formats, this defaults to false.
+
+.. option:: --format=(text|json|sexp|mbox|raw)
+
+   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
+     be delimited by easily-parsed markers. Each marker consists of
+     a Control-L character (ASCII decimal 12), the name of the
+     marker, and then either an opening or closing brace, ('{' or
+     '}'), to either open or close the component. For a multipart
+     MIME message, these parts will be nested.
+
+   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
+     messages is reflected in nested JSON output. By default JSON
+     output includes all messages in a matching thread; that is, by
+     default, ``--format=json`` sets ``--entire-thread``. The
+     caller can disable this behaviour by setting
+     ``--entire-thread=false``.  The JSON output is always encoded
+     as UTF-8 and any message content included in the output will
+     be charset-converted to UTF-8.
+
+   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
+     colon). True is formatted as ``t`` and both false and null are
+     formatted as ``nil``. As for JSON, the s-expression output is
+     always encoded as UTF-8.
+
+   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
+     in the message content beginning with "From " (preceded by
+     zero or more '>' characters) have an additional '>' character
+     added. This reversible escaping is termed "mboxrd" format and
+     described in detail here:
+
+       http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/mail-mbox-formats.html
+
+   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.
+
+     If the specified part is a leaf part, this outputs the body of
+     the part after performing content transfer decoding (but no
+     charset conversion). This is suitable for saving attachments,
+     for example.
+
+     For a multipart or message part, the output includes the part
+     headers as well as the body (including all child parts). No
+     decoding is performed because multipart and message parts
+     cannot have non-trivial content transfer encoding. Consumers
+     of this may need to implement MIME decoding and similar
+     functions.
+
+.. option:: --format-version=N
+
+   Use the specified structured output format version. This is
+   intended for programs that invoke :any:`notmuch(1)` internally. If
+   omitted, the latest supported version will be used.
+
+.. option:: --part=N
+
+   Output the single decoded MIME part N of a single message. The
+   search terms must match only a single message. Message parts are
+   numbered in a depth-first walk of the message MIME structure, and
+   are identified in the 'json', 'sexp' or 'text' output formats.
+
+   Note that even a message with no MIME structure or a single body
+   part still has two MIME parts: part 0 is the whole message
+   (headers and body) and part 1 is just the body.
+
+.. option:: --sort=(newest-first|oldest-first)
+
+   This option can be used to present results in either chronological
+   order (**oldest-first**) or reverse chronological order
+   (**newest-first**).
+
+   Only threads as a whole are reordered.  Ordering of messages within
+   each thread will not be affected by this flag, since that order is
+   always determined by the thread's replies.
+
+   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
+   signatures found in the selected content (e.g., "multipart/signed"
+   parts). Status of the signature will be reported (currently only
+   supported with ``--format=json`` and ``--format=sexp``), and the
+   multipart/signed part will be replaced by the signed data.
+
+.. option:: --decrypt=(false|auto|true|stash)
+
+   If ``true``, decrypt any MIME encrypted parts found in the
+   selected content (e.g., "multipart/encrypted" parts). Status of
+   the decryption will be reported (currently only supported
+   with ``--format=json`` and ``--format=sexp``) and on successful
+   decryption the multipart/encrypted part will be replaced by
+   the decrypted content.
+
+   ``stash`` behaves like ``true``, but upon successful decryption it
+   will also stash the message's session key in the database, and
+   index the cleartext of the message, enabling automatic decryption
+   in the future.
+
+   If ``auto``, and a session key is already known for the
+   message, then it will be decrypted, but notmuch will not try
+   to access the user's keys.
+
+   Use ``false`` to avoid even automatic decryption.
+
+   Non-automatic decryption (``stash`` or ``true``, in the absence of
+   a stashed session key) expects a functioning :manpage:`gpg-agent(1)` to
+   provide any needed credentials. Without one, the decryption will
+   fail.
+
+   Note: setting either ``true`` or ``stash`` here implies
+   ``--verify``.
+
+   Here is a table that summarizes each of these policies:
+
+   +------------------------+-------+------+------+-------+
+   |                        | false | auto | true | stash |
+   +========================+=======+======+======+=======+
+   | Show cleartext if      |       |  X   |  X   |   X   |
+   | session key is         |       |      |      |       |
+   | already known          |       |      |      |       |
+   +------------------------+-------+------+------+-------+
+   | Use secret keys to     |       |      |  X   |   X   |
+   | show cleartext         |       |      |      |       |
+   +------------------------+-------+------+------+-------+
+   | Stash any newly        |       |      |      |   X   |
+   | recovered session keys,|       |      |      |       |
+   | reindexing message if  |       |      |      |       |
+   | found                  |       |      |      |       |
+   +------------------------+-------+------+------+-------+
+
+   Note: ``--decrypt=stash`` requires write access to the database.
+   Otherwise, ``notmuch show`` operates entirely in read-only mode.
+
+   Default: ``auto``
+
+.. option:: --exclude=(true|false)
+
+   Specify whether to omit threads only matching search.exclude\_tags
+   from the search results (the default) or not. In either case the
+   excluded message will be marked with the exclude flag (except when
+   output=mbox when there is nowhere to put the flag).
+
+   If ``--entire-thread`` is specified then complete threads are returned
+   regardless (with the excluded flag being set when appropriate) but
+   threads that only match in an excluded message are not returned
+   when ``--exclude=true.``
+
+   The default is ``--exclude=true.``
+
+.. option:: --body=(true|false)
+
+   If true (the default) **notmuch show** includes the bodies of the
+   messages in the output; if false, bodies are omitted.
+   ``--body=false`` is only implemented for the text, json and sexp
+   formats and it is incompatible with ``--part > 0.``
+
+   This is useful if the caller only needs the headers as body-less
+   output is much faster and substantially smaller.
+
+.. option:: --include-html
+
+   Include "text/html" parts as part of the output (currently
+   only supported with ``--format=text``, ``--format=json`` and
+   ``--format=sexp``). By default, unless ``--part=N`` is used to
+   select a specific part or ``--include-html`` is used to include all
+   "text/html" parts, no part with content type "text/html" is included
+   in the output.
+
+A common use of **notmuch show** is to display a single thread of
+email messages. For this, use a search term of "thread:<thread-id>" 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
 ===========
@@ -208,15 +257,15 @@ This command supports the following special exit status codes
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-tag(1)`
index c2324f5a94ea88b4e40410176347ac41a3f39135..ae311a230f55e5e2a02836c4ebeeefad8770e994 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-tag(1):
+
 ===========
 notmuch-tag
 ===========
@@ -14,7 +16,7 @@ DESCRIPTION
 
 Add/remove tags for all messages matching the search terms.
 
-See **notmuch-search-terms(7)** for details of the supported syntax for
+See :any:`notmuch-search-terms(7)` for details of the supported syntax for
 <*search-term*\ >.
 
 Tags prefixed by '+' are added while those prefixed by '-' are removed.
@@ -28,26 +30,31 @@ beginning with '+' or '-' is provided by allowing the user to specify a
 
 **notmuch tag** updates the maildir flags according to tag changes if
 the **maildir.synchronize\_flags** configuration option is enabled. See
-**notmuch-config(1)** for details.
+:any:`notmuch-config(1)` for details.
 
 Supported options for **tag** include
 
-``--remove-all``
-    Remove all tags from each message matching the search terms before
-    applying the tag changes appearing on the command line.  This
-    means setting the tags of each message to the tags to be added. If
-    there are no tags to be added, the messages will have no tags.
+.. program:: tag
+
+.. option:: --remove-all
+
+   Remove all tags from each message matching the search terms before
+   applying the tag changes appearing on the command line.  This
+   means setting the tags of each message to the tags to be added. If
+   there are no tags to be added, the messages will have no tags.
+
+.. option:: --batch
+
+   Read batch tagging operations from a file (stdin by default).
+   This is more efficient than repeated **notmuch tag**
+   invocations. See `TAG FILE FORMAT <#tag_file_format>`__ below for
+   the input format. This option is not compatible with specifying
+   tagging on the command line.
 
-``--batch``
-    Read batch tagging operations from a file (stdin by default).
-    This is more efficient than repeated **notmuch tag**
-    invocations. See `TAG FILE FORMAT <#tag_file_format>`__ below for
-    the input format. This option is not compatible with specifying
-    tagging on the command line.
+.. option:: --input=<filename>
 
-``--input=``\ <filename>
-    Read input from given file, instead of from stdin. Implies
-    ``--batch``.
+   Read input from given file, instead of from stdin. Implies
+   ``--batch``.
 
 TAG FILE FORMAT
 ===============
@@ -100,15 +107,15 @@ of the tag **space in tags**
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
index 48351588a3eafc7a20ed69161fa2ae44485c7486..c488f12a9e7a58193486b5dd5fdf76fe31f62057 100644 (file)
@@ -1,3 +1,6 @@
+.. _notmuch(1):
+.. _notmuch-setup(1):
+
 =======
 notmuch
 =======
@@ -15,8 +18,9 @@ reading, and tagging large collections of email messages.
 
 This page describes how to get started using notmuch from the command
 line, and gives a brief overview of the commands available. For more
-information on e.g. **notmuch show** consult the **notmuch-show(1)** man
-page, also accessible via **notmuch help show**
+information on e.g. **notmuch show** consult the
+:any:`notmuch-show(1)` man page, also accessible via **notmuch help
+show**
 
 The quickest way to get started with Notmuch is to simply invoke the
 ``notmuch`` command with no arguments, which will interactively guide
@@ -39,25 +43,31 @@ OPTIONS
 
 Supported global options for ``notmuch`` include
 
-``--help`` [command-name]
-    Print a synopsis of available commands and exit. With an optional
-    command name, show the man page for that subcommand.
+.. program:: notmuch
+
+.. option:: --help [command-name]
+
+   Print a synopsis of available commands and exit. With an optional
+   command name, show the man page for that subcommand.
+
+.. option:: --version
 
-``--version``
-    Print the installed version of notmuch, and exit.
+   Print the installed version of notmuch, and exit.
 
-``--config=FILE``
-    Specify the configuration file to use. This overrides any
-    configuration file specified by ${NOTMUCH\_CONFIG}. The empty
-    string is a permitted and sometimes useful value of *FILE*, which
-    tells ``notmuch`` to use only configuration metadata from the database.
+.. option:: --config=FILE
 
-``--uuid=HEX``
-    Enforce that the database UUID (a unique identifier which persists
-    until e.g. the database is compacted) is HEX; exit with an error
-    if it is not. This is useful to detect rollover in modification
-    counts on messages. You can find this UUID using e.g. ``notmuch
-    count --lastmod``
+   Specify the configuration file to use. This overrides any
+   configuration file specified by :envvar:`NOTMUCH_CONFIG`. The empty
+   string is a permitted and sometimes useful value of *FILE*, which
+   tells ``notmuch`` to use only configuration metadata from the database.
+
+.. option:: --uuid=HEX
+
+   Enforce that the database UUID (a unique identifier which persists
+   until e.g. the database is compacted) is HEX; exit with an error
+   if it is not. This is useful to detect rollover in modification
+   counts on messages. You can find this UUID using e.g. ``notmuch
+   count --lastmod``
 
 All global options except ``--config`` can also be specified after the
 command. For example, ``notmuch subcommand --uuid=HEX`` is equivalent
@@ -75,7 +85,7 @@ use, (or to reconfigure it later).
 The setup command will prompt for your full name, your primary email
 address, any alternate email addresses you use, and the directory
 containing your email archives. Your answers will be written to a
-configuration file in ${NOTMUCH\_CONFIG} (if set) or
+configuration file in :envvar:`NOTMUCH_CONFIG` (if set) or
 ${HOME}/.notmuch-config . This configuration file will be created with
 descriptive comments, making it easy to edit by hand later to change the
 configuration. Or you can run **notmuch setup** again to change the
@@ -90,7 +100,8 @@ will do its best to detect those and ignore them.
 Mail storage that uses mbox format, (where one mbox file contains many
 messages), will not work with notmuch. If that's how your mail is
 currently stored, it is recommended you first convert it to maildir
-format with a utility such as mb2md before running **notmuch setup .**
+format with a utility such as :manpage:`mb2md(1)` before running
+**notmuch setup**.
 
 Invoking ``notmuch`` with no command argument will run **setup** if the
 setup command has not previously been completed.
@@ -99,32 +110,36 @@ OTHER COMMANDS
 --------------
 
 Several of the notmuch commands accept search terms with a common
-syntax. See **notmuch-search-terms**\ (7) for more details on the
+syntax. See :any:`notmuch-search-terms(7)` for more details on the
 supported syntax.
 
-The **search**, **show**, **address** and **count** commands are used
-to query the email database.
+The :any:`notmuch-search(1)`, :any:`notmuch-show(1)`,
+:any:`notmuch-address(1)` and :any:`notmuch-count(1)` commands are
+used to query the email database.
 
-The **reply** command is useful for preparing a template for an email
-reply.
+The :any:`notmuch-reply(1)` command is useful for preparing a template
+for an email reply.
 
-The **tag** command is the only command available for manipulating
-database contents.
+The :any:`notmuch-tag(1)` command is the only command available for
+manipulating database contents.
 
-The **dump** and **restore** commands can be used to create a textual
-dump of email tags for backup purposes, and to restore from that dump.
+The :any:`notmuch-dump(1)` and :any:`notmuch-restore(1)` commands can
+be used to create a textual dump of email tags for backup purposes,
+and to restore from that dump.
 
-The **config** command can be used to get or set settings in the notmuch
-configuration file.
+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-<subcommand>** in ${PATH} instead. This allows
-users to have their own notmuch related tools to be run via the
+the external **notmuch-<subcommand>** 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
 -------------
@@ -140,44 +155,74 @@ 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
 ===========
 
 The following environment variables can be used to control the behavior
 of notmuch.
 
-**NOTMUCH\_CONFIG**
-    Specifies the location of the notmuch configuration file. Notmuch
-    will use ${HOME}/.notmuch-config if this variable is not set.
+.. envvar:: NOTMUCH_CONFIG
+
+   Specifies the location of the notmuch configuration file. See
+   :any:`notmuch-config(1)` for details.
+
+.. envvar:: NOTMUCH_DATABASE
+
+   Specifies the location of the notmuch database. See
+   :any:`notmuch-config(1)` for details.
+
+.. envvar:: NOTMUCH_PROFILE
+
+   Selects among notmuch configurations. See :any:`notmuch-config(1)`
+   for details.
+
+.. envvar:: NOTMUCH_TALLOC_REPORT
+
+   Location to write a talloc memory usage report. See
+   **talloc\_enable\_leak\_report\_full** in :manpage:`talloc(3)` for more
+   information.
 
-**NOTMUCH\_TALLOC\_REPORT**
-    Location to write a talloc memory usage report. See
-    **talloc\_enable\_leak\_report\_full** in **talloc(3)** for more
-    information.
+.. envvar:: NOTMUCH_DEBUG_QUERY
 
-**NOTMUCH\_DEBUG\_QUERY**
-    If set to a non-empty value, the notmuch library will print (to
-    stderr) Xapian queries it constructs.
+   If set to a non-empty value, the notmuch library will print (to
+   stderr) Xapian queries it constructs.
 
 SEE ALSO
 ========
 
-**notmuch-address(1)**,
-**notmuch-compact(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-properties(7)**,
-**notmuch-reindex(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch-address(1)`,
+:any:`notmuch-compact(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-properties(7)`,
+:any:`notmuch-reindex(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
 
 The notmuch website: **https://notmuchmail.org**
 
@@ -189,4 +234,4 @@ list <notmuch@notmuchmail.org> . Subscription is not required before
 posting, but is available from the notmuchmail.org website.
 
 Real-time interaction with the Notmuch community is available via IRC
-(server: irc.freenode.net, channel: #notmuch).
+(server: irc.libera.chat, channel: #notmuch).
index c509afb39bb5944c72763769bb582ca89c183c9e..d778bdb8133692e8b542677fd513c7460265a522 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-hooks(5):
+
 =============
 notmuch-hooks
 =============
@@ -12,35 +14,35 @@ DESCRIPTION
 
 Hooks are scripts (or arbitrary executables or symlinks to such) that
 notmuch invokes before and after certain actions. These scripts reside
-in a directory defined as described in **notmuch-config(1)**. They
+in a directory defined as described in :any:`notmuch-config(1)`. They
 must have executable permissions.
 
 The currently available hooks are described below.
 
-**pre-new**
-    This hook is invoked by the **new** command before scanning or
-    importing new messages into the database. If this hook exits with
-    a non-zero status, notmuch will abort further processing of the
-    **new** command.
+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
+    processing of the :any:`notmuch-new(1)` command.
 
     Typically this hook is used for fetching or delivering new mail to
     be imported into the database.
 
-**post-new**
-    This hook is invoked by the **new** 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.
+post-new
+    This hook is invoked by the :any:`notmuch-new(1)` command after
+    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**
-    This hook is invoked by the **insert** 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 been any
-    errors during the message delivery; what is regarded as successful
-    delivery depends on the ``--keep`` option.
+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
+    been any errors during the message delivery; what is regarded as
+    successful delivery depends on the ``--keep`` option.
 
     Typically this hook is used to perform additional query-based
     tagging on the delivered messages.
@@ -48,15 +50,15 @@ The currently available hooks are described below.
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-**notmuch-search-terms(7)**,
-**notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
index a7d91d674a89f92e37989e6bb1ca3846839360c2..ff79f4c2f2c9774b885600d64682267d314ea227 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-properties(7):
+
 ==================
 notmuch-properties
 ==================
@@ -45,7 +47,7 @@ CONVENTIONS
 ===========
 
 Any property with a key that starts with "index." will be removed (and
-possibly re-set) upon reindexing (see **notmuch-reindex(1)**).
+possibly re-set) upon reindexing (see :any:`notmuch-reindex(1)`).
 
 MESSAGE PROPERTIES
 ==================
@@ -53,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
@@ -70,15 +72,14 @@ of its normal activity.
 
     If notmuch never tried to decrypt an encrypted message during
     indexing (which is the default, see ``index.decrypt`` in
-    **notmuch-config(1)**), then this property will not be set on that
+    :any:`notmuch-config(1)`), then this property will not be set on that
     message.
 
-**session-key**
-
-    When **notmuch-show(1)** or **nomtuch-reply** encounters a message
-    with an encrypted part, if notmuch finds a ``session-key``
-    property associated with the message, it will try that stashed
-    session key for decryption.
+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
+    that stashed session key for decryption.
 
     If you do not want to use any stashed session keys that might be
     present, you should pass those programs ``--decrypt=false``.
@@ -97,7 +98,7 @@ of its normal activity.
     message.  This includes attachments, cryptographic signatures, and
     other material that cannot be reconstructed from the index alone.
 
-    See ``index.decrypt`` in **notmuch-config(1)** for more
+    See ``index.decrypt`` in :any:`notmuch-config(1)` for more
     details about how to set notmuch's policy on when to store session
     keys.
 
@@ -109,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
@@ -136,13 +136,13 @@ of its normal activity.
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-dump(1)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reindex(1)**,
-**notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-show(1)**,
-***notmuch-search-terms(7)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-reindex(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search-terms(7)`,
+:any:`notmuch-show(1)`
index 28fca7379b7bfb4e84a3e1abe0b79eab88e9fdea..acc1c967f7c8470b50237796a693ec59735195b2 100644 (file)
@@ -1,3 +1,5 @@
+.. _notmuch-search-terms(7):
+
 ====================
 notmuch-search-terms
 ====================
@@ -39,7 +41,7 @@ indicate user-supplied values).
 
 Some of the prefixes with <regex> forms can be also used to restrict
 the results to those whose value matches a regular expression (see
-**regex(7)**) delimited with //, for example::
+:manpage:`regex(7)`) delimited with //, for example::
 
    notmuch search 'from:"/bob@.*[.]example[.]com/"'
 
@@ -71,8 +73,9 @@ mimetype:<word>
 
 tag:<tag> or tag:/<regex>/ or is:<tag> or is:/<regex>/
     For **tag:** and **is:** valid tag values include **inbox** and
-    **unread** by default for new messages added by **notmuch new** as
-    well as any other tag values added manually with **notmuch tag**.
+    **unread** by default for new messages added by
+    :any:`notmuch-new(1)` as well as any other tag values added
+    manually with :any:`notmuch-tag(1)`.
 
 id:<message-id> or mid:<message-id> or mid:/<regex>/
     For **id:** and **mid:**, message ID values are the literal
@@ -83,7 +86,7 @@ thread:<thread-id>
     The **thread:** prefix can be used with the thread ID values that
     are generated internally by notmuch (and do not appear in email
     messages). These thread ID values can be seen in the first column
-    of output from **notmuch search**
+    of output from :any:`notmuch-search(1)`
 
 thread:{<notmuch query>}
     Threads may be searched for indirectly by providing an arbitrary
@@ -150,22 +153,30 @@ date:<since>..<until> or date:<date>
 lastmod:<initial-revision>..<final-revision>
     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 **notmuch search** 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:<name>
     The **query:** prefix allows queries to refer to previously saved
-    queries added with **notmuch-config(1)**.
+    queries added with :any:`notmuch-config(1)`.
 
 property:<key>=<value>
     The **property:** prefix searches for messages with a particular
     <key>=<value> property pair. Properties are used internally by
     notmuch (and extensions) to add metadata to messages. A given key
     can be present on a given message with several different values.
-    See **notmuch-properties(7)** for more details.
+    See :any:`notmuch-properties(7)` for more details.
+
+sexp:<subquery>
+    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 **notmuch-config(1)** for
+User defined prefixes are also supported, see :any:`notmuch-config(1)` for
 details.
 
 Operators
@@ -253,7 +264,7 @@ Boolean
 Probabilistic
   **body:**, **to:**, **attachment:**, **mimetype:**
 Special
-   **from:**, **query:**, **subject:**
+   **from:**, **query:**, **subject:**, **sexp:**
 
 Terms and phrases
 -----------------
@@ -271,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
 
@@ -291,6 +304,8 @@ Both of these will match a subject "Free Delicious Pizza" while
 
 will not.
 
+.. _quoting:
+
 Quoting
 -------
 
@@ -318,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
 ====================
 
@@ -443,17 +465,17 @@ Some time zone codes, e.g. UTC, EET.
 SEE ALSO
 ========
 
-**notmuch(1)**,
-**notmuch-config(1)**,
-**notmuch-count(1)**,
-**notmuch-dump(1)**,
-**notmuch-hooks(5)**,
-**notmuch-insert(1)**,
-**notmuch-new(1)**,
-**notmuch-reindex(1)**,
-**notmuch-properties(1)**,
-***notmuch-reply(1)**,
-**notmuch-restore(1)**,
-**notmuch-search(1)**,
-***notmuch-show(1)**,
-**notmuch-tag(1)**
+:any:`notmuch(1)`,
+:any:`notmuch-config(1)`,
+:any:`notmuch-count(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-hooks(5)`,
+:any:`notmuch-insert(1)`,
+:any:`notmuch-new(1)`,
+:any:`notmuch-properties(7)`,
+:any:`notmuch-reindex(1)`,
+:any:`notmuch-reply(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-search(1)`,
+:any:`notmuch-show(1)`,
+:any:`notmuch-tag(1)`
diff --git a/doc/man7/notmuch-sexp-queries.rst b/doc/man7/notmuch-sexp-queries.rst
new file mode 100644 (file)
index 0000000..858ff68
--- /dev/null
@@ -0,0 +1,366 @@
+.. _notmuch-sexp-queries(7):
+
+====================
+notmuch-sexp-queries
+====================
+
+SYNOPSIS
+========
+
+**notmuch** *subcommand* ``--query=sexp`` [option ...]  ``--`` '(and (to santa) (date december))'
+
+DESCRIPTION
+===========
+
+Notmuch supports an alternative query syntax based on `S-expressions
+<https://en.wikipedia.org/wiki/S-expression>`_ . It can be selected
+with the command line ``--query=sexp`` or with the appropriate option
+to the library function :c:func:`notmuch_query_create_with_syntax`.
+Support for this syntax is currently optional, you can test if your
+build of notmuch supports it with
+
+::
+
+   $ notmuch config get built_with.sexp_queries
+
+
+S-EXPRESSIONS
+-------------
+
+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 \""``.
+
+S-EXPRESSION QUERIES
+--------------------
+
+An s-expression query is either an atom, the empty list, or a
+*compound query* consisting of a prefix atom (first element) defining
+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*
+
+    Match all messages containing *term*, possibly after stemming or
+    phrase splitting. For discussion of stemming in notmuch see
+    :any:`notmuch-search-terms(7)`. Stemming only applies to unquoted
+    terms (basic values) in s-expression queries.  For information on
+    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
+    prefixed with ``,`` to be expanded (see :any:`macro_examples`).
+    Macros may refer to other macros, but only to their own
+    parameters [#macro-details]_.
+
+.. _fields:
+
+FIELDS
+``````
+
+*Fields* [#aka-pref]_
+correspond to attributes of mail messages. Some are inherent (and
+immutable) like ``subject``, while others ``tag`` and ``property`` are
+settable by the user.  Each concrete field in
+:any:`the table below <field-table>`
+is discussed further under "Search prefixes" in
+:any:`notmuch-search-terms(7)`. The row *user* refers to user defined
+fields, described in :any:`notmuch-config(1)`.
+
+Most fields are either *phrase fields* [#aka-prob]_ (which match
+sequences of words), or *term fields* [#aka-bool]_ (which match exact
+strings). *Phrase splitting* breaks the term (basic value or quoted
+string) into words, ignore punctuation. Phrase splitting is applied to
+terms in phrase (probabilistic) fields. Both phrase splitting and
+stemming apply only in phrase fields.
+
+Each term or phrase field has an associated combining operator
+(``and`` or ``or``) used to combine the queries from each element of
+the tail of the list. This is generally ``or`` for those fields where
+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
+
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |   field    |  combine  |   type    |  expand   | wildcard  |  regex   |
+  +============+===========+===========+===========+===========+==========+
+  |   *none*   |    and    |           |    no     |    yes    |    no    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |   *user*   |    and    |  phrase   |    no     |    yes    |    no    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  | attachment |    and    |  phrase   |    yes    |    yes    |    no    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |    body    |    and    |  phrase   |    no     |    no     |    no    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |    date    |           |   range   |    no     |    no     |    no    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |   folder   |    or     |  phrase   |    yes    |    yes    |   yes    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |    from    |    and    |  phrase   |    yes    |    yes    |   yes    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |     id     |    or     |   term    |    no     |    yes    |   yes    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |     is     |    and    |   term    |    yes    |    yes    |   yes    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |  lastmod   |           |   range   |    no     |    no     |    no    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |    mid     |    or     |   term    |    no     |    yes    |   yes    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |  mimetype  |    or     |  phrase   |    yes    |    yes    |    no    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |    path    |    or     |   term    |    no     |    yes    |   yes    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |  property  |    and    |   term    |    yes    |    yes    |   yes    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |  subject   |    and    |  phrase   |    yes    |    yes    |   yes    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |    tag     |    and    |   term    |    yes    |    yes    |   yes    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |   thread   |    or     |   term    |    yes    |    yes    |   yes    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+  |     to     |    and    |  phrase   |    yes    |    yes    |    no    |
+  +------------+-----------+-----------+-----------+-----------+----------+
+
+.. _modifiers:
+
+MODIFIERS
+`````````
+
+*Modifiers* refer to any prefixes (first elements of compound queries)
+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 <fields>`, or outside of fields [#not-body]_. Note that
+    a ``starts-with`` query cannot be part of a phrase. The
+    atom ``*`` is a synonym for ``(starts-with "")``.
+
+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.
+
+``"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".
+
+``(id 1234@invalid 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-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".
+
+``(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\@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
+    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``.
+
+.. _macro_examples:
+
+MACRO EXAMPLES
+--------------
+
+A macro that takes two parameters and applies different fields to them.
+
+::
+
+   $ notmuch config set squery.TagSubject '(macro (tagname subj) (and (tag ,tagname) (subject ,subj)))'
+   $ notmuch search --query=sexp '(TagSubject inbox maildir)'
+
+Nested macros are allowed.
+
+::
+
+    $ notmuch config set squery.Inner '(macro (x) (subject ,x))'
+    $ notmuch config set squery.Outer  '(macro (x y) (and (tag ,x) (Inner ,y)))'
+    $ notmuch search --query=sexp '(Outer inbox maildir)'
+
+Parameters can be re-used to reduce boilerplate. Any field, including
+user defined fields is permitted within a macro.
+
+::
+
+    $ notmuch config set squery.About '(macro (name) (or (subject ,name) (List ,name)))'
+    $ notmuch search --query=sexp '(About notmuch)'
+
+
+NOTES
+=====
+
+.. [#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.
+
+.. [#aka-pref] a.k.a. prefixes
+
+.. [#aka-prob] a.k.a. probabilistic prefixes
+
+.. [#aka-bool] a.k.a. boolean prefixes
+
+.. [#not-phrase] Due to the implementation of phrase fields in Xapian,
+                 regex queries could only match individual words.
+
+.. [#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:: `q`\ :sub:`1`
+.. |q2| replace:: `q`\ :sub:`2`
+.. |qn| replace:: `q`\ :sub:`n`
+
+.. |p1| replace:: `p`\ :sub:`1`
+.. |p2| replace:: `p`\ :sub:`2`
+.. |pn| replace:: `p`\ :sub:`n`
index de47b72688e3a07a96fd4c925e2a833e1e41311f..91af6d146f0d9682d125bd38a879d9941245d634 100644 (file)
@@ -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 ``<return>``
 |                    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 <n>
+
+       A lower bound on the number of characters that will
+       be used to display each column.
+
+    .. describe:: float <f>
+
+       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
 --------------------------
 
-``<tab>``
+.. el:define-key:: <tab>
+
     Move to the next widget (button or text entry field)
 
-``<backspace>``
+.. el:define-key:: <backtab>
+
     Move to the previous widget.
 
-``<return>``
+.. el:define-key:: <return>
+
     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-searches-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
 ``<return>`` on the appropriate line.
 
-``n,C-n,<down>``
+.. el:define-key:: n
+   C-n
+   <down>
+
     Move to next line
 
-``p,C-p,<up>``
+.. el:define-key::
+   p
+   C-p
+   <up>
+
     Move to previous line
 
-``<return>``
+.. el:define-key:: <return>
+
     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,113 @@ 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.
 
-``<space>``
+.. el:define-key:: <space>
+
     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:: 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
 
-``+,-``
+.. 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
 
-Display of messages can be controlled by the following variables
+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 +427,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 <max@example.com> (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:
 
@@ -281,42 +524,173 @@ email archives. Each line in the buffer represents a single
 message giving the relative date, the author, subject, and any
 tags.
 
-``c``
+.. el:define-key:: c
+
     :ref:`show-copy`
 
-``<return>``
+.. el:define-key:: <return>
+
    Displays that message.
 
-``N``
+.. el:define-key:: N
+
     Move to next message
 
-``P``
+.. el:define-key:: P
+
     Move to previous message
 
-``n``
+.. el:define-key:: n
+
     Move to next matching message
 
-``p``
+.. el:define-key:: p
+
     Move to previous matching message
 
-``g`` ``=``
+.. el:define-key:: o
+   M-x notmuch-tree-toggle-order
+
+   |docstring::notmuch-tree-toggle-order|
+
+.. el:define-key:: l
+   M-x notmuch-tree-filter
+
+   Filter or LIMIT the current search results based on an additional query string
+
+.. 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
+   =
+
     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:: <tab>
+
+   Cycle visibility state of the current message's tree.
+
+.. el:define-key:: <M-tab>
+
+   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`.
+
 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
@@ -342,14 +716,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
@@ -360,12 +752,33 @@ 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
+------------
+
+.. 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
+       :code:`compose-mail`.  To use ``notmuch`` for this, customize this
+       variable to the symbol :code:`notmuch-user-agent`.
+
+.. 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.
+
 Init File
 ---------
 
diff --git a/doc/queries.rst b/doc/queries.rst
new file mode 100644 (file)
index 0000000..b76e71e
--- /dev/null
@@ -0,0 +1,9 @@
+Notmuch Queries
+===============
+
+.. toctree::
+   :titlesonly:
+
+   man7/notmuch-search-terms
+   man7/notmuch-sexp-queries
+   man7/notmuch-properties
index d1b320c33d28a7303c4977ad692daf5da03ef5ae..0f1f0eb2ef69f50cf86a7903b25932257157edf7 100644 (file)
@@ -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)
index d820525b3f0240a3afe0e41a5faa82b85cd16cd2..79d2a1b7f03db509b95705b1701fa601cf7987ca 100644 (file)
@@ -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)
index f0af666754b798ac0f83b90db079f8aecd94422a..f756254cb8513ec062ca692299fbb39e4f683fe1 100644 (file)
@@ -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))
 
@@ -217,7 +216,7 @@ requiring external commands."
       ;; harvest if necessary.
       (notmuch-address-harvest-trigger)))
    (t
-    (process-lines notmuch-address-command original))))
+    (notmuch--process-lines notmuch-address-command original))))
 
 (defun notmuch-address-expand-name ()
   (cond
@@ -308,7 +307,7 @@ execution, CALLBACK is called when harvesting finishes."
                            (and config-query
                                 (format " and (%s)" config-query)))
                  from-or-to-me-query))
-        (args `("address" "--format=sexp" "--format-version=4"
+        (args `("address" "--format=sexp" "--format-version=5"
                 ,(if sent "--output=recipients" "--output=sender")
                 "--deduplicate=address"
                 ,query)))
index c6a004aebe92bcd447ee93729a54a2518052200b..7e05dc8f2a796aaf2c664fdcc4be63f6a443057f 100644 (file)
@@ -70,7 +70,8 @@
        (completion-ignore-case t))
     (cl-case command
       (interactive (company-begin-backend 'notmuch-company))
-      (prefix (and (derived-mode-p 'message-mode)
+      (prefix (and (or (derived-mode-p 'message-mode)
+                      (derived-mode-p 'org-msg-edit-mode))
                   (looking-back
                    (concat notmuch-address-completion-headers-regexp ".*")
                    (line-beginning-position))
index ad134dfee99c273f7ffb5895730364902abe40af..179bf59ca86116e79b64393ff76c41d65db53be9 100644 (file)
 (unless (fboundp 'message--fold-long-headers)
   (add-hook 'message-header-hook 'notmuch-message--fold-long-headers))
 
+;; `dlet' isn't available until Emacs 28.1.  Below is a copy, with the
+;; addition of `with-no-warnings'.
+(defmacro notmuch-dlet (binders &rest body)
+  "Like `let*' but using dynamic scoping."
+  (declare (indent 1) (debug let))
+  `(let (_)
+     (with-no-warnings  ; Quiet "lacks a prefix" warning.
+       ,@(mapcar (lambda (binder)
+                  `(defvar ,(if (consp binder) (car binder) binder)))
+                binders))
+     (let* ,binders ,@body)))
+
 (provide 'notmuch-compat)
 
 ;;; notmuch-compat.el ends here
index db7cb75d43a954cd06ba4400d378ba2b74ae41da..a1cf3ddd93e176748e49472750c6138c76c4b27a 100644 (file)
@@ -119,14 +119,14 @@ mode."
     (cond
      ((string= status "good")
       (let ((fingerprint (concat "0x" (plist-get sigstatus :fingerprint)))
-           (userid (plist-get sigstatus :userid)))
-       ;; If userid is present it has full or greater validity.
-       (if userid
-           (progn
-             (setq label (concat "Good signature by: " userid))
-             (setq face 'notmuch-crypto-signature-good))
-         (setq label (concat "Good signature by key: " fingerprint))
-         (setq face 'notmuch-crypto-signature-good-key))
+           (email-or-userid (or (plist-get sigstatus :email)
+                                 (plist-get sigstatus :userid))))
+       ;; If email or userid are present, they have full or greater validity.
+       (setq label (concat "Good signature by key: " fingerprint))
+       (setq face 'notmuch-crypto-signature-good-key)
+       (when email-or-userid
+         (setq label (concat "Good signature by: " email-or-userid))
+         (setq face 'notmuch-crypto-signature-good))
        (setq button-action 'notmuch-crypto-sigstatus-good-callback)
        (setq help-msg (concat "Click to list key ID 0x" fingerprint "."))))
      ((string= status "error")
@@ -164,7 +164,7 @@ mode."
        (goto-char (point-max))
        (insert (format "-- Key %s in message %s:\n"
                        fingerprint id))
-       (call-process notmuch-crypto-gpg-program nil t t
+       (notmuch--call-process notmuch-crypto-gpg-program nil t t
                      "--batch" "--no-tty" "--list-keys" fingerprint))
       (recenter -1))))
 
@@ -224,7 +224,7 @@ corresponding key when the status button is pressed."
          (with-current-buffer buffer
            (goto-char (point-max))
            (insert (format "--- Retrieving key %s:\n" keyid)))
-         (let ((p (make-process
+         (let ((p (notmuch--make-process
                    :name "notmuch GPG key retrieval"
                    :connection-type 'pipe
                    :buffer buffer
@@ -240,9 +240,9 @@ corresponding key when the status button is pressed."
          (with-current-buffer buffer
            (goto-char (point-max))
            (insert (format "--- Retrieving key %s:\n" keyid))
-           (call-process notmuch-crypto-gpg-program nil t t "--recv-keys" keyid)
+           (notmuch--call-process notmuch-crypto-gpg-program nil t t "--recv-keys" keyid)
            (insert "\n")
-           (call-process notmuch-crypto-gpg-program nil t t "--list-keys" keyid))
+           (notmuch--call-process notmuch-crypto-gpg-program nil t t "--list-keys" keyid))
          (recenter -1))
        (notmuch-show-refresh-view)))))
 
index a68b7d8da002a7f3f76b2ccded426bb465724de7..fcc45503c6b0816d4ab12ab98a6c86cf6b2d3eb9 100644 (file)
@@ -42,7 +42,7 @@
   :group 'notmuch)
 
 (defcustom notmuch-draft-tags '("+draft")
-  "List of tags changes to apply to a draft message when it is saved in the database.
+  "List of tag changes to apply when saving a draft message in the database.
 
 Tags starting with \"+\" (or not starting with either \"+\" or
 \"-\") in the list will be added, and tags starting with \"-\"
@@ -239,7 +239,7 @@ applied to newly inserted messages)."
 (defun notmuch-draft-resume (id)
   "Resume editing of message with id ID."
   ;; Used by command `notmuch-show-resume-message'.
-  (let* ((tags (process-lines notmuch-command "search" "--output=tags"
+  (let* ((tags (notmuch--process-lines notmuch-command "search" "--output=tags"
                              "--exclude=false" id))
         (draft (equal tags (notmuch-update-tags tags notmuch-draft-tags))))
     (when (or draft
@@ -249,7 +249,7 @@ applied to newly inserted messages)."
       (setq buffer-read-only nil)
       (erase-buffer)
       (let ((coding-system-for-read 'no-conversion))
-       (call-process notmuch-command nil t nil "show" "--format=raw" id))
+       (notmuch--call-process notmuch-command nil t nil "show" "--format=raw" id))
       (mime-to-mml)
       (goto-char (point-min))
       (when (re-search-forward "^$" nil t)
index a521497784ec27e91cf9c7ee905641b3d4459012..254e6407cecefbbd8656321d9283cf63d360afca 100755 (executable)
@@ -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}\")"
index 24d2d19e20d4c24e7d77e77312916eab818fcc10..b6d1e2aefa67d40c03268e8294716846942c2dc0 100644 (file)
 (require 'notmuch-mua)
 
 (declare-function notmuch-search "notmuch"
-                 (&optional query oldest-first target-thread target-line continuation))
-(declare-function notmuch-poll "notmuch" ())
+                 (&optional query oldest-first target-thread target-line
+                            no-display))
+(declare-function notmuch-poll "notmuch-lib" ())
 (declare-function notmuch-tree "notmuch-tree"
-                 (&optional query query-context target buffer-name open-target unthreaded))
-(declare-function notmuch-unthreaded
-                 (&optional query query-context target buffer-name open-target))
+                 (&optional query query-context target buffer-name
+                            open-target unthreaded parent-buffer
+                            oldest-first hide-excluded))
+(declare-function notmuch-unthreaded "notmuch-tree"
+                 (&optional query query-context target buffer-name
+                            open-target oldest-first hide-excluded))
 
 
 ;;; Options
@@ -140,10 +144,16 @@ 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 unthreaded mode, and set to nil
-                   (or anything except tree and unthreaded) to specify search mode.
+                   tree mode or unthreaded mode. Set to `tree' to
+                   specify tree mode, \\='unthreaded to specify
+                   unthreaded mode, and set to nil (or anything
+                   except tree and unthreaded) to specify search
+                   mode.
 
 Other accepted forms are a cons cell of the form (NAME . QUERY)
 or a list of the form (NAME QUERY COUNT-QUERY)."
@@ -193,7 +203,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?"
@@ -479,17 +489,22 @@ 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)))
-   (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 (process-lines notmuch-command "count" search)))
+  (car (notmuch--process-lines notmuch-command "count" search)))
 
 (defun notmuch-hello-tags-per-line (widest)
   "Determine how many tags to show per line and how wide they
@@ -550,7 +565,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)
@@ -562,8 +578,12 @@ options will be handled as specified for
                                        (or (plist-get options :filter-count)
                                            (plist-get options :filter))))
         "\n")))
-    (unless (= (call-process-region (point-min) (point-max) notmuch-command
-                                   t t nil "count" "--batch") 0)
+    (unless (= (notmuch--call-process-region (point-min) (point-max) notmuch-command
+                                            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
@@ -628,6 +648,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 "
@@ -637,6 +661,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)))))))
@@ -695,12 +720,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 "<C-tab>") '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
@@ -741,7 +769,7 @@ Complete list of currently available key bindings:
                    (list (cons tag
                                (concat "tag:"
                                        (notmuch-escape-boolean-term tag))))))
-            (process-lines notmuch-command "search" "--output=tags" "*")))
+            (notmuch--process-lines notmuch-command "search" "--output=tags" "*")))
 
 (defun notmuch-hello-insert-header ()
   "Insert the default notmuch-hello header."
@@ -779,7 +807,7 @@ Complete list of currently available key bindings:
                   :help-echo "Refresh"
                   (notmuch-hello-nice-number
                    (string-to-number
-                    (car (process-lines notmuch-command "count")))))
+                    (car (notmuch--process-lines notmuch-command "count" "--exclude=false")))))
     (widget-insert " messages.\n")))
 
 (defun notmuch-hello-insert-saved-searches ()
@@ -836,7 +864,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
@@ -850,13 +879,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))
@@ -864,16 +896,16 @@ Supports the following entries in OPTIONS as a plist:
        (start (point)))
     (if is-hidden
        (widget-create 'push-button
-                      :notify `(lambda (widget &rest _ignore)
-                                 (setq notmuch-hello-hidden-sections
-                                       (delete ,title notmuch-hello-hidden-sections))
-                                 (notmuch-hello-update))
+                      :notify (lambda (&rest _ignore)
+                                (setq notmuch-hello-hidden-sections
+                                      (delete title notmuch-hello-hidden-sections))
+                                (notmuch-hello-update))
                       "show")
       (widget-create 'push-button
-                    :notify `(lambda (widget &rest _ignore)
-                               (add-to-list 'notmuch-hello-hidden-sections
-                                            ,title)
-                               (notmuch-hello-update))
+                    :notify (lambda (&rest _ignore)
+                              (add-to-list 'notmuch-hello-hidden-sections
+                                           title)
+                              (notmuch-hello-update))
                     "hide"))
     (widget-insert "\n")
     (unless is-hidden
@@ -911,7 +943,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."
index 6fab5a792ec6b4da7daaacd2ad789653403b49ad..3161ed9526d52d5b8de9a9d08eb3bef626eb38ec 100644 (file)
 (require 'notmuch-lib)
 (require 'notmuch-hello)
 
+(declare-function notmuch-search "notmuch")
+(declare-function notmuch-tree "notmuch-tree")
+(declare-function notmuch-unthreaded "notmuch-tree")
+
 ;;;###autoload
 (defun notmuch-jump-search ()
   "Jump to a saved search by shortcut key.
@@ -46,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
@@ -62,6 +72,11 @@ fast way to jump to a saved search from anywhere in Notmuch."
       (error "To use notmuch-jump, %s"
             "please customize shortcut keys in notmuch-saved-searches."))))
 
+(defface notmuch-jump-key
+  '((t :inherit minibuffer-prompt))
+  "Default face used for keys in `notmuch-jump' and related."
+  :group 'notmuch-faces)
+
 (defvar notmuch-jump--action nil)
 
 ;;;###autoload
@@ -123,7 +138,7 @@ buffer."
     ;; Format each action
     (mapcar (pcase-lambda (`(,key ,desc))
              (setq key (format-kbd-macro key))
-             (concat (propertize key 'face 'minibuffer-prompt)
+             (concat (propertize key 'face 'notmuch-jump-key)
                      (make-string (- key-width (length key)) ? )
                      " " desc))
            action-map)))
@@ -163,9 +178,10 @@ buffer."
     (pcase-dolist (`(,key ,_name ,fn) action-map)
       (when (= (length key) 1)
        (define-key map key
-         `(lambda () (interactive)
-            (setq notmuch-jump--action ',fn)
-            (exit-minibuffer)))))
+         (lambda ()
+           (interactive)
+           (setq notmuch-jump--action fn)
+           (exit-minibuffer)))))
     ;; By doing this in two passes (and checking if we already have a
     ;; binding) we avoid problems if the user specifies a binding which
     ;; is a prefix of another binding.
@@ -186,12 +202,13 @@ buffer."
                  action-submap)
            (setq action-submap (nreverse action-submap))
            (define-key map keystr
-             `(lambda () (interactive)
-                (setq notmuch-jump--action
-                      ',(apply-partially #'notmuch-jump
-                                         action-submap
-                                         new-prompt))
-                (exit-minibuffer)))))))
+             (lambda ()
+               (interactive)
+               (setq notmuch-jump--action
+                     (apply-partially #'notmuch-jump
+                                      action-submap
+                                      new-prompt))
+               (exit-minibuffer)))))))
     map))
 
 (provide 'notmuch-jump)
index c7bb2091f8edcc457ed2f36569ce9bf744c90253..bf9c4a534a24f5d5c088857ce420600604259326 100644 (file)
@@ -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.")
 
@@ -195,7 +206,7 @@ will be signaled.
 
 Otherwise the output will be returned."
   (with-temp-buffer
-    (let ((status (apply #'call-process notmuch-command nil t nil args))
+    (let ((status (apply #'notmuch--call-process notmuch-command nil t nil args))
          (output (buffer-string)))
       (notmuch-check-exit-status status (cons notmuch-command args) output)
       output)))
@@ -206,7 +217,7 @@ Otherwise the output will be returned."
 (defun notmuch-cli-sane-p ()
   "Return t if the cli seems to be configured sanely."
   (unless notmuch--cli-sane-p
-    (let ((status (call-process notmuch-command nil nil nil
+    (let ((status (notmuch--call-process notmuch-command nil nil nil
                                "config" "get" "user.primary_email")))
       (setq notmuch--cli-sane-p (= status 0))))
   notmuch--cli-sane-p)
@@ -286,7 +297,7 @@ depending on the value of `notmuch-poll-script'."
   (message "Polling mail...")
   (if (stringp notmuch-poll-script)
       (unless (string-empty-p notmuch-poll-script)
-       (unless (equal (call-process notmuch-poll-script nil nil) 0)
+       (unless (equal (notmuch--call-process notmuch-poll-script nil nil) 0)
          (error "Notmuch: poll script `%s' failed!" notmuch-poll-script)))
     (notmuch-call-notmuch-process "new"))
   (message "Polling mail...done"))
@@ -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."
@@ -639,7 +658,7 @@ the given type."
                                  ;; charset is US-ASCII. RFC6657
                                  ;; complicates this somewhat.
                                  'us-ascii)))))
-                      (apply #'call-process
+                      (apply #'notmuch--call-process
                              notmuch-command nil '(t nil) nil args)
                       (buffer-string))))))
     (when (and cache data)
@@ -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
@@ -860,6 +880,32 @@ You may need to restart Emacs or upgrade your notmuch package."))
       ;; `notmuch-logged-error' does not return.
       ))))
 
+(defmacro notmuch--apply-with-env (func &rest args)
+  `(let ((default-directory "~"))
+     (apply ,func ,@args)))
+
+(defun notmuch--process-lines (program &rest args)
+  "Wrap process-lines, binding DEFAULT-DIRECTORY to a safe
+default"
+  (notmuch--apply-with-env #'process-lines program args))
+
+(defun notmuch--make-process (&rest args)
+  "Wrap make-process, binding DEFAULT-DIRECTORY to a safe
+default"
+  (notmuch--apply-with-env #'make-process args))
+
+(defun notmuch--call-process-region (start end program
+                                          &optional delete buffer display
+                                          &rest args)
+  "Wrap call-process-region, binding DEFAULT-DIRECTORY to a safe
+default"
+  (notmuch--apply-with-env
+   #'call-process-region start end program delete buffer display args))
+
+(defun notmuch--call-process (program &optional infile destination display &rest args)
+  "Wrap call-process, binding DEFAULT-DIRECTORY to a safe default"
+  (notmuch--apply-with-env #'call-process program infile destination display args))
+
 (defun notmuch-call-notmuch--helper (destination args)
   "Helper for synchronous notmuch invocation commands.
 
@@ -874,9 +920,9 @@ for `call-process'.  ARGS is as described for
        (otherwise
         (error "Unknown keyword argument: %s" (car args)))))
     (if (null stdin-string)
-       (apply #'call-process notmuch-command nil destination nil args)
+       (apply #'notmuch--call-process notmuch-command nil destination nil args)
       (insert stdin-string)
-      (apply #'call-process-region (point-min) (point-max)
+      (apply #'notmuch--call-process-region (point-min) (point-max)
             notmuch-command t destination nil args))))
 
 (defun notmuch-call-notmuch-process (&rest args)
@@ -933,7 +979,7 @@ status."
   (let* ((command (or (executable-find notmuch-command)
                      (error "Command not found: %s" notmuch-command)))
         (err-buffer (generate-new-buffer " *notmuch-stderr*"))
-        (proc (make-process
+        (proc (notmuch--make-process
                :name name
                :buffer buffer
                :command (cons command args)
@@ -994,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 ()
@@ -1010,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 (file)
index 53b5e6a..0000000
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 (file)
index 0000000..2c65a73
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"
+     viewbox="0 0 100 100" fill="#000" stroke-width="2">
+  <circle cx="50" cy="5" r="5" />
+  <g transform="translate(50 20) rotate(20)">
+    <circle cx="-47" cy="0" r="3" />
+    <circle cx="47"  cy="0" r="3" />
+    <path d="M-47 -1  L0 -3  L47 -1 L47 1  L0 3  L-47 1 Z" />
+  </g>
+  <path d="M49 4  L45 88
+           A5 5  0  0 1  40 93  L20 93  A5 5  0  0 0  15 100
+           L85 100
+           A5 5  0  0 0  80 93  L60 93  A5 5  0  0 1  55 88
+           L55 90  L51 4 Z" />
+  <g fill="#fff" stroke="#000">
+    <rect x="7" y="33" width="30" height="18" />
+    <line x1="7"  y1="51" x2="18" y2="41" />
+    <line x1="37" y1="51" x2="26" y2="41" />
+    <polyline points="7 33  22 44  37 33" fill="none" />
+  </g>
+  <path d="M-18 0  A24 20  0  0 0  18 0" transform="translate(22 51.0)" />
+  <path d="M-18 0  A24 20  0  0 0  18 0" transform="translate(78 71.5)" />
+  <g fill="none" stroke="#000">
+    <path d="M9  53.0  l 12 -42  l 2 0  l 12 42" />
+    <path d="M91 73.5  l-12 -42  l-2 0  l-12 42" />
+  </g>
+</svg>
index ae8f5140f34d2b2283bc9507b0744c6f30640700..5102078849d629a2da37bac13c9a3548153ff996 100644 (file)
@@ -21,6 +21,8 @@
 
 ;;; Code:
 
+(require 'seq)
+
 (require 'message)
 
 (require 'notmuch-lib)
@@ -39,16 +41,17 @@ Three types of values are permitted:
 - a string: the value of `notmuch-fcc-dirs' is the Fcc header to
   be used.
 
-- a list: the folder is chosen based on the From address of the
-  current message using a list of regular expressions and
-  corresponding folders:
+- an alist: the folder is chosen based on the From address of
+  the current message according to an alist mapping regular
+  expressions to folders or nil:
 
      ((\"Sebastian@SSpaeth.de\" . \"privat\")
       (\"spaetz@sspaeth.de\" . \"OUTBOX.OSS\")
       (\".*\" . \"defaultinbox\"))
 
-  If none of the regular expressions match the From address, no
-  Fcc header will be added.
+  If none of the regular expressions match the From address, or
+  if the cdr of the matching entry is nil, then no Fcc header
+  will be added.
 
 If `notmuch-maildir-use-notmuch-insert' is set (the default) then
 the header should be of the form \"folder +tag1 -tag2\" where
@@ -72,7 +75,8 @@ directory if it does not exist yet when sending a mail."
          (const :tag "No FCC header" nil)
          (string :tag "A single folder")
          (repeat :tag "A folder based on the From header"
-                 (cons regexp (string :tag "Folder"))))
+                 (cons regexp (choice (const :tag "No FCC header" nil)
+                                      (string :tag "Folder")))))
   :require 'notmuch-fcc-initialization
   :group 'notmuch-send)
 
@@ -103,13 +107,14 @@ Otherwise set it according to `notmuch-fcc-dirs'."
           ;; Old style - no longer works.
           (error "Invalid `notmuch-fcc-dirs' setting (old style)"))
          ((listp notmuch-fcc-dirs)
-          (or (seq-some (let ((from (message-field-value "From")))
-                          (pcase-lambda (`(,regexp . ,folder))
-                            (and (string-match-p regexp from)
-                                 folder)))
-                        notmuch-fcc-dirs)
-              (progn (message "No Fcc header added.")
-                     nil)))
+          (if-let ((match (seq-some (let ((from (message-field-value "From")))
+                                      (pcase-lambda (`(,regexp . ,folder))
+                                        (and (string-match-p regexp from)
+                                             (cons t folder))))
+                                    notmuch-fcc-dirs)))
+               (cdr match)
+             (message "No Fcc header added.")
+            nil))
          (t
           (error "Invalid `notmuch-fcc-dirs' setting (neither string nor list)")))))
     (when subdir
@@ -144,6 +149,7 @@ Otherwise set it according to `notmuch-fcc-dirs'."
         (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)))
@@ -153,7 +159,10 @@ Otherwise set it according to `notmuch-fcc-dirs'."
 
 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))
@@ -174,12 +183,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)))))))
index bbf059a22cb5487f669497631363a7c924b1d3d8..bf62b65691810f491cb9f98d04e0acc760a4a3ad 100644 (file)
 
 ;;; Code:
 
+(eval-when-compile (require 'subr-x))
+
 (require 'message)
+(require 'gmm-utils)
 (require 'mm-view)
 (require 'format-spec)
 
@@ -234,11 +237,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=4"))
-       (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 +320,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))
@@ -341,8 +347,8 @@ Typically this is added to `notmuch-mua-send-hook'."
 
 (defvar notmuch-message-mode-map
   (let ((map (make-sparse-keymap)))
-    (define-key map (kbd "C-c C-c") #'notmuch-mua-send-and-exit)
-    (define-key map (kbd "C-c C-s") #'notmuch-mua-send)
+    (define-key map [remap message-send-and-exit] #'notmuch-mua-send-and-exit)
+    (define-key map [remap message-send] #'notmuch-mua-send)
     (define-key map (kbd "C-c C-p") #'notmuch-draft-postpone)
     (define-key map (kbd "C-x C-s") #'notmuch-draft-save)
     map)
@@ -380,20 +386,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 +435,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)
 
@@ -474,7 +506,7 @@ the From: address."
                (with-current-buffer temp-buffer
                  (erase-buffer)
                  (let ((coding-system-for-read 'no-conversion))
-                   (call-process notmuch-command nil t nil
+                   (notmuch--call-process notmuch-command nil t nil
                                  "show" "--format=raw" id))
                  ;; Because we process the messages in reverse order,
                  ;; always generate a forwarded subject, then use the
@@ -510,12 +542,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 +563,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 +623,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
index f04b07c2e51b28e34f03469b88205d279c45a587..710c60e15ec12c7b3f5d6ca02d869d5be45f03d6 100644 (file)
@@ -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
index d00614999147c100c4b94d472669d459cdbc6b2b..8d9f1b0830319c5d2bb2262029334d89b5174a10 100644 (file)
 ;;; 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 #'call-process-region (point-min) (point-max)
+  (apply #'notmuch--call-process-region (point-min) (point-max)
         ;; Reads from stdin.
         "muttprint"
         nil nil nil
index d7349b771e4ff4efde18249d54a32a8dc2adde45..2a46144c49846dcf3301ac5b365951fcab731239 100644 (file)
 
 ;;; 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=4")))
-    (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)
 
index ba93febb34ff90f96e95b5cae8a0af3796130638..4c0ad74d5043cefcefaa86ab65313660dade6884 100644 (file)
 
 (require 'notmuch-lib)
 (require 'notmuch-tag)
-(require 'notmuch-query)
 (require 'notmuch-wash)
 (require 'notmuch-mua)
 (require 'notmuch-crypto)
 (require 'notmuch-print)
 (require 'notmuch-draft)
 
-(declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
+(declare-function notmuch-call-notmuch-process "notmuch-lib" (&rest args))
 (declare-function notmuch-search-next-thread "notmuch" nil)
 (declare-function notmuch-search-previous-thread "notmuch" nil)
-(declare-function notmuch-search-show-thread "notmuch" nil)
+(declare-function notmuch-search-show-thread "notmuch")
 (declare-function notmuch-foreach-mime-part "notmuch" (function mm-handle))
 (declare-function notmuch-count-attachments "notmuch" (mm-handle))
 (declare-function notmuch-save-attachments "notmuch" (mm-handle &optional queryp))
 (declare-function notmuch-tree "notmuch-tree"
                  (&optional query query-context target buffer-name
-                            open-target unthreaded))
+                            open-target unthreaded parent-buffer))
 (declare-function notmuch-tree-get-message-properties "notmuch-tree" nil)
-(declare-function notmuch-unthreaded
-                 (&optional query query-context target buffer-name open-target))
+(declare-function notmuch-unthreaded "notmuch-tree"
+                 (&optional query query-context target buffer-name
+                            open-target))
 (declare-function notmuch-read-query "notmuch" (prompt))
 (declare-function notmuch-draft-resume "notmuch-draft" (id))
 
 (defvar shr-blocked-images)
 (defvar gnus-blocked-images)
 (defvar shr-content-function)
+(defvar w3m-ignored-image-url-regexp)
 
 ;;; Options
 
@@ -82,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" ni)
+                 (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
@@ -178,6 +232,8 @@ indentation."
 
 (defvar-local notmuch-show-indent-content t)
 
+(defvar-local notmuch-show-single-message nil)
+
 (defvar notmuch-show-attachment-debug nil
   "If t log stdout and stderr from attachment handlers.
 
@@ -189,10 +245,10 @@ each attachment handler is logged in buffers with names beginning
 ;;; Options
 
 (defcustom notmuch-show-stash-mlarchive-link-alist
-  '(("Gmane" . "https://mid.gmane.org/")
-    ("MARC" . "https://marc.info/?i=")
+  '(("MARC" . "https://marc.info/?i=")
     ("Mail Archive, The" . "https://mid.mail-archive.com/")
-    ("LKML" . "https://lkml.kernel.org/r/")
+    ("Lore" . "https://lore.kernel.org/r/")
+    ("Notmuch" . "https://nmbug.notmuchmail.org/nmweb/show/")
     ;; FIXME: can these services be searched by `Message-Id' ?
     ;; ("MarkMail" . "http://markmail.org/")
     ;; ("Nabble" . "http://nabble.com/")
@@ -217,7 +273,7 @@ return the ML archive reference URI."
                             (function :tag "Function returning the URL")))
   :group 'notmuch-show)
 
-(defcustom notmuch-show-stash-mlarchive-link-default "Gmane"
+(defcustom notmuch-show-stash-mlarchive-link-default "MARC"
   "Default Mailing List Archive to use when stashing links.
 
 This is used when `notmuch-show-stash-mlarchive-link' isn't
@@ -275,7 +331,7 @@ position of the message in the thread."
        (let ((buf (generate-new-buffer (concat "*notmuch-msg-" id "*"))))
         (with-current-buffer buf
           (let ((coding-system-for-read 'no-conversion))
-            (call-process notmuch-command nil t nil "show" "--format=raw" id))
+            (notmuch--call-process notmuch-command nil t nil "show" "--format=raw" id))
           ,@body)
         (kill-buffer buf)))))
 
@@ -396,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
@@ -462,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
@@ -481,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)))
 
@@ -558,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
@@ -715,21 +817,23 @@ will return nil if the CID is unknown or cannot be retrieved."
   t)
 
 (defun notmuch-show-insert-part-message/rfc822 (msg part _content-type _nth depth _button)
-  (let* ((message (car (plist-get part :content)))
-        (body (car (plist-get message :body)))
-        (start (point)))
-    ;; Override `notmuch-message-headers' to force `From' to be
-    ;; displayed.
-    (let ((notmuch-message-headers '("From" "Subject" "To" "Cc" "Date")))
-      (notmuch-show-insert-headers (plist-get message :headers)))
-    ;; Blank line after headers to be compatible with the normal
-    ;; message display.
-    (insert "\n")
-    ;; Show the body
-    (notmuch-show-insert-bodypart msg body depth)
-    (when notmuch-show-indent-multipart
-      (indent-rigidly start (point) 1)))
-  t)
+  (let ((message (car (plist-get part :content))))
+    (and
+     message
+     (let ((body (car (plist-get message :body)))
+          (start (point)))
+       ;; Override `notmuch-message-headers' to force `From' to be
+       ;; displayed.
+       (let ((notmuch-message-headers '("From" "Subject" "To" "Cc" "Date")))
+        (notmuch-show-insert-headers (plist-get message :headers)))
+       ;; Blank line after headers to be compatible with the normal
+       ;; message display.
+       (insert "\n")
+       ;; Show the body
+       (notmuch-show-insert-bodypart msg body depth)
+       (when notmuch-show-indent-multipart
+        (indent-rigidly start (point) 1))
+       t))))
 
 (defun notmuch-show-insert-part-text/plain (msg part _content-type _nth depth button)
   ;; For backward compatibility we want to apply the text/plain hook
@@ -820,7 +924,8 @@ will return nil if the CID is unknown or cannot be retrieved."
     (let ((mm-inline-text-html-with-w3m-keymap nil)
          ;; FIXME: If we block an image, offer a button to load external
          ;; images.
-         (gnus-blocked-images notmuch-show-text/html-blocked-images))
+         (gnus-blocked-images notmuch-show-text/html-blocked-images)
+         (w3m-ignored-image-url-regexp notmuch-show-text/html-blocked-images))
       (notmuch-show-insert-part-*/* msg part content-type nth depth button))))
 
 ;;; Functions used by notmuch-show--insert-part-text/html-shr
@@ -930,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
@@ -993,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))
@@ -1048,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))
@@ -1058,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
@@ -1151,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))))
 
@@ -1252,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)
@@ -1307,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.
 
@@ -1314,9 +1471,10 @@ Apply the previously saved STATE if supplied, otherwise show the
 first relevant message.
 
 If no messages match the query return NIL."
-  (let* ((cli-args (cons "--exclude=false"
-                        (and notmuch-show-elide-non-matching-messages
-                             (list "--entire-thread=false"))))
+  (let* ((cli-args (list "--exclude=false"))
+        (cli-args (if notmuch-show-elide-non-matching-messages (cons "--entire-thread=false" cli-args) cli-args))
+        ;; "part 0 is the whole message (headers and body)" notmuch-show(1)
+        (cli-args (if notmuch-show-single-message (cons "--part=0" cli-args) cli-args))
         (queries (notmuch-show--build-queries
                   notmuch-show-thread-id notmuch-show-query-context))
         (forest nil)
@@ -1325,8 +1483,10 @@ 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)))))
       (setq queries (cdr queries)))
     (when forest
       (notmuch-show-insert-forest forest)
@@ -1334,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)
@@ -1411,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
@@ -1499,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)
@@ -1693,10 +1850,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."
@@ -1708,6 +1865,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))
 
@@ -1902,13 +2063,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")
@@ -2019,12 +2182,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))
-      (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)
@@ -2053,7 +2220,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
@@ -2070,19 +2237,19 @@ message."
     (let ((cwd default-directory)
          (buf (get-buffer-create (concat "*notmuch-pipe*"))))
       (with-current-buffer buf
-       (setq buffer-read-only nil)
-       (erase-buffer)
-       ;; Use the originating buffer's working directory instead of
-       ;; that of the pipe buffer.
-       (cd cwd)
-       (let ((exit-code (call-process-shell-command shell-command nil buf)))
-         (goto-char (point-max))
-         (set-buffer-modified-p nil)
-         (setq buffer-read-only t)
-         (unless (zerop exit-code)
-           (pop-to-buffer buf)
-           (message (format "Command '%s' exited abnormally with code %d"
-                            shell-command exit-code))))))))
+       (setq buffer-read-only t)
+       (let ((inhibit-read-only t))
+         (erase-buffer)
+         ;; Use the originating buffer's working directory instead of
+         ;; that of the pipe buffer.
+         (cd cwd)
+         (let ((exit-code (call-process-shell-command shell-command nil buf)))
+           (goto-char (point-max))
+           (set-buffer-modified-p nil)
+           (unless (zerop exit-code)
+             (pop-to-buffer buf)
+             (message (format "Command '%s' exited abnormally with code %d"
+                              shell-command exit-code)))))))))
 
 (defun notmuch-show-tag-message (&rest tag-changes)
   "Change tags for the current message.
@@ -2189,7 +2356,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)
@@ -2338,10 +2507,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
@@ -2358,12 +2529,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)))
index f348d4ae2dc82c1b0c57570269cf4015a54d939b..811018288f373b1df099fa7b791b27a47e313709 100644 (file)
@@ -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'."
   "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>
-<svg version=\"1.1\" width=\"16\" height=\"16\">
+<svg version=\"1.1\" width=\"16\" height=\"16\" xmlns=\"http://www.w3.org/2000/svg\">
   <g transform=\"translate(-242.81601,-315.59635)\">
     <path
        d=\"m 290.25762,334.31206 -17.64143,-11.77975 -19.70508,7.85447 5.75171,-20.41814 -13.55925,-16.31348 21.19618,-0.83936 11.325,-17.93675 7.34825,19.89939 20.55849,5.22795 -16.65471,13.13786 z\"
@@ -254,7 +254,7 @@ This can be used with `notmuch-tag-format-image-data'."
   "Return SVG data representing an empty star icon.
 This can be used with `notmuch-tag-format-image-data'."
   "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>
-<svg version=\"1.1\" width=\"16\" height=\"16\">
+<svg version=\"1.1\" width=\"16\" height=\"16\" xmlns=\"http://www.w3.org/2000/svg\">
   <g transform=\"translate(-242.81601,-315.59635)\">
     <path
        d=\"m 290.25762,334.31206 -17.64143,-11.77975 -19.70508,7.85447 5.75171,-20.41814 -13.55925,-16.31348 21.19618,-0.83936 11.325,-17.93675 7.34825,19.89939 20.55849,5.22795 -16.65471,13.13786 z\"
@@ -267,7 +267,7 @@ This can be used with `notmuch-tag-format-image-data'."
   "Return SVG data representing a tag icon.
 This can be used with `notmuch-tag-format-image-data'."
   "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>
-<svg version=\"1.1\" width=\"16\" height=\"16\">
+<svg version=\"1.1\" width=\"16\" height=\"16\" xmlns=\"http://www.w3.org/2000/svg\">
   <g transform=\"translate(0,-1036.3622)\">
     <path
        d=\"m 0.44642857,1040.9336 12.50000043,0 2.700893,3.6161 -2.700893,3.616 -12.50000043,0 z\"
@@ -275,6 +275,11 @@ This can be used with `notmuch-tag-format-image-data'."
   </g>
 </svg>")
 
+;;; 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)
@@ -397,7 +402,7 @@ Return all tags if no search terms are given."
   (split-string
    (with-output-to-string
      (with-current-buffer standard-output
-       (apply 'call-process notmuch-command nil t
+       (apply 'notmuch--call-process notmuch-command nil t
              nil "search" "--output=tags" "--exclude=false" search-terms)))
    "\n+" t))
 
@@ -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.")
 
-(defun notmuch-tag (query tag-changes)
+(make-obsolete-variable 'notmuch-tag-argument-limit nil "notmuch 0.36")
+
+(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\" ..)
@@ -486,16 +488,34 @@ notmuch-after-tag-hook will be run."
   (unless query
     (error "Nothing to tag!"))
   (when tag-changes
-    (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-before-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.
 
@@ -549,7 +569,7 @@ and vice versa."
                                name)
                            (mapconcat #'identity tag-change " "))))
        (push (list key name-string
-                   `(lambda () (,tag-function ',tag-change)))
+                   (lambda () (funcall tag-function tag-change)))
              action-map)))
     (push (list notmuch-tag-jump-reverse-key
                (if reverse
index 13007a134d0a9f4e8cd40a7a57bb2f9663884ee1..faec89c48a9a456a294d1202f240a17dde136ffa 100644 (file)
 (require 'mail-parse)
 
 (require 'notmuch-lib)
-(require 'notmuch-query)
 (require 'notmuch-show)
 (require 'notmuch-tag)
 (require 'notmuch-parser)
 (require 'notmuch-jump)
 
 (declare-function notmuch-search "notmuch"
-                 (&optional query oldest-first target-thread target-line))
-(declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
+                 (&optional query oldest-first target-thread target-line
+                            no-display))
+(declare-function notmuch-call-notmuch-process "notmuch-lib" (&rest args))
 (declare-function notmuch-read-query "notmuch" (prompt))
 (declare-function notmuch-search-find-thread-id "notmuch" (&optional bare))
 (declare-function notmuch-search-find-subject "notmuch" ())
@@ -45,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.")
       notmuch-unthreaded-show-out
     notmuch-tree-show-out))
 
+(defcustom notmuch-tree-thread-symbols
+  '((prefix . " ")
+    (top . "─")
+    (top-tee . "┬")
+    (vertical . "│")
+    (vertical-tee . "├")
+    (bottom . "╰")
+    (arrow . "►"))
+  "Strings used to draw trees in notmuch tree results.
+Symbol keys denote where the corresponding string value is used:
+`prefix' is used at the top of the tree, followed by `top' if it
+has no children or `top-tee' if it does; `vertical' is a bar
+connecting with a response down the list skipping the current
+one, while `vertical-tee' marks the current message as a reply to
+the previous one; `bottom' is used at the bottom of threads.
+Finally, the `arrrow' string in the list is used as a pointer to
+every message.
+
+Common customizations include setting `prefix' to \"-\", to see
+equal-length prefixes, and `arrow' to an empty string or to a
+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")
-    ((("tree" . "%s")("subject" . "%s")) ." %-54s ")
+    ((("tree" . "%s")
+      ("subject" . "%s"))
+     . " %-54s ")
     ("tags" . "(%s)"))
-  "Result formatting for tree view. Supported fields are: date,
-authors, subject, tree, tags.  Tree means the thread tree
-box graphics. The field may also be a list in which case
-the formatting rules are applied recursively and then the
-output of all the fields in the list is inserted
-according to format-string.
-
-Note 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 (string) :value-type (string))
+  "Result formatting for tree view.
+
+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
+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)."
+
+  :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
@@ -98,19 +137,26 @@ For example:
     ("authors" . "%-20s")
     ((("subject" . "%s")) ." %-54s ")
     ("tags" . "(%s)"))
-  "Result formatting for unthreaded tree view. Supported fields are: date,
-authors, subject, tree, tags.  Tree means the thread tree
-box graphics. The field may also be a list in which case
-the formatting rules are applied recursively and then the
-output of all the fields in the list is inserted
-according to format-string.
-
-Note 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 (string) :value-type (string))
+  "Result formatting for unthreaded tree view.
+
+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
+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)."
+
+  :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 ()
@@ -142,7 +188,7 @@ For example:
      (: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)
 
@@ -154,7 +200,8 @@ For example:
 
 (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)
 
@@ -193,13 +240,14 @@ For example:
 
 (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)
 
@@ -320,11 +368,13 @@ then NAME behaves like CMD."
     ;; These bindings shadow common bindings with variants
     ;; that additionally close the message window.
     (define-key map [remap notmuch-bury-or-kill-this-buffer] 'notmuch-tree-quit)
-    (define-key map [remap notmuch-search]       'notmuch-tree-to-search)
-    (define-key map [remap notmuch-help]         'notmuch-tree-help)
-    (define-key map [remap notmuch-mua-new-mail] 'notmuch-tree-new-mail)
-    (define-key map [remap notmuch-jump-search]  'notmuch-tree-jump-search)
+    (define-key map [remap notmuch-search]        'notmuch-tree-to-search)
+    (define-key map [remap notmuch-help]          'notmuch-tree-help)
+    (define-key map [remap notmuch-mua-new-mail]  'notmuch-tree-new-mail)
+    (define-key map [remap notmuch-jump-search]   'notmuch-tree-jump-search)
 
+    (define-key map "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)
@@ -347,6 +397,9 @@ then NAME behaves like CMD."
     (define-key map "r" 'notmuch-tree-reply-sender)
     (define-key map "R" 'notmuch-tree-reply)
     (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)
@@ -538,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."
@@ -574,7 +629,7 @@ NOT change the database."
       (with-selected-window notmuch-tree-message-window
        (let (;; Since we are only displaying one message do not indent.
              (notmuch-show-indent-messages-width 0)
-             (notmuch-show-only-matching-messages t)
+             (notmuch-show-single-message t)
              ;; Ensure that `pop-to-buffer-same-window' uses the
              ;; window we want it to use.
              (display-buffer-overriding-action
@@ -598,7 +653,9 @@ NOT change the database."
     (when id
       ;; We close the window to kill off un-needed buffers.
       (notmuch-tree-close-message-window)
-      (notmuch-show id))))
+      ;; n-s-s-m is buffer local, so use inner let.
+      (let ((notmuch-show-single-message t))
+       (notmuch-show id)))))
 
 (defun notmuch-tree-show-message (arg)
   "Show the current message.
@@ -748,7 +805,9 @@ nil otherwise."
                         query-context
                         target
                         nil
-                        unthreaded)))
+                        unthreaded
+                        notmuch-search-oldest-first
+                        notmuch-search-hide-excluded)))
 
 (defun notmuch-tree-thread-top ()
   (when (notmuch-tree-get-message-properties)
@@ -866,6 +925,9 @@ unchanged ADDRESS if parsing fails."
      ((listp field)
       (format format-string (notmuch-tree-format-field-list field msg)))
 
+     ((functionp field)
+      (funcall field format-string msg))
+
      ((string-equal field "date")
       (let ((face (if match
                      'notmuch-tree-match-date-face
@@ -949,7 +1011,8 @@ unchanged ADDRESS if parsing fails."
       (goto-char (point-max))
       (forward-line -1)
       (when notmuch-tree-open-target
-       (notmuch-tree-show-message-in)))))
+       (notmuch-tree-show-message-in)
+       (notmuch-tree-command-hook)))))
 
 (defun notmuch-tree-insert-tree (tree depth tree-status first last)
   "Insert the message tree TREE at depth DEPTH in the current thread.
@@ -957,36 +1020,41 @@ 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 "├" tree-status))
+      (push (alist-get 'vertical-tee  notmuch-tree-thread-symbols) tree-status))
      ((and (< 0 depth) last)
-      (push "╰" tree-status))
+      (push (alist-get 'bottom notmuch-tree-thread-symbols) tree-status))
      ((and (eq 0 depth) first last)
-      ;; Choice between these two variants is a matter of taste.
-      ;; (push "─" tree-status))
-      (push " " tree-status))
+      (push (alist-get 'prefix notmuch-tree-thread-symbols) tree-status))
      ((and (eq 0 depth) first (not last))
-      (push "┬" tree-status))
+      (push (alist-get 'top-tee notmuch-tree-thread-symbols) tree-status))
      ((and (eq 0 depth) (not first) last)
-      (push "╰" tree-status))
+      (push (alist-get 'bottom notmuch-tree-thread-symbols) tree-status))
      ((and (eq 0 depth) (not first) (not last))
-      (push "├" tree-status)))
-    (push (concat (if replies "┬" "─") "►") tree-status)
+      (push (alist-get 'vertical-tee notmuch-tree-thread-symbols) tree-status)))
+    (push (concat (alist-get (if replies 'top-tee 'top) notmuch-tree-thread-symbols)
+                 (alist-get 'arrow notmuch-tree-thread-symbols))
+         tree-status)
     (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)
     (if last
        (push " " tree-status)
-      (push "│" tree-status))
+      (push (alist-get 'vertical notmuch-tree-thread-symbols) tree-status))
     (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
@@ -996,10 +1064,9 @@ message together with all its descendents."
 
 (defun notmuch-tree-insert-forest-thread (forest-thread)
   "Insert a single complete thread."
-  (let (tree-status)
-    ;; Reset at the start of each main thread.
-    (setq notmuch-tree-previous-subject nil)
-    (notmuch-tree-insert-thread forest-thread 0 tree-status)))
+  ;; Reset at the start of each main thread.
+  (setq notmuch-tree-previous-subject nil)
+  (notmuch-tree-insert-thread forest-thread 0 nil))
 
 (defun notmuch-tree-insert-forest (forest)
   "Insert a forest of threads.
@@ -1024,7 +1091,14 @@ Complete list of currently available key bindings:
   (setq notmuch-buffer-refresh-function #'notmuch-tree-refresh-view)
   (hl-line-mode 1)
   (setq buffer-read-only t)
-  (setq truncate-lines t))
+  (setq truncate-lines 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."
@@ -1044,7 +1118,8 @@ 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"))))
+         (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."
@@ -1061,7 +1136,9 @@ Complete list of currently available key bindings:
        (notmuch-sexp-parse-partial-list 'notmuch-tree-insert-forest-thread
                                         results-buf)))))
 
-(defun notmuch-tree-worker (basic-query &optional query-context target open-target unthreaded)
+(defun notmuch-tree-worker (basic-query &optional query-context target
+                                       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
@@ -1069,6 +1146,8 @@ the same as for the function notmuch-tree."
   (interactive)
   (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 "")
@@ -1087,14 +1166,16 @@ the same as for the function notmuch-tree."
   (let* ((search-args (concat basic-query
                              (and query-context
                                   (concat " and (" query-context ")"))))
-        (message-arg (if unthreaded "--unthreaded" "--entire-thread")))
-    (when (equal (car (process-lines notmuch-command "count" search-args)) "0")
+        (sort-arg (if oldest-first "--sort=oldest-first" "--sort=newest-first"))
+        (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=4"
-                message-arg search-args))
+                "show" "--body=false" "--format=sexp" "--format-version=5"
+                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.
@@ -1112,7 +1193,28 @@ the same as for the function notmuch-tree."
              ")")
     notmuch-tree-basic-query))
 
-(defun notmuch-tree (&optional query query-context target buffer-name open-target unthreaded parent-buffer)
+(defun notmuch-tree-toggle-order ()
+  "Toggle the current search order.
+
+This command toggles the sort order for the current search. The
+default sort order is defined by `notmuch-search-oldest-first'."
+  (interactive)
+  (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 hide-excluded)
   "Display threads matching QUERY in tree view.
 
 The arguments are:
@@ -1127,27 +1229,273 @@ 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)
+  (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)
+(defun notmuch-unthreaded (&optional query query-context target buffer-name
+                                    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.
+
+Runs a new tree search matching only messages that match both the
+current search results AND the additional query string provided."
+  (interactive (list (notmuch-read-query "Filter search: ")))
+  (let ((notmuch-show-process-crypto (notmuch-tree--message-process-crypto))
+       (grouped-query (notmuch-group-disjunctive-query-string query))
+       (grouped-original-query (notmuch-group-disjunctive-query-string
+                                (notmuch-tree-get-query))))
+    (notmuch-tree-close-message-window)
+    (notmuch-tree (if (string= grouped-original-query "*")
+                     grouped-query
+                   (concat grouped-original-query " and " grouped-query)))))
+
+(defun notmuch-tree-filter-by-tag (tag)
+  "Filter the current search results based on a single TAG.
+
+Run a new search matching only messages that match the current
+search results and that are also tagged with the given TAG."
+  (interactive
+   (list (notmuch-select-tag-with-completion "Filter by tag: "
+                                            notmuch-tree-basic-query)))
+  (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
+                 nil
+                 notmuch-tree-unthreaded
+                 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)
-  (notmuch-tree query query-context target buffer-name open-target t))
+  (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))))
 
 ;;; _
 
index 653ecc2ae7d5d4e07ff8a87c9ed95315fb3c30bb..fd8a9d1e2296db96e6cf6642bbe0ba35819942ae 100644 (file)
@@ -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
index 6d37c623e6597cb4f43c0d6029b2f9502f93c89f..2a73ffa571c41a04b0836a50a991102249fad658 100644 (file)
     ("authors" . "%-20s ")
     ("subject" . "%s ")
     ("tags" . "(%s)"))
-  "Search result formatting. Supported fields are:
-       date, count, authors, subject, tags
-For example:
-       (setq notmuch-search-result-format \(\(\"authors\" . \"%-40s\"\)
-                                            \(\"subject\" . \"%s\"\)\)\)
+  "Search result formatting.
+
+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
 \"authors\" field will get elided if the authors list is long;
 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
@@ -175,9 +187,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)
@@ -517,7 +531,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
@@ -529,17 +545,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."
@@ -548,7 +571,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."
@@ -825,31 +850,34 @@ 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))))
 
 (defun notmuch-search-insert-field (field format-string result)
-  (cond
-   ((string-equal field "date")
-    (insert (propertize (format format-string (plist-get result :date_relative))
-                       'face 'notmuch-search-date)))
-   ((string-equal field "count")
-    (insert (propertize (format format-string
-                               (format "[%s/%s]" (plist-get result :matched)
-                                       (plist-get result :total)))
-                       'face 'notmuch-search-count)))
-   ((string-equal field "subject")
-    (insert (propertize (format format-string
-                               (notmuch-sanitize (plist-get result :subject)))
-                       'face 'notmuch-search-subject)))
-   ((string-equal field "authors")
-    (notmuch-search-insert-authors
-     format-string (notmuch-sanitize (plist-get result :authors))))
-   ((string-equal field "tags")
-    (let ((tags (plist-get result :tags))
-         (orig-tags (plist-get result :orig-tags)))
-      (insert (format format-string (notmuch-tag-format-tags tags orig-tags)))))))
+  (pcase field
+    ((pred functionp)
+     (insert (funcall field format-string result)))
+    ("date"
+     (insert (propertize (format format-string (plist-get result :date_relative))
+                        'face 'notmuch-search-date)))
+    ("count"
+     (insert (propertize (format format-string
+                                (format "[%s/%s]" (plist-get result :matched)
+                                        (plist-get result :total)))
+                        'face 'notmuch-search-count)))
+    ("subject"
+     (insert (propertize (format format-string
+                                (notmuch-sanitize (plist-get result :subject)))
+                        'face 'notmuch-search-subject)))
+    ("authors"
+     (notmuch-search-insert-authors format-string
+                                   (notmuch-sanitize (plist-get result :authors))))
+    ("tags"
+     (let ((tags (plist-get result :tags))
+          (orig-tags (plist-get result :orig-tags)))
+       (insert (format format-string (notmuch-tag-format-tags tags orig-tags)))))))
 
 (defun notmuch-search-show-result (result pos)
   "Insert RESULT at POS."
@@ -875,6 +903,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))
@@ -887,7 +923,9 @@ 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
+       (notmuch--search-hook-wrapper)))))
 
 ;;; Commands (and some helper functions used by them)
 
@@ -900,7 +938,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
@@ -915,19 +985,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.
@@ -935,7 +1006,7 @@ See `notmuch-tag' for information on the format of TAG-CHANGES."
 PROMPT is the string to prompt with."
   (let* ((all-tags
          (mapcar (lambda (tag) (notmuch-escape-boolean-term tag))
-                 (process-lines notmuch-command "search" "--output=tags" "*")))
+                 (notmuch--process-lines notmuch-command "search" "--output=tags" "*")))
         (completions
          (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:"
                        "subject:" "attachment:")
@@ -973,13 +1044,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
@@ -994,9 +1068,10 @@ 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))))
@@ -1010,6 +1085,7 @@ the configured default sort order."
     (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))
@@ -1019,10 +1095,13 @@ the configured default sort order."
       (save-excursion
        (let ((proc (notmuch-start-notmuch
                     "notmuch-search" buffer #'notmuch-search-process-sentinel
-                    "search" "--format=sexp" "--format-version=4"
+                    "search" "--format=sexp" "--format-version=5"
                     (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
@@ -1030,8 +1109,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.
@@ -1044,11 +1122,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.
 
@@ -1077,7 +1165,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.
@@ -1088,13 +1177,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 ()
@@ -1162,8 +1260,6 @@ Point should be at the beginning of the line."
 
 ;;; _
 
-(setq mail-user-agent 'notmuch-user-agent)
-
 (provide 'notmuch)
 
 ;; After provide to avoid loops if notmuch was require'd via notmuch-init-file.
index c7c130154a582841b084343f38787431e0bbe77a..5b8a9d01311b4d3055bec7af4d6f45832d661f51 100644 (file)
@@ -71,6 +71,7 @@
      ("`" . "\\\\`")
      ("\001" . "'")
      ("\002" . "`")
+     ("[*]" . "\\\\*")
      ("^[[:space:]]*$" . "|br|")
      ("^[[:space:]]" . "|indent| "))
     "list of (regex . replacement) pairs")
index 2b0676698b8ff7225b164c9db52ab5ad5b5ea314..35349cc8170fdd7abcaa08684e83320eb9f1df08 100644 (file)
@@ -43,29 +43,31 @@ static void filter_reset (GMimeFilter *filter);
 
 
 static GMimeFilterClass *parent_class = NULL;
+static GType type = 0;
+static const GTypeInfo info = {
+    .class_size = sizeof (GMimeFilterReplyClass),
+    .base_init = NULL,
+    .base_finalize = NULL,
+    .class_init = (GClassInitFunc) g_mime_filter_reply_class_init,
+    .class_finalize = NULL,
+    .class_data = NULL,
+    .instance_size = sizeof (GMimeFilterReply),
+    .n_preallocs = 0,
+    .instance_init = (GInstanceInitFunc) g_mime_filter_reply_init,
+    .value_table = NULL,
+};
+
+
+void
+g_mime_filter_reply_module_init (void)
+{
+    type = g_type_register_static (GMIME_TYPE_FILTER, "GMimeFilterReply", &info, (GTypeFlags) 0);
+    parent_class = (GMimeFilterClass *) g_type_class_ref (GMIME_TYPE_FILTER);
+}
 
 GType
 g_mime_filter_reply_get_type (void)
 {
-    static GType type = 0;
-
-    if (! type) {
-       static const GTypeInfo info = {
-           .class_size = sizeof (GMimeFilterReplyClass),
-           .base_init = NULL,
-           .base_finalize = NULL,
-           .class_init = (GClassInitFunc) g_mime_filter_reply_class_init,
-           .class_finalize = NULL,
-           .class_data = NULL,
-           .instance_size = sizeof (GMimeFilterReply),
-           .n_preallocs = 0,
-           .instance_init = (GInstanceInitFunc) g_mime_filter_reply_init,
-           .value_table = NULL,
-       };
-
-       type = g_type_register_static (GMIME_TYPE_FILTER, "GMimeFilterReply", &info, (GTypeFlags) 0);
-    }
-
     return type;
 }
 
@@ -76,8 +78,6 @@ g_mime_filter_reply_class_init (GMimeFilterReplyClass *klass, unused (void *clas
     GObjectClass *object_class = G_OBJECT_CLASS (klass);
     GMimeFilterClass *filter_class = GMIME_FILTER_CLASS (klass);
 
-    parent_class = (GMimeFilterClass *) g_type_class_ref (GMIME_TYPE_FILTER);
-
     object_class->finalize = g_mime_filter_reply_finalize;
 
     filter_class->copy = filter_copy;
index 7cdefcd1f127ff45e5ca9cbd63bf0f541816e1c9..988fe2d6e8c5122fcbc523e8ceefdab4e38bd38e 100644 (file)
@@ -21,6 +21,8 @@
 
 #include <gmime/gmime-filter.h>
 
+void g_mime_filter_reply_module_init (void);
+
 G_BEGIN_DECLS
 
 #define GMIME_TYPE_FILTER_REPLY            (g_mime_filter_reply_get_type ())
diff --git a/hooks.c b/hooks.c
index ec89b22ec75497ee69a5d9cce888c6c2a1a95c81..0cf72e748bc983d60aa09a074375b5fef2c96d97 100644 (file)
--- 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
index 01cbb3f2821c8e0a1e0145192e2e3368b625d6c7..4e7663056b57ba3e2599368f1e12b2b381d20f08 100644 (file)
@@ -62,7 +62,11 @@ libnotmuch_cxx_srcs =                \
        $(dir)/thread-fp.cc     \
        $(dir)/features.cc      \
        $(dir)/prefix.cc        \
-       $(dir)/open.cc
+       $(dir)/open.cc          \
+       $(dir)/init.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)
 
index 485debad225239211e9a6f165f24be519080a0ed..b16748fda20ebf4128f4e8fd78f4a66f9c9a83c2 100644 (file)
@@ -40,17 +40,14 @@ parse_references (void *ctx,
 static const char *
 _notmuch_database_generate_thread_id (notmuch_database_t *notmuch)
 {
-    /* 16 bytes (+ terminator) for hexadecimal representation of
-     * a 64-bit integer. */
-    static char thread_id[17];
 
     notmuch->last_thread_id++;
 
-    sprintf (thread_id, "%016" PRIx64, notmuch->last_thread_id);
+    sprintf (notmuch->thread_id_str, "%016" PRIx64, notmuch->last_thread_id);
 
-    notmuch->writable_xapian_db->set_metadata ("last_thread_id", thread_id);
+    notmuch->writable_xapian_db->set_metadata ("last_thread_id", notmuch->thread_id_str);
 
-    return thread_id;
+    return notmuch->thread_id_str;
 }
 
 static char *
@@ -407,14 +404,17 @@ static notmuch_status_t
 _notmuch_database_link_message (notmuch_database_t *notmuch,
                                notmuch_message_t *message,
                                notmuch_message_file_t *message_file,
-                               bool is_ghost)
+                               bool is_ghost,
+                               bool is_new)
 {
     void *local = talloc_new (NULL);
     notmuch_status_t status;
     const char *thread_id = NULL;
 
     /* Check if the message already had a thread ID */
-    if (notmuch->features & NOTMUCH_FEATURE_GHOSTS) {
+    if (! is_new) {
+       thread_id = notmuch_message_get_thread_id (message);
+    } else if (notmuch->features & NOTMUCH_FEATURE_GHOSTS) {
        if (is_ghost)
            thread_id = notmuch_message_get_thread_id (message);
     } else {
@@ -529,7 +529,9 @@ notmuch_database_index_file (notmuch_database_t *notmuch,
            goto DONE;
        }
 
-       _notmuch_message_add_filename (message, filename);
+       ret = _notmuch_message_add_filename (message, filename);
+       if (ret)
+           goto DONE;
 
        if (is_new || is_ghost) {
            _notmuch_message_add_term (message, "type", "mail");
@@ -539,7 +541,7 @@ notmuch_database_index_file (notmuch_database_t *notmuch,
        }
 
        ret = _notmuch_database_link_message (notmuch, message,
-                                             message_file, is_ghost);
+                                             message_file, is_ghost, is_new);
        if (ret)
            goto DONE;
 
index 0c70010beca5548c012b531edb70691703df5ca6..275e72b89e84d3de95ac73d628576d8da1211d0d 100644 (file)
@@ -32,6 +32,8 @@ 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, "sexp_queries") == 0) {
+       return HAVE_SFSEXP;
     } else {
        return false;
     }
index 7a0da9716bf9a36ef4b1b5abad7cc5aabdee3049..6cd15fab26bc16610e1c56e1b5e0019b41279259 100644 (file)
@@ -46,6 +46,7 @@ struct _notmuch_config_pairs {
 };
 
 static const char *_notmuch_config_key_to_string (notmuch_config_key_t key);
+static char *_expand_path (void *ctx, const char *key, const char *val);
 
 static int
 _notmuch_config_list_destroy (notmuch_config_list_t *list)
@@ -257,9 +258,18 @@ _notmuch_config_load_from_database (notmuch_database_t *notmuch)
        return status;
 
     for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
-       _notmuch_string_map_append (notmuch->config,
-                                   notmuch_config_list_key (list),
-                                   notmuch_config_list_value (list));
+       const char *key = notmuch_config_list_key (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);
     }
 
     return status;
@@ -316,7 +326,7 @@ notmuch_config_values_valid (notmuch_config_values_t *values)
 const char *
 notmuch_config_values_get (notmuch_config_values_t *values)
 {
-    return talloc_strndup (values, values->iterator, values->tok_len);
+    return talloc_strndup (values->children, values->iterator, values->tok_len);
 }
 
 void
@@ -387,9 +397,27 @@ notmuch_config_pairs_destroy (notmuch_config_pairs_t *pairs)
     talloc_free (pairs);
 }
 
+static char *
+_expand_path (void *ctx, const char *key, const char *val)
+{
+    char *expanded_val;
+
+    if ((strcmp (key, "database.path") == 0 ||
+        strcmp (key, "database.mail_root") == 0 ||
+        strcmp (key, "database.hook_dir") == 0 ||
+        strcmp (key, "database.backup_path") == 0 ) &&
+       val[0] != '/')
+       expanded_val = talloc_asprintf (ctx, "%s/%s", getenv ("HOME"), val);
+    else
+       expanded_val = talloc_strdup (ctx, val);
+
+    return expanded_val;
+}
+
 notmuch_status_t
 _notmuch_config_load_from_file (notmuch_database_t *notmuch,
-                               GKeyFile *file)
+                               GKeyFile *file,
+                               char **status_string)
 {
     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
     gchar **groups = NULL, **keys, *val;
@@ -407,14 +435,33 @@ _notmuch_config_load_from_file (notmuch_database_t *notmuch,
        keys = g_key_file_get_keys (file, *grp, NULL, NULL);
        for (gchar **keys_p = keys; *keys_p; keys_p++) {
            char *absolute_key = talloc_asprintf (notmuch, "%s.%s", *grp,  *keys_p);
-           val = g_key_file_get_value (file, *grp, *keys_p, NULL);
+           char *normalized_val;
+           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;
            }
-           _notmuch_string_map_set (notmuch->config, absolute_key, val);
+
+           normalized_val = _expand_path (notmuch, absolute_key, val);
+           _notmuch_string_map_set (notmuch->config, absolute_key, normalized_val);
            g_free (val);
            talloc_free (absolute_key);
+           talloc_free (normalized_val);
            if (status)
                goto DONE;
        }
@@ -521,28 +568,13 @@ _get_username_from_passwd_file (void *ctx)
 static const char *
 _get_email_from_passwd_file (void *ctx)
 {
-
-    char hostname[256];
-    struct hostent *hostent;
-    const char *domainname;
     char *email;
 
     char *username = _get_username_from_passwd_file (ctx);
 
-    gethostname (hostname, 256);
-    hostname[255] = '\0';
-
-    hostent = gethostbyname (hostname);
-    if (hostent && (domainname = strchr (hostent->h_name, '.')))
-       domainname += 1;
-    else
-       domainname = "(none)";
-
-    email = talloc_asprintf (ctx, "%s@%s.%s",
-                            username, hostname, domainname);
+    email = talloc_asprintf (ctx, "%s@localhost", username);
 
     talloc_free (username);
-    talloc_free (email);
     return email;
 }
 
@@ -572,6 +604,12 @@ _notmuch_config_key_to_string (notmuch_config_key_t key)
        return "user.other_email";
     case NOTMUCH_CONFIG_USER_NAME:
        return "user.name";
+    case NOTMUCH_CONFIG_AUTOCOMMIT:
+       return "database.autocommit";
+    case NOTMUCH_CONFIG_EXTRA_HEADERS:
+       return "show.extra_headers";
+    case NOTMUCH_CONFIG_INDEX_AS_TEXT:
+       return "index.as_text";
     default:
        return NULL;
     }
@@ -615,8 +653,12 @@ _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_EXTRA_HEADERS:
     case NOTMUCH_CONFIG_HOOK_DIR:
     case NOTMUCH_CONFIG_BACKUP_DIR:
     case NOTMUCH_CONFIG_OTHER_EMAIL:
@@ -631,6 +673,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;
@@ -640,11 +686,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 *
index 0d12ec1efde57b4862f1001facdbfafe3dec54cd..61232f1ae3ffcc51bb0dd6b96c91e1b9296e33d3 100644 (file)
 
 #include <xapian.h>
 
+#if HAVE_SFSEXP
+#include <sexp.h>
+#endif
+
 /* Bit masks for _notmuch_database::features.  Features are named,
  * independent aspects of the database schema.
  *
@@ -156,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;
 
 /*
@@ -186,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<notmuch_open_param_t>(
+       static_cast<unsigned>(a) | static_cast<unsigned>(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<notmuch_open_param_t>(
+       static_cast<unsigned>(a) & static_cast<unsigned>(b));
+}
+
 struct _notmuch_database {
     bool exception_reported;
 
@@ -206,8 +249,17 @@ struct _notmuch_database {
     enum _notmuch_features features;
 
     unsigned int last_doc_id;
+
+    /* 16 bytes (+ terminator) for hexadecimal representation of
+     * a 64-bit integer. */
+    char thread_id_str[17];
     uint64_t last_thread_id;
 
+    /* How many transactions have successfully completed since we last committed */
+    int transaction_count;
+    /* when to commit and reset the counter */
+    int transaction_threshold;
+
     /* error reporting; this value persists only until the
      * next library call. May be NULL */
     char *status_string;
@@ -223,6 +275,7 @@ struct _notmuch_database {
      */
     unsigned long view;
     Xapian::QueryParser *query_parser;
+    Xapian::Stem *stemmer;
     Xapian::TermGenerator *term_gen;
     Xapian::RangeProcessor *value_range_processor;
     Xapian::RangeProcessor *date_range_processor;
@@ -235,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
@@ -291,4 +351,45 @@ _notmuch_database_setup_standard_query_fields (notmuch_database_t *notmuch);
 notmuch_status_t
 _notmuch_database_setup_user_query_fields (notmuch_database_t *notmuch);
 
+#if __cplusplus
+/* query.cc */
+notmuch_status_t
+_notmuch_query_string_to_xapian_query (notmuch_database_t *notmuch,
+                                      std::string query_string,
+                                      Xapian::Query &output,
+                                      std::string &msg);
+
+notmuch_status_t
+_notmuch_query_expand (notmuch_database_t *notmuch, const char *field, Xapian::Query subquery,
+                      Xapian::Query &output, std::string &msg);
+
+/* regexp-fields.cc */
+notmuch_status_t
+_notmuch_regexp_to_query (notmuch_database_t *notmuch, Xapian::valueno slot, std::string field,
+                         std::string regexp_str,
+                         Xapian::Query &output, std::string &msg);
+
+/* thread-fp.cc */
+notmuch_status_t
+_notmuch_query_name_to_query (notmuch_database_t *notmuch, const std::string name,
+                             Xapian::Query &output);
+
+#if HAVE_SFSEXP
+/* parse-sexp.cc */
+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
index 96458f6ff104a18ba28d59929b7597cf284feeec..737a3f3060677b4e7fd5a55fbe5268082f0867f2 100644 (file)
@@ -309,6 +309,10 @@ notmuch_status_to_string (notmuch_status_t status)
        return "No database found";
     case NOTMUCH_STATUS_DATABASE_EXISTS:
        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";
@@ -472,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;
 }
 
@@ -502,17 +511,9 @@ notmuch_database_close (notmuch_database_t *notmuch)
      * close it.  Thus, we explicitly close it here. */
     if (notmuch->open) {
        try {
-           /* If there's an outstanding transaction, it's unclear if
-            * closing the Xapian database commits everything up to
-            * that transaction, or may discard committed (but
-            * unflushed) transactions.  To be certain, explicitly
-            * cancel any outstanding transaction before closing. */
-           if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_WRITE &&
-               notmuch->atomic_nesting)
-               notmuch->writable_xapian_db->cancel_transaction ();
-
            /* Close the database.  This implicitly flushes
-            * outstanding changes. */
+            * outstanding changes. If there is an open (non-flushed)
+            * transaction, ALL pending changes will be discarded */
            notmuch->xapian_db->close ();
        } catch (const Xapian::Error &error) {
            status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
@@ -596,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,
-                                        &notmuch,
-                                        &message);
+    ret = notmuch_database_open_with_config (path,
+                                            NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                            "",
+                                            NULL,
+                                            &notmuch,
+                                            &message);
     if (ret) {
        if (status_cb) status_cb (message, closure);
        return ret;
@@ -733,6 +736,15 @@ notmuch_status_t
 notmuch_database_destroy (notmuch_database_t *notmuch)
 {
     notmuch_status_t status;
+    const char *talloc_report;
+
+    talloc_report = getenv ("NOTMUCH_TALLOC_REPORT");
+    if (talloc_report && strcmp (talloc_report, "") != 0) {
+       FILE *report = fopen (talloc_report, "a");
+       if (report) {
+           talloc_report_full (notmuch, report);
+       }
+    }
 
     status = notmuch_database_close (notmuch);
 
@@ -748,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);
 
@@ -843,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;
 
@@ -1133,13 +1146,21 @@ notmuch_database_end_atomic (notmuch_database_t *notmuch)
     db = notmuch->writable_xapian_db;
     try {
        db->commit_transaction ();
-
-       /* This is a hack for testing.  Xapian never flushes on a
-        * non-flushed commit, even if the flush threshold is 1.
-        * However, we rely on flushing to test atomicity. */
+       notmuch->transaction_count++;
+
+       /* Xapian never flushes on a non-flushed commit, even if the
+        * flush threshold is 1.  However, we rely on flushing to test
+        * atomicity. On the other hand, we can't straight replace
+        * XAPIAN_FLUSH_THRESHOLD with our autocommit counter, because
+        * the former also applies outside notmuch atomic
+        * commits. Hence the follow complicated  test */
        const char *thresh = getenv ("XAPIAN_FLUSH_THRESHOLD");
-       if (thresh && atoi (thresh) == 1)
+       if ((notmuch->transaction_threshold > 0 &&
+            notmuch->transaction_count >= notmuch->transaction_threshold) ||
+           (thresh && atoi (thresh) == 1)) {
            db->commit ();
+           notmuch->transaction_count = 0;
+       }
     } catch (const Xapian::Error &error) {
        _notmuch_database_log (notmuch, "A Xapian exception occurred committing transaction: %s.\n",
                               error.get_msg ().c_str ());
@@ -1433,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);
 
@@ -1552,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 (&notmuch->index_as_text[i], mime_string, 0, NULL, 0) == 0) {
+           return true;
+       }
+    }
+
+    return false;
+}
index 55c8372e3aa1c146913222369f7185cb6805531a..629dcb222be8545cfde471ce5184f96c88115881 100644 (file)
@@ -148,8 +148,6 @@ notmuch_filter_discard_non_term_class_init (NotmuchFilterDiscardNonTermClass *kl
     GObjectClass *object_class = G_OBJECT_CLASS (klass);
     GMimeFilterClass *filter_class = GMIME_FILTER_CLASS (klass);
 
-    parent_class = (GMimeFilterClass *) g_type_class_ref (GMIME_TYPE_FILTER);
-
     object_class->finalize = notmuch_filter_discard_non_term_finalize;
 
     filter_class->copy = filter_copy;
@@ -240,30 +238,33 @@ filter_reset (GMimeFilter *gmime_filter)
  *
  * Returns: a new #NotmuchFilterDiscardNonTerm filter.
  **/
+static GType type = 0;
+
+static const GTypeInfo info = {
+    .class_size = sizeof (NotmuchFilterDiscardNonTermClass),
+    .base_init = NULL,
+    .base_finalize = NULL,
+    .class_init = (GClassInitFunc) notmuch_filter_discard_non_term_class_init,
+    .class_finalize = NULL,
+    .class_data = NULL,
+    .instance_size = sizeof (NotmuchFilterDiscardNonTerm),
+    .n_preallocs = 0,
+    .instance_init = NULL,
+    .value_table = NULL,
+};
+
+void
+_notmuch_filter_init () {
+    type = g_type_register_static (GMIME_TYPE_FILTER, "NotmuchFilterDiscardNonTerm", &info,
+                                  (GTypeFlags) 0);
+    parent_class = (GMimeFilterClass *) g_type_class_ref (GMIME_TYPE_FILTER);
+}
+
 static GMimeFilter *
 notmuch_filter_discard_non_term_new (GMimeContentType *content_type)
 {
-    static GType type = 0;
     NotmuchFilterDiscardNonTerm *filter;
 
-    if (! type) {
-       static const GTypeInfo info = {
-           .class_size = sizeof (NotmuchFilterDiscardNonTermClass),
-           .base_init = NULL,
-           .base_finalize = NULL,
-           .class_init = (GClassInitFunc) notmuch_filter_discard_non_term_class_init,
-           .class_finalize = NULL,
-           .class_data = NULL,
-           .instance_size = sizeof (NotmuchFilterDiscardNonTerm),
-           .n_preallocs = 0,
-           .instance_init = NULL,
-           .value_table = NULL,
-       };
-
-       type = g_type_register_static (GMIME_TYPE_FILTER, "NotmuchFilterDiscardNonTerm", &info,
-                                      (GTypeFlags) 0);
-    }
-
     filter = (NotmuchFilterDiscardNonTerm *) g_object_new (type, NULL);
     filter->content_type = content_type;
     filter->state = 0;
@@ -379,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,
@@ -496,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 ();
index 4a8608586a356c083ed8ad35e70fe8882fa21a25..2ffd19424ea9752d2976a2438c6214c63dba5e41 100644 (file)
 
 #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/init.cc b/lib/init.cc
new file mode 100644 (file)
index 0000000..cf29200
--- /dev/null
@@ -0,0 +1,21 @@
+#include "notmuch-private.h"
+
+#include <mutex>
+
+static void do_init ()
+{
+    /* Initialize the GLib type system and threads */
+#if ! GLIB_CHECK_VERSION (2, 35, 1)
+    g_type_init ();
+#endif
+
+    g_mime_init ();
+    _notmuch_filter_init ();
+}
+
+void
+_notmuch_init ()
+{
+    static std::once_flag initialized;
+    std::call_once (initialized, do_init);
+}
diff --git a/lib/lastmod-fp.cc b/lib/lastmod-fp.cc
new file mode 100644 (file)
index 0000000..f85efd2
--- /dev/null
@@ -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 <david@tethera.net>
+ */
+
+#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 (file)
index 0000000..8168fe7
--- /dev/null
@@ -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 <david@tethera.net>
+ */
+
+#ifndef NOTMUCH_LASTMOD_FP_H
+#define NOTMUCH_LASTMOD_FP_H
+
+#include <xapian.h>
+
+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 */
index 9e9b387fcc697a8e8b3d13a8bbbd03147714ef58..68f646a4e56d878459d37a665ca0298621e0c1d9 100644 (file)
@@ -141,7 +141,6 @@ _notmuch_message_file_parse (notmuch_message_file_t *message)
 {
     GMimeParser *parser;
     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
-    static int initialized = 0;
     bool is_mbox;
 
     if (message->message)
@@ -149,10 +148,7 @@ _notmuch_message_file_parse (notmuch_message_file_t *message)
 
     is_mbox = _is_mbox (message->stream);
 
-    if (! initialized) {
-       g_mime_init ();
-       initialized = 1;
-    }
+    _notmuch_init ();
 
     message->headers = g_hash_table_new_full (strcase_hash, strcase_equal,
                                              free, g_free);
@@ -295,11 +291,16 @@ _notmuch_message_file_get_header (notmuch_message_file_t *message,
     if (value)
        return value;
 
-    if (strcasecmp (header, "received") == 0) {
+    if (strcasecmp (header, "received") == 0 ||
+       strcasecmp (header, "delivered-to") == 0) {
        /*
-        * The Received: header is special. We concatenate all
-        * instances of the header as we use this when analyzing the
-        * path the mail has taken from sender to recipient.
+        * The Received: header is special. We concatenate all instances of the
+        * header as we use this when analyzing the path the mail has taken
+        * from sender to recipient.
+        *
+        * Similarly, multiple instances of Delivered-To may be present. We
+        * concatenate them so the one with highest priority may be picked (eg.
+        * primary_email before other_email).
         */
        decoded = _notmuch_message_file_get_combined_header (message, header);
     } else {
index d5afa30c8e2a3e4d74d50a02efa15659febafbaf..7f5203403723d3514e8d85f738d43b546adcabe7 100644 (file)
 #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;
 }
index 0c2eeab55b6e3eceb08bade87b08e6c9ac333daa..46638f800997d4c5250b3592e4be8ee79abc2a21 100644 (file)
@@ -68,7 +68,7 @@ struct maildir_flag_tag {
 };
 
 /* ASCII ordered table of Maildir flags and associated tags */
-static struct maildir_flag_tag flag2tag[] = {
+static const struct maildir_flag_tag flag2tag[] = {
     { 'D', "draft",   false },
     { 'F', "flagged", false },
     { 'P', "passed",  false },
@@ -169,6 +169,7 @@ _notmuch_message_create_for_document (const void *talloc_owner,
 
     message->doc = doc;
     message->termpos = 0;
+    message->modified = false;
 
     return message;
 }
@@ -288,7 +289,7 @@ _notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
 
        doc_id = _notmuch_database_generate_doc_id (notmuch);
     } catch (const Xapian::Error &error) {
-       _notmuch_database_log (notmuch_message_get_database (message),
+       _notmuch_database_log (notmuch,
                               "A Xapian exception occurred creating message: %s\n",
                               error.get_msg ().c_str ());
        notmuch->exception_reported = true;
@@ -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");
 }
 
 
@@ -742,7 +728,7 @@ _notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix)
  * properties, along with any automatic tags*/
 /* According to Xapian API docs, none of these calls throw
  * exceptions */
-notmuch_private_status_t
+static notmuch_private_status_t
 _notmuch_message_remove_indexed_terms (notmuch_message_t *message)
 {
     Xapian::TermIterator i;
@@ -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");
+    }
+
+    status = _notmuch_message_add_folder_terms (message, directory);
+    if (status)
+       return status;
 
-    _notmuch_message_add_folder_terms (message, directory);
-    _notmuch_message_add_path_terms (message, directory);
+    status = _notmuch_message_add_path_terms (message, directory);
+    if (status)
+       return status;
 
     talloc_free (local);
 
@@ -1356,11 +1378,10 @@ notmuch_status_t
 _notmuch_message_delete (notmuch_message_t *message)
 {
     notmuch_status_t status;
-    const char *mid, *tid, *query_string;
+    const char *mid, *tid;
     notmuch_message_t *ghost;
     notmuch_private_status_t private_status;
     notmuch_database_t *notmuch;
-    notmuch_query_t *query;
     unsigned int count = 0;
     bool is_ghost;
 
@@ -1372,26 +1393,44 @@ _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;
+    try {
+       Xapian::PostingIterator thread_doc, thread_doc_end;
+       Xapian::PostingIterator mail_doc, mail_doc_end;
 
-    query_string = talloc_asprintf (message, "thread:%s", tid);
-    query = notmuch_query_create (notmuch, query_string);
-    if (query == NULL)
-       return NOTMUCH_STATUS_OUT_OF_MEMORY;
-    status = notmuch_query_count_messages (query, &count);
-    if (status) {
-       notmuch_query_destroy (query);
-       return status;
+       /* look for a non-ghost message in the same thread */
+       /* 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);
+
+       while (count == 0 &&
+              thread_doc != thread_doc_end &&
+              mail_doc != mail_doc_end) {
+           thread_doc.skip_to (*mail_doc);
+           if (thread_doc != thread_doc_end) {
+               if (*thread_doc == *mail_doc) {
+                   count++;
+               } else {
+                   mail_doc.skip_to (*thread_doc);
+                   if (mail_doc != mail_doc_end && *thread_doc == *mail_doc)
+                       count++;
+               }
+           }
+       }
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
     }
-
     if (count > 0) {
        /* reintroduce a ghost in its place because there are still
         * other active messages in this thread: */
@@ -1410,27 +1449,21 @@ _notmuch_message_delete (notmuch_message_t *message)
        notmuch_message_destroy (ghost);
        status = COERCE_STATUS (private_status, "Error converting to ghost message");
     } else {
-       /* the thread is empty; drop all ghost messages from it */
-       notmuch_messages_t *messages;
-       status = _notmuch_query_search_documents (query,
-                                                 "ghost",
-                                                 &messages);
-       if (status == NOTMUCH_STATUS_SUCCESS) {
-           notmuch_status_t last_error = NOTMUCH_STATUS_SUCCESS;
-           while (notmuch_messages_valid (messages)) {
-               message = notmuch_messages_get (messages);
-               status = _notmuch_message_delete (message);
-               if (status) /* we'll report the last failure we see;
-                                        * if there is more than one failure, we
-                                        * forget about previous ones */
-                   last_error = status;
-               notmuch_message_destroy (message);
-               notmuch_messages_move_to_next (messages);
+       /* the thread now contains only ghosts: delete them */
+       try {
+           Xapian::PostingIterator doc, doc_end;
+
+           _notmuch_database_find_doc_ids (message->notmuch, "thread", tid, &doc, &doc_end);
+
+           for (; doc != doc_end; doc++) {
+               message->notmuch->writable_xapian_db->delete_document (*doc);
            }
-           status = last_error;
+       } catch (Xapian::Error &error) {
+           LOG_XAPIAN_EXCEPTION (message, error);
+           return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
        }
+
     }
-    notmuch_query_destroy (query);
     return status;
 }
 
@@ -1472,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
@@ -1541,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)
@@ -1560,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);
@@ -2010,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);
@@ -2273,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);
@@ -2291,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);
index 10b1b0245b16d12b8caca6f0bb109a6cde3ba89a..367e23e610d0220df108eed1cc078694b10bfea9 100644 (file)
@@ -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 *
@@ -469,11 +476,18 @@ _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
                                           const char **thread_id);
 /* index.cc */
 
+void
+_notmuch_filter_init ();
+
 notmuch_status_t
 _notmuch_message_index_file (notmuch_message_t *message,
                             notmuch_indexopts_t *indexopts,
                             notmuch_message_file_t *message_file);
 
+/* init.cc */
+void
+_notmuch_init ();
+
 /* messages.c */
 
 typedef struct _notmuch_message_node {
@@ -579,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
@@ -704,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."
 
@@ -717,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);
@@ -748,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
index 4b0539323da826d99aea2875cd0fd1583b60fb33..4e2b0fa48d3daf46dc75f51ded735a95424e9477 100644 (file)
@@ -58,7 +58,7 @@ NOTMUCH_BEGIN_DECLS
  * version in Makefile.local.
  */
 #define LIBNOTMUCH_MAJOR_VERSION        5
-#define LIBNOTMUCH_MINOR_VERSION        3
+#define LIBNOTMUCH_MINOR_VERSION        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.
      */
@@ -220,6 +220,18 @@ typedef enum _notmuch_status {
      * Database exists, so not (re)-created
      */
     NOTMUCH_STATUS_DATABASE_EXISTS,
+    /**
+     * 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.
@@ -281,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.
@@ -316,22 +330,22 @@ typedef enum {
 
 /**
  * Deprecated alias for notmuch_database_open_with_config with
- * config_path=error_message=NULL
+ * 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,
                       notmuch_database_t **database);
 /**
  * Deprecated alias for notmuch_database_open_with_config with
- * config_path=NULL
+ * config_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,
@@ -429,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
@@ -454,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.
@@ -485,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
@@ -529,11 +551,11 @@ notmuch_database_status_string (const notmuch_database_t *notmuch);
  * have no effect.
  *
  * For writable databases, notmuch_database_close commits all changes
- * to disk before closing the database.  If the caller is currently in
- * an atomic section (there was a notmuch_database_begin_atomic
- * without a matching notmuch_database_end_atomic), this will discard
- * changes made in that atomic section (but still commit changes made
- * prior to entering the atomic section).
+ * to disk before closing the database, unless the caller is currently
+ * in an atomic section (there was a notmuch_database_begin_atomic
+ * without a matching notmuch_database_end_atomic). In this case
+ * changes since the last commit are discarded. @see
+ * notmuch_database_end_atomic for more information.
  *
  * Return value:
  *
@@ -670,7 +692,10 @@ notmuch_status_t
 notmuch_database_begin_atomic (notmuch_database_t *notmuch);
 
 /**
- * Indicate the end of an atomic database operation.
+ * Indicate the end of an atomic database operation.  If repeated
+ * (with matching notmuch_database_begin_atomic) "database.autocommit"
+ * times, commit the the transaction and all previous (non-cancelled)
+ * transactions to the database.
  *
  * Return value:
  *
@@ -697,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,
@@ -958,6 +984,16 @@ notmuch_query_t *
 notmuch_query_create (notmuch_database_t *database,
                      const char *query_string);
 
+typedef enum {
+    NOTMUCH_QUERY_SYNTAX_XAPIAN,
+    NOTMUCH_QUERY_SYNTAX_SEXP
+} notmuch_query_syntax_t;
+
+notmuch_status_t
+notmuch_query_create_with_syntax (notmuch_database_t *database,
+                                 const char *query_string,
+                                 notmuch_query_syntax_t syntax,
+                                 notmuch_query_t **output);
 /**
  * Sort values for notmuch_query_set_sort.
  */
@@ -1137,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))
  *     {
@@ -1661,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,
 
@@ -2233,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.
  */
@@ -2507,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,
@@ -2520,6 +2562,9 @@ typedef enum _notmuch_config_key {
     NOTMUCH_CONFIG_PRIMARY_EMAIL,
     NOTMUCH_CONFIG_OTHER_EMAIL,
     NOTMUCH_CONFIG_USER_NAME,
+    NOTMUCH_CONFIG_AUTOCOMMIT,
+    NOTMUCH_CONFIG_EXTRA_HEADERS,
+    NOTMUCH_CONFIG_INDEX_AS_TEXT,
     NOTMUCH_CONFIG_LAST
 } notmuch_config_key_t;
 
index 5d80a884fb35aecbc6f084505ccb80ce85c8fec7..463e38bf0d8f733fe933da033ab993d828255fb7 100644 (file)
@@ -3,6 +3,8 @@
 
 #include "database-private.h"
 #include "parse-time-vrp.h"
+#include "lastmod-fp.h"
+#include "path-util.h"
 
 #if HAVE_XAPIAN_DB_RETRY_LOCK
 #define DB_ACTION (Xapian::DB_CREATE_OR_OPEN | Xapian::DB_RETRY_LOCK)
@@ -18,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);
@@ -186,30 +187,48 @@ _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) {
-       *database_path = _xdg_dir (ctx, "XDG_DATA_HOME", ".local/share", profile);
-       *split = true;
+       *database_path = _xdg_dir (notmuch, "XDG_DATA_HOME", ".local/share", profile);
+       status = _db_dir_exists (*database_path, message);
+       if (status) {
+           *database_path = NULL;
+       } else {
+           notmuch->params |= NOTMUCH_PARAM_SPLIT;
+       }
+    }
+
+    if (! *database_path) {
+       *database_path = getenv ("MAILDIR");
+    }
+
+    if (! *database_path) {
+       *database_path = talloc_asprintf (notmuch, "%s/mail", getenv ("HOME"));
+       status = _db_dir_exists (*database_path, message);
+       if (status) {
+           *database_path = NULL;
+       }
     }
 
     if (*database_path == NULL) {
@@ -221,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;
 }
 
-notmuch_database_t *
-_alloc_notmuch ()
+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 (const char *database_path, const char *config_path, const char *profile)
 {
     notmuch_database_t *notmuch;
 
@@ -238,7 +322,20 @@ _alloc_notmuch ()
     notmuch->writable_xapian_db = NULL;
     notmuch->config_path = NULL;
     notmuch->atomic_nesting = 0;
+    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;
 }
 
@@ -307,21 +404,81 @@ _set_database_path (notmuch_database_t *notmuch,
 }
 
 static void
-_init_libs ()
+_load_database_state (notmuch_database_t *notmuch)
 {
+    std::string last_thread_id;
+    std::string last_mod;
 
-    static int initialized = 0;
+    notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid ();
+    last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
+    if (last_thread_id.empty ()) {
+       notmuch->last_thread_id = 0;
+    } else {
+       const char *str;
+       char *end;
 
-    /* Initialize the GLib type system and threads */
-#if ! GLIB_CHECK_VERSION (2, 35, 1)
-    g_type_init ();
-#endif
+       str = last_thread_id.c_str ();
+       notmuch->last_thread_id = strtoull (str, &end, 16);
+       if (*end != '\0')
+           INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
+    }
+
+    /* Get current highest revision number. */
+    last_mod = notmuch->xapian_db->get_value_upper_bound (
+       NOTMUCH_VALUE_LAST_MOD);
+    if (last_mod.empty ())
+       notmuch->revision = 0;
+    else
+       notmuch->revision = Xapian::sortable_unserialise (last_mod);
+    notmuch->uuid = talloc_strdup (
+       notmuch, notmuch->xapian_db->get_uuid ().c_str ());
+}
 
-    /* Initialize gmime */
-    if (! initialized) {
-       g_mime_init ();
-       initialized = 1;
+/* 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 = &regexv[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
@@ -334,12 +491,12 @@ _finish_open (notmuch_database_t *notmuch,
     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
     char *incompat_features;
     char *message = NULL;
+    const char *autocommit_str;
+    char *autocommit_end;
     unsigned int version;
     const char *database_path = notmuch_database_get_path (notmuch);
 
     try {
-       std::string last_thread_id;
-       std::string last_mod;
 
        if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
            notmuch->writable_xapian_db = new Xapian::WritableDatabase (notmuch->xapian_path,
@@ -359,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;
        }
@@ -377,35 +532,11 @@ _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;
        }
 
-       notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid ();
-       last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
-       if (last_thread_id.empty ()) {
-           notmuch->last_thread_id = 0;
-       } else {
-           const char *str;
-           char *end;
-
-           str = last_thread_id.c_str ();
-           notmuch->last_thread_id = strtoull (str, &end, 16);
-           if (*end != '\0')
-               INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
-       }
-
-       /* Get current highest revision number. */
-       last_mod = notmuch->xapian_db->get_value_upper_bound (
-           NOTMUCH_VALUE_LAST_MOD);
-       if (last_mod.empty ())
-           notmuch->revision = 0;
-       else
-           notmuch->revision = Xapian::sortable_unserialise (last_mod);
-       notmuch->uuid = talloc_strdup (
-           notmuch, notmuch->xapian_db->get_uuid ().c_str ());
+       _load_database_state (notmuch);
 
        notmuch->query_parser = new Xapian::QueryParser;
        notmuch->term_gen = new Xapian::TermGenerator;
@@ -413,11 +544,11 @@ _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->query_parser->set_stemmer (Xapian::Stem ("english"));
+       notmuch->stemmer = new Xapian::Stem ("english");
+       notmuch->query_parser->set_stemmer (*notmuch->stemmer);
        notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
        notmuch->query_parser->add_rangeprocessor (notmuch->value_range_processor);
        notmuch->query_parser->add_rangeprocessor (notmuch->date_range_processor);
@@ -429,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;
 
@@ -454,6 +585,18 @@ _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");
+       }
+       notmuch->transaction_threshold = strtoul (autocommit_str, &autocommit_end, 10);
+       if (*autocommit_end != '\0')
+           INTERNAL_ERROR ("Malformed database database.autocommit value: %s", autocommit_str);
+
        status = _notmuch_database_setup_standard_query_fields (notmuch);
        if (status)
            goto DONE;
@@ -465,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:
@@ -484,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;
 
-    _init_libs ();
+    _notmuch_init ();
 
-    notmuch = _alloc_notmuch ();
+    notmuch = _alloc_notmuch (database_path, config_path, profile);
     if (! notmuch) {
        status = NOTMUCH_STATUS_OUT_OF_MEMORY;
        goto DONE;
@@ -504,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;
 
@@ -523,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);
 
@@ -535,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;
@@ -583,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;
 
-    _init_libs ();
+    _notmuch_init ();
 
-    notmuch = _alloc_notmuch ();
+    notmuch = _alloc_notmuch (database_path, config_path, profile);
     if (! notmuch) {
        status = NOTMUCH_STATUS_OUT_OF_MEMORY;
        goto DONE;
@@ -601,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) {
-       char *mail_root = canonicalize_file_name (
-           g_key_file_get_value (key_file, "database", "mail_root", NULL));
-       char *db_path = canonicalize_file_name (database_path);
+    if (key_file && ! (notmuch->params & NOTMUCH_PARAM_SPLIT)) {
+       char *mail_root = notmuch_canonicalize_file_name (
+           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"))) {
@@ -682,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);
 
@@ -693,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;
 }
 
@@ -732,6 +870,8 @@ notmuch_database_reopen (notmuch_database_t *notmuch,
                                                           DB_ACTION);
            }
        }
+
+       _load_database_state (notmuch);
     } catch (const Xapian::Error &error) {
        if (! notmuch->exception_reported) {
            _notmuch_database_log (notmuch, "Error: A Xapian exception reopening database: %s\n",
@@ -746,7 +886,7 @@ notmuch_database_reopen (notmuch_database_t *notmuch,
     return NOTMUCH_STATUS_SUCCESS;
 }
 
-notmuch_status_t
+static notmuch_status_t
 _maybe_load_config_from_database (notmuch_database_t *notmuch,
                                  GKeyFile *key_file,
                                  const char *database_path,
@@ -775,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;
 
-    _init_libs ();
+    _notmuch_init ();
 
-    notmuch = _alloc_notmuch ();
+    notmuch = _alloc_notmuch (database_path, config_path, profile);
     if (! notmuch) {
        status = NOTMUCH_STATUS_OUT_OF_MEMORY;
        goto DONE;
@@ -801,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:
@@ -828,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;
     }
@@ -837,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
new file mode 100644 (file)
index 0000000..9cadbc1
--- /dev/null
@@ -0,0 +1,731 @@
+#include "database-private.h"
+
+#if HAVE_SFSEXP
+#include "sexp.h"
+#include "unicode-util.h"
+
+/* _sexp is used for file scope symbols to avoid clashing with
+ * definitions from sexp.h */
+
+/* sexp_binding structs attach name to a sexp and a defining
+ * context. The latter allows lazy evaluation of parameters whose
+ * definition contains other parameters.  Lazy evaluation is needed
+ * because a primary goal of macros is to change the parent field for
+ * a sexp.
+ */
+
+typedef struct sexp_binding {
+    const char *name;
+    const sexp_t *sx;
+    const struct sexp_binding *context;
+    const struct sexp_binding *next;
+} _sexp_binding_t;
+
+typedef enum {
+    SEXP_FLAG_NONE     = 0,
+    SEXP_FLAG_FIELD    = 1 << 0,
+    SEXP_FLAG_BOOLEAN  = 1 << 1,
+    SEXP_FLAG_SINGLE   = 1 << 2,
+    SEXP_FLAG_WILDCARD = 1 << 3,
+    SEXP_FLAG_REGEX    = 1 << 4,
+    SEXP_FLAG_DO_REGEX = 1 << 5,
+    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
+operator| (_sexp_flag_t a, _sexp_flag_t b)
+{
+    return static_cast<_sexp_flag_t>(
+       static_cast<unsigned>(a) | static_cast<unsigned>(b));
+}
+
+inline _sexp_flag_t
+operator& (_sexp_flag_t a, _sexp_flag_t b)
+{
+    return static_cast<_sexp_flag_t>(
+       static_cast<unsigned>(a) & static_cast<unsigned>(b));
+}
+
+typedef struct  {
+    const char *name;
+    Xapian::Query::op xapian_op;
+    Xapian::Query initial;
+    _sexp_flag_t flags;
+} _sexp_prefix_t;
+
+static _sexp_prefix_t prefixes[] =
+{
+    { "and",            Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_NONE },
+    { "attachment",     Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_EXPAND },
+    { "body",           Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_FIELD },
+    { "date",           Xapian::Query::OP_INVALID,      Xapian::Query::MatchAll,
+      SEXP_FLAG_RANGE },
+    { "from",           Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      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 |
+      SEXP_FLAG_PATHNAME },
+    { "id",             Xapian::Query::OP_OR,           Xapian::Query::MatchNothing,
+      SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX },
+    { "infix",          Xapian::Query::OP_INVALID,      Xapian::Query::MatchAll,
+      SEXP_FLAG_SINGLE | SEXP_FLAG_ORPHAN },
+    { "is",             Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND },
+    { "lastmod",           Xapian::Query::OP_INVALID,      Xapian::Query::MatchAll,
+      SEXP_FLAG_RANGE },
+    { "matching",       Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_DO_EXPAND },
+    { "mid",            Xapian::Query::OP_OR,           Xapian::Query::MatchNothing,
+      SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX },
+    { "mimetype",       Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_EXPAND },
+    { "not",            Xapian::Query::OP_AND_NOT,      Xapian::Query::MatchAll,
+      SEXP_FLAG_NONE },
+    { "of",             Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_DO_EXPAND },
+    { "or",             Xapian::Query::OP_OR,           Xapian::Query::MatchNothing,
+      SEXP_FLAG_NONE },
+    { "path",           Xapian::Query::OP_OR,           Xapian::Query::MatchNothing,
+      SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX |
+      SEXP_FLAG_PATHNAME },
+    { "property",       Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND },
+    { "query",          Xapian::Query::OP_INVALID,      Xapian::Query::MatchNothing,
+      SEXP_FLAG_SINGLE | SEXP_FLAG_ORPHAN },
+    { "regex",          Xapian::Query::OP_INVALID,      Xapian::Query::MatchAll,
+      SEXP_FLAG_SINGLE | SEXP_FLAG_DO_REGEX },
+    { "rx",             Xapian::Query::OP_INVALID,      Xapian::Query::MatchAll,
+      SEXP_FLAG_SINGLE | SEXP_FLAG_DO_REGEX },
+    { "starts-with",    Xapian::Query::OP_WILDCARD,     Xapian::Query::MatchAll,
+      SEXP_FLAG_SINGLE },
+    { "subject",        Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND },
+    { "tag",            Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND },
+    { "thread",         Xapian::Query::OP_OR,           Xapian::Query::MatchNothing,
+      SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND },
+    { "to",             Xapian::Query::OP_AND,          Xapian::Query::MatchAll,
+      SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_EXPAND },
+    { }
+};
+
+static notmuch_status_t _sexp_to_xapian_query (notmuch_database_t *notmuch,
+                                              const _sexp_prefix_t *parent,
+                                              const _sexp_binding_t *env,
+                                              const sexp_t *sx,
+                                              Xapian::Query &output);
+
+static notmuch_status_t
+_sexp_combine_query (notmuch_database_t *notmuch,
+                    const _sexp_prefix_t *parent,
+                    const _sexp_binding_t *env,
+                    Xapian::Query::op operation,
+                    Xapian::Query left,
+                    const sexp_t *sx,
+                    Xapian::Query &output)
+{
+    Xapian::Query subquery;
+
+    notmuch_status_t status;
+
+    /* if we run out elements, return accumulator */
+
+    if (! sx) {
+       output = left;
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    status = _sexp_to_xapian_query (notmuch, parent, env, sx, subquery);
+    if (status)
+       return status;
+
+    return _sexp_combine_query (notmuch,
+                               parent,
+                               env,
+                               operation,
+                               Xapian::Query (operation, left, subquery),
+                               sx->next, output);
+}
+
+static notmuch_status_t
+_sexp_parse_phrase (std::string term_prefix, const char *phrase, Xapian::Query &output)
+{
+    Xapian::Utf8Iterator p (phrase);
+    Xapian::Utf8Iterator end;
+    std::vector<std::string> terms;
+
+    while (p != end) {
+       Xapian::Utf8Iterator start;
+       while (p != end && ! Xapian::Unicode::is_wordchar (*p))
+           p++;
+
+       if (p == end)
+           break;
+
+       start = p;
+
+       while (p != end && Xapian::Unicode::is_wordchar (*p))
+           p++;
+
+       if (p != start) {
+           std::string word (start, p);
+           word = Xapian::Unicode::tolower (word);
+           terms.push_back (term_prefix + word);
+       }
+    }
+    output = Xapian::Query (Xapian::Query::OP_PHRASE, terms.begin (), terms.end ());
+    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,
+                     unused(const _sexp_binding_t *env),
+                     std::string match,
+                     Xapian::Query &output)
+{
+
+    std::string term_prefix = parent ? _notmuch_database_prefix (notmuch, parent->name) : "";
+
+    if (parent && ! (parent->flags & SEXP_FLAG_WILDCARD)) {
+       _notmuch_database_log (notmuch, "'%s' does not support wildcard queries\n", parent->name);
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    output = Xapian::Query (Xapian::Query::OP_WILDCARD,
+                           term_prefix + Xapian::Unicode::tolower (match));
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_sexp_parse_one_term (notmuch_database_t *notmuch, std::string term_prefix, const sexp_t *sx,
+                     Xapian::Query &output)
+{
+    Xapian::Stem stem = *(notmuch->stemmer);
+
+    if (sx->aty == SEXP_BASIC && unicode_word_utf8 (sx->val)) {
+       std::string term = Xapian::Unicode::tolower (sx->val);
+
+       output = Xapian::Query ("Z" + term_prefix + stem (term));
+       return NOTMUCH_STATUS_SUCCESS;
+    } else {
+       return _sexp_parse_phrase (term_prefix, sx->val, output);
+    }
+
+}
+
+notmuch_status_t
+_sexp_parse_regex (notmuch_database_t *notmuch,
+                  const _sexp_prefix_t *prefix, const _sexp_prefix_t *parent,
+                  const _sexp_binding_t *env,
+                  const sexp_t *term, Xapian::Query &output)
+{
+    if (! parent) {
+       _notmuch_database_log (notmuch, "illegal '%s' outside field\n",
+                              prefix->name);
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    if (! (parent->flags & SEXP_FLAG_REGEX)) {
+       _notmuch_database_log (notmuch, "'%s' not supported in field '%s'\n",
+                              prefix->name, parent->name);
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    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,
+                                    str, output, msg);
+}
+
+
+static notmuch_status_t
+_sexp_expand_query (notmuch_database_t *notmuch,
+                   const _sexp_prefix_t *prefix, const _sexp_prefix_t *parent,
+                   unused(const _sexp_binding_t *env), const sexp_t *sx, Xapian::Query &output)
+{
+    Xapian::Query subquery;
+    notmuch_status_t status;
+    std::string msg;
+
+    if (! (parent->flags & SEXP_FLAG_EXPAND)) {
+       _notmuch_database_log (notmuch, "'%s' unsupported inside '%s'\n", prefix->name, parent->name);
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    status = _sexp_combine_query (notmuch, NULL, NULL, prefix->xapian_op, prefix->initial, sx,
+                                 subquery);
+    if (status)
+       return status;
+
+    status = _notmuch_query_expand (notmuch, parent->name, subquery, output, msg);
+    if (status) {
+       _notmuch_database_log (notmuch, "error expanding query %s\n", msg.c_str ());
+    }
+    return status;
+}
+
+static notmuch_status_t
+_sexp_parse_infix (notmuch_database_t *notmuch, const sexp_t *sx, Xapian::Query &output)
+{
+    try {
+       output = notmuch->query_parser->parse_query (sx->val, NOTMUCH_QUERY_PARSER_FLAGS);
+    } catch (const Xapian::QueryParserError &error) {
+       _notmuch_database_log (notmuch, "Syntax error in infix query: %s\n", sx->val);
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    } catch (const Xapian::Error &error) {
+       if (! notmuch->exception_reported) {
+           _notmuch_database_log (notmuch,
+                                  "A Xapian exception occurred parsing query: %s\n",
+                                  error.get_msg ().c_str ());
+           _notmuch_database_log_append (notmuch,
+                                         "Query string was: %s\n",
+                                         sx->val);
+           notmuch->exception_reported = true;
+           return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+       }
+    }
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_sexp_parse_header (notmuch_database_t *notmuch, const _sexp_prefix_t *parent,
+                   const _sexp_binding_t *env, const sexp_t *sx, Xapian::Query &output)
+{
+    _sexp_prefix_t user_prefix;
+
+    user_prefix.name = sx->list->val;
+    user_prefix.flags = SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD;
+
+    if (parent) {
+       _notmuch_database_log (notmuch, "nested field: '%s' inside '%s'\n",
+                              sx->list->val, parent->name);
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    parent = &user_prefix;
+
+    return _sexp_combine_query (notmuch, parent, env, Xapian::Query::OP_AND, Xapian::Query::MatchAll,
+                               sx->list->next, output);
+}
+
+static _sexp_binding_t *
+_sexp_bind (void *ctx, const _sexp_binding_t *env, const char *name, const sexp_t *sx, const
+           _sexp_binding_t *context)
+{
+    _sexp_binding_t *binding = talloc (ctx, _sexp_binding_t);
+
+    binding->name = talloc_strdup (ctx, name);
+    binding->sx = sx;
+    binding->context = context;
+    binding->next = env;
+    return binding;
+}
+
+static notmuch_status_t
+maybe_apply_macro (notmuch_database_t *notmuch, const _sexp_prefix_t *parent,
+                  const _sexp_binding_t *env, const sexp_t *sx, const sexp_t *args,
+                  Xapian::Query &output)
+{
+    const sexp_t *params, *param, *arg, *body;
+    void *local = talloc_new (notmuch);
+    _sexp_binding_t *new_env = NULL;
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+
+    if (sx->list->ty != SEXP_VALUE || strcmp (sx->list->val, "macro") != 0) {
+       status = NOTMUCH_STATUS_IGNORED;
+       goto DONE;
+    }
+
+    params = sx->list->next;
+
+    if (! params || (params->ty != SEXP_LIST)) {
+       _notmuch_database_log (notmuch, "missing (possibly empty) list of arguments to macro\n");
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    body = params->next;
+
+    if (! body) {
+       _notmuch_database_log (notmuch, "missing body of macro\n");
+       status = NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+       goto DONE;
+    }
+
+    for (param = params->list, arg = args;
+        param && arg;
+        param = param->next, arg = arg->next) {
+       if (param->ty != SEXP_VALUE || param->aty != SEXP_BASIC) {
+           _notmuch_database_log (notmuch, "macro parameters must be unquoted atoms\n");
+           status = NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+           goto DONE;
+       }
+       new_env = _sexp_bind (local, new_env, param->val, arg, env);
+    }
+
+    if (param && ! arg) {
+       _notmuch_database_log (notmuch, "too few arguments to macro\n");
+       status = NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+       goto DONE;
+    }
+
+    if (! param && arg) {
+       _notmuch_database_log (notmuch, "too many arguments to macro\n");
+       status = NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+       goto DONE;
+    }
+
+    status = _sexp_to_xapian_query (notmuch, parent, new_env, body, output);
+
+  DONE:
+    if (local)
+       talloc_free (local);
+
+    return status;
+}
+
+static notmuch_status_t
+maybe_saved_squery (notmuch_database_t *notmuch, const _sexp_prefix_t *parent,
+                   const _sexp_binding_t *env, const sexp_t *sx, Xapian::Query &output)
+{
+    char *key;
+    char *expansion = NULL;
+    notmuch_status_t status;
+    sexp_t *saved_sexp;
+    void *local = talloc_new (notmuch);
+    char *buf;
+
+    key = talloc_asprintf (local, "squery.%s", sx->list->val);
+    if (! key) {
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    status = notmuch_database_get_config (notmuch, key, &expansion);
+    if (status)
+       goto DONE;
+    if (EMPTY_STRING (expansion)) {
+       status = NOTMUCH_STATUS_IGNORED;
+       goto DONE;
+    }
+
+    buf = talloc_strdup (local, expansion);
+    /* XXX TODO: free this memory */
+    saved_sexp = parse_sexp (buf, strlen (expansion));
+    if (! saved_sexp) {
+       _notmuch_database_log (notmuch, "invalid saved s-expression query: '%s'\n", expansion);
+       status = NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+       goto DONE;
+    }
+
+    status = maybe_apply_macro (notmuch, parent, env, saved_sexp, sx->list->next, output);
+    if (status == NOTMUCH_STATUS_IGNORED)
+       status =  _sexp_to_xapian_query (notmuch, parent, env, saved_sexp, output);
+
+  DONE:
+    if (local)
+       talloc_free (local);
+
+    return status;
+}
+
+static notmuch_status_t
+_sexp_expand_param (notmuch_database_t *notmuch, const _sexp_prefix_t *parent,
+                   const _sexp_binding_t *env, const char *name,
+                   Xapian::Query &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::MatchAll;
+       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;
+    }
+
+    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;
+}
+
+/* Here we expect the s-expression to be a proper list, with first
+ * element defining and operation, or as a special case the empty
+ * list */
+
+static notmuch_status_t
+_sexp_to_xapian_query (notmuch_database_t *notmuch, const _sexp_prefix_t *parent,
+                      const _sexp_binding_t *env, const sexp_t *sx, Xapian::Query &output)
+{
+    notmuch_status_t status;
+
+    if (sx->ty == SEXP_VALUE && sx->aty == SEXP_BASIC && sx->val[0] == ',') {
+       return _sexp_expand_param (notmuch, parent, env, sx->val + 1, output);
+    }
+
+    if (sx->ty == SEXP_VALUE) {
+       std::string term_prefix = parent ? _notmuch_database_prefix (notmuch, parent->name) : "";
+
+       if (sx->aty == SEXP_BASIC && strcmp (sx->val, "*") == 0) {
+           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 + atom);
+           return NOTMUCH_STATUS_SUCCESS;
+       }
+
+       if (parent) {
+           return _sexp_parse_one_term (notmuch, term_prefix, sx, output);
+       } else {
+           Xapian::Query accumulator;
+           for (_sexp_prefix_t *prefix = prefixes; prefix->name; prefix++) {
+               if (prefix->flags & SEXP_FLAG_FIELD) {
+                   Xapian::Query subquery;
+                   term_prefix = _notmuch_database_prefix (notmuch, prefix->name);
+                   status = _sexp_parse_one_term (notmuch, term_prefix, sx, subquery);
+                   if (status)
+                       return status;
+                   accumulator = Xapian::Query (Xapian::Query::OP_OR, accumulator, subquery);
+               }
+           }
+           output = accumulator;
+           return NOTMUCH_STATUS_SUCCESS;
+       }
+    }
+
+    /* Empty list */
+    if (! sx->list) {
+       output = Xapian::Query::MatchAll;
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    if (sx->list->ty == SEXP_LIST) {
+       _notmuch_database_log (notmuch, "unexpected list in field/operation position\n",
+                              sx->list->val);
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    status = maybe_saved_squery (notmuch, parent, env, sx, output);
+    if (status != NOTMUCH_STATUS_IGNORED)
+       return status;
+
+    /* Check for user defined field */
+    if (_notmuch_string_map_get (notmuch->user_prefix, sx->list->val)) {
+       return _sexp_parse_header (notmuch, parent, env, sx, output);
+    }
+
+    if (strcmp (sx->list->val, "macro") == 0) {
+       _notmuch_database_log (notmuch, "macro definition not permitted here\n");
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    for (_sexp_prefix_t *prefix = prefixes; prefix && prefix->name; prefix++) {
+       if (strcmp (prefix->name, sx->list->val) == 0) {
+           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);
+                   return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+               }
+               parent = prefix;
+           }
+
+           if (parent && (prefix->flags & SEXP_FLAG_ORPHAN)) {
+               _notmuch_database_log (notmuch, "'%s' not supported inside '%s'\n",
+                                      prefix->name, parent->name);
+               return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+           }
+
+           if ((prefix->flags & SEXP_FLAG_SINGLE) &&
+               (! sx->list->next || sx->list->next->next || sx->list->next->ty != SEXP_VALUE)) {
+               _notmuch_database_log (notmuch, "'%s' expects single atom as argument\n",
+                                      prefix->name);
+               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);
+           }
+
+           if (strcmp (prefix->name, "query") == 0) {
+               return _notmuch_query_name_to_query (notmuch, 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, 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,
+                                       sx->list->next, output);
+       }
+    }
+
+    _notmuch_database_log (notmuch, "unknown prefix '%s'\n", sx->list->val);
+    return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+}
+
+notmuch_status_t
+_notmuch_sexp_string_to_xapian_query (notmuch_database_t *notmuch, const char *querystr,
+                                     Xapian::Query &output)
+{
+    const sexp_t *sx = NULL;
+    char *buf = talloc_strdup (notmuch, querystr);
+
+    sx = parse_sexp (buf, strlen (querystr));
+    if (! sx) {
+       _notmuch_database_log (notmuch, "invalid s-expression: '%s'\n", querystr);
+       return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+    }
+
+    return _sexp_to_xapian_query (notmuch, NULL, NULL, sx, output);
+}
+#endif
index 22bf2ab5336980f8cbd9660e3ae4eb58bd7a9b97..6b07970b7960254b240bb68312f61200fa976336 100644 (file)
 #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? */
index 0d92bdd7ddaecfc75801afd2f6f41bf1fac8621a..06e2333a096894ab64ef0a0d8f3b35ad5b5cc7a5 100644 (file)
@@ -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 ();
index b980b7f06669402c2f592ed0159a8c034ca1a157..75b1d875f4a733a732bf86e98fbba26acd01b201 100644 (file)
 #include "query-fp.h"
 #include <iostream>
 
-Xapian::Query
-QueryFieldProcessor::operator() (const std::string & name)
+notmuch_status_t
+_notmuch_query_name_to_query (notmuch_database_t *notmuch, const std::string name,
+                             Xapian::Query &output)
 {
     std::string key = "query." + name;
     char *expansion;
     notmuch_status_t status;
 
     status = notmuch_database_get_config (notmuch, key.c_str (), &expansion);
+    if (status)
+       return status;
+
+    output = notmuch->query_parser->parse_query (expansion, NOTMUCH_QUERY_PARSER_FLAGS);
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+Xapian::Query
+QueryFieldProcessor::operator() (const std::string & name)
+{
+    notmuch_status_t status;
+    Xapian::Query output;
+
+    status = _notmuch_query_name_to_query (notmuch, name, output);
     if (status) {
        throw Xapian::QueryParserError ("error looking up key" + name);
     }
 
-    return parser.parse_query (expansion, NOTMUCH_QUERY_PARSER_FLAGS);
+    return output;
+
 }
index 792aba219fa48d53e3b761089eac1d4349b5c0fb..1c60c122c8e9eedd22b2e5481e8e189719551994 100644 (file)
@@ -20,6 +20,7 @@
 
 #include "notmuch-private.h"
 #include "database-private.h"
+#include "xapian-extra.h"
 
 #include <glib.h> /* GHashTable, GPtrArray */
 
@@ -30,6 +31,7 @@ struct _notmuch_query {
     notmuch_string_list_t *exclude_terms;
     notmuch_exclude_t omit_excluded;
     bool parsed;
+    notmuch_query_syntax_t syntax;
     Xapian::Query xapian_query;
     std::set<std::string> terms;
 };
@@ -84,9 +86,9 @@ _notmuch_query_destructor (notmuch_query_t *query)
     return 0;
 }
 
-notmuch_query_t *
-notmuch_query_create (notmuch_database_t *notmuch,
-                     const char *query_string)
+static notmuch_query_t *
+_notmuch_query_constructor (notmuch_database_t *notmuch,
+                           const char *query_string)
 {
     notmuch_query_t *query;
 
@@ -105,7 +107,10 @@ notmuch_query_create (notmuch_database_t *notmuch,
 
     query->notmuch = notmuch;
 
-    query->query_string = talloc_strdup (query, query_string);
+    if (query_string)
+       query->query_string = talloc_strdup (query, query_string);
+    else
+       query->query_string = NULL;
 
     query->sort = NOTMUCH_SORT_NEWEST_FIRST;
 
@@ -116,44 +121,146 @@ notmuch_query_create (notmuch_database_t *notmuch,
     return query;
 }
 
-static notmuch_status_t
-_notmuch_query_ensure_parsed (notmuch_query_t *query)
+notmuch_query_t *
+notmuch_query_create (notmuch_database_t *notmuch,
+                     const char *query_string)
 {
-    if (query->parsed)
-       return NOTMUCH_STATUS_SUCCESS;
 
-    try {
-       query->xapian_query =
-           query->notmuch->query_parser->
-           parse_query (query->query_string, NOTMUCH_QUERY_PARSER_FLAGS);
+    notmuch_query_t *query;
+    notmuch_status_t status;
 
-       /* Xapian doesn't support skip_to on terms from a query since
-        *  they are unordered, so cache a copy of all terms in
-        *  something searchable.
-        */
+    status = notmuch_query_create_with_syntax (notmuch, query_string,
+                                              NOTMUCH_QUERY_SYNTAX_XAPIAN,
+                                              &query);
+    if (status)
+       return NULL;
+
+    return query;
+}
+
+notmuch_status_t
+notmuch_query_create_with_syntax (notmuch_database_t *notmuch,
+                                 const char *query_string,
+                                 notmuch_query_syntax_t syntax,
+                                 notmuch_query_t **output)
+{
+
+    notmuch_query_t *query;
+
+    if (! output)
+       return NOTMUCH_STATUS_NULL_POINTER;
 
-       for (Xapian::TermIterator t = query->xapian_query.get_terms_begin ();
-            t != query->xapian_query.get_terms_end (); ++t)
-           query->terms.insert (*t);
+    query = _notmuch_query_constructor (notmuch, query_string);
+    if (! query)
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+    if (syntax == NOTMUCH_QUERY_SYNTAX_SEXP && ! HAVE_SFSEXP) {
+       _notmuch_database_log (notmuch, "sexp query parser not available");
+       return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
+    }
 
-       query->parsed = true;
+    query->syntax = syntax;
 
+    *output = query;
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static void
+_notmuch_query_cache_terms (notmuch_query_t *query)
+{
+    /* Xapian doesn't support skip_to on terms from a query since
+     *  they are unordered, so cache a copy of all terms in
+     *  something searchable.
+     */
+
+    for (Xapian::TermIterator t = query->xapian_query.get_terms_begin ();
+        t != query->xapian_query.get_terms_end (); ++t)
+       query->terms.insert (*t);
+}
+
+notmuch_status_t
+_notmuch_query_string_to_xapian_query (notmuch_database_t *notmuch,
+                                      std::string query_string,
+                                      Xapian::Query &output,
+                                      std::string &msg)
+{
+    try {
+       if (query_string == "" || query_string == "*") {
+           output = xapian_query_match_all ();
+       } else {
+           output =
+               notmuch->query_parser->
+               parse_query (query_string, NOTMUCH_QUERY_PARSER_FLAGS);
+       }
     } catch (const Xapian::Error &error) {
-       if (! query->notmuch->exception_reported) {
-           _notmuch_database_log (query->notmuch,
+       if (! notmuch->exception_reported) {
+           _notmuch_database_log (notmuch,
                                   "A Xapian exception occurred parsing query: %s\n",
                                   error.get_msg ().c_str ());
-           _notmuch_database_log_append (query->notmuch,
+           _notmuch_database_log_append (notmuch,
                                          "Query string was: %s\n",
-                                         query->query_string);
-           query->notmuch->exception_reported = true;
+                                         query_string.c_str ());
+           notmuch->exception_reported = true;
        }
 
+       msg = error.get_msg ();
        return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
     }
     return NOTMUCH_STATUS_SUCCESS;
 }
 
+static notmuch_status_t
+_notmuch_query_ensure_parsed_xapian (notmuch_query_t *query)
+{
+    notmuch_status_t status;
+    std::string msg; /* ignored */
+
+    status =  _notmuch_query_string_to_xapian_query (query->notmuch, query->query_string,
+                                                    query->xapian_query, msg);
+    if (status)
+       return status;
+
+    query->parsed = true;
+
+    _notmuch_query_cache_terms (query);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+#if HAVE_SFSEXP
+static notmuch_status_t
+_notmuch_query_ensure_parsed_sexpr (notmuch_query_t *query)
+{
+    notmuch_status_t status;
+
+    if (query->parsed)
+       return NOTMUCH_STATUS_SUCCESS;
+
+    status = _notmuch_sexp_string_to_xapian_query (query->notmuch, query->query_string,
+                                                  query->xapian_query);
+    if (status)
+       return status;
+
+    _notmuch_query_cache_terms (query);
+    return NOTMUCH_STATUS_SUCCESS;
+}
+#endif
+
+static notmuch_status_t
+_notmuch_query_ensure_parsed (notmuch_query_t *query)
+{
+    if (query->parsed)
+       return NOTMUCH_STATUS_SUCCESS;
+
+#if HAVE_SFSEXP
+    if (query->syntax == NOTMUCH_QUERY_SYNTAX_SEXP)
+       return _notmuch_query_ensure_parsed_sexpr (query);
+#endif
+
+    return _notmuch_query_ensure_parsed_xapian (query);
+}
+
 const char *
 notmuch_query_get_query_string (const notmuch_query_t *query)
 {
@@ -249,7 +356,6 @@ _notmuch_query_search_documents (notmuch_query_t *query,
                                 notmuch_messages_t **out)
 {
     notmuch_database_t *notmuch = query->notmuch;
-    const char *query_string = query->query_string;
     notmuch_mset_messages_t *messages;
     notmuch_status_t status;
 
@@ -279,13 +385,9 @@ _notmuch_query_search_documents (notmuch_query_t *query,
        Xapian::MSet mset;
        Xapian::MSetIterator iterator;
 
-       if (strcmp (query_string, "") == 0 ||
-           strcmp (query_string, "*") == 0) {
-           final_query = mail_query;
-       } else {
-           final_query = Xapian::Query (Xapian::Query::OP_AND,
-                                        mail_query, query->xapian_query);
-       }
+       final_query = Xapian::Query (Xapian::Query::OP_AND,
+                                    mail_query, query->xapian_query);
+
        messages->base.excluded_doc_ids = NULL;
 
        if ((query->omit_excluded != NOTMUCH_EXCLUDE_FALSE) && (query->exclude_terms)) {
@@ -606,7 +708,6 @@ notmuch_status_t
 _notmuch_query_count_documents (notmuch_query_t *query, const char *type, unsigned *count_out)
 {
     notmuch_database_t *notmuch = query->notmuch;
-    const char *query_string = query->query_string;
     Xapian::doccount count = 0;
     notmuch_status_t status;
 
@@ -622,13 +723,8 @@ _notmuch_query_count_documents (notmuch_query_t *query, const char *type, unsign
        Xapian::Query final_query, exclude_query;
        Xapian::MSet mset;
 
-       if (strcmp (query_string, "") == 0 ||
-           strcmp (query_string, "*") == 0) {
-           final_query = mail_query;
-       } else {
-           final_query = Xapian::Query (Xapian::Query::OP_AND,
-                                        mail_query, query->xapian_query);
-       }
+       final_query = Xapian::Query (Xapian::Query::OP_AND,
+                                    mail_query, query->xapian_query);
 
        exclude_query = _notmuch_exclude_tags (query);
 
@@ -728,3 +824,51 @@ notmuch_query_get_database (const notmuch_query_t *query)
 {
     return query->notmuch;
 }
+
+notmuch_status_t
+_notmuch_query_expand (notmuch_database_t *notmuch, const char *field, Xapian::Query subquery,
+                      Xapian::Query &output, std::string &msg)
+{
+    std::set<std::string> terms;
+    const std::string term_prefix =  _find_prefix (field);
+
+    if (_debug_query ()) {
+       fprintf (stderr, "Expanding subquery:\n%s\n",
+                subquery.get_description ().c_str ());
+    }
+
+    try {
+       Xapian::Enquire enquire (*notmuch->xapian_db);
+       Xapian::MSet mset;
+
+       enquire.set_weighting_scheme (Xapian::BoolWeight ());
+       enquire.set_query (subquery);
+
+       mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
+
+       for (Xapian::MSetIterator iterator = mset.begin (); iterator != mset.end (); iterator++) {
+           Xapian::docid doc_id = *iterator;
+           Xapian::Document doc = notmuch->xapian_db->get_document (doc_id);
+           Xapian::TermIterator i = doc.termlist_begin ();
+
+           for (i.skip_to (term_prefix);
+                i != doc.termlist_end () && ((*i).rfind (term_prefix, 0) == 0); i++) {
+               terms.insert (*i);
+           }
+       }
+       output = Xapian::Query (Xapian::Query::OP_OR, terms.begin (), terms.end ());
+       if (_debug_query ()) {
+           fprintf (stderr, "Expanded query:\n%s\n",
+                    subquery.get_description ().c_str ());
+       }
+
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch,
+                              "A Xapian exception occurred expanding query: %s\n",
+                              error.get_msg ().c_str ());
+       msg = error.get_msg ();
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
index 0feb50e586ba84f1c4ef84cb62e78ea679b472c3..3a775261fc3a1c2f31b82f345766e231fbf45f81 100644 (file)
 #include "regexp-fields.h"
 #include "notmuch-private.h"
 #include "database-private.h"
+#include "xapian-extra.h"
 
-static void
-compile_regex (regex_t &regexp, const char *str)
+notmuch_status_t
+compile_regex (regex_t &regexp, const char *str, std::string &msg)
 {
     int err = regcomp (&regexp, str, REG_EXTENDED | REG_NOSUB);
 
     if (err != 0) {
        size_t len = regerror (err, &regexp, NULL, 0);
        char *buffer = new char[len];
-       std::string msg = "Regexp error: ";
+       msg = "Regexp error: ";
        (void) regerror (err, &regexp, buffer, len);
        msg.append (buffer, len);
        delete[] buffer;
 
-       throw Xapian::QueryParserError (msg);
+       return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
     }
+    return NOTMUCH_STATUS_SUCCESS;
 }
 
 RegexpPostingSource::RegexpPostingSource (Xapian::valueno slot, const std::string &regexp)
     : slot_ (slot)
 {
-    compile_regex (regexp_, regexp.c_str ());
+    std::string msg;
+    notmuch_status_t status = compile_regex (regexp_, regexp.c_str (), msg);
+
+    if (status)
+       throw Xapian::QueryParserError (msg);
 }
 
 RegexpPostingSource::~RegexpPostingSource ()
@@ -141,25 +147,61 @@ _find_slot (std::string prefix)
        return Xapian::BAD_VALUENO;
 }
 
-RegexpFieldProcessor::RegexpFieldProcessor (std::string prefix,
+RegexpFieldProcessor::RegexpFieldProcessor (std::string field_,
                                            notmuch_field_flag_t options_,
                                            Xapian::QueryParser &parser_,
                                            notmuch_database_t *notmuch_)
-    : slot (_find_slot (prefix)),
-    term_prefix (_find_prefix (prefix.c_str ())),
+    : slot (_find_slot (field_)),
+    field (field_),
+    term_prefix (_find_prefix (field_.c_str ())),
     options (options_),
     parser (parser_),
     notmuch (notmuch_)
 {
 };
 
+notmuch_status_t
+_notmuch_regexp_to_query (notmuch_database_t *notmuch, Xapian::valueno slot, std::string field,
+                         std::string regexp_str,
+                         Xapian::Query &output, std::string &msg)
+{
+    regex_t regexp;
+    notmuch_status_t status;
+
+    status = compile_regex (regexp, regexp_str.c_str (), msg);
+    if (status) {
+       _notmuch_database_log_append (notmuch, "error compiling regex %s", msg.c_str ());
+       return status;
+    }
+
+    if (slot == Xapian::BAD_VALUENO)
+       slot = _find_slot (field);
+
+    if (slot == Xapian::BAD_VALUENO) {
+       std::string term_prefix = _find_prefix (field.c_str ());
+       std::vector<std::string> terms;
+
+       for (Xapian::TermIterator it = notmuch->xapian_db->allterms_begin (term_prefix);
+            it != notmuch->xapian_db->allterms_end (); ++it) {
+           if (regexec (&regexp, (*it).c_str () + term_prefix.size (),
+                        0, NULL, 0) == 0)
+               terms.push_back (*it);
+       }
+       output = Xapian::Query (Xapian::Query::OP_OR, terms.begin (), terms.end ());
+    } else {
+       RegexpPostingSource *postings = new RegexpPostingSource (slot, regexp_str);
+       output = Xapian::Query (postings->release ());
+    }
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
 Xapian::Query
 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);
@@ -168,23 +210,15 @@ RegexpFieldProcessor::operator() (const std::string & str)
 
     if (str.at (0) == '/') {
        if (str.length () > 1 && str.at (str.size () - 1) == '/') {
+           Xapian::Query query;
            std::string regexp_str = str.substr (1, str.size () - 2);
-           if (slot != Xapian::BAD_VALUENO) {
-               RegexpPostingSource *postings = new RegexpPostingSource (slot, regexp_str);
-               return Xapian::Query (postings->release ());
-           } else {
-               std::vector<std::string> terms;
-               regex_t regexp;
-
-               compile_regex (regexp, regexp_str.c_str ());
-               for (Xapian::TermIterator it = notmuch->xapian_db->allterms_begin (term_prefix);
-                    it != notmuch->xapian_db->allterms_end (); ++it) {
-                   if (regexec (&regexp, (*it).c_str () + term_prefix.size (),
-                                0, NULL, 0) == 0)
-                       terms.push_back (*it);
-               }
-               return Xapian::Query (Xapian::Query::OP_OR, terms.begin (), terms.end ());
-           }
+           std::string msg;
+           notmuch_status_t status;
+
+           status = _notmuch_regexp_to_query (notmuch, slot, field, regexp_str, query, msg);
+           if (status)
+               throw Xapian::QueryParserError (msg);
+           return query;
        } else {
            throw Xapian::QueryParserError ("unmatched regex delimiter in '" + str + "'");
        }
@@ -194,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;
@@ -202,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);
        }
     }
index a8cca2431d737d9284c66ba311f9f9f26f4d0268..9c871de7dce50c75b5215a1ec42934e6ef9c1864 100644 (file)
 #include "database-private.h"
 #include "notmuch-private.h"
 
+notmuch_status_t
+_notmuch_regex_to_query (notmuch_database_t *notmuch, Xapian::valueno slot, std::string field,
+                        std::string regexp_str,
+                        Xapian::Query &output, std::string &msg);
+
 /* A posting source that returns documents where a value matches a
  * regexp.
  */
@@ -64,6 +69,7 @@ public:
 class RegexpFieldProcessor : public Xapian::FieldProcessor {
 protected:
     Xapian::valueno slot;
+    std::string field;
     std::string term_prefix;
     notmuch_field_flag_t options;
     Xapian::QueryParser &parser;
diff --git a/lib/sexp-fp.cc b/lib/sexp-fp.cc
new file mode 100644 (file)
index 0000000..1fdf522
--- /dev/null
@@ -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 <david@tethera.net>
+ */
+
+#include "database-private.h"
+#include "sexp-fp.h"
+#include <iostream>
+
+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 (file)
index 0000000..341dfa7
--- /dev/null
@@ -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 <david@tethera.net>
+ */
+
+#ifndef NOTMUCH_SEXP_FP_H
+#define NOTMUCH_SEXP_FP_H
+
+#include <xapian.h>
+#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 */
index e3a81b4fe1457a030154fb3cb05cfc59cd3bb059..99bc2ea2450fe7a188f527106d5f45fb711f8f10 100644 (file)
@@ -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
index c7d3f66f491b5ec02495817ba85b6c6981c5fc6d..ec5366ffa23681bf4ddd37a576b2ee202199b737 100644 (file)
@@ -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 *
index 06708ef291852f8ab5e1888c7e5755c18921f288..3aa9c4238c6f80a6d2e9d359006e0b1b2ea9bc44 100644 (file)
@@ -34,28 +34,20 @@ ThreadFieldProcessor::operator() (const std::string & str)
        if (str.size () <= 1 || str.at (str.size () - 1) != '}') {
            throw Xapian::QueryParserError ("missing } in '" + str + "'");
        } else {
+           Xapian::Query subquery;
+           Xapian::Query query;
+           std::string msg;
            std::string subquery_str = str.substr (1, str.size () - 2);
-           notmuch_query_t *subquery = notmuch_query_create (notmuch, subquery_str.c_str ());
-           notmuch_messages_t *messages;
-           std::set<std::string> terms;
 
-           if (! subquery)
-               throw Xapian::QueryParserError ("failed to create subquery for '" + subquery_str +
-                                               "'");
+           status = _notmuch_query_string_to_xapian_query (notmuch, subquery_str, subquery, msg);
+           if (status)
+               throw Xapian::QueryParserError (msg);
 
-           status = notmuch_query_search_messages (subquery, &messages);
+           status = _notmuch_query_expand (notmuch, "thread", subquery, query, msg);
            if (status)
-               throw Xapian::QueryParserError ("failed to search messages for '" + subquery_str +
-                                               "'");
+               throw Xapian::QueryParserError (msg);
 
-           for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) {
-               std::string term = thread_prefix;
-               notmuch_message_t *message;
-               message = notmuch_messages_get (messages);
-               term += _notmuch_message_get_thread_id_only (message);
-               terms.insert (term);
-           }
-           return Xapian::Query (Xapian::Query::OP_OR, terms.begin (), terms.end ());
+           return query;
        }
     } else {
        /* literal thread id */
index 46a50e80cb5992729875d7d56698743ddcf8c3c3..60e9a666ec1b348c7b2f413c6da94d4655188831 100644 (file)
@@ -25,7 +25,8 @@
 #include <glib.h>                                       /* GHashTable */
 
 #ifdef DEBUG_THREADING
-#define THREAD_DEBUG(format, ...) fprintf (stderr, format " (%s).\n", ##__VA_ARGS__, __location__)
+#define THREAD_DEBUG(format, ...) fprintf (stderr, "DT: " format " (%s).\n", ##__VA_ARGS__, \
+                                          __location__)
 #else
 #define THREAD_DEBUG(format, ...) do {} while (0)       /* ignored */
 #endif
@@ -400,7 +401,7 @@ _parent_via_in_reply_to (notmuch_thread_t *thread, notmuch_message_t *message)
     const char *in_reply_to;
 
     in_reply_to = _notmuch_message_get_in_reply_to (message);
-    THREAD_DEBUG ("checking message = %s in_reply_to=%s\n",
+    THREAD_DEBUG ("checking message = %s in_reply_to=%s",
                  notmuch_message_get_message_id (message), in_reply_to);
 
     if (in_reply_to && (! EMPTY_STRING (in_reply_to)) &&
@@ -423,31 +424,31 @@ _parent_or_toplevel (notmuch_thread_t *thread, notmuch_message_t *message)
     const notmuch_string_list_t *references =
        _notmuch_message_get_references (message);
 
-    THREAD_DEBUG ("trying to reparent via references: %s\n",
+    THREAD_DEBUG ("trying to reparent via references: %s",
                  notmuch_message_get_message_id (message));
 
     for (notmuch_string_node_t *ref_node = references->head;
         ref_node; ref_node = ref_node->next) {
-       THREAD_DEBUG ("checking reference=%s\n", ref_node->string);
+       THREAD_DEBUG ("checking reference=%s", ref_node->string);
        if ((g_hash_table_lookup_extended (thread->message_hash,
                                           ref_node->string, NULL,
                                           (void **) &new_parent))) {
            size_t new_depth = _notmuch_message_get_thread_depth (new_parent);
-           THREAD_DEBUG ("got depth %lu\n", new_depth);
+           THREAD_DEBUG ("got depth %lu", new_depth);
            if (new_depth > max_depth || ! parent) {
-               THREAD_DEBUG ("adding at depth %lu parent=%s\n", new_depth, ref_node->string);
+               THREAD_DEBUG ("adding at depth %lu parent=%s", new_depth, ref_node->string);
                max_depth = new_depth;
                parent = new_parent;
            }
        }
     }
     if (parent) {
-       THREAD_DEBUG ("adding reply %s to parent=%s\n",
+       THREAD_DEBUG ("adding reply %s to parent=%s",
                      notmuch_message_get_message_id (message),
                      notmuch_message_get_message_id (parent));
        _notmuch_message_add_reply (parent, message);
     } else {
-       THREAD_DEBUG ("adding as toplevel %s\n",
+       THREAD_DEBUG ("adding as toplevel %s",
                      notmuch_message_get_message_id (message));
        _notmuch_message_list_add_message (thread->toplevel_list, message);
     }
@@ -482,13 +483,13 @@ _resolve_thread_relationships (notmuch_thread_t *thread)
      */
     if (first_node) {
        message = first_node->message;
-       THREAD_DEBUG ("checking first message  %s\n",
+       THREAD_DEBUG ("checking first message  %s",
                      notmuch_message_get_message_id (message));
 
        if (_notmuch_message_list_empty (maybe_toplevel_list) ||
            ! _parent_via_in_reply_to (thread, message)) {
 
-           THREAD_DEBUG ("adding first message as toplevel = %s\n",
+           THREAD_DEBUG ("adding first message as toplevel = %s",
                          notmuch_message_get_message_id (message));
            _notmuch_message_list_add_message (maybe_toplevel_list, message);
        }
index d29c4e48a8054055b7e49aa99fa9f83edfb897db..1c5d619b352a1601315947ee15c4b5f5dcb50dcc 100644 (file)
@@ -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-init.c b/notmuch-client-init.c
new file mode 100644 (file)
index 0000000..60db6ba
--- /dev/null
@@ -0,0 +1,18 @@
+#include "notmuch-client.h"
+#include "gmime-filter-reply.h"
+
+/* Caller is responsible for only calling this once */
+
+void
+notmuch_client_init (void)
+{
+#if ! GLIB_CHECK_VERSION (2, 35, 1)
+    g_type_init ();
+#endif
+
+    g_mime_init ();
+
+    g_mime_filter_reply_module_init ();
+
+    talloc_enable_null_tracking ();
+}
index 8815bb11b79ef8f0d6b7346789b06974a3565ea7..1a87240d3c21de8e51736ede9b408cdd36e23652 100644 (file)
@@ -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;
@@ -137,7 +140,7 @@ chomp_newline (char *str)
  * this.  New (required) map fields can be added without increasing
  * this.
  */
-#define NOTMUCH_FORMAT_CUR 4
+#define NOTMUCH_FORMAT_CUR 5
 /* The minimum supported structured output format version.  Requests
  * for format versions below this will return an error. */
 #define NOTMUCH_FORMAT_MIN 1
@@ -156,7 +159,7 @@ chomp_newline (char *str)
  */
 extern int notmuch_format_version;
 
-typedef struct _notmuch_config notmuch_config_t;
+typedef struct _notmuch_conffile notmuch_conffile_t;
 
 /* Commands that support structured output should support the
  * following argument
@@ -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);
 
@@ -250,6 +254,10 @@ json_quote_chararray (const void *ctx, const char *str, const size_t len);
 char *
 json_quote_str (const void *ctx, const char *str);
 
+/* notmuch-client-init.c */
+
+void notmuch_client_init (void);
+
 /* notmuch-config.c */
 
 typedef enum {
@@ -260,55 +268,55 @@ typedef enum {
     NOTMUCH_COMMAND_CONFIG_LOAD                = 1 << 5,
 } notmuch_command_mode_t;
 
-notmuch_config_t *
-notmuch_config_open (notmuch_database_t *notmuch,
-                    const char *filename,
-                    bool create);
+notmuch_conffile_t *
+notmuch_conffile_open (notmuch_database_t *notmuch,
+                      const char *filename,
+                      bool create);
 
 void
-notmuch_config_close (notmuch_config_t *config);
+notmuch_conffile_close (notmuch_conffile_t *config);
 
 int
-notmuch_config_save (notmuch_config_t *config);
+notmuch_conffile_save (notmuch_conffile_t *config);
 
 bool
-notmuch_config_is_new (notmuch_config_t *config);
+notmuch_conffile_is_new (notmuch_conffile_t *config);
 
 void
-notmuch_config_set_database_path (notmuch_config_t *config,
-                                 const char *database_path);
+notmuch_conffile_set_database_path (notmuch_conffile_t *config,
+                                   const char *database_path);
 
 void
-notmuch_config_set_user_name (notmuch_config_t *config,
-                             const char *user_name);
+notmuch_conffile_set_user_name (notmuch_conffile_t *config,
+                               const char *user_name);
 
 void
-notmuch_config_set_user_primary_email (notmuch_config_t *config,
-                                      const char *primary_email);
+notmuch_conffile_set_user_primary_email (notmuch_conffile_t *config,
+                                        const char *primary_email);
 
 void
-notmuch_config_set_user_other_email (notmuch_config_t *config,
-                                    const char *other_email[],
-                                    size_t length);
+notmuch_conffile_set_user_other_email (notmuch_conffile_t *config,
+                                      const char *other_email[],
+                                      size_t length);
 
 void
-notmuch_config_set_new_tags (notmuch_config_t *config,
-                            const char *new_tags[],
-                            size_t length);
+notmuch_conffile_set_new_tags (notmuch_conffile_t *config,
+                              const char *new_tags[],
+                              size_t length);
 
 void
-notmuch_config_set_new_ignore (notmuch_config_t *config,
-                              const char *new_ignore[],
-                              size_t length);
+notmuch_conffile_set_new_ignore (notmuch_conffile_t *config,
+                                const char *new_ignore[],
+                                size_t length);
 
 void
-notmuch_config_set_maildir_synchronize_flags (notmuch_config_t *config,
-                                             bool synchronize_flags);
+notmuch_conffile_set_maildir_synchronize_flags (notmuch_conffile_t *config,
+                                               bool synchronize_flags);
 
 void
-notmuch_config_set_search_exclude_tags (notmuch_config_t *config,
-                                       const char *list[],
-                                       size_t length);
+notmuch_conffile_set_search_exclude_tags (notmuch_conffile_t *config,
+                                         const char *list[],
+                                         size_t length);
 int
 notmuch_run_hook (notmuch_database_t *notmuch, const char *hook);
 
@@ -384,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,
@@ -402,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.
@@ -422,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
@@ -481,11 +491,11 @@ print_status_gzbytes (const char *loc,
 
 #include "command-line-arguments.h"
 
-extern const char *notmuch_requested_db_uuid;
 extern const notmuch_opt_desc_t notmuch_shared_options [];
-void notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch);
 
-void notmuch_process_shared_options (const char *subcommand_name);
+notmuch_query_syntax_t shared_option_query_syntax ();
+
+void notmuch_process_shared_options (notmuch_database_t *notmuch, const char *subcommand_name);
 int notmuch_minimal_options (const char *subcommand_name,
                             int argc, char **argv);
 
@@ -495,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
index 2648434e35ccaad22305326041bab0041cfefe4c..40ffb4286b5f7db086a89720a657b89176a2aea0 100644 (file)
@@ -45,12 +45,7 @@ notmuch_compact_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    if (notmuch_requested_db_uuid) {
-       fprintf (stderr, "Error: --uuid not implemented for compact\n");
-       return EXIT_FAILURE;
-    }
-
-    notmuch_process_shared_options (argv[0]);
+    notmuch_process_shared_options (NULL, argv[0]);
 
     if (! quiet)
        printf ("Compacting database...\n");
index 34482b4e1e4e82a77ed437d61a34cec23b8b616e..8123e4381003bd89509edc8d7970737893b1992b 100644 (file)
@@ -24,6 +24,7 @@
 #include <netdb.h>
 #include <assert.h>
 
+#include "path-util.h"
 #include "unicode-util.h"
 
 static const char toplevel_config_comment[] =
@@ -31,7 +32,7 @@ static const char toplevel_config_comment[] =
     "\n"
     " For more information about notmuch, see https://notmuchmail.org";
 
-struct config_group {
+static const struct config_group {
     const char *group_name;
     const char *comment;
 } group_comment_table [] = {
@@ -114,14 +115,14 @@ struct config_group {
     },
 };
 
-struct _notmuch_config {
+struct _notmuch_conffile {
     char *filename;
     GKeyFile *key_file;
     bool is_new;
 };
 
 static int
-notmuch_config_destructor (notmuch_config_t *config)
+notmuch_conffile_destructor (notmuch_conffile_t *config)
 {
     if (config->key_file)
        g_key_file_free (config->key_file);
@@ -130,7 +131,7 @@ notmuch_config_destructor (notmuch_config_t *config)
 }
 
 static bool
-get_config_from_file (notmuch_config_t *config, bool create_new)
+get_config_from_file (notmuch_conffile_t *config, bool create_new)
 {
     #define BUF_SIZE 4096
     char *config_str = NULL;
@@ -245,21 +246,21 @@ get_config_from_file (notmuch_config_t *config, bool create_new)
  *     The default configuration also contains comments to guide the
  *     user in editing the file directly.
  */
-notmuch_config_t *
-notmuch_config_open (notmuch_database_t *notmuch,
-                    const char *filename,
-                    bool create)
+notmuch_conffile_t *
+notmuch_conffile_open (notmuch_database_t *notmuch,
+                      const char *filename,
+                      bool create)
 {
     char *notmuch_config_env = NULL;
 
-    notmuch_config_t *config = talloc_zero (notmuch, notmuch_config_t);
+    notmuch_conffile_t *config = talloc_zero (notmuch, notmuch_conffile_t);
 
     if (config == NULL) {
        fprintf (stderr, "Out of memory.\n");
        return NULL;
     }
 
-    talloc_set_destructor (config, notmuch_config_destructor);
+    talloc_set_destructor (config, notmuch_conffile_destructor);
 
     if (filename) {
        config->filename = talloc_strdup (config, filename);
@@ -277,31 +278,37 @@ notmuch_config_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;
 }
 
-/* Close the given notmuch_config_t object, freeing all resources.
+/* Close the given notmuch_conffile_t object, freeing all resources.
  *
  * Note: Any changes made to the configuration are *not* saved by this
- * function. To save changes, call notmuch_config_save before
- * notmuch_config_close.
+ * function. To save changes, call notmuch_conffile_save before
+ * notmuch_conffile_close.
  */
 void
-notmuch_config_close (notmuch_config_t *config)
+notmuch_conffile_close (notmuch_conffile_t *config)
 {
     talloc_free (config);
 }
@@ -314,7 +321,7 @@ notmuch_config_close (notmuch_config_t *config)
  * printing a description of the error to stderr).
  */
 int
-notmuch_config_save (notmuch_config_t *config)
+notmuch_conffile_save (notmuch_conffile_t *config)
 {
     size_t length;
     char *data, *filename;
@@ -327,7 +334,7 @@ notmuch_config_save (notmuch_config_t *config)
     }
 
     /* Try not to overwrite symlinks. */
-    filename = canonicalize_file_name (config->filename);
+    filename = notmuch_canonicalize_file_name (config->filename);
     if (! filename) {
        if (errno == ENOENT) {
            filename = strdup (config->filename);
@@ -364,76 +371,79 @@ notmuch_config_save (notmuch_config_t *config)
 }
 
 bool
-notmuch_config_is_new (notmuch_config_t *config)
+notmuch_conffile_is_new (notmuch_conffile_t *config)
 {
     return config->is_new;
 }
 
 static void
-_config_set (notmuch_config_t *config,
+_config_set (notmuch_conffile_t *config,
             const char *group, const char *key, const char *value)
 {
     g_key_file_set_string (config->key_file, group, key, value);
 }
 
 static void
-_config_set_list (notmuch_config_t *config,
+_config_set_list (notmuch_conffile_t *config,
                  const char *group, const char *key,
                  const char *list[],
                  size_t length)
 {
-    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
-notmuch_config_set_database_path (notmuch_config_t *config,
-                                 const char *database_path)
+notmuch_conffile_set_database_path (notmuch_conffile_t *config,
+                                   const char *database_path)
 {
     _config_set (config, "database", "path", database_path);
 }
 
 void
-notmuch_config_set_user_name (notmuch_config_t *config,
-                             const char *user_name)
+notmuch_conffile_set_user_name (notmuch_conffile_t *config,
+                               const char *user_name)
 {
     _config_set (config, "user", "name", user_name);
 }
 
 void
-notmuch_config_set_user_primary_email (notmuch_config_t *config,
-                                      const char *primary_email)
+notmuch_conffile_set_user_primary_email (notmuch_conffile_t *config,
+                                        const char *primary_email)
 {
     _config_set (config, "user", "primary_email", primary_email);
 }
 
 void
-notmuch_config_set_user_other_email (notmuch_config_t *config,
-                                    const char *list[],
-                                    size_t length)
+notmuch_conffile_set_user_other_email (notmuch_conffile_t *config,
+                                      const char *list[],
+                                      size_t length)
 {
     _config_set_list (config, "user", "other_email", list, length);
 }
 
 void
-notmuch_config_set_new_tags (notmuch_config_t *config,
-                            const char *list[],
-                            size_t length)
+notmuch_conffile_set_new_tags (notmuch_conffile_t *config,
+                              const char *list[],
+                              size_t length)
 {
     _config_set_list (config, "new", "tags", list, length);
 }
 
 void
-notmuch_config_set_new_ignore (notmuch_config_t *config,
-                              const char *list[],
-                              size_t length)
+notmuch_conffile_set_new_ignore (notmuch_conffile_t *config,
+                                const char *list[],
+                                size_t length)
 {
     _config_set_list (config, "new", "ignore", list, length);
 }
 
 void
-notmuch_config_set_search_exclude_tags (notmuch_config_t *config,
-                                       const char *list[],
-                                       size_t length)
+notmuch_conffile_set_search_exclude_tags (notmuch_conffile_t *config,
+                                         const char *list[],
+                                         size_t length)
 {
     _config_set_list (config, "search", "exclude_tags", list, length);
 }
@@ -511,14 +521,15 @@ typedef struct config_key {
     bool (*validate)(const char *);
 } config_key_info_t;
 
-static struct config_key
+static const struct config_key
     config_key_table[] = {
     { "index.decrypt",   false,  NULL },
     { "index.header.",   true,   validate_field_name },
     { "query.",          true,   NULL },
+    { "squery.",         true,   validate_field_name },
 };
 
-static config_key_info_t *
+static const config_key_info_t *
 _config_key_info (const char *item)
 {
     for (size_t i = 0; i < ARRAY_SIZE (config_key_table); i++) {
@@ -537,11 +548,18 @@ notmuch_config_command_get (notmuch_database_t *notmuch, char *item)
 {
     notmuch_config_values_t *list;
 
-    for (list = notmuch_config_get_values_string (notmuch, item);
-        notmuch_config_values_valid (list);
-        notmuch_config_values_move_to_next (list)) {
-       const char *val = notmuch_config_values_get (list);
-       puts (val);
+    if (STRNCMP_LITERAL (item, BUILT_WITH_PREFIX) == 0) {
+       if (notmuch_built_with (item + strlen (BUILT_WITH_PREFIX)))
+           puts ("true");
+       else
+           puts ("false");
+    } else {
+       for (list = notmuch_config_get_values_string (notmuch, item);
+            notmuch_config_values_valid (list);
+            notmuch_config_values_move_to_next (list)) {
+           const char *val = notmuch_config_values_get (list);
+           puts (val);
+       }
     }
     return EXIT_SUCCESS;
 }
@@ -582,8 +600,8 @@ notmuch_config_command_set (notmuch_database_t *notmuch,
                            int argc, char *argv[])
 {
     char *group, *key;
-    config_key_info_t *key_info;
-    notmuch_config_t *config;
+    const config_key_info_t *key_info;
+    notmuch_conffile_t *config;
     bool update_database = false;
     int opt_index, ret;
     char *item;
@@ -626,8 +644,8 @@ notmuch_config_command_set (notmuch_database_t *notmuch,
     if (_item_split (item, &group, &key))
        return 1;
 
-    config = notmuch_config_open (notmuch,
-                                 notmuch_config_path (notmuch), false);
+    config = notmuch_conffile_open (notmuch,
+                                   notmuch_config_path (notmuch), false);
     if (! config)
        return 1;
 
@@ -651,9 +669,9 @@ notmuch_config_command_set (notmuch_database_t *notmuch,
        break;
     }
 
-    ret = notmuch_config_save (config);
+    ret = notmuch_conffile_save (config);
 
-    notmuch_config_close (config);
+    notmuch_conffile_close (config);
 
     return ret;
 }
@@ -671,6 +689,9 @@ _notmuch_config_list_built_with ()
     printf ("%sretry_lock=%s\n",
            BUILT_WITH_PREFIX,
            notmuch_built_with ("retry_lock") ? "true" : "false");
+    printf ("%ssexp_queries=%s\n",
+           BUILT_WITH_PREFIX,
+           notmuch_built_with ("sexp_queries") ? "true" : "false");
 }
 
 static int
@@ -700,10 +721,6 @@ notmuch_config_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    if (notmuch_requested_db_uuid)
-       fprintf (stderr, "Warning: ignoring --uuid=%s\n",
-                notmuch_requested_db_uuid);
-
     /* skip at least subcommand argument */
     argc -= opt_index;
     argv += opt_index;
@@ -735,8 +752,8 @@ notmuch_config_command (notmuch_database_t *notmuch, int argc, char *argv[])
 }
 
 void
-notmuch_config_set_maildir_synchronize_flags (notmuch_config_t *config,
-                                             bool synchronize_flags)
+notmuch_conffile_set_maildir_synchronize_flags (notmuch_conffile_t *config,
+                                               bool synchronize_flags)
 {
     g_key_file_set_boolean (config->key_file,
                            "maildir", "synchronize_flags", synchronize_flags);
index 5ac4292bc495c2cad1107eb74d67f7ce008d7acf..0d9046a8a20835f910ed4ac0dc8447f0e67df069 100644 (file)
@@ -74,10 +74,12 @@ print_count (notmuch_database_t *notmuch, const char *query_str,
     int ret = 0;
     notmuch_status_t status;
 
-    query = notmuch_query_create (notmuch, query_str);
-    if (query == NULL) {
-       fprintf (stderr, "Out of memory\n");
-       return -1;
+    status = notmuch_query_create_with_syntax (notmuch, query_str,
+                                              shared_option_query_syntax (),
+                                              &query);
+    if (print_status_database ("notmuch count", notmuch, status)) {
+       ret = -1;
+       goto DONE;
     }
 
     for (notmuch_config_values_start (exclude_tags);
@@ -182,7 +184,7 @@ notmuch_count_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    notmuch_process_shared_options (argv[0]);
+    notmuch_process_shared_options (notmuch, argv[0]);
 
     if (input_file_name) {
        batch = true;
@@ -201,8 +203,6 @@ notmuch_count_command (notmuch_database_t *notmuch, int argc, char *argv[])
        return EXIT_FAILURE;
     }
 
-    notmuch_exit_if_unmatched_db_uuid (notmuch);
-
     query_str = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
     if (query_str == NULL) {
        fprintf (stderr, "Out of memory.\n");
index ae89e4da80ccbf90f53109539115a81b45410eb9..cb82d61fe424176f4a902194cd48a9858939aab7 100644 (file)
@@ -232,11 +232,12 @@ database_dump_file (notmuch_database_t *notmuch, gzFile output,
     if (! query_str)
        query_str = "";
 
-    query = notmuch_query_create (notmuch, query_str);
-    if (query == NULL) {
-       fprintf (stderr, "Out of memory\n");
+    status = notmuch_query_create_with_syntax (notmuch, query_str,
+                                              shared_option_query_syntax (),
+                                              &query);
+    if (print_status_database ("notmuch dump", notmuch, status))
        return EXIT_FAILURE;
-    }
+
     /* Don't ask xapian to sort by Message-ID. Xapian optimizes returning the
      * first results quickly at the expense of total time.
      */
@@ -366,8 +367,6 @@ notmuch_dump_command (notmuch_database_t *notmuch, int argc, char *argv[])
     const char *query_str = NULL;
     int ret;
 
-    notmuch_exit_if_unmatched_db_uuid (notmuch);
-
     const char *output_file_name = NULL;
     int opt_index;
 
@@ -394,7 +393,7 @@ notmuch_dump_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    notmuch_process_shared_options (argv[0]);
+    notmuch_process_shared_options (notmuch, argv[0]);
 
     if (include == 0)
        include = DUMP_INCLUDE_CONFIG | DUMP_INCLUDE_TAGS | DUMP_INCLUDE_PROPERTIES;
diff --git a/notmuch-git.py b/notmuch-git.py
new file mode 100644 (file)
index 0000000..97073c8
--- /dev/null
@@ -0,0 +1,1217 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2011-2014 David Bremner <david@tethera.net>
+#                         W. Trevor King <wking@tremily.us>
+#
+# 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<id>[^/]*)/(?P<tag>[^/]*)'),
+                    _re.compile(_TAG_DIRECTORY + '([0-9a-f]{2}/){2}(?P<id>[^/]*)/(?P<tag>[^/]*)'))
+
+# 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_safe_fraction(status):
+    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)
+    change = len(status['added'])+len(status['deleted'])
+    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 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.<name>.repository, likely 'origin', and
+    branch.<name>.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 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 ... <command> --help'."))
+    for command in [
+            'archive',
+            'checkout',
+            'clone',
+            'commit',
+            'fetch',
+            'help',
+            'init',
+            '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 <tree-ish>, 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.<name>.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 <refspec> 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 <refspec> entry in the OPTIONS section of '
+                    'git-push(1) for other possibilities.'))
+
+    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)
index 00c0046873f50cc788717ebb86ef6a10314bf70a..e44607ad45a8fdf9058f3ddf9fa3fc88acb7614e 100644 (file)
@@ -34,7 +34,7 @@ static volatile sig_atomic_t interrupted;
 static void
 handle_sigint (unused (int sig))
 {
-    static char msg[] = "Stopping...         \n";
+    static const char msg[] = "Stopping...         \n";
 
     /* This write is "opportunistic", so it's okay to ignore the
      * result.  It is not required for correctness, and if it does
@@ -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[] = {
@@ -478,7 +500,7 @@ notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    notmuch_process_shared_options (argv[0]);
+    notmuch_process_shared_options (notmuch, argv[0]);
 
     mail_root = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT);
 
@@ -550,9 +572,7 @@ notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[])
        return EXIT_FAILURE;
     }
 
-    notmuch_exit_if_unmatched_db_uuid (notmuch);
-
-    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));
@@ -560,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);
index 993359d603a9bf75d9e606014741ee9cc6f0d83f..4a53e3ebcc064cc531b8cb6255c422ff35938b30 100644 (file)
@@ -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;
@@ -81,7 +82,7 @@ static volatile sig_atomic_t interrupted;
 static void
 handle_sigint (unused (int sig))
 {
-    static char msg[] = "Stopping...         \n";
+    static const char msg[] = "Stopping...         \n";
 
     /* This write is "opportunistic", so it's okay to ignore the
      * result.  It is not required for correctness, and if it does
@@ -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:
@@ -403,12 +404,19 @@ add_file (notmuch_database_t *notmuch, const char *filename,
        break;
     /* Non-fatal issues (go on to next file). */
     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
-       if (state->synchronize_flags)
-           notmuch_message_maildir_flags_to_tags (message);
+       if (state->synchronize_flags) {
+           status = notmuch_message_maildir_flags_to_tags (message);
+           if (print_status_message ("add_file", message, status))
+               goto DONE;
+       }
        break;
     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++;
@@ -597,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);
@@ -1139,7 +1148,7 @@ notmuch_new_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    notmuch_process_shared_options (argv[0]);
+    notmuch_process_shared_options (notmuch, argv[0]);
 
     /* quiet trumps verbose */
     if (quiet)
@@ -1147,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 (
@@ -1194,8 +1205,6 @@ notmuch_new_command (notmuch_database_t *notmuch, int argc, char *argv[])
            return EXIT_FAILURE;
     }
 
-    notmuch_exit_if_unmatched_db_uuid (notmuch);
-
     if (notmuch_database_get_revision (notmuch, NULL) == 0) {
        int count = 0;
        count_files (mail_root, &count, &add_files_state);
@@ -1216,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));
index 8904c1f41dca4d880a23708616705b76ede399eb..e9a6545688034993b822c22c27e13f3cb9063515 100644 (file)
@@ -26,7 +26,7 @@ static volatile sig_atomic_t interrupted;
 static void
 handle_sigint (unused (int sig))
 {
-    static char msg[] = "Stopping...         \n";
+    static const char msg[] = "Stopping...         \n";
 
     /* This write is "opportunistic", so it's okay to ignore the
      * result.  It is not required for correctness, and if it does
@@ -49,11 +49,11 @@ reindex_query (notmuch_database_t *notmuch, const char *query_string,
 
     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
 
-    query = notmuch_query_create (notmuch, query_string);
-    if (query == NULL) {
-       fprintf (stderr, "Out of memory.\n");
+    status = notmuch_query_create_with_syntax (notmuch, query_string,
+                                              shared_option_query_syntax (),
+                                              &query);
+    if (print_status_database ("notmuch reindex", notmuch, status))
        return 1;
-    }
 
     /* reindexing is not interested in any special sort order */
     notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);
@@ -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));
@@ -108,11 +109,9 @@ notmuch_reindex_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    notmuch_process_shared_options (argv[0]);
-
-    notmuch_exit_if_unmatched_db_uuid (notmuch);
+    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));
@@ -130,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);
 
index 08140799e2b965967ac2496883d091b1fb3a3032..44297251d02183121685a0cb5bda2cae76cdbea4 100644 (file)
@@ -464,8 +464,8 @@ guess_from_in_received_by (notmuch_database_t *notmuch, const char *received)
  * (last Received: header added) and try to extract from them
  * indications to which email address this message was delivered.
  *
- * The Received: header is special in our get_header function and is
- * always concatenated.
+ * The Received: header is among special ones in our get_header function
+ * and is always concatenated.
  *
  * Return the address that was found, if any, and NULL otherwise.
  */
@@ -499,6 +499,9 @@ guess_from_in_received_headers (notmuch_message_t *message)
  * headers: Envelope-To, X-Original-To, and Delivered-To (searched in
  * that order).
  *
+ * The Delivered-To: header is among special ones in our get_header
+ * function and is always concatenated.
+ *
  * Return the address that was found, if any, and NULL otherwise.
  */
 static const char *
@@ -660,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, &params->crypto, &node))
+       if (mime_node_open (notmuch, message, params->duplicate, &params->crypto, &node))
            return 1;
 
        reply = create_reply_message (notmuch, message,
@@ -680,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);
@@ -712,10 +715,12 @@ 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;
     int reply_all = true;
+    notmuch_status_t status;
 
     notmuch_opt_desc_t options[] = {
        { .opt_keyword = &format, .name = "format", .keywords =
@@ -735,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 = &params.duplicate, .name = "duplicate" },
        { .opt_inherit = notmuch_shared_options },
        { }
     };
@@ -743,7 +749,7 @@ notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    notmuch_process_shared_options (argv[0]);
+    notmuch_process_shared_options (notmuch, argv[0]);
 
     notmuch_exit_if_unsupported_format ();
 
@@ -758,13 +764,11 @@ notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[])
        return EXIT_FAILURE;
     }
 
-    notmuch_exit_if_unmatched_db_uuid (notmuch);
-
-    query = notmuch_query_create (notmuch, query_string);
-    if (query == NULL) {
-       fprintf (stderr, "Out of memory\n");
+    status = notmuch_query_create_with_syntax (notmuch, query_string,
+                                              shared_option_query_syntax (),
+                                              &query);
+    if (print_status_database ("notmuch reply", notmuch, status))
        return EXIT_FAILURE;
-    }
 
     if (do_reply (notmuch, query, &params, format, reply_all) != 0)
        return EXIT_FAILURE;
index 1a81212f0f9810e4db2cb6b07a832b1bc9f1ce2f..1cce004a06d4dd8b0d8a402474a771c94e8fe155 100644 (file)
@@ -272,8 +272,7 @@ notmuch_restore_command (notmuch_database_t *notmuch, int argc, char *argv[])
        goto DONE;
     }
 
-    notmuch_process_shared_options (argv[0]);
-    notmuch_exit_if_unmatched_db_uuid (notmuch);
+    notmuch_process_shared_options (notmuch, argv[0]);
 
     if (include == 0) {
        include = DUMP_INCLUDE_CONFIG | DUMP_INCLUDE_PROPERTIES | DUMP_INCLUDE_TAGS;
index 244817a987e1adc2a85fcf5bc6b19be34877c338..327e144564de48e0b339036528505d5a227bc40a 100644 (file)
@@ -56,6 +56,7 @@ typedef struct {
     int format_sel;
     sprinter_t *format;
     int exclude;
+    int query_syntax;
     notmuch_query_t *query;
     int sort;
     int output;
@@ -709,8 +710,6 @@ _notmuch_search_prepare (search_context_t *ctx, int argc, char *argv[])
 
     notmuch_exit_if_unsupported_format ();
 
-    notmuch_exit_if_unmatched_db_uuid (ctx->notmuch);
-
     query_str = query_string_from_args (ctx->notmuch, argc, argv);
     if (query_str == NULL) {
        fprintf (stderr, "Out of memory.\n");
@@ -721,11 +720,11 @@ _notmuch_search_prepare (search_context_t *ctx, int argc, char *argv[])
        return EXIT_FAILURE;
     }
 
-    ctx->query = notmuch_query_create (ctx->notmuch, query_str);
-    if (ctx->query == NULL) {
-       fprintf (stderr, "Out of memory\n");
+    if (print_status_database ("notmuch search", ctx->notmuch,
+                              notmuch_query_create_with_syntax (ctx->notmuch, query_str,
+                                                                shared_option_query_syntax (),
+                                                                &ctx->query)))
        return EXIT_FAILURE;
-    }
 
     notmuch_query_set_sort (ctx->query, ctx->sort);
 
@@ -771,6 +770,7 @@ static search_context_t search_context = {
     .format_sel = NOTMUCH_FORMAT_TEXT,
     .exclude = NOTMUCH_EXCLUDE_TRUE,
     .sort = NOTMUCH_SORT_NEWEST_FIRST,
+    .query_syntax = NOTMUCH_QUERY_SYNTAX_XAPIAN,
     .output = 0,
     .offset = 0,
     .limit = -1, /* unlimited */
@@ -827,7 +827,7 @@ notmuch_search_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    notmuch_process_shared_options (argv[0]);
+    notmuch_process_shared_options (notmuch, argv[0]);
 
     if (ctx->output != OUTPUT_FILES && ctx->output != OUTPUT_MESSAGES &&
        ctx->dupe != -1) {
@@ -893,7 +893,7 @@ notmuch_address_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    notmuch_process_shared_options (argv[0]);
+    notmuch_process_shared_options (notmuch, argv[0]);
 
     if (! (ctx->output & (OUTPUT_SENDER | OUTPUT_RECIPIENTS)))
        ctx->output |= OUTPUT_SENDER;
index 567e3b565359b64018573044828a24d18eb46328..9382e2794a86dacdabcdee158c36f271a5dfd9ab 100644 (file)
@@ -74,7 +74,7 @@ welcome_message_post_setup (void)
 {
     printf ("\n"
            "Notmuch is now configured, and the configuration settings are saved in\n"
-           "a file in your home directory named .notmuch-config . If you'd like to\n"
+           "a file in your home directory named .notmuch-config. If you'd like to\n"
            "change the configuration in the future, you can either edit that file\n"
            "directly or run \"notmuch setup\".  To choose an alternate configuration\n"
            "location, set ${NOTMUCH_CONFIG}.\n\n"
@@ -131,7 +131,7 @@ notmuch_setup_command (notmuch_database_t *notmuch,
     size_t response_size = 0;
     GPtrArray *other_emails;
     notmuch_config_values_t *new_tags, *search_exclude_tags, *emails;
-    notmuch_config_t *config;
+    notmuch_conffile_t *config;
 
 #define prompt(format, ...)                                     \
     do {                                                        \
@@ -147,26 +147,22 @@ notmuch_setup_command (notmuch_database_t *notmuch,
     if (notmuch_minimal_options ("setup", argc, argv) < 0)
        return EXIT_FAILURE;
 
-    if (notmuch_requested_db_uuid)
-       fprintf (stderr, "Warning: ignoring --uuid=%s\n",
-                notmuch_requested_db_uuid);
-
-    config = notmuch_config_open (notmuch,
-                                 notmuch_config_path (notmuch), true);
+    config = notmuch_conffile_open (notmuch,
+                                   notmuch_config_path (notmuch), true);
     if (! config)
        return EXIT_FAILURE;
 
-    if (notmuch_config_is_new (config))
+    if (notmuch_conffile_is_new (config))
        welcome_message_pre_setup ();
 
     prompt ("Your full name [%s]: ", notmuch_config_get (notmuch, NOTMUCH_CONFIG_USER_NAME));
     if (strlen (response))
-       notmuch_config_set_user_name (config, response);
+       notmuch_conffile_set_user_name (config, response);
 
     prompt ("Your primary email address [%s]: ",
            notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL));
     if (strlen (response))
-       notmuch_config_set_user_primary_email (config, response);
+       notmuch_conffile_set_user_primary_email (config, response);
 
     other_emails = g_ptr_array_new ();
 
@@ -188,10 +184,10 @@ notmuch_setup_command (notmuch_database_t *notmuch,
            g_ptr_array_add (other_emails, talloc_strdup (config, response));
     } while (strlen (response));
     if (other_emails->len)
-       notmuch_config_set_user_other_email (config,
-                                            (const char **)
-                                            other_emails->pdata,
-                                            other_emails->len);
+       notmuch_conffile_set_user_other_email (config,
+                                              (const char **)
+                                              other_emails->pdata,
+                                              other_emails->len);
     g_ptr_array_free (other_emails, true);
 
     prompt ("Top-level directory of your email archive [%s]: ",
@@ -200,7 +196,7 @@ notmuch_setup_command (notmuch_database_t *notmuch,
        const char *absolute_path;
 
        absolute_path = make_path_absolute (config, response);
-       notmuch_config_set_database_path (config, absolute_path);
+       notmuch_conffile_set_database_path (config, absolute_path);
     }
 
     new_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_TAGS);
@@ -212,8 +208,8 @@ notmuch_setup_command (notmuch_database_t *notmuch,
     if (strlen (response)) {
        GPtrArray *tags = parse_tag_list (config, response);
 
-       notmuch_config_set_new_tags (config, (const char **) tags->pdata,
-                                    tags->len);
+       notmuch_conffile_set_new_tags (config, (const char **) tags->pdata,
+                                      tags->len);
 
        g_ptr_array_free (tags, true);
     }
@@ -227,20 +223,20 @@ notmuch_setup_command (notmuch_database_t *notmuch,
     if (strlen (response)) {
        GPtrArray *tags = parse_tag_list (config, response);
 
-       notmuch_config_set_search_exclude_tags (config,
-                                               (const char **) tags->pdata,
-                                               tags->len);
+       notmuch_conffile_set_search_exclude_tags (config,
+                                                 (const char **) tags->pdata,
+                                                 tags->len);
 
        g_ptr_array_free (tags, true);
     }
 
-    if (notmuch_config_save (config))
+    if (notmuch_conffile_save (config))
        return EXIT_FAILURE;
 
     if (config)
-       notmuch_config_close (config);
+       notmuch_conffile_close (config);
 
-    if (notmuch_config_is_new (config))
+    if (notmuch_conffile_is_new (config))
        welcome_message_post_setup ();
 
     return EXIT_SUCCESS;
index bdb87321f1f02d74a400cffe3c6a4cc02c82b40a..7fb40ce9ab5d3f0179707a7c0ea88f9440673f25 100644 (file)
 #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);
 }
@@ -475,6 +517,11 @@ format_part_sigstatus_sprinter (sprinter_t *sp, GMimeSignatureList *siglist)
                    sp->map_key (sp, "userid");
                    sp->string (sp, uid);
                }
+               const char *email = g_mime_certificate_get_valid_email (certificate);
+               if (email) {
+                   sp->map_key (sp, "email");
+                   sp->string (sp, email);
+               }
            }
        } else if (certificate) {
            const char *key_id = g_mime_certificate_get_fpr16 (certificate);
@@ -626,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)
 {
@@ -637,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);
        }
 
@@ -804,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++)
@@ -818,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;
 }
@@ -893,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;
@@ -972,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));
@@ -1107,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))
@@ -1114,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)
@@ -1149,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))
@@ -1156,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);
 
@@ -1234,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 },
@@ -1244,8 +1332,13 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
     bool single_message;
     bool unthreaded = FALSE;
     notmuch_status_t status;
+    int sort = NOTMUCH_SORT_NEWEST_FIRST;
 
     notmuch_opt_desc_t options[] = {
+       { .opt_keyword = &sort, .name = "sort", .keywords =
+             (notmuch_keyword_t []){ { "oldest-first", NOTMUCH_SORT_OLDEST_FIRST },
+                                     { "newest-first", NOTMUCH_SORT_NEWEST_FIRST },
+                                     { 0, 0 } } },
        { .opt_keyword = &format, .name = "format", .keywords =
              (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
                                      { "text", NOTMUCH_FORMAT_TEXT },
@@ -1269,6 +1362,9 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
        { .opt_bool = &params.crypto.verify, .name = "verify" },
        { .opt_bool = &params.output_body, .name = "body" },
        { .opt_bool = &params.include_html, .name = "include-html" },
+       { .opt_int = &params.duplicate, .name = "duplicate" },
+       { .opt_int = &params.limit, .name = "limit" },
+       { .opt_int = &params.offset, .name = "offset" },
        { .opt_inherit = notmuch_shared_options },
        { }
     };
@@ -1277,7 +1373,7 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    notmuch_process_shared_options (argv[0]);
+    notmuch_process_shared_options (notmuch, argv[0]);
 
     /* explicit decryption implies verification */
     if (params.crypto.decrypt == NOTMUCH_DECRYPT_NOSTASH ||
@@ -1287,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)
@@ -1343,8 +1442,6 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
        }
     }
 
-    notmuch_exit_if_unmatched_db_uuid (notmuch);
-
     query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
     if (query_string == NULL) {
        fprintf (stderr, "Out of memory\n");
@@ -1356,11 +1453,13 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
        return EXIT_FAILURE;
     }
 
-    query = notmuch_query_create (notmuch, query_string);
-    if (query == NULL) {
-       fprintf (stderr, "Out of memory\n");
+    status = notmuch_query_create_with_syntax (notmuch, query_string,
+                                              shared_option_query_syntax (),
+                                              &query);
+    if (print_status_database ("notmuch show", notmuch, status))
        return EXIT_FAILURE;
-    }
+
+    notmuch_query_set_sort (query, sort);
 
     /* Create structure printer. */
     formatter = formatters[format];
index 667a75d6b960c3b2cc27b75d08753a813badc037..71ff06bffe504325de83fb10fab9b9a1e5eb38c0 100644 (file)
@@ -27,7 +27,7 @@ static volatile sig_atomic_t interrupted;
 static void
 handle_sigint (unused (int sig))
 {
-    static char msg[] = "Stopping...         \n";
+    static const char msg[] = "Stopping...         \n";
 
     /* This write is "opportunistic", so it's okay to ignore the
      * result.  It is not required for correctness, and if it does
@@ -39,8 +39,8 @@ handle_sigint (unused (int sig))
 
 
 static char *
-_optimize_tag_query (void *ctx, const char *orig_query_string,
-                    const tag_op_list_t *list)
+_optimize_tag_query_infix (void *ctx, const char *orig_query_string,
+                          const tag_op_list_t *list)
 {
     /* This is subtler than it looks.  Xapian ignores the '-' operator
      * at the beginning both queries and parenthesized groups and,
@@ -88,6 +88,33 @@ _optimize_tag_query (void *ctx, const char *orig_query_string,
     return query_string;
 }
 
+static char *
+_optimize_tag_query (void *ctx, const char *orig_query_string,
+                    notmuch_query_syntax_t stx,
+                    const tag_op_list_t *list)
+{
+    char *query_string;
+
+    if (stx == NOTMUCH_QUERY_SYNTAX_XAPIAN)
+       return _optimize_tag_query_infix (ctx, orig_query_string, list);
+
+    /* Don't optimize if there are no tag changes. */
+    if (tag_op_list_size (list) == 0)
+       return talloc_strdup (ctx, orig_query_string);
+
+    query_string = talloc_asprintf (ctx, "(and %s", orig_query_string);
+    for (size_t i = 0; i < tag_op_list_size (list) && query_string; i++) {
+       query_string = talloc_asprintf_append_buffer (
+           query_string, tag_op_list_isremove (list, i) ? " (tag \"%s\")" : " (not (tag \"%s\"))",
+           tag_op_list_tag (list, i));
+    }
+
+    if (query_string)
+       query_string = talloc_strdup_append_buffer (query_string, ")");
+
+    return query_string;
+}
+
 /* Tag messages matching 'query_string' according to 'tag_ops'
  */
 static int
@@ -104,7 +131,9 @@ tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string,
     if (! (flags & TAG_FLAG_REMOVE_ALL)) {
        /* Optimize the query so it excludes messages that already
         * have the specified set of tags. */
-       query_string = _optimize_tag_query (ctx, query_string, tag_ops);
+       query_string = _optimize_tag_query (ctx, query_string,
+                                           shared_option_query_syntax (),
+                                           tag_ops);
        if (query_string == NULL) {
            fprintf (stderr, "Out of memory.\n");
            return 1;
@@ -112,11 +141,11 @@ tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string,
        flags |= TAG_FLAG_PRE_OPTIMIZED;
     }
 
-    query = notmuch_query_create (notmuch, query_string);
-    if (query == NULL) {
-       fprintf (stderr, "Out of memory.\n");
+    status = notmuch_query_create_with_syntax (notmuch, query_string,
+                                              shared_option_query_syntax (),
+                                              &query);
+    if (print_status_database ("notmuch tag", notmuch, status))
        return 1;
-    }
 
     /* tagging is not interested in any special sort order */
     notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);
@@ -220,7 +249,7 @@ notmuch_tag_command (notmuch_database_t *notmuch, int argc, char *argv[])
     if (opt_index < 0)
        return EXIT_FAILURE;
 
-    notmuch_process_shared_options (argv[0]);
+    notmuch_process_shared_options (notmuch, argv[0]);
 
     if (input_file_name) {
        batch = true;
@@ -261,8 +290,6 @@ notmuch_tag_command (notmuch_database_t *notmuch, int argc, char *argv[])
        }
     }
 
-    notmuch_exit_if_unmatched_db_uuid (notmuch);
-
     if (print_status_database (
            "notmuch restore",
            notmuch,
index 2429999c2694345dd71b556e282b6dbb5a943dbe..814b9e42a59f46ae675f24f688005ec22ab58914 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
@@ -49,22 +49,38 @@ notmuch_command (notmuch_database_t *notmuch, int argc, char *argv[]);
 static int
 _help_for (const char *topic);
 
+static void
+notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch);
+
 static bool print_version = false, print_help = false;
-const char *notmuch_requested_db_uuid = NULL;
+static const char *notmuch_requested_db_uuid = NULL;
+static int query_syntax = NOTMUCH_QUERY_SYNTAX_XAPIAN;
 
 const notmuch_opt_desc_t notmuch_shared_options [] = {
     { .opt_bool = &print_version, .name = "version" },
     { .opt_bool = &print_help, .name = "help" },
     { .opt_string = &notmuch_requested_db_uuid, .name = "uuid" },
+    { .opt_keyword = &query_syntax, .name = "query", .keywords =
+         (notmuch_keyword_t []){ { "infix", NOTMUCH_QUERY_SYNTAX_XAPIAN },
+                                 { "sexp", NOTMUCH_QUERY_SYNTAX_SEXP },
+                                 { 0, 0 } } },
+
     { }
 };
 
+notmuch_query_syntax_t
+shared_option_query_syntax ()
+{
+    return query_syntax;
+}
+
 /* any subcommand wanting to support these options should call
  * inherit notmuch_shared_options and call
- * notmuch_process_shared_options (subcommand_name);
+ * notmuch_process_shared_options (notmuch, subcommand_name);
+ * with notmuch = an open database, or NULL.
  */
 void
-notmuch_process_shared_options (const char *subcommand_name)
+notmuch_process_shared_options (notmuch_database_t *notmuch, const char *subcommand_name)
 {
     if (print_version) {
        printf ("notmuch " STRINGIFY (NOTMUCH_VERSION) "\n");
@@ -75,6 +91,14 @@ notmuch_process_shared_options (const char *subcommand_name)
        int ret = _help_for (subcommand_name);
        exit (ret);
     }
+
+    if (notmuch) {
+       notmuch_exit_if_unmatched_db_uuid (notmuch);
+    } else {
+       if (notmuch_requested_db_uuid)
+           fprintf (stderr, "Warning: ignoring --uuid=%s\n",
+                    notmuch_requested_db_uuid);
+    }
 }
 
 /* This is suitable for subcommands that do not actually open the
@@ -97,7 +121,7 @@ notmuch_minimal_options (const char *subcommand_name,
        return -1;
 
     /* We can't use argv here as it is sometimes NULL */
-    notmuch_process_shared_options (subcommand_name);
+    notmuch_process_shared_options (NULL, subcommand_name);
     return opt_index;
 }
 
@@ -117,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;
        }
     }
@@ -139,7 +160,7 @@ notmuch_process_shared_indexing_options (notmuch_database_t *notmuch)
 }
 
 
-static command_t commands[] = {
+static const command_t commands[] = {
     { NULL, notmuch_command, NOTMUCH_COMMAND_CONFIG_CREATE | NOTMUCH_COMMAND_CONFIG_LOAD,
       "Notmuch main command." },
     { "setup", notmuch_setup_command, NOTMUCH_COMMAND_CONFIG_CREATE | NOTMUCH_COMMAND_CONFIG_LOAD,
@@ -189,16 +210,18 @@ typedef struct help_topic {
     const char *summary;
 } help_topic_t;
 
-static help_topic_t help_topics[] = {
-    { "search-terms",
-      "Common search term syntax." },
+static const help_topic_t help_topics[] = {
     { "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 command_t *
+static const command_t *
 find_command (const char *name)
 {
     size_t i;
@@ -216,8 +239,8 @@ int notmuch_format_version;
 static void
 usage (FILE *out)
 {
-    command_t *command;
-    help_topic_t *topic;
+    const command_t *command;
+    const help_topic_t *topic;
     unsigned int i;
 
     fprintf (out,
@@ -280,7 +303,7 @@ be supported in the future.\n", notmuch_format_version);
     }
 }
 
-void
+static void
 notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch)
 {
     const char *uuid = NULL;
@@ -308,9 +331,7 @@ exec_man (const char *page)
 static int
 _help_for (const char *topic_name)
 {
-    command_t *command;
-    help_topic_t *topic;
-    unsigned int i;
+    char *page;
 
     if (! topic_name) {
        printf ("The notmuch mail system.\n\n");
@@ -327,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;
 }
 
@@ -422,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);
 
     /*
@@ -452,7 +466,7 @@ main (int argc, char *argv[])
     void *local;
     char *talloc_report;
     const char *command_name = NULL;
-    command_t *command;
+    const command_t *command;
     const char *config_file_name = NULL;
     notmuch_database_t *notmuch = NULL;
     int opt_index;
@@ -464,15 +478,10 @@ main (int argc, char *argv[])
        { }
     };
 
-    talloc_enable_null_tracking ();
+    notmuch_client_init ();
 
     local = talloc_new (NULL);
 
-    g_mime_init ();
-#if ! GLIB_CHECK_VERSION (2, 35, 1)
-    g_type_init ();
-#endif
-
     /* Globally default to the current output format version. */
     notmuch_format_version = NOTMUCH_FORMAT_CUR;
 
@@ -485,13 +494,13 @@ main (int argc, char *argv[])
     if (opt_index < argc)
        command_name = argv[opt_index];
 
-    notmuch_process_shared_options (command_name);
+    notmuch_process_shared_options (NULL, command_name);
 
     command = find_command (command_name);
     /* 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;
@@ -522,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;
            }
@@ -554,15 +563,16 @@ main (int argc, char *argv[])
                                               NULL,
                                               &notmuch,
                                               &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;
@@ -580,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;
        }
 
@@ -594,7 +606,7 @@ main (int argc, char *argv[])
         * talloc_enable_null_tracking
         */
 
-       FILE *report = fopen (talloc_report, "w");
+       FILE *report = fopen (talloc_report, "a");
        if (report) {
            talloc_report_full (NULL, report);
        } else {
index fbc61028d2db4dfdc204cd4a78e2613cadd78c3c..1ca0df2f664c9e711a6ec0977ff76b8fce60036e 100644 (file)
@@ -16,6 +16,7 @@ In addition to having notmuch, you need:
 - xz. Some speedup can be gotten by installing "pixz", but this is
   probably only worthwhile if you are debugging the tests.
 - valgrind (for the memory tests)
+- perf (optional, for more fine-grained timing)
 
 Getting set up to run tests:
 ----------------------------
@@ -23,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").
 
@@ -56,11 +57,24 @@ supports the following arguments
 
 --small / --medium / --large   Choose corpus size.
 --debug                                Enable debugging. In particular don't delete
-                               temporary directories.
+                                temporary directories.
+--perf                          Run perf record in place of /usr/bin/time. Perf output can be
+                                found in a log directory.
+--call-graph {fp,lbr,dwarf}     Call graph option for perf record. Default is 'lbr'.
 
 When using the make targets, you can pass arguments to all test
 scripts by defining the make variable OPTIONS.
 
+Log Directory
+-------------
+
+The memory tests, and the time tests when option '--perf' is given
+save their output in a directory named as follows
+
+     log.$test_name-$corpus_size-$timestamp
+
+These directories are removed by "make clean".
+
 Writing tests
 -------------
 
index a14dd13f1477a7cab465e1ca6c3fefe88b53c690..de260b2d7116e41c5e31a3836bf18c2fb072a310 100755 (executable)
@@ -5,16 +5,16 @@ test_description='notmuch new'
 . $(dirname "$0")/perf-test-lib.sh || exit 1
 
 uncache_database
-
 time_start
 
+manifest=$(mktemp manifestXXXXXX)
+find mail -type f ! -path 'mail/.notmuch/*' | sed -n '1~4 p' > $manifest
+xargs tar uf backup.tar < $manifest
+
 for i in $(seq 2 6); do
     time_run "notmuch new #$i" 'notmuch new'
 done
 
-manifest=$(mktemp manifestXXXXXX)
-
-find mail -type f ! -path 'mail/.notmuch/*' | sed -n '1~4 p' > $manifest
 # arithmetic context is to eat extra whitespace on e.g. some BSDs
 count=$((`wc -l < $manifest`))
 
@@ -26,6 +26,14 @@ perl -nle 'rename "$_.renamed", $_' $manifest
 
 time_run "new ($count mv back)" 'notmuch new'
 
+perl -nle 'unlink $_; unlink $_.copy' $manifest
+
+time_run "new ($count rm)" 'notmuch new'
+
+tar xf backup.tar
+
+time_run "new ($count restore)" 'notmuch new'
+
 perl -nle 'link $_, "$_.copy"' $manifest
 
 time_run "new ($count cp)" 'notmuch new'
index 9c895d6a910145266dde528039a61caabcc8fa70..47fdb0c27ab54df243403ba5a1cfe3595d256c8b 100755 (executable)
@@ -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
index 8db52a33a9e2d8c78ca3720bad83ebfceea07079..b58950d72b606927f36ec116357f8f456b907a98 100755 (executable)
@@ -10,4 +10,32 @@ time_run 'reindex *' "notmuch reindex '*'"
 time_run 'reindex *' "notmuch reindex '*'"
 time_run 'reindex *' "notmuch reindex '*'"
 
+manifest=$(mktemp manifestXXXXXX)
+
+find mail -type f ! -path 'mail/.notmuch/*' | sed -n '1~4 p' > $manifest
+# arithmetic context is to eat extra whitespace on e.g. some BSDs
+count=$((`wc -l < $manifest`))
+
+xargs tar uf backup.tar < $manifest
+
+perl -nle 'rename $_, "$_.renamed"' $manifest
+
+time_run "reindex ($count mv)" "notmuch reindex '*'"
+
+perl -nle 'rename "$_.renamed", $_' $manifest
+
+time_run "reindex ($count mv back)" "notmuch reindex '*'"
+
+perl -nle 'unlink $_; unlink $_.copy' $manifest
+
+time_run "reindex ($count rm)" "notmuch reindex '*'"
+
+tar xf backup.tar
+
+time_run "reindex ($count restore)" "notmuch reindex '*'"
+
+perl -nle 'link $_, "$_.copy"' $manifest
+
+time_run "reindex ($count cp)" "notmuch reindex '*'"
+
 time_done
diff --git a/performance-test/T05-ruby.sh b/performance-test/T05-ruby.sh
new file mode 100755 (executable)
index 0000000..527ab28
--- /dev/null
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+test_description='ruby bindings'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+if [ "${NOTMUCH_HAVE_RUBY_DEV}" = "0" ]; then
+    echo "missing prerequisites: ruby development files"
+    exit 0
+fi
+
+time_start
+
+time_run 'print all messages' "$NOTMUCH_RUBY -I '$NOTMUCH_BUILDDIR/bindings/ruby' <<'EOF'
+require 'notmuch'
+db = Notmuch::Database.new('$MAIL_DIR')
+100.times.each do
+    db.query('').search_messages.each do |msg|
+       puts msg.message_id
+    end
+end
+EOF"
+
+time_done
diff --git a/performance-test/T06-emacs.sh b/performance-test/T06-emacs.sh
new file mode 100755 (executable)
index 0000000..c92bbd6
--- /dev/null
@@ -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 (executable)
index 0000000..11dfec0
--- /dev/null
@@ -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 (file)
index 0000000..2318c2f
--- /dev/null
@@ -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-----
index b70288ccaf316158b5c6ab16a46bff799c1b46a0..c34f8cd6ba975d4779c69313ab19b7af6697d616 100644 (file)
@@ -1,6 +1,9 @@
 . $(dirname "$0")/version.sh || exit 1
 
+debug=""
 corpus_size=large
+perf_callgraph=lbr
+use_perf=0
 
 while test "$#" -ne 0
 do
@@ -9,6 +12,15 @@ do
                debug=t;
                shift
                ;;
+       -p|--perf)
+               use_perf=1;
+               shift
+               ;;
+       -c|--call-graph)
+               shift
+               perf_callgraph=$1
+               shift
+               ;;
        -s|--small)
                corpus_size=small;
                shift
@@ -29,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
 
@@ -127,10 +141,20 @@ notmuch_new_with_cache ()
     fi
 }
 
+make_log_dir () {
+    local timestamp=$(date +%Y%m%dT%H%M%S)
+    log_dir=${TEST_DIRECTORY}/log.$(basename $0)-$corpus_size-${timestamp}
+    mkdir -p "${log_dir}"
+}
+
 time_start ()
 {
     add_email_corpus
 
+    if [[ "$use_perf" = 1 ]]; then
+       make_log_dir
+    fi
+
     print_header
 
     notmuch_new_with_cache time_run
@@ -140,9 +164,7 @@ memory_start ()
 {
     add_email_corpus
 
-    local timestamp=$(date +%Y%m%dT%H%M%S)
-    log_dir="${TEST_DIRECTORY}/log.$(basename $0)-$corpus_size-${timestamp}"
-    mkdir -p ${log_dir}
+    make_log_dir
 
     notmuch_new_with_cache memory_run
 }
@@ -188,12 +210,23 @@ 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"
     test_count=$(($test_count+1))
-    if test "$verbose" != "t"; then exec 4>test.output 3>&4; fi
-    if ! eval >&3 "/usr/bin/time -f '%e\t%U\t%S\t%M\t%I/%O' $2" ; then
+    if test "$verbose" != "t"; then exec 4>test.output 3>&4; else exec 3>&1; fi
+    if [[ "$use_perf" = 1 ]]; then
+       command_str="perf record --call-graph=${perf_callgraph} -o ${log_dir}/${test_count}.perf $2"
+    else
+       command_str="/usr/bin/time -f '%e\t%U\t%S\t%M\t%I/%O' $2"
+    fi
+
+    if ! eval >&3 "$command_str" ; then
        test_failure=$(($test_failure + 1))
        return 1
     fi
index f02527a7061f05c5135263e6ff483efafd5f2af4..357b9dad2e8c2828ca75bcca4845115b453b612b 100644 (file)
@@ -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
index c7f4851cdd53bc6231d7a56e1d19e80834a09536..502f89fbb70ef8424543009278dc9d8c23b17f74 100644 (file)
@@ -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;
 }
index 63b254284b4a6d73861896587b12bc2a6281144f..e37cb1f97e30cc48366fdf7fc4ae0c00fe6efa0d 100644 (file)
@@ -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;
 }
index c75ec5be1cbc5632f56332b40563c87959367547..99330a94768605ebbd9977ae2563b90c7cda4e15 100644 (file)
@@ -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;
 
index 528d8a2db332bdaea6801424a15f7ca42db673ec..fd08641cf0a4aa792ab969524fe3560e01998648 100644 (file)
@@ -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
index 10f127cbbef171be491b3bc8f5b742b8e8c67111..a81808b164f0cec0ec6c0010cb023fe77d7e098c 100644 (file)
@@ -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
index 7fbdcfa3036cd860b13647a1678efde2063ce621..642f918d20398c4b9b8f028183cfd10e130da5e8 100755 (executable)
@@ -33,7 +33,7 @@ test_begin_subtest 'failure to clean up causes the test to fail'
 test_expect_code 2 'test_when_finished "(exit 2)"'
 
 EXPECTED=$NOTMUCH_SRCDIR/test/test.expected-output
-suppress_diff_date() {
+suppress_diff_date () {
     sed -e 's/\(.*\-\-\- test-verbose\.4\.\expected\).*/\1/' \
        -e 's/\(.*\+\+\+ test-verbose\.4\.\output\).*/\1/'
 }
@@ -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$,,')"
index da45d3aecf7ae7a0c6e86be06f6d21155c50129b..827edc149a727bb5427fcb53d8a11ab6516b9bc1 100755 (executable)
@@ -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'
 
index b22d8f291f5c0dfae01aeae5274281fca3112857..621e0b69721091a5f7383db7db726004fadc2c4f 100755 (executable)
@@ -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,10 +53,13 @@ cat <<EOF > EXPECTED
 built_with.compact=something
 built_with.field_processor=something
 built_with.retry_lock=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
@@ -65,6 +70,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
+\rother'
+output=$(notmuch config get foo.bar)
+test_expect_equal "${output}" "thing
+\rother"
+
 test_begin_subtest "Top level --config=FILE option"
 cp "${NOTMUCH_CONFIG}" alt-config
 notmuch --config=alt-config config set user.name "Another Name"
@@ -117,12 +151,12 @@ test_expect_equal "$(notmuch config get database.path)" \
 
 ln -s `pwd`/mail home/Maildir
 add_email_corpus
-test_begin_subtest "Relative database path expanded in open"
+test_begin_subtest "Relative database path expanded"
 notmuch config set database.path Maildir
-path=$(notmuch config get database.path)
+path=$(notmuch config get database.path | notmuch_dir_sanitize)
 count=$(notmuch count '*')
 test_expect_equal "${path} ${count}" \
-                 "Maildir 52"
+                 "CWD/home/Maildir 52"
 
 test_begin_subtest "Add config to database"
 notmuch new
@@ -144,4 +178,34 @@ notmuch config set --database ${key} ${value}
 output=$(notmuch config get ${key})
 test_expect_equal "${output}" "${value}"
 
+test_begin_subtest "set built_with.* yields error"
+test_expect_code 1 "notmuch config set built_with.compact false"
+
+test_begin_subtest "get built_with.{compact,field_processor} prints true"
+for key in compact field_processor; do
+    notmuch config get built_with.${key}
+done > OUTPUT
+cat <<EOF > EXPECTED
+true
+true
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get built_with.nonexistent prints false"
+output=$(notmuch config get built_with.nonexistent)
+test_expect_equal "$output" "false"
+
+test_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 <<EOF > 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
index 42c621c87b26c216c8172982f939c2b12dc0dacd..39846d34b7f32f3912619bf49d30d90c4ab67f4d 100755 (executable)
@@ -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
index 2985e24c66beb9ce13816ebdacf85c5a9638e139..52888be22a86acdf15a83a532c5d80316b738aae 100755 (executable)
@@ -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 <<EOF >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,23 @@ 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 <<EOF > 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:"
+NOTMUCH_NEW > OUTPUT
+output=$(notmuch search --output=messages to:undisclosed-recipients)
+test_expect_equal "${output}" "${gen_msg_id}"
+
 test_begin_subtest "Long directory names don't cause rescan"
 test_subtest_known_broken
 printf -v name 'z%.0s' {1..234}
@@ -354,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 <<EOF > 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<<EOF
+#include <notmuch-test.h>
 
-# 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 <<EOF > 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 <<EOF > EXPECTED
 Unexpected error with file ${MAIL_DIR}/vanish
 add_file: Something went wrong trying to read or write a file
@@ -394,6 +433,30 @@ exit status: 75
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "Relative database path expanded in new"
+ln -s "$PWD/mail" home/Maildir
+notmuch config set database.path Maildir
+generate_message
+NOTMUCH_NEW > OUTPUT
+cat <<EOF >EXPECTED
+Added 1 new message to the database.
+EOF
+notmuch config set database.path ${MAIL_DIR}
+rm home/Maildir
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Relative mail root (in db) expanded in new"
+ln -s "$PWD/mail" home/Maildir
+notmuch config set --database database.mail_root Maildir
+generate_message
+NOTMUCH_NEW > OUTPUT
+cat <<EOF >EXPECTED
+Added 1 new message to the database.
+EOF
+notmuch config set database.mail_root
+rm home/Maildir
+test_expect_equal_file EXPECTED OUTPUT
+
 add_email_corpus broken
 test_begin_subtest "reference loop does not crash"
 test_expect_code 0 "notmuch show --format=json id:mid-loop-12@example.org id:mid-loop-21@example.org > OUTPUT"
diff --git a/test/T051-new-renames.sh b/test/T051-new-renames.sh
new file mode 100755 (executable)
index 0000000..ebd06be
--- /dev/null
@@ -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
index 2045a55568da7f7b5e9d5cc3c120e4113df7662e..1feb5624c41b8af06fab0750f7bc70395b7a4504 100755 (executable)
@@ -16,6 +16,8 @@ restore_config () {
     unset DATABASE_PATH
     unset NOTMUCH_PROFILE
     unset XAPIAN_PATH
+    unset MAILDIR
+    rm -f "$HOME/mail"
     cp notmuch-config-backup.${test_name} ${NOTMUCH_CONFIG}
 }
 
@@ -42,6 +44,30 @@ symlink_config () {
     unset DATABASE_PATH
 }
 
+
+home_mail_config () {
+    local dir
+    backup_config
+    dir="${HOME}/mail"
+    ln -s $MAIL_DIR $dir
+    notmuch config set database.path
+    notmuch config set database.mail_root
+    XAPIAN_PATH="$MAIL_DIR/.notmuch/xapian"
+    unset DATABASE_PATH
+}
+
+maildir_env_config () {
+    local dir
+    backup_config
+    dir="${HOME}/env_points_here"
+    ln -s $MAIL_DIR $dir
+    export MAILDIR=$dir
+    notmuch config set database.path
+    notmuch config set database.mail_root
+    XAPIAN_PATH="${MAIL_DIR}/.notmuch/xapian"
+    unset DATABASE_PATH
+}
+
 xdg_config () {
     local dir
     local profile=${1:-default}
@@ -66,7 +92,20 @@ xdg_config () {
     notmuch --config=${CONFIG_PATH} config set database.path
 }
 
-for config in traditional split XDG XDG+profile symlink; 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
 
@@ -90,6 +129,15 @@ for config in traditional split XDG XDG+profile symlink; do
        symlink)
            symlink_config
            ;;
+       home_mail)
+           home_mail_config
+           ;;
+       maildir_env)
+           maildir_env_config
+           ;;
+       mailroot_only)
+           mailroot_only_config
+           ;;
     esac
 
     test_begin_subtest "count ($config)"
@@ -125,13 +173,13 @@ EOF
     notmuch tag -inbox '*'
     notmuch restore < EXPECTED
     notmuch dump > OUTPUT
-    test_expect_equal_file EXPECTED OUTPUT
+    test_expect_equal_file_nonempty EXPECTED OUTPUT
 
     test_begin_subtest "reindex ($config)"
     notmuch search --output=messages '*' > EXPECTED
     notmuch reindex '*'
     notmuch search --output=messages '*' > OUTPUT
-    test_expect_equal_file EXPECTED OUTPUT
+    test_expect_equal_file_nonempty EXPECTED OUTPUT
 
     test_begin_subtest "use existing database ($config)"
     output=$(notmuch new)
@@ -153,7 +201,7 @@ EOF
     test_begin_subtest "Show a raw message ($config)"
     add_message
     notmuch show --format=raw id:$gen_msg_id > OUTPUT
-    test_expect_equal_file $gen_msg_filename OUTPUT
+    test_expect_equal_file_nonempty $gen_msg_filename OUTPUT
     rm -f $gen_msg_filename
 
     test_begin_subtest "reply ($config)"
@@ -174,6 +222,7 @@ On Tue, 05 Jan 2010 15:43:56 -0000, Sender <sender@example.com> wrote:
 > basic reply test
 EOF
     test_expect_equal_file EXPECTED OUTPUT
+
     test_begin_subtest "insert+search ($config)"
     generate_message \
        "[subject]=\"insert-subject\"" \
@@ -182,14 +231,13 @@ EOF
     mkdir -p "$MAIL_DIR"/{cur,new,tmp}
     notmuch insert < "$gen_msg_filename"
     cur_msg_filename=$(notmuch search --output=files "subject:insert-subject")
-    test_expect_equal_file "$cur_msg_filename" "$gen_msg_filename"
-
+    test_expect_equal_file_nonempty "$cur_msg_filename" "$gen_msg_filename"
 
     test_begin_subtest "compact+search ($config)"
     notmuch search --output=messages '*' | sort > EXPECTED
     notmuch compact
     notmuch search --output=messages '*' | sort > OUTPUT
-    test_expect_equal_file EXPECTED OUTPUT
+    test_expect_equal_file_nonempty EXPECTED OUTPUT
 
     test_begin_subtest "upgrade backup ($config)"
     features=$(xapian-metadata get $XAPIAN_PATH features | grep -v "^relative directory paths")
@@ -234,17 +282,24 @@ EOF
    test_expect_equal "${output}+${output2}" "${value}+"
 
    test_begin_subtest "Config list ($config)"
-   notmuch config list | notmuch_dir_sanitize | sed -e "s/^database.backup_dir=.*$/database.backup_dir/"  \
-                                                   -e "s/^database.hook_dir=.*$/database.hook_dir/" \
-                                                   -e "s/^database.path=.*$/database.path/" > OUTPUT
+   notmuch config list | notmuch_config_sanitize | \
+       sed -e "s/^database.backup_dir=.*$/database.backup_dir/"  \
+          -e "s/^database.hook_dir=.*$/database.hook_dir/" \
+          -e "s/^database.path=.*$/database.path/"  \
+          -e "s,^database.mail_root=CWD/home/mail,database.mail_root=MAIL_DIR," \
+          -e "s,^database.mail_root=CWD/home/env_points_here,database.mail_root=MAIL_DIR," \
+          > OUTPUT
    cat <<EOF > EXPECTED
-built_with.compact=true
-built_with.field_processor=true
-built_with.retry_lock=true
+built_with.compact=something
+built_with.field_processor=something
+built_with.retry_lock=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
@@ -255,6 +310,27 @@ user.primary_email=test_suite@notmuchmail.org
 EOF
    test_expect_equal_file EXPECTED OUTPUT
 
+   test_begin_subtest "Config list from python ($config)"
+   test_python <<EOF > OUTPUT
+from notmuch2 import Database
+db=Database(config=Database.CONFIG.SEARCH)
+for key in list(db.config):
+    print(key)
+EOF
+   cat <<EOF > EXPECTED
+database.autocommit
+database.backup_dir
+database.hook_dir
+database.mail_root
+database.path
+maildir.synchronize_flags
+new.tags
+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)"
@@ -268,8 +344,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 <<EOF
+from notmuch2 import Database
+db=Database(config=Database.CONFIG.SEARCH)
+m=db.find('20091117232137.GA7669@griffis1.net')
+to=m.header('To')
+print(to)
+EOF
+          test_expect_equal_file EXPECTED OUTPUT
+
+          test_begin_subtest ".notmuch not ignored in split config ($config)"
+          test_subtest_known_broken
+          generate_message '[dir]=.notmuch/cur' '[subject]="Do not ignore, very important"'
+          NOTMUCH_NEW > OUTPUT
+          notmuch search subject:Do-not-ignore | notmuch_search_sanitize >> OUTPUT
+          cat <<EOF > 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
index a1ebf8ba527ea30948c3abd79427a6fa5e83277b..6e855b591af5594677c73d1d0a7271e0eda05ece 100755 (executable)
@@ -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 <<EOF > 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<<EOF
+#include <notmuch-test.h>
+
+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 <<EOF > EXPECTED
 notmuch count: A Xapian exception occurred
 A Xapian exception occurred performing query
@@ -154,4 +157,28 @@ print("4: {} messages".format(query.count_messages()))
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then
+
+    test_begin_subtest "and of exact terms (query=sexp)"
+    output=$(notmuch count --query=sexp '(and "wonderful" "wizard")')
+    test_expect_equal "$output" 1
+
+    test_begin_subtest "or of exact terms (query=sexp)"
+    output=$(notmuch count --query=sexp '(or "php" "wizard")')
+    test_expect_equal "$output" 2
+
+    test_begin_subtest "starts-with, case-insensitive (query=sexp)"
+    output=$(notmuch count --query=sexp '(starts-with FreeB)')
+    test_expect_equal "$output" 5
+
+    test_begin_subtest "query that matches no messages (query=sexp)"
+    count=$(notmuch count --query=sexp '(and (from keithp) (to keithp))')
+    test_expect_equal 0 "$count"
+
+    test_begin_subtest "Compound subquery (query=sexp)"
+    output=$(notmuch count --query=sexp '(thread (of (from keithp) (subject Maildir)))')
+    test_expect_equal "$output" 7
+
+fi
+
 test_done
index b37a9b672bbad63bc34751f14c7363a6e3f5650d..73953272bf505edaf5f4b2ac885afa0c9e50fcfe 100755 (executable)
@@ -15,7 +15,7 @@ notmuch new > /dev/null
 # They happen to be in the mail directory already but that is okay
 # since we do not call notmuch new hereafter.
 
-gen_insert_msg() {
+gen_insert_msg () {
     generate_message \
        "[subject]=\"insert-subject\"" \
        "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" \
@@ -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 <<EOF >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
index a3f0dead1420239fd654c89ea04531d1ee141729..515eb88f34b3f9c69bca5b13cd6c5cc9c1b8c44d 100755 (executable)
@@ -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
new file mode 100755 (executable)
index 0000000..8800b54
--- /dev/null
@@ -0,0 +1,1322 @@
+#!/usr/bin/env bash
+test_description='"notmuch search" in several variations'
+. $(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
+
+add_email_corpus
+
+for query in '()' '(not)' '(and)' '(or ())' '(or (not))' '(or (and))' \
+            '(or (and) (or) (not (and)))'; do
+    test_begin_subtest "all messages: $query"
+    notmuch search '*' > EXPECTED
+    notmuch search --query=sexp "$query" > OUTPUT
+    test_expect_equal_file EXPECTED OUTPUT
+done
+
+for query in '(or)' '(not ())' '(not (not))' '(not (and))' \
+                   '(not (or (and) (or) (not (and))))'; do
+    test_begin_subtest "no messages: $query"
+    notmuch search --query=sexp "$query" > OUTPUT
+    test_expect_equal_file /dev/null OUTPUT
+done
+
+test_begin_subtest "and of exact terms"
+notmuch search --query=sexp '(and "wonderful" "wizard")' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > 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 "and of stemmed terms"
+notmuch search --query=sexp '(and wonderful wizard)' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > 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 <<EOF > 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 "or of exact terms via field processor"
+notmuch search  'sexp:"(or ""php"" ""wizard"")"' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > 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 <<EOF > 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 "single term in body (case insensitive)"
+notmuch search --query=sexp 'Wizard' | notmuch_search_sanitize>OUTPUT
+cat <<EOF > 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 "single term in body, stemmed version"
+notmuch search arriv > EXPECTED
+notmuch search --query=sexp arriv > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "single term in body, unstemmed version"
+notmuch search --query=sexp '"arriv"' > OUTPUT
+test_expect_equal_file /dev/null OUTPUT
+
+test_begin_subtest "Search by 'subject'"
+add_message [subject]=subjectsearchtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(notmuch search --query=sexp '(subject subjectsearchtest)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; subjectsearchtest (inbox unread)"
+
+test_begin_subtest "Search by 'subject' (case insensitive)"
+notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(subject "Maildir")' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'subject' (utf-8):"
+add_message [subject]=utf8-sübjéct '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(notmuch search --query=sexp '(subject utf8 sübjéct)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread)"
+
+test_begin_subtest "Search by 'subject' (utf-8, and):"
+output=$(notmuch search --query=sexp '(subject (and utf8 sübjéct))' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread)"
+
+test_begin_subtest "Search by 'subject' (utf-8, and outside):"
+output=$(notmuch search --query=sexp '(and (subject utf8) (subject sübjéct))' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread)"
+
+test_begin_subtest "Search by 'subject' (utf-8, or):"
+notmuch search --query=sexp '(subject (or utf8 subjectsearchtest))' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+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)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'subject' (utf-8, or outside):"
+notmuch search --query=sexp '(or (subject utf8) (subject subjectsearchtest))' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+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)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'attachment'"
+notmuch search attachment:notmuch-help.patch > EXPECTED
+notmuch search --query=sexp '(attachment notmuch-help.patch)' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'body'"
+add_message '[subject]="body search"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [body]=bodysearchtest
+output=$(notmuch search --query=sexp '(body bodysearchtest)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; body search (inbox unread)"
+
+test_begin_subtest "Search by 'body' (phrase)"
+add_message '[subject]="body search (phrase)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="body search (phrase)"'
+add_message '[subject]="negative result"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="This phrase should not match the body search"'
+output=$(notmuch search --query=sexp '(body "body search phrase")' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; body search (phrase) (inbox unread)"
+
+test_begin_subtest "Search by 'body' (utf-8):"
+add_message '[subject]="utf8-message-body-subject"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="message body utf8: bödý"'
+output=$(notmuch search --query=sexp '(body bödý)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; utf8-message-body-subject (inbox unread)"
+
+add_message "[body]=thebody-1" "[subject]=kryptonite-1"
+add_message "[body]=nothing-to-see-here-1" "[subject]=thebody-1"
+
+test_begin_subtest 'search without body: prefix'
+notmuch search thebody > EXPECTED
+notmuch search --query=sexp '(and thebody)' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'negated body: prefix'
+notmuch search thebody and not body:thebody > EXPECTED
+notmuch search --query=sexp '(and (not (body thebody)) thebody)' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'search unprefixed for prefixed term'
+notmuch search kryptonite > EXPECTED
+notmuch search --query=sexp '(and kryptonite)' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'search with body: prefix for term only in subject'
+notmuch search body:kryptonite > EXPECTED
+notmuch search --query=sexp '(body kryptonite)' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'from'"
+add_message '[subject]="search by from"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom
+output=$(notmuch search --query=sexp '(from searchbyfrom)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] searchbyfrom; search by from (inbox unread)"
+
+test_begin_subtest "Search by 'from' (address)"
+add_message '[subject]="search by from (address)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom@example.com
+output=$(notmuch search --query=sexp '(from searchbyfrom@example.com)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] searchbyfrom@example.com; search by from (address) (inbox unread)"
+
+test_begin_subtest "Search by 'from' (name)"
+add_message '[subject]="search by from (name)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[from]="Search By From Name <test@example.com>"'
+output=$(notmuch search --query=sexp '(from "Search By From Name")' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Search By From Name; search by from (name) (inbox unread)"
+
+test_begin_subtest "Search by 'from' (name and address)"
+output=$(notmuch search --query=sexp '(from "Search By From Name <test@example.com>")' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Search By From Name; search by from (name) (inbox unread)"
+
+add_message '[dir]=bad' '[subject]="To the bone"'
+add_message '[dir]=.' '[subject]="Top level"'
+add_message '[dir]=bad/news' '[subject]="Bears"'
+mkdir -p "${MAIL_DIR}/duplicate/bad/news"
+cp "$gen_msg_filename" "${MAIL_DIR}/duplicate/bad/news"
+
+add_message '[dir]=things' '[subject]="These are a few"'
+add_message '[dir]=things/favorite' '[subject]="Raindrops, whiskers, kettles"'
+add_message '[dir]=things/bad' '[subject]="Bites, stings, sad feelings"'
+
+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': top level."
+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)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by id (inbox unread)"
+
+test_begin_subtest "Search by 'id' (or)"
+add_message '[subject]="search by id"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(notmuch search --query=sexp "(id non-existent-mid ${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 "Search by 'is' (multiple)"
+notmuch tag -inbox tag:searchbytag
+notmuch search is:inbox AND is:unread | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(is inbox unread)' | notmuch_search_sanitize > OUTPUT
+notmuch tag +inbox tag:searchbytag
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'mid'"
+add_message '[subject]="search by mid"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(notmuch search --query=sexp "(mid ${gen_msg_id})" | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by mid (inbox unread)"
+
+test_begin_subtest "Search by 'mid' (or)"
+add_message '[subject]="search by mid"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(notmuch search --query=sexp "(mid non-existent-mid ${gen_msg_id})" | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by mid (inbox unread)"
+
+test_begin_subtest "Search by 'mimetype'"
+notmuch search mimetype:text/html > EXPECTED
+notmuch search --query=sexp '(mimetype text html)'  > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+QUERYSTR="date:2009-11-18..2009-11-18 and tag:unread"
+QUERYSTR2="query:test and subject:Maildir"
+notmuch config set --database query.test "$QUERYSTR"
+notmuch config set query.test2 "$QUERYSTR2"
+
+test_begin_subtest "ill-formed named query search"
+notmuch search --query=sexp '(query)' > OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'query' expects single atom as argument
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "ill-formed named query search 2"
+notmuch search --query=sexp '(to (query))' > OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'query' not supported inside 'to'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search named query"
+notmuch search --query=sexp '(query test)' > OUTPUT
+notmuch search $QUERYSTR > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'subject' (utf-8, phrase-token):"
+output=$(notmuch search --query=sexp '(subject utf8-sübjéct)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread)"
+
+test_begin_subtest "search named query with other terms"
+notmuch search --query=sexp '(and (query test) (subject Maildir))' > OUTPUT
+notmuch search $QUERYSTR and subject:Maildir > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search nested named query"
+notmuch search --query=sexp '(query test2)' > OUTPUT
+notmuch search $QUERYSTR2 > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'subject' (utf-8, quoted string):"
+output=$(notmuch search --query=sexp '(subject "utf8 sübjéct")' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread)"
+
+test_begin_subtest "Search by 'subject' (combine phrase, term):"
+output=$(notmuch search --query=sexp '(subject Mac "compatibility issues")' | 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 "Search by 'subject' (combine phrase, term 2):"
+notmuch search --query=sexp '(subject (or utf8 "compatibility issues"))' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (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; utf8-message-body-subject (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'subject' (combine phrase, term 3):"
+notmuch search --query=sexp '(subject issues X/Darwin)' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'tag'"
+add_message '[subject]="search by tag"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+notmuch tag +searchbytag id:${gen_msg_id}
+output=$(notmuch search --query=sexp '(tag searchbytag)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread)"
+
+test_begin_subtest "Search by 'tag' (multiple)"
+notmuch tag -inbox tag:searchbytag
+notmuch search tag:inbox AND tag:unread | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(tag inbox unread)' | notmuch_search_sanitize > OUTPUT
+notmuch tag +inbox tag:searchbytag
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'tag' and 'subject'"
+notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(and (tag inbox) (subject maildir))' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search by 'thread'"
+add_message '[subject]="search by thread"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+thread_id=$(notmuch search id:${gen_msg_id} | sed -e "s/thread:\([a-f0-9]*\).*/\1/")
+output=$(notmuch search --query=sexp "(thread ${thread_id})" | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by thread (inbox unread)"
+
+test_begin_subtest "Search by 'to'"
+add_message '[subject]="search by to"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto
+output=$(notmuch search --query=sexp '(to searchbyto)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by to (inbox unread)"
+
+test_begin_subtest "Search by 'to' (address)"
+add_message '[subject]="search by to (address)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto@example.com
+output=$(notmuch search --query=sexp '(to searchbyto@example.com)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by to (address) (inbox unread)"
+
+test_begin_subtest "Search by 'to' (name)"
+add_message '[subject]="search by to (name)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[to]="Search By To Name <test@example.com>"'
+output=$(notmuch search --query=sexp '(to "Search By To Name")' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by to (name) (inbox unread)"
+
+test_begin_subtest "Search by 'to' (name and address)"
+output=$(notmuch search --query=sexp '(to "Search By To Name <test@example.com>")' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by to (name) (inbox unread)"
+
+test_begin_subtest "starts-with, no prefix"
+output=$(notmuch search --query=sexp '(starts-with prelim)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2009-11-17 [2/2] Alex Botero-Lowry, Carl Worth; [notmuch] preliminary FreeBSD support (attachment inbox unread)"
+
+test_begin_subtest "starts-with, case-insensitive"
+notmuch search --query=sexp '(starts-with FreeB)' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-18 [3/4] Alexander Botero-Lowry, Jjgod Jiang; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)
+thread:XXX   2009-11-17 [2/2] Alex Botero-Lowry, Carl Worth; [notmuch] preliminary FreeBSD support (attachment inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, no prefix, all messages"
+notmuch search --query=sexp '(starts-with "")' | notmuch_search_sanitize > OUTPUT
+notmuch search '*' | notmuch_search_sanitize > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, attachment"
+output=$(notmuch search --query=sexp '(attachment (starts-with not))' | notmuch_search_sanitize)
+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_search_files_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+MAIL_DIR/bad/msg-XXX
+MAIL_DIR/bad/news/msg-XXX
+MAIL_DIR/duplicate/bad/news/msg-XXX
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, from"
+notmuch search --query=sexp '(from (starts-with Mik))' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-17 [1/1] Mikhail Gusarov; [notmuch] [PATCH] Handle rename of message file (inbox unread)
+thread:XXX   2009-11-17 [2/7] Mikhail Gusarov| Lars Kellogg-Stedman, Keith Packard, Carl Worth; [notmuch] Working with Maildir storage? (inbox signed unread)
+thread:XXX   2009-11-17 [2/5] Mikhail Gusarov| Carl Worth, Keith Packard; [notmuch] [PATCH 2/2] Include <stdint.h> to get uint32_t in C++ file with gcc 4.4 (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, id"
+notmuch search --query=sexp --output=messages '(id (starts-with 877))' > OUTPUT
+cat <<EOF > EXPECTED
+id:877h1wv7mg.fsf@inf-8657.int-evry.fr
+id:877htoqdbo.fsf@yoom.home.cworth.org
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, is"
+output=$(notmuch search --query=sexp '(is (starts-with searchby))' | notmuch_search_sanitize)
+test_expect_equal "$output" 'thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread)'
+
+test_begin_subtest "starts-with, mid"
+notmuch search --query=sexp --output=messages '(mid (starts-with 877))' > OUTPUT
+cat <<EOF > EXPECTED
+id:877h1wv7mg.fsf@inf-8657.int-evry.fr
+id:877htoqdbo.fsf@yoom.home.cworth.org
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, mimetype"
+notmuch search --query=sexp '(mimetype (starts-with sig))' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-18 [2/2] Lars Kellogg-Stedman; [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread)
+thread:XXX   2009-11-18 [4/7] Lars Kellogg-Stedman, Mikhail Gusarov| Keith Packard, Carl Worth; [notmuch] Working with Maildir storage? (inbox signed unread)
+thread:XXX   2009-11-17 [1/3] Adrian Perez de Castro| Keith Packard, Carl Worth; [notmuch] Introducing myself (inbox signed unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+add_message '[subject]="message with properties"'
+notmuch restore <<EOF
+#= ${gen_msg_id} foo=bar
+EOF
+
+test_begin_subtest "starts-with, property"
+notmuch search --query=sexp '(property (starts-with foo=))' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; message with properties (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, subject"
+notmuch search --query=sexp '(subject (starts-with FreeB))' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-17 [2/2] Alex Botero-Lowry, Carl Worth; [notmuch] preliminary FreeBSD support (attachment inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, tag"
+output=$(notmuch search --query=sexp '(tag (starts-with searchby))' | notmuch_search_sanitize)
+test_expect_equal "$output" 'thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread)'
+
+add_message '[subject]="no tags"'
+notag_mid=${gen_msg_id}
+notmuch tag -unread -inbox id:${notag_mid}
+
+test_begin_subtest "negated starts-with, tag"
+output=$(notmuch search --query=sexp '(tag (not (starts-with in)))' | notmuch_search_sanitize)
+test_expect_equal "$output" 'thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; no tags ()'
+
+test_begin_subtest "negated starts-with, tag 2"
+output=$(notmuch search --query=sexp '(not (tag (starts-with in)))' | notmuch_search_sanitize)
+test_expect_equal "$output" 'thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; no tags ()'
+
+test_begin_subtest "negated starts-with, tag 3"
+output=$(notmuch search --query=sexp '(not (tag (starts-with "")))' | notmuch_search_sanitize)
+test_expect_equal "$output" 'thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; no tags ()'
+
+test_begin_subtest "starts-with, thread"
+notmuch search --query=sexp '(thread (starts-with "00"))' > OUTPUT
+notmuch search '*' > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, to"
+notmuch search --query=sexp '(to (starts-with "search"))' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+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; search by to (address) (inbox unread)
+thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by to (name) (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "wildcard search for 'is'"
+notmuch search not id:${notag_mid} > EXPECTED
+notmuch search --query=sexp '(is *)' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "negated wildcard search for 'is'"
+notmuch search id:${notag_mid} > EXPECTED
+notmuch search --query=sexp '(not (is *))' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "wildcard search for 'property'"
+notmuch search property:foo=bar > EXPECTED
+notmuch search --query=sexp '(property *)' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "wildcard search for 'tag'"
+notmuch search not id:${notag_mid} > EXPECTED
+notmuch search --query=sexp '(tag *)' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "negated wildcard search for 'tag'"
+notmuch search id:${notag_mid} > EXPECTED
+notmuch search --query=sexp '(not (tag *))' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+add_message '[subject]="message with tag \"*\""'
+notmuch tag '+*' id:${gen_msg_id}
+
+test_begin_subtest "search for 'tag' \"*\""
+output=$(notmuch search --query=sexp --output=messages '(tag "*")')
+test_expect_equal "$output" "id:$gen_msg_id"
+
+test_begin_subtest "search for missing / empty to"
+add_message [to]="undisclosed-recipients:"
+notmuch search --query=sexp '(not (to *))' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; search for missing / empty to (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Unbalanced parens"
+# A code 1 indicates the error was handled (a crash will return e.g. 139).
+test_expect_code 1 "notmuch search --query=sexp '('"
+
+test_begin_subtest "Unbalanced parens, error message"
+notmuch search --query=sexp '(' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+invalid s-expression: '('
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "unknown prefix"
+notmuch search --query=sexp '(foo)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+unknown prefix 'foo'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "list as prefix"
+notmuch search --query=sexp '((foo))' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+unexpected list in field/operation position
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "illegal nesting"
+notmuch search --query=sexp '(subject (subject foo))' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+nested field: 'subject' inside 'subject'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, no argument"
+notmuch search --query=sexp '(starts-with)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'starts-with' expects single atom as argument
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, list argument"
+notmuch search --query=sexp '(starts-with (stuff))' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'starts-with' expects single atom as argument
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, too many arguments"
+notmuch search --query=sexp '(starts-with a b c)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'starts-with' expects single atom as argument
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "starts-with, illegal field"
+notmuch search --query=sexp '(body (starts-with foo))' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'body' does not support wildcard queries
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "wildcard, illegal field"
+notmuch search --query=sexp '(body *)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'body' does not support wildcard queries
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search, exclude \"deleted\" messages from search"
+notmuch config set search.exclude_tags deleted
+generate_message '[subject]="Not deleted"'
+not_deleted_id=$gen_msg_id
+generate_message '[subject]="Deleted"'
+notmuch new > /dev/null
+notmuch tag +deleted id:$gen_msg_id
+deleted_id=$gen_msg_id
+output=$(notmuch search --query=sexp '(subject deleted)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)"
+
+test_begin_subtest "Search, exclude \"deleted\" messages from message search --exclude=false"
+output=$(notmuch search --query=sexp --exclude=false --output=messages '(subject deleted)' | notmuch_search_sanitize)
+test_expect_equal "$output" "id:$not_deleted_id
+id:$deleted_id"
+
+test_begin_subtest "Search, exclude \"deleted\" messages from search, overridden"
+notmuch search --query=sexp '(and (subject deleted) (tag deleted))' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Deleted (deleted inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search, exclude \"deleted\" messages from threads"
+add_message '[subject]="Not deleted reply"' '[in-reply-to]="<$gen_msg_id>"'
+output=$(notmuch search --query=sexp '(subject deleted)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+thread:XXX   2001-01-05 [1/2] Notmuch Test Suite; Not deleted reply (deleted inbox unread)"
+
+test_begin_subtest "Search, don't exclude \"deleted\" messages when --exclude=flag specified"
+output=$(notmuch search --query=sexp --exclude=flag '(subject deleted)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+thread:XXX   2001-01-05 [1/2] Notmuch Test Suite; Deleted (deleted inbox unread)"
+
+test_begin_subtest "Search, don't exclude \"deleted\" messages from search if not configured"
+notmuch config set search.exclude_tags
+output=$(notmuch search --query=sexp '(subject deleted)' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+thread:XXX   2001-01-05 [2/2] Notmuch Test Suite; Deleted (deleted inbox unread)"
+
+test_begin_subtest "regex at top level"
+notmuch search --query=sexp '(rx foo)' >& OUTPUT
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+illegal 'rx' outside field
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regex in illegal field"
+notmuch search --query=sexp '(body (regex foo))' >& OUTPUT
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'regex' not supported in field 'body'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+notmuch search --output=messages from:cworth > cworth.msg-ids
+
+test_begin_subtest "regexp 'from' search"
+notmuch search --output=messages --query=sexp '(from (rx cworth))' > OUTPUT
+test_expect_equal_file cworth.msg-ids OUTPUT
+
+test_begin_subtest "regexp search for 'from' 2"
+notmuch search from:/cworth@cworth.org/ and subject:patch | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(and (from (rx cworth@cworth.org)) (subject patch))' \
+    | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp 'folder' search"
+notmuch search 'folder:/^bar$/' | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(folder (rx ^bar$))' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp 'id' search"
+notmuch search --output=messages --query=sexp '(id (rx yoom))' > OUTPUT
+test_expect_equal_file cworth.msg-ids OUTPUT
+
+test_begin_subtest "unanchored 'is' search"
+notmuch search tag:signed or tag:inbox > EXPECTED
+notmuch search --query=sexp '(is (rx i))' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "anchored 'is' search"
+notmuch search tag:signed > EXPECTED
+notmuch search --query=sexp '(is (rx ^si))' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "combine regexp mid and subject"
+notmuch search subject:/-C/ and mid:/y..m/ | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(and (subject (rx -C)) (mid (rx y..m)))' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp 'path' search"
+notmuch search 'path:/^bar$/' | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(path (rx ^bar$))' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp 'property' search"
+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
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp 'thread' search"
+notmuch search --output=threads '*' | grep '7$' > EXPECTED
+notmuch search --output=threads --query=sexp '(thread (rx 7$))' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Basic query that matches no messages"
+count=$(notmuch count from:keithp and to:keithp)
+test_expect_equal 0 "$count"
+
+test_begin_subtest "Same query against threads"
+notmuch search --query=sexp '(and (thread (of (from keithp))) (thread (matching (to keithp))))' \
+    | notmuch_search_sanitize > OUTPUT
+cat<<EOF > 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 "Mix thread and non-threads query"
+notmuch search --query=sexp '(and (thread (matching keithp)) (to keithp))' | notmuch_search_sanitize > OUTPUT
+cat<<EOF > EXPECTED
+thread:XXX   2009-11-18 [1/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 "Compound subquery"
+notmuch search --query=sexp '(thread (of (from keithp) (subject Maildir)))' | notmuch_search_sanitize > OUTPUT
+cat<<EOF > 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 "Compound subquery via field processor"
+notmuch search 'sexp:"(thread (of (from keithp) (subject Maildir)))"' | notmuch_search_sanitize > OUTPUT
+cat<<EOF > 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
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "illegal expansion"
+notmuch search --query=sexp '(id (of ego))' 1>OUTPUT 2>&1
+cat<<EOF > EXPECTED
+notmuch search: Syntax error in query
+'of' unsupported inside 'id'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "(folder (of subquery))"
+notmuch search --query=sexp --output=messages '(folder (of (id yun3a4cegoa.fsf@aiko.keithp.com)))' > OUTPUT
+cat <<EOF > EXPECTED
+id:yun1vjwegii.fsf@aiko.keithp.com
+id:yun3a4cegoa.fsf@aiko.keithp.com
+id:1258509400-32511-1-git-send-email-stewart@flamingspork.com
+id:1258506353-20352-1-git-send-email-stewart@flamingspork.com
+id:20091118010116.GC25380@dottiness.seas.harvard.edu
+id:20091118005829.GB25380@dottiness.seas.harvard.edu
+id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "infix query"
+notmuch search to:searchbyto | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(infix "to:searchbyto")' |  notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "bad infix query 1"
+notmuch search --query=sexp '(infix "from:/unbalanced")' 2>&1|  notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+Syntax error in infix query: from:/unbalanced
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "bad infix query 2"
+notmuch search --query=sexp '(infix "thread:{unbalanced")' 2>&1|  notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+Syntax error in infix query: thread:{unbalanced
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "bad infix query 3: bad nesting"
+notmuch search --query=sexp '(subject (infix "tag:inbox"))' 2>&1|  notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+'infix' not supported inside 'subject'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "infix query that matches no messages"
+notmuch search --query=sexp '(and (infix "from:keithp") (infix "to:keithp"))' > OUTPUT
+test_expect_equal_file /dev/null OUTPUT
+
+test_begin_subtest "compound infix query"
+notmuch search date:2009-11-18..2009-11-18 and tag:unread > EXPECTED
+notmuch search --query=sexp  '(infix "date:2009-11-18..2009-11-18 and tag:unread")' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "compound infix query 2"
+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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > EXPECTED
+notmuch search: Syntax error in query
+unknown prefix 'FooBar'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "adding user header"
+test_expect_code 0 "notmuch config set index.header.List \"List-Id\""
+
+test_begin_subtest "reindexing"
+test_expect_code 0 'notmuch reindex "*"'
+
+test_begin_subtest "wildcard search for user header"
+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
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search for user header"
+notmuch search List:notmuch | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(List notmuch)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search for user header (list token)"
+notmuch search List:notmuch | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(List notmuch.notmuchmail.org)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search for user header (quoted string)"
+notmuch search 'List:"notmuch notmuchmail org"' | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(List "notmuch notmuchmail org")' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search for user header (atoms)"
+notmuch search 'List:"notmuch notmuchmail org"' | notmuch_search_sanitize > EXPECTED
+notmuch search --query=sexp '(List notmuch notmuchmail org)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "check saved query name"
+test_expect_code 1 "notmuch config set squery.test '(subject utf8-sübjéct)'"
+
+test_begin_subtest "roundtrip saved query (database)"
+notmuch config set --database squery.Test '(subject utf8-sübjéct)'
+output=$(notmuch config get squery.Test)
+test_expect_equal "$output" '(subject utf8-sübjéct)'
+
+test_begin_subtest "roundtrip saved query"
+notmuch config set squery.Test '(subject override subject)'
+output=$(notmuch config get squery.Test)
+test_expect_equal "$output" '(subject override subject)'
+
+test_begin_subtest "unknown saved query"
+notmuch search --query=sexp '(Unknown foo bar)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+unknown prefix 'Unknown'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "syntax error in saved query"
+notmuch config set squery.Bad '(Bad'
+notmuch search --query=sexp '(Bad foo bar)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+invalid saved s-expression query: '(Bad'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Saved Search by 'tag' and 'subject'"
+notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED
+notmuch config set squery.TagSubject  '(and (tag inbox) (subject maildir))'
+notmuch search --query=sexp '(TagSubject)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Saved Search: illegal nesting"
+notmuch config set squery.TagSubject  '(and (tag inbox) (subject maildir))'
+notmuch search --query=sexp '(subject (TagSubject))' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+nested field: 'tag' inside 'subject'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Saved Search: list as prefix"
+notmuch config set squery.Bad2  '((and) (tag inbox) (subject maildir))'
+notmuch search --query=sexp '(Bad2)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+unexpected list in field/operation position
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Saved Search: bad parameter syntax"
+notmuch config set squery.Bad3  '(macro a b)'
+notmuch search --query=sexp '(Bad3)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+missing (possibly empty) list of arguments to macro
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Saved Search: bad parameter syntax 2"
+notmuch config set squery.Bad4  '(macro ((a b)) a)'
+notmuch search --query=sexp '(Bad4 1)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+macro parameters must be unquoted atoms
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Saved Search: bad parameter syntax 3"
+notmuch config set squery.Bad5  '(macro (a b) a)'
+notmuch search --query=sexp '(Bad5 1)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+too few arguments to macro
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Saved Search: bad parameter syntax 4"
+notmuch search --query=sexp '(Bad5 1 2 3)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+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 <<EOF > 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 <<EOF > 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 <<EOF > 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
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+missing body of macro
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "macro in query"
+notmuch search --query=sexp '(macro (a) (and ,b (subject maildir)))' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+macro definition not permitted here
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "zero argument macro"
+notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED
+notmuch config set squery.TagSubject0  '(macro () (and (tag inbox) (subject maildir)))'
+notmuch search --query=sexp '(TagSubject0)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "undefined argument"
+notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED
+notmuch config set squery.Bad6  '(macro (a) (and ,b (subject maildir)))'
+notmuch search --query=sexp '(Bad6 foo)' >OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+undefined parameter 'b'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Single argument macro"
+notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED
+notmuch config set squery.TagSubject1  '(macro (tagname) (and (tag ,tagname) (subject maildir)))'
+notmuch search --query=sexp '(TagSubject1 inbox)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Single argument macro, list argument"
+notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED
+notmuch config set squery.ThingSubject  '(macro (thing) (and ,thing (subject maildir)))'
+notmuch search --query=sexp '(ThingSubject (tag inbox))' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "two argument macro"
+notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED
+notmuch config set squery.TagSubject2  '(macro (tagname subj) (and (tag ,tagname) (subject ,subj)))'
+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))'
+notmuch config set squery.Outer  '(macro (x y) (and (tag ,x) (Inner ,y)))'
+notmuch search --query=sexp '(Outer inbox maildir)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "nested macros (no dynamic scope)"
+notmuch config set squery.Inner2 '(macro (x) (subject ,y))'
+notmuch config set squery.Outer2  '(macro (x y) (and (tag ,x) (Inner2 ,y)))'
+notmuch search --query=sexp '(Outer2 inbox maildir)' > OUTPUT 2>&1
+cat <<EOF > EXPECTED
+notmuch search: Syntax error in query
+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
+notmuch search --query=sexp '(About notmuch)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file 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
+notmuch search --query=sexp '(About notmuch)' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_done
index bf28d220a760515bfeeba06a7e2fa92d389ad42b..0d85c6097f31c4f5e7bdcce048e036d2a025ec06 100755 (executable)
@@ -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"
index 817be5380a45d9a98a7a6bbd2031269575dd13ba..0cafbe20f11a0140cac8df350d6e9900ec52251e 100755 (executable)
@@ -325,4 +325,11 @@ cat <<EOF >EXPECTED
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then
+    test_begin_subtest "sexpr query: all messages"
+    notmuch address '*' > EXPECTED
+    notmuch address --query=sexp '()' > OUTPUT
+    test_expect_equal_file EXPECTED OUTPUT
+fi
+
 test_done
index 409cfdcc851c2eeb9f4011fdefa60f875ea6b138..b4f6294e306edc2daf222f847b9e4ccddb8a5fd2 100755 (executable)
@@ -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 (executable)
index 0000000..30d1f25
--- /dev/null
@@ -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\{.*\<depth:0\>.*/&/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
index acab5381b94334bed54f1453eaf61a1e54807eba..e9cc9cb078d73352539973937ac44eee65b34ac6 100755 (executable)
@@ -5,8 +5,7 @@ test_description='"notmuch search, count and show" with excludes in several vari
 # Generates a thread consisting of a top level message and 'length'
 # replies. The subject of the top message 'subject: top message"
 # and the subject of the nth reply in the thread is "subject: reply n"
-generate_thread ()
-{
+generate_thread () {
     local subject="$1"
     local length="$2"
     generate_message '[subject]="'"${subject}: top message"'"' '[body]="'"body of top message"'"'
@@ -22,7 +21,7 @@ generate_thread ()
     done
     notmuch new > /dev/null
     # We cannot retrieve the thread_id until after we have run notmuch new.
-    gen_thread_id=`notmuch search --output=threads id:${gen_thread_msg_id[0]}`
+    gen_thread_id=$(notmuch search --output=threads id:${gen_thread_msg_id[0]} 2>/dev/null)
 }
 
 #############################################
index c292b24e2490f6ab8b19536c6815b49c6b70a812..273c0af35927e092c37b7a9c47cacfc9b8aaac89 100755 (executable)
@@ -2,6 +2,21 @@
 test_description='"notmuch tag"'
 . $(dirname "$0")/test-lib.sh || exit 1
 
+test_query_syntax () {
+    # use a tag with a space to stress the query string munging code.
+    local new_tag="${RANDOM} ${RANDOM}"
+    test_begin_subtest "sexpr query: $1"
+    backup_database
+    notmuch tag --query=sexp "+${new_tag}" -- "$1"
+    notmuch dump > OUTPUT
+    restore_database
+    backup_database
+    notmuch tag "+${new_tag}" -- "$2"
+    notmuch dump > EXPECTED
+    restore_database
+    test_expect_equal_file_nonempty EXPECTED OUTPUT
+}
+
 add_message '[subject]=One'
 add_message '[subject]=Two'
 
@@ -305,9 +320,38 @@ 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/*.*
 test_expect_equal "$output" "A Xapian exception occurred opening database"
 
+add_email_corpus
+
+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'
+    test_query_syntax 'wizard' 'wizard'
+    test_query_syntax 'Wizard' 'Wizard'
+    test_query_syntax '(attachment notmuch-help.patch)' 'attachment:notmuch-help.patch'
+    test_query_syntax '(mimetype text/html)' 'mimetype:text/html'
+
+    test_begin_subtest "--batch --query=sexp"
+    notmuch dump --format=batch-tag > backup.tags
+    notmuch tag --batch --query=sexp  <<EOF
+    +all -- (or One Two)
+    +none -- (and One Two)
+    EOF
+    notmuch dump > OUTPUT
+    cat <<EOF > EXPECTED
+    #notmuch-dump batch-tag:3 config,properties,tags
+    +all +inbox +tag5 +unread -- id:msg-001@notmuch-test-suite
+    +all +inbox +tag4 +tag5 +unread -- id:msg-002@notmuch-test-suite
+EOF
+    notmuch restore --format=batch-tag < backup.tags
+    test_expect_equal_file EXPECTED OUTPUT
+
+fi
+
 test_done
index e8b75605fd2a391eaa0c51351d8f8bd6e0d80499..318c97880150d12dcb36f312c5794a04e3898af8 100755 (executable)
@@ -1,20 +1,21 @@
 #!/usr/bin/env bash
 test_description="--format=json output"
 . $(dirname "$0")/test-lib.sh || exit 1
+.  $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
 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 <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"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 <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"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 <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"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 <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"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 <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"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 <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"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\""
@@ -32,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 <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"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 <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"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'
@@ -48,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 <test_suite@notmuchmail.org>\", \"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 <test_suite@notmuchmail.org>\", \"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\""
@@ -64,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\""
@@ -80,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 \\*"
@@ -96,6 +99,7 @@ cat <<EOF > EXPECTED
         [
             {
                 "date_relative": "2001-01-05",
+               "duplicate": 1,
                 "excluded": false,
                 "filename": [
                     "${MAIL_DIR}/copy1",
@@ -131,6 +135,7 @@ cat <<EOF > EXPECTED
         [
             {
                 "date_relative": "2001-01-05",
+               "duplicate": 1,
                 "excluded": false,
                 "filename": "${MAIL_DIR}/copy1",
                 "headers": {
@@ -155,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]=\"<parent@notmuch-test-suite>\"" "[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 <test_suite_other@notmuchmail.org>; 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 <<EOF > 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 <test_suite@notmuchmail.org>",
+                    "In-Reply-To": "<parent@notmuch-test-suite>",
+                    "Received": "from mail.example.com (mail.example.com [1.1.1.1])\tby mail.notmuchmail.org (some MTA) with ESMTP id 12345678\tfor <test_suite_other@notmuchmail.org>; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)",
+                    "Subject": "extra-headers",
+                    "To": "Notmuch Test Suite <test_suite@notmuchmail.org>"
+                },
+                "id": "XXXXX",
+                "match": true,
+                "tags": [
+                    "inbox",
+                    "unread"
+                ],
+                "timestamp": 946728000
+            },
+            []
+        ]
+    ]
+]
+EOF
+test_expect_equal_json "${output}" "$(cat EXPECTED)"
+
 test_done
index 24be8351006e3d5bc64e9c4589f238104a9af14f..0be94bd2348fcb68c252a10c469fe7b1a8ef3db7 100755 (executable)
@@ -1,35 +1,36 @@
 #!/usr/bin/env bash
 test_description="--format=sexp output"
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
 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 <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :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 <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :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 <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :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 <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :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 <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :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 <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :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 <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :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 <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :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'
@@ -44,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 <test_suite@notmuchmail.org>\" :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 <test_suite@notmuchmail.org>\" :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]=\"<parent@notmuch-test-suite>\"" "[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 <test_suite_other@notmuchmail.org>; 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 <<EOF > 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 <test_suite@notmuchmail.org>" :To "Notmuch Test Suite <test_suite@notmuchmail.org>" :Date "GENERATED_DATE" :In-Reply-To "<parent@notmuch-test-suite>" :Received "from mail.example.com (mail.example.com [1.1.1.1])\011by mail.notmuchmail.org (some MTA) with ESMTP id 12345678\011for <test_suite_other@notmuchmail.org>; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)")) ())))
+EOF
+test_expect_equal_file EXPECTED OUTPUT
 
 test_done
index 6f715ff9b42e834b03ef67e1ebc8f25b89756a59..cfe48ac505dd39eccfc2c9ae12973c02b045b63e 100755 (executable)
@@ -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 <<EOF >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 <cworth@cworth.org>", "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 <cworth@cworth.org>", "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 <cworth@cworth.org>", "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 <cworth@cworth.org>", "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
 
 
@@ -725,13 +725,12 @@ EOF
 
 notmuch new > /dev/null
 
-cat_expected_head ()
-{
+cat_expected_head () {
         cat <<EOF
-[[[{"id": "htmlmessage", "match":true, "excluded": false, "date_relative":"2000-01-01",
+[[[{"id": "XXXXX", "match":true, "excluded": false, "date_relative":"2000-01-01",
    "crypto": {},
    "timestamp": 946684800,
-   "filename": ["${MAIL_DIR}/include-html"],
+   "filename": ["YYYYY"],
    "tags": ["inbox", "unread"],
    "headers": { "Date": "Sat, 01 Jan 2000 00:00:00 +0000", "From": "A <a@example.com>",
                 "Subject": "html message", "To": "B <b@example.com>"},
@@ -743,8 +742,8 @@ EOF
 cat_expected_head > EXPECTED.nohtml
 cat <<EOF >> 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
@@ -760,11 +759,11 @@ cat <<EOF >> 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"
index e1d50bf90a0a15881d1a7712b1fad813a13009ee..440820286a00169956aca2101c9a12e60aeb5803 100755 (executable)
@@ -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
index b6d8f42a933f3fd36e615255e4871c229d4ba1ff..120d7138d61809fd46cb724501bce921de13db87 100755 (executable)
@@ -2,15 +2,14 @@
 test_description="\"notmuch reply\" in several variations"
 . $(dirname "$0")/test-lib.sh || exit 1
 
-test_begin_subtest "Basic reply"
 add_message '[from]="Sender <sender@example.com>"' \
             [to]=test_suite@notmuchmail.org \
             [subject]=notmuch-reply-test \
            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
            '[body]="basic reply test"'
 
-output=$(notmuch reply id:${gen_msg_id} 2>&1 && echo OK)
-test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+cat <<EOF > basic.expected
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
 Subject: Re: notmuch-reply-test
 To: Sender <sender@example.com>
 In-Reply-To: <${gen_msg_id}>
@@ -18,7 +17,19 @@ References: <${gen_msg_id}>
 
 On Tue, 05 Jan 2010 15:43:56 -0000, Sender <sender@example.com> wrote:
 > basic reply test
-OK"
+OK
+EOF
+
+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-0}" = "1" ]; then
+
+    test_begin_subtest "Basic reply (query=sexp)"
+    notmuch reply --query=sexp "(id ${gen_msg_id})" >OUTPUT 2>&1 && echo OK >> OUTPUT
+    test_expect_equal_file basic.expected OUTPUT
+fi
 
 test_begin_subtest "Multiple recipients"
 add_message '[from]="Sender <sender@example.com>"' \
@@ -245,6 +256,26 @@ On Tue, 05 Jan 2010 15:43:56 -0000, Sender <sender@example.com> wrote:
 > From guessing
 OK"
 
+test_begin_subtest "From guessing: multiple Delivered-To"
+add_message '[from]="Sender <sender@example.com>"' \
+           '[to]="Recipient <recipient@example.com>"' \
+           '[subject]="From guessing"' \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="From guessing"' \
+           '[header]="Delivered-To: test_suite_other@notmuchmail.org
+Delivered-To: test_suite@notmuchmail.org"'
+
+output=$(notmuch reply id:${gen_msg_id} 2>&1 && echo OK)
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: From guessing
+To: Sender <sender@example.com>, Recipient <recipient@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Sender <sender@example.com> wrote:
+> From guessing
+OK"
+
 test_begin_subtest "Reply with RFC 2047-encoded headers"
 add_message '[subject]="=?iso-8859-1?q?=e0=df=e7?="' \
            '[from]="=?utf-8?q?=e2=98=83?= <snowman@example.com>"' \
@@ -267,7 +298,8 @@ On Tue, 05 Jan 2010 15:43:56 -0000, ☃ <snowman@example.com> 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": {
@@ -281,14 +313,14 @@ test_expect_equal_json "$output" '
         "crypto": {},
         "date_relative": "2010-01-05",
         "excluded": false,
-        "filename": ["'${MAIL_DIR}'/msg-014"],
+        "filename": ["YYYYY"],
         "headers": {
             "Date": "Tue, 05 Jan 2010 15:43:56 +0000",
             "From": "\u2603 <snowman@example.com>",
             "Subject": "\u00e0\u00df\u00e7",
             "To": "Notmuch Test Suite <test_suite@notmuchmail.org>"
         },
-        "id": "'${gen_msg_id}'",
+        "id": "XXXXX",
         "match": false,
         "tags": [
             "inbox",
@@ -321,4 +353,40 @@ On Thu, 16 Jun 2016 22:14:41 -0400, Alice <alice@example.org> 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
index 105de130bf240c69a1f232159c23f6b7902e3811..c3f188398b87e6943d6e3a568191ed8303431605 100755 (executable)
@@ -117,6 +117,19 @@ test_begin_subtest "dump -- from:cworth"
 notmuch dump -- from:cworth > dump-dash-cworth.actual
 test_expect_equal_file dump-cworth.expected dump-dash-cworth.actual
 
+
+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
+    test_expect_equal_file_nonempty dump-cworth.expected dump-dash-cworth.actual2
+
+    test_begin_subtest "dump --query=sexp --output=outfile '(from cworth)'"
+    notmuch dump --output=dump-outfile-cworth.actual2 --query=sexp '(from cworth)'
+    test_expect_equal_file dump-cworth.expected dump-outfile-cworth.actual2
+
+fi
+
 test_begin_subtest "dump --output=outfile from:cworth"
 notmuch dump --output=dump-outfile-cworth.actual from:cworth
 test_expect_equal_file dump-cworth.expected dump-outfile-cworth.actual
@@ -126,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
@@ -233,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
index 1e9d2a3da010a7c5c5d27fad4e3ddbb4993154cc..6fcd10c5d4197e182c2334fe18a1abdb2b210fec 100755 (executable)
@@ -31,18 +31,18 @@ test_expect_equal "$output" "thread:0000000000000002   2001-01-05 [1/1] Notmuch
 
 test_begin_subtest "RFC 2047 encoded word with spaces"
 add_message '[subject]="=?utf-8?q?encoded word with spaces?="'
-output=$(notmuch search id:${gen_msg_id} 2>&1 | notmuch_show_sanitize)
-test_expect_equal "$output" "thread:0000000000000003   2001-01-05 [1/1] Notmuch Test Suite; encoded word with spaces (inbox unread)"
+output=$(notmuch search id:${gen_msg_id} 2>&1 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; encoded word with spaces (inbox unread)"
 
 test_begin_subtest "RFC 2047 encoded words back to back"
 add_message '[subject]="=?utf-8?q?encoded-words-back?==?utf-8?q?to-back?="'
-output=$(notmuch search id:${gen_msg_id} 2>&1 | notmuch_show_sanitize)
-test_expect_equal "$output" "thread:0000000000000004   2001-01-05 [1/1] Notmuch Test Suite; encoded-words-backto-back (inbox unread)"
+output=$(notmuch search id:${gen_msg_id} 2>&1 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; encoded-words-backto-back (inbox unread)"
 
 test_begin_subtest "RFC 2047 encoded words without space before or after"
 add_message '[subject]="=?utf-8?q?encoded?=word without=?utf-8?q?space?=" '
-output=$(notmuch search id:${gen_msg_id} 2>&1 | notmuch_show_sanitize)
-test_expect_equal "$output" "thread:0000000000000005   2001-01-05 [1/1] Notmuch Test Suite; encodedword withoutspace (inbox unread)"
+output=$(notmuch search id:${gen_msg_id} 2>&1 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; encodedword withoutspace (inbox unread)"
 
 test_begin_subtest "Mislabeled Windows-1252 encoding"
 add_message '[content-type]="text/plain; charset=iso-8859-1"'                           \
index 78ac19a8655c150bd428657fcd387336b7b689d4..e96c16013443f57eab16ae55581a73d4256e9dee 100755 (executable)
@@ -2,9 +2,11 @@
 
 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
 
 # syntax errors in test-lib.el cause mysterious failures
@@ -39,6 +41,20 @@ test_emacs '(notmuch-search "tag:inbox")
            (test-output)'
 test_expect_equal_file $EXPECTED/notmuch-search-tag-inbox OUTPUT
 
+test_begin_subtest "Functions in search-result-format"
+test_emacs '(let
+               ((notmuch-search-result-format
+                 (quote ((notmuch-test-result-flags . "%s ")
+                         ("date" . "%12s ")
+                         ("count" . "%9s ")
+                         ("authors" . "%-30s ")
+                         ("subject" . "%s ")
+                         ("tags" . "(%s)")))))
+             (notmuch-search "tag:inbox")
+             (notmuch-test-wait)
+             (test-output))'
+test_expect_equal_file $EXPECTED/search-result-format-function OUTPUT
+
 test_begin_subtest "Incremental parsing of search results"
 test_emacs "(cl-letf* (((symbol-function 'orig)
                        (symbol-function 'notmuch-search-process-filter))
@@ -114,53 +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 "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\"")
@@ -289,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)"
@@ -381,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)
@@ -447,6 +418,31 @@ Sender <sender@example.com> 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 <sender@example.com>"' \
+            [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 <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+To: Sender <sender@example.com>
+Subject: Re: ${test_subtest_name}
+In-Reply-To: <${gen_msg_id}>
+Fcc: ${MAIL_DIR}/sent
+References: <${gen_msg_id}>
+--text follows this line--
+Sender <sender@example.com> 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 <sender@example.com>"' \
             '[to]=group:test_suite@notmuchmail.org,someone@example.com\;' \
@@ -642,7 +638,7 @@ References: <XXX>
 --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
 
@@ -845,7 +841,7 @@ test_emacs '(notmuch-show "id:\"bought\"")
        (notmuch-show-stash-message-id-stripped)
        (notmuch-show-stash-tags)
        (notmuch-show-stash-filename)
-       (notmuch-show-stash-mlarchive-link "Gmane")
+       (notmuch-show-stash-mlarchive-link "Notmuch")
        (notmuch-show-stash-mlarchive-link "MARC")
        (notmuch-show-stash-mlarchive-link "Mail Archive, The")
        (switch-to-buffer
@@ -866,7 +862,7 @@ id:bought
 bought
 inbox,stashtest
 ${gen_msg_filename}
-https://mid.gmane.org/bought
+https://nmbug.notmuchmail.org/nmweb/show/bought
 https://marc.info/?i=bought
 https://mid.mail-archive.com/bought
 EOF
@@ -1035,9 +1031,8 @@ End of search results.
 === MESSAGES ===
 YYY/notmuch_fail exited with status 1 (see *Notmuch errors* for more details)
 === ERROR ===
-[XXX]
 YYY/notmuch_fail exited with status 1
-command: YYY/notmuch_fail search --format\=sexp --format-version\=4 --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"
@@ -1071,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
        \"\"
@@ -1131,4 +1102,10 @@ This text added by the hook.
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "notmuch-search with nonexistent CWD"
+test_emacs '(test-log-error
+             (let ((default-directory "/nonexistent"))
+               (notmuch-search "*")))'
+test_expect_equal "$(cat MESSAGES)" "COMPLETE"
+
 test_done
diff --git a/test/T315-emacs-tagging.sh b/test/T315-emacs-tagging.sh
new file mode 100755 (executable)
index 0000000..c26413c
--- /dev/null
@@ -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 <<EOF > 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
index f61e8a973fc58b1cd8a74a623aaa48dc786be06a..617985e6185edb110ddcbcccd7e21ffa56e65187 100755 (executable)
@@ -1,11 +1,14 @@
 #!/usr/bin/env bash
 test_description="Emacs with large search results buffer"
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
 x=xxxxxxxxxx # 10
 x=$x$x$x$x$x$x$x$x$x$x # 100
 x=$x$x$x$x$x$x$x$x$x # 900
 
+test_require_emacs
+
 # We generate a long subject here (over 900 bytes) so that the emacs
 # search results get large quickly. With 30 such messages we should
 # cross several 4kB page boundaries and see the bug.
index eaf7c980903bed112fc2df81f7af8c7307f1eda1..405b063bb7e9f978030ebb07edcad867021abd4a 100755 (executable)
@@ -2,6 +2,9 @@
 
 test_description="emacs: mail subject to filename"
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
+
+test_require_emacs
 
 # emacs server can't be started in a child process with $(test_emacs ...)
 test_emacs '(ignore)' > /dev/null
index 0aada4dfe7023a15384f121fb794f97126d9a30a..27c0e86d42b672b6442c8ab6981a468a48bc34e0 100755 (executable)
@@ -6,21 +6,37 @@
 
 test_description='PGP/MIME signature verification and decryption'
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
 ##################################################
 
+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 <<EOF >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" \
@@ -33,7 +49,7 @@ expected='[[[{"id": "XXXXX",
  "timestamp": 946728000,
  "date_relative": "2000-01-01",
  "tags": ["inbox","signed"],
- "crypto": {"signed": {"status": [{ "status": "good", "created": 946728000, "fingerprint": "'$FINGERPRINT'", "userid": "'"$SELF_USERID"'"}]}},
+ "crypto": {"signed": {"status": [{ "status": "good", "created": 946728000, "email": "'"$SELF_EMAIL"'", "fingerprint": "'$FINGERPRINT'", "userid": "'"$SELF_USERID"'"}]}},
  "headers": {"Subject": "test signed message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
@@ -42,6 +58,7 @@ expected='[[[{"id": "XXXXX",
  "sigstatus": [{"status": "good",
  "fingerprint": "'$FINGERPRINT'",
  "created": 946728000,
+ "email": "'"$SELF_EMAIL"'",
  "userid": "'"$SELF_USERID"'"}],
  "content-type": "multipart/signed",
  "content": [{"id": 2,
@@ -365,7 +382,7 @@ expected='[[[{"id": "XXXXX",
  "timestamp": 946728000,
  "date_relative": "2000-01-01",
  "tags": ["encrypted","inbox"],
- "crypto": {"signed": {"status": [{ "status": "good", "created": 946728000, "fingerprint": "'$FINGERPRINT'", "userid": "'"$SELF_USERID"'"}],
+ "crypto": {"signed": {"status": [{ "status": "good", "created": 946728000, "fingerprint": "'$FINGERPRINT'", "email": "'"$SELF_EMAIL"'", "userid": "'"$SELF_USERID"'"}],
                        "encrypted": true },
             "decrypted": {"status": "full"}},
  "headers": {"Subject": "test encrypted message 002",
@@ -377,6 +394,7 @@ expected='[[[{"id": "XXXXX",
  "sigstatus": [{"status": "good",
  "fingerprint": "'$FINGERPRINT'",
  "created": 946728000,
+ "email": "'"$SELF_EMAIL"'",
  "userid": "'"$SELF_USERID"'"}],
  "content-type": "multipart/encrypted",
  "content": [{"id": 2,
@@ -450,7 +468,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,
index 8b2b52be032a34ec539ee673ffa8fee23dc8dc3d..d2118c0492efb9e3bd9bedf639b9a4f859a30ea5 100755 (executable)
@@ -2,7 +2,9 @@
 
 test_description='S/MIME signature verification and decryption'
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
+test_require_emacs
 test_require_external_prereq openssl
 test_require_external_prereq gpgsm
 
@@ -33,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='<test_suite@notmuchmail.org>'
+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' \
@@ -44,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","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 <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
@@ -53,6 +60,7 @@ expected='[[[{"id": "XXXXX",
  "sigstatus": [{"fingerprint": "'$FINGERPRINT'",
  "status": "good",
  "userid": "CN=Notmuch Test Suite",
+ "email": "'$EXPECTED_EMAIL_ADDR'",
  "expires": 424242424,
  "created": 946728000}],
  "content-type": "multipart/signed",
@@ -175,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)
@@ -186,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
index 074a23451cf7cf07aaadd1503e7dd90709e30d52..9f64033112b0cf81b81dc3f929ba42d283889a5a 100755 (executable)
@@ -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'", "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'", "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'", "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"'", "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"'
 
index 1ed5f28cfc455bd74d30f0f047032f75595c2886..a7497489293b6d9e7df7d835385dbef0d9d92e41 100755 (executable)
@@ -4,9 +4,11 @@
 
 test_description='indexing decrypted mail'
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
 ##################################################
 
+test_require_emacs
 add_gnupg_home
 
 # create a test encrypted message
@@ -112,12 +114,10 @@ test_expect_equal \
     "$expected"
 
 # try inserting it with decryption, should appear as a single copy
-# (note: i think thread id skips 4 because of duplicate message-id
-# insertion, above)
 test_begin_subtest "message cleartext is present with insert --decrypt=true"
 notmuch insert --folder=sent --decrypt=true <<<"$contents"
-output=$(notmuch search wumpus)
-expected='thread:0000000000000005   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (encrypted inbox unread)'
+output=$(notmuch search wumpus | notmuch_search_sanitize)
+expected='thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (encrypted inbox unread)'
 test_expect_equal \
     "$output" \
     "$expected"
@@ -127,9 +127,9 @@ test_expect_equal \
 test_begin_subtest 'tagging all messages'
 test_expect_success 'notmuch tag +blarney "encrypted message"'
 test_begin_subtest "verify that tags have not changed"
-output=$(notmuch search tag:blarney)
-expected='thread:0000000000000001   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 001 (blarney encrypted inbox)
-thread:0000000000000005   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (blarney encrypted inbox unread)'
+output=$(notmuch search tag:blarney | notmuch_search_sanitize)
+expected='thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 001 (blarney encrypted inbox)
+thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (blarney encrypted inbox unread)'
 test_expect_equal \
     "$output" \
     "$expected"
@@ -138,14 +138,14 @@ test_expect_equal \
 test_begin_subtest 'reindex old messages'
 test_expect_success 'notmuch reindex --decrypt=true tag:encrypted and not property:index.decryption=success'
 test_begin_subtest "reindexed encrypted message, including cleartext"
-output=$(notmuch search wumpus)
+output=$(notmuch search wumpus | notmuch_search_sanitize)
 test_expect_equal \
     "$output" \
     "$expected"
 
 # and the same search, but by property ($expected is untouched):
 test_begin_subtest "emacs search by property for both messages"
-output=$(notmuch search property:index.decryption=success)
+output=$(notmuch search property:index.decryption=success | notmuch_search_sanitize)
 test_expect_equal \
     "$output" \
     "$expected"
@@ -154,7 +154,7 @@ test_expect_equal \
 test_begin_subtest 'reindex in auto mode'
 test_expect_success 'notmuch reindex tag:encrypted and property:index.decryption=success'
 test_begin_subtest "reindexed encrypted messages, should not have changed"
-output=$(notmuch search wumpus)
+output=$(notmuch search wumpus | notmuch_search_sanitize)
 test_expect_equal \
     "$output" \
     "$expected"
@@ -188,9 +188,9 @@ test_expect_equal \
 
 # ensure that the tags remain even when we are dropping the cleartext.
 test_begin_subtest "verify that tags remain without cleartext"
-output=$(notmuch search tag:blarney)
-expected='thread:0000000000000001   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 001 (blarney encrypted inbox)
-thread:0000000000000005   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (blarney encrypted inbox unread)'
+output=$(notmuch search tag:blarney | notmuch_search_sanitize)
+expected='thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 001 (blarney encrypted inbox)
+thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (blarney encrypted inbox unread)'
 test_expect_equal \
     "$output" \
     "$expected"
@@ -199,7 +199,7 @@ test_begin_subtest "index cleartext without keeping session keys"
 test_expect_success "notmuch reindex --decrypt=nostash tag:blarney"
 
 test_begin_subtest "Ensure that the indexed terms are present"
-output=$(notmuch search wumpus)
+output=$(notmuch search wumpus | notmuch_search_sanitize)
 test_expect_equal \
     "$output" \
     "$expected"
@@ -226,6 +226,7 @@ output=$(notmuch dump | LC_ALL=C sort)
 expected='#= simple-encrypted@crypto.notmuchmail.org index.decryption=failure
 #notmuch-dump batch-tag:3 config,properties,tags
 +encrypted +inbox +unread -- id:basic-encrypted@crypto.notmuchmail.org
++encrypted +inbox +unread -- id:encrypted-rfc822-attachment@crypto.notmuchmail.org
 +encrypted +inbox +unread -- id:encrypted-signed@crypto.notmuchmail.org
 +encrypted +inbox +unread -- id:simple-encrypted@crypto.notmuchmail.org'
 test_expect_equal \
index bca78531af616f9258fef8efc964264def92d71a..96e42bf37eaccfc2152e10093d87a3d84662b2c7 100755 (executable)
@@ -2,8 +2,10 @@
 
 test_description="protected headers in emacs interface"
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
 # testing protected headers with emacs
+test_require_emacs
 add_gnupg_home
 add_email_corpus protected-headers
 
index 642457bf2c1f3220e7840631c76174e4a9cf9790..ff06ff691b785c96bbe5762a247868d663685864 100755 (executable)
@@ -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 \
index 0a2727e74fb4573a36294ac3f68260b9f26f3e2e..cf202bb3e0a03f24fcce0c51b14e6e33de76d28b 100755 (executable)
@@ -24,8 +24,8 @@ test_expect_equal "$output" "No new mail."
 
 test_begin_subtest "Multiple files for same message"
 cat <<EOF >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
index 45de22284cb83af03294390bb4d2c52aab20810f..0f9e6d2e2bc4169127304d75669adabf2f557e2f 100755 (executable)
@@ -67,12 +67,12 @@ if test_require_external_prereq gdb; then
     ${TEST_GDB} -tty /dev/null -batch -x $NOTMUCH_SRCDIR/test/atomicity.py notmuch 1>gdb.out 2>&1
 
     # Get the final, golden output
-    notmuch search '*' > expected
+    notmuch search '*' 2>/dev/null > expected
 
     # 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/T385-transactions.sh b/test/T385-transactions.sh
new file mode 100755 (executable)
index 0000000..d8bb502
--- /dev/null
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+test_description='transactions'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+make_shim no-close <<EOF
+#include <notmuch.h>
+#include <stdio.h>
+notmuch_status_t
+notmuch_database_close (notmuch_database_t *notmuch)
+{
+  return notmuch_database_begin_atomic (notmuch);
+}
+EOF
+
+for i in `seq 1 1024`
+do
+    generate_message '[subject]="'"subject $i"'"' \
+                    '[body]="'"body $i"'"'
+done
+
+test_begin_subtest "initial new"
+NOTMUCH_NEW > OUTPUT
+cat <<EOF > EXPECTED
+Added 1024 new messages to the database.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Some changes saved with open transaction"
+notmuch config set database.autocommit 1000
+rm -r ${MAIL_DIR}/.notmuch
+notmuch_with_shim no-close new
+output=$(notmuch count '*')
+test_expect_equal "$output" "1000"
+
+test_done
index 9f71ce3c414916a65bb20f18273d2635378a8ecb..21912431d3af15f6cc9cf83b728d918e17e3208c 100755 (executable)
@@ -4,6 +4,10 @@ test_description="python bindings"
 
 test_require_external_prereq ${NOTMUCH_PYTHON}
 
+if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then
+    test_done
+fi
+
 add_email_corpus
 add_gnupg_home
 
index d54bad279e12dfb1444ac365c4ac45b84bbc5572..0059b050c718a1452ae994e32d605d370e18c65b 100755 (executable)
@@ -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 (executable)
index 0000000..0616121
--- /dev/null
@@ -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 <<EOF > 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 <<EOF > EXPECTED
+   OwnedMessage
+     MessageIter
+   OwnedMessage
+     MessageIter
+       OwnedMessage
+         MessageIter
+   OwnedMessage
+     MessageIter
+   OwnedMessage
+     MessageIter
+EOF
+test_expect_equal_file EXPECTED OUTPUT.sample
+
+test_done
index a0b76eb89ca1ba1da02c861a39f1e280105dbaf6..d0c6bb17bdda217e1e4aff594f77592fa04636d8 100755 (executable)
 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
 
 add_email_corpus
 
+test_ruby() {
+    (
+       cat <<-EOF
+       require 'notmuch'
+       db = Notmuch::Database.new()
+       EOF
+       cat
+    ) | if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then
+       ruby
+    else
+       $NOTMUCH_RUBY -I "$NOTMUCH_BUILDDIR/bindings/ruby"
+    fi> OUTPUT
+    test_expect_equal_file EXPECTED OUTPUT
+}
+
 test_begin_subtest "compare thread ids"
+notmuch search --sort=oldest-first --output=threads tag:inbox > EXPECTED
 test_ruby <<"EOF"
-require 'notmuch'
-$maildir = ENV['MAIL_DIR']
-if not $maildir then
-  abort('environment variable MAIL_DIR must be set')
-end
-@db = Notmuch::Database.new($maildir)
-@q = @db.query('tag:inbox')
-@q.sort = Notmuch::SORT_OLDEST_FIRST
-for t in @q.search_threads do
-  print t.thread_id, "\n"
+q = db.query('tag:inbox')
+q.sort = Notmuch::SORT_OLDEST_FIRST
+q.search_threads.each do |t|
+  puts 'thread:%s' % t.thread_id
 end
 EOF
-notmuch search --sort=oldest-first --output=threads tag:inbox | sed s/^thread:// > EXPECTED
-test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "compare message ids"
+notmuch search --sort=oldest-first --output=messages tag:inbox > EXPECTED
 test_ruby <<"EOF"
-require 'notmuch'
-$maildir = ENV['MAIL_DIR']
-if not $maildir then
-  abort('environment variable MAIL_DIR must be set')
-end
-@db = Notmuch::Database.new($maildir)
-@q = @db.query('tag:inbox')
-@q.sort = Notmuch::SORT_OLDEST_FIRST
-for m in @q.search_messages do
-  print m.message_id, "\n"
+q = db.query('tag:inbox')
+q.sort = Notmuch::SORT_OLDEST_FIRST
+q.search_messages.each do |m|
+  puts 'id:%s' % m.message_id
 end
 EOF
-notmuch search --sort=oldest-first --output=messages tag:inbox | sed s/^id:// > EXPECTED
-test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "get non-existent file"
+echo nil > EXPECTED
 test_ruby <<"EOF"
-require 'notmuch'
-$maildir = ENV['MAIL_DIR']
-if not $maildir then
-  abort('environment variable MAIL_DIR must be set')
-end
-@db = Notmuch::Database.new($maildir)
-result = @db.find_message_by_filename('i-dont-exist')
-print (result == nil)
+p db.find_message_by_filename('i-dont-exist')
 EOF
-test_expect_equal "$(cat OUTPUT)" "true"
 
 test_begin_subtest "count messages"
+notmuch count --output=messages tag:inbox > EXPECTED
 test_ruby <<"EOF"
-require 'notmuch'
-$maildir = ENV['MAIL_DIR']
-if not $maildir then
-  abort('environment variable MAIL_DIR must be set')
-end
-@db = Notmuch::Database.new($maildir)
-@q = @db.query('tag:inbox')
-print @q.count_messages(),"\n"
+puts db.query('tag:inbox').count_messages()
 EOF
-notmuch count --output=messages tag:inbox > EXPECTED
-test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "count threads"
+notmuch count --output=threads tag:inbox > EXPECTED
 test_ruby <<"EOF"
-require 'notmuch'
-$maildir = ENV['MAIL_DIR']
-if not $maildir then
-  abort('environment variable MAIL_DIR must be set')
-end
-@db = Notmuch::Database.new($maildir)
-@q = @db.query('tag:inbox')
-print @q.count_threads(),"\n"
+puts db.query('tag:inbox').count_threads()
 EOF
-notmuch count --output=threads tag:inbox > EXPECTED
-test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "get all tags"
+notmuch search --output=tags '*' > EXPECTED
+test_ruby <<"EOF"
+db.all_tags.each do |tag|
+  puts tag
+end
+EOF
+
+notmuch config set search.exclude_tags deleted
+generate_message '[subject]="Good"'
+generate_message '[subject]="Bad"' "[in-reply-to]=\<$gen_msg_id\>"
+notmuch new > /dev/null
+notmuch tag +deleted id:$gen_msg_id
+
+test_begin_subtest "omit excluded all"
+notmuch search --output=threads --exclude=all tag:inbox > EXPECTED
+test_ruby <<"EOF"
+q = db.query('tag:inbox')
+q.add_tag_exclude('deleted')
+q.omit_excluded = Notmuch::EXCLUDE_ALL
+q.search_threads.each do |t|
+  puts 'thread:%s' % t.thread_id
+end
+EOF
+
+test_begin_subtest "check sort argument"
+notmuch search --sort=oldest-first --output=threads tag:inbox > EXPECTED
 test_ruby <<"EOF"
-require 'notmuch'
-$maildir = ENV['MAIL_DIR']
-if not $maildir then
-  abort('environment variable MAIL_DIR must be set')
+q = db.query('tag:inbox', sort: Notmuch::SORT_OLDEST_FIRST)
+q.search_threads.each do |t|
+  puts 'thread:%s' % t.thread_id
 end
-@db = Notmuch::Database.new($maildir)
-@t = @db.all_tags()
-for tag in @t do
-   print tag,"\n"
+EOF
+
+test_begin_subtest "check exclude_tags argument"
+notmuch search --output=threads --exclude=all tag:inbox > EXPECTED
+test_ruby <<"EOF"
+q = db.query('tag:inbox', exclude_tags: %w[deleted], omit_excluded: Notmuch::EXCLUDE_ALL)
+q.search_threads.each do |t|
+  puts 'thread:%s' % t.thread_id
 end
 EOF
-notmuch search --output=tags '*' > EXPECTED
-test_expect_equal_file EXPECTED OUTPUT
 
 test_done
index 3a2df2f4efabe5c7c097a2d6240ac75f80305c7c..35bf70c0df4a155fe498fe2976de6bf5bf7deed9 100755 (executable)
@@ -15,6 +15,15 @@ EOF
     echo "${TOKEN}" > ${2}
 }
 
+create_printenv_hook () {
+    mkdir -p ${HOOK_DIR}
+    cat <<EOF >"${HOOK_DIR}/${1}"
+#!/bin/sh
+printenv "${2}" > "${3}"
+EOF
+    chmod +x "${HOOK_DIR}/${1}"
+}
+
 create_write_hook () {
     local TOKEN="${RANDOM}"
     mkdir -p ${HOOK_DIR}
@@ -28,6 +37,16 @@ EOF
     echo "${TOKEN}" > ${2}
 }
 
+create_change_hook () {
+    mkdir -p ${HOOK_DIR}
+    cat <<EOF >"${HOOK_DIR}/${1}"
+#!/bin/sh
+notmuch insert --no-hooks < ${2} > /dev/null
+rm -f ${2}
+EOF
+    chmod +x "${HOOK_DIR}/${1}"
+}
+
 create_failing_hook () {
     local HOOK_DIR=${2}
     mkdir -p ${HOOK_DIR}
@@ -43,8 +62,11 @@ add_message
 # create maildir structure for notmuch-insert
 mkdir -p "$MAIL_DIR"/{cur,new,tmp}
 
-for config in traditional profile explicit XDG split; do
+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
@@ -55,14 +77,21 @@ for config in traditional profile explicit 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
            mkdir -p $HOOK_DIR
            notmuch config set database.hook_dir $HOOK_DIR
            ;;
+       relative)
+           HOOK_DIR=${HOME}/.notmuch-hooks
+           mkdir -p $HOOK_DIR
+           notmuch config set database.hook_dir .notmuch-hooks
+           ;;
        XDG)
            HOOK_DIR=${HOME}/.config/notmuch/default/hooks
            ;;
@@ -171,6 +200,37 @@ EOF
     NOTMUCH_NEW
     test_expect_equal_file write.expected write.output
 
+    test_begin_subtest "add message in pre-new [${config}]"
+    rm -rf ${HOOK_DIR}
+    generate_message '[subject]="add msg in pre-new"'
+    id1=$gen_msg_id
+    create_change_hook "pre-new" $gen_msg_filename $HOOK_DIR
+    generate_message '[subject]="add msg in new"'
+    NOTMUCH_NEW
+    notmuch search id:$id1 or id:$gen_msg_id | notmuch_search_sanitize > OUTPUT
+    cat <<EOF | sed s'/^[ \t]*//' > EXPECTED
+    thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; add msg in pre-new (inbox unread)
+    thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; add msg in new (inbox unread)
+EOF
+    test_expect_equal_file EXPECTED OUTPUT
+
+    test_begin_subtest "NOTMUCH_CONFIG is set"
+    create_printenv_hook "pre-new" NOTMUCH_CONFIG OUTPUT
+    NOTMUCH_NEW
+    cat <<EOF > 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 <<EOF > 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 (executable)
index 0000000..0e1d964
--- /dev/null
@@ -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 <<EOF >"${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 <<EOF >"${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 <<EOF > 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 <<EOF > EXPECTED
+${NOTMUCH_CONFIG}.alternate
+EOF
+notmuch --config "${NOTMUCH_CONFIG}.alternate" printenv
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_done
index d9aa8e2d01c811b77a0373cde8105e6e32e29965..40b625feddbfce84db3934ab8db429469efcf7d0 100755 (executable)
@@ -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 <<EOF > EXPECTED
index bfc10be3b7b5101ee213a620a0628962336f9e09..2dfd7c56584fc8ad2de2aafc999e2ce333db52a6 100755 (executable)
@@ -2,6 +2,7 @@
 
 test_description="emacs test function sanity"
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
 test_begin_subtest "emacs test function sanity"
 test_emacs_expect_t 't'
index 02d3b4117b9407f8fed83330681e45ddfa6d9e13..7d3d61ffd56f09af494b9efc536e213c8d222575 100755 (executable)
@@ -2,6 +2,9 @@
 
 test_description="emacs address cleaning"
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
+
+test_require_emacs
 
 test_begin_subtest "notmuch-test-address-clean part 1"
 test_emacs_expect_t '(notmuch-test-address-cleaning-1)'
index d23c1fca9876bb656065fb7cd9ac77221ee071ca..842781a48c3512c82c259688947f8b87b1f53167 100755 (executable)
@@ -2,9 +2,11 @@
 
 test_description="emacs notmuch-hello view"
 . $(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 "User-defined section with inbox tag"
@@ -66,4 +68,22 @@ 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)
+      (test-log-error
+        (let ((default-directory "/nonexistent"))
+         (notmuch-hello-update)))'
+test_expect_equal "$(cat MESSAGES)" "COMPLETE"
+
 test_done
index cca56ca32cd6875e6da4dbcd7b3b90932b3e3bef..559df8aaea3f15d73fab7e1f5b4d6739de130a20 100755 (executable)
@@ -2,9 +2,11 @@
 
 test_description="emacs notmuch-show view"
 . $(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
 
 test_begin_subtest "Hiding Original Message region at beginning of a message"
@@ -60,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\"")
@@ -78,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\"")
@@ -189,9 +258,8 @@ test_expect_equal "$(notmuch_emacs_error_sanitize notmuch_fail OUTPUT MESSAGES E
 === MESSAGES ===
 This is an error (see *Notmuch errors* for more details)
 === ERROR ===
-[XXX]
 This is an error
-command: YYY/notmuch_fail show --format\\=sexp --format-version\\=4 --decrypt\\=true --exclude\\=false \\' \\* \\'
+command: YYY/notmuch_fail show --format\\=sexp --format-version\\=5 --decrypt\\=true --exclude\\=false \\' \\* \\'
 exit status: 1
 stderr:
 This is an error
@@ -208,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_suite@notmuchmail.org>"
 
+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
@@ -218,6 +290,14 @@ test_emacs '(notmuch-show "id:basic-encrypted@crypto.notmuchmail.org")
             (test-visible-output)'
 test_expect_equal_file $EXPECTED/notmuch-show-decrypted-message OUTPUT
 
+test_begin_subtest "show encrypted rfc822 message"
+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'
+
 test_begin_subtest "show undecryptable message"
 test_emacs '(notmuch-show "id:simple-encrypted@crypto.notmuchmail.org")
             (test-visible-output)'
@@ -229,4 +309,107 @@ test_emacs '(let ((notmuch-crypto-process-mime nil))
              (test-visible-output))'
 test_expect_equal_file $EXPECTED/notmuch-show-decrypted-message-no-crypto OUTPUT
 
+test_begin_subtest "notmuch-show with nonexistent CWD"
+tid=$(notmuch search --limit=1 --output=threads '*' | sed s/thread://)
+test_emacs "(test-log-error
+             (let ((default-directory \"/nonexistent\"))
+               (notmuch-show \"$tid\")))"
+test_expect_equal "$(cat MESSAGES)" "COMPLETE"
+
+add_email_corpus attachment
+
+test_begin_subtest "tar not inlined by default"
+test_emacs '(notmuch-show "id:874llc2bkp.fsf@curie.anarc.at")
+       (test-visible-output "OUTPUT")'
+cat <<EOF > EXPECTED
+Antoine Beaupré <anarcat@orangeseeds.org> (2018-03-19) (attachment inbox)
+Subject: Re: bug: "no top level messages" crash on Zen email loops
+To: David Bremner <david@tethera.net>, 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 <<EOF > EXPECTED
+Antoine Beaupré <anarcat@orangeseeds.org> (2018-03-19) (attachment inbox)
+Subject: Re: bug: "no top level messages" crash on Zen email loops
+To: David Bremner <david@tethera.net>, 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 <<EOF > 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 <<EOF > 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_done
diff --git a/test/T453-emacs-reply.sh b/test/T453-emacs-reply.sh
new file mode 100755 (executable)
index 0000000..0a27d06
--- /dev/null
@@ -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 <<EOF > EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Antoine Beaupré <anarcat@orangeseeds.org>
+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é <anarcat@orangeseeds.org> 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 (executable)
index 0000000..3a77017
--- /dev/null
@@ -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 <<EOF > EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Mikhail Gusarov <dottedmag@dottedmag.net>
+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 <<EOF > EXPECTED-PRED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Lars Kellogg-Stedman <lars@seas.harvard.edu>
+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 <<EOF > EXPECTED-NIL
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Lars Kellogg-Stedman <lars@seas.harvard.edu>, Mikhail Gusarov <dottedmag@dottedmag.net>
+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
index cb1297caa5dc3f1421c57de1bbeddea41dd3e194..db03bb67ba3a70a68d0f7eb2f6c6ed104bd20808 100755 (executable)
@@ -2,11 +2,14 @@
 
 test_description="emacs notmuch-show charset handling"
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
 
 UTF8_YEN=$'\xef\xbf\xa5'
 BIG5_YEN=$'\xa2\x44'
 
+test_require_emacs
+
 # Add four messages with unusual encoding requirements:
 #
 # 1) text/plain in quoted-printable big5
index cb2c90b80991ec4a1bf31529bc1f6e74493eefdd..6ef5c54ab8579dfa7a22e2d04c71487fd2d3d352 100755 (executable)
@@ -2,9 +2,11 @@
 
 test_description="emacs tree view interface"
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
 EXPECTED=$NOTMUCH_SRCDIR/test/emacs-tree.expected-output
 
+test_require_emacs
 add_email_corpus
 
 test_begin_subtest "Basic notmuch-tree view in emacs"
@@ -98,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)
@@ -177,4 +179,71 @@ output=$(test_emacs '(notmuch-tree "tag:inbox")
                     (notmuch-show-stash-message-id)')
 test_expect_equal "$output" "\"Stashed: id:1258493565-13508-1-git-send-email-keithp@keithp.com\""
 
+test_begin_subtest "Functions in tree-result-format"
+test_emacs '
+(let
+    ((notmuch-tree-result-format
+     (quote (("date" . "%12s  ")
+            ("authors" . "%-20s")
+            ((("tree" . "%s")
+              ("subject" . "%s")) . " %-54s ")
+            (notmuch-test-result-flags . "(%s)")))))
+  (notmuch-tree "tag:inbox")
+  (notmuch-test-wait)
+  (test-output))
+'
+test_expect_equal_file $EXPECTED/result-format-function OUTPUT
+
+test_begin_subtest "notmuch-tree with nonexistent CWD"
+test_emacs '(test-log-error
+             (let ((default-directory "/nonexistent"))
+               (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_done
+
+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 (executable)
index 0000000..47f7468
--- /dev/null
@@ -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
new file mode 100755 (executable)
index 0000000..a3ff85f
--- /dev/null
@@ -0,0 +1,85 @@
+#!/usr/bin/env bash
+
+test_description="emacs unthreaded interface"
+. $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
+
+test_require_emacs
+
+EXPECTED=$NOTMUCH_SRCDIR/test/emacs-unthreaded.expected-output
+
+generate_message "[id]=large-thread-1" '[subject]="large thread"'
+printf "  2001-01-05  Notmuch Test Suite   large thread%43s(inbox unread)\n" >> EXPECTED.unthreaded
+
+for num in $(seq 2 64); do
+    prev=$((num - 1))
+    generate_message '[subject]="large thread"' "[id]=large-thread-$num" "[in-reply-to]=\<large-thread-$prev\>"
+    printf "  2001-01-05  Notmuch Test Suite   large thread%43s(inbox unread)\n" >> EXPECTED.unthreaded
+done
+printf "End of search results.\n" >> EXPECTED.unthreaded
+
+notmuch new > new.output 2>&1
+
+test_begin_subtest "large thread"
+test_emacs '(let ((max-lisp-eval-depth 10))
+             (notmuch-unthreaded "subject:large-thread")
+             (notmuch-test-wait)
+             (test-output))'
+test_expect_equal_file EXPECTED.unthreaded OUTPUT
+
+test_begin_subtest "message from large thread (status)"
+output=$(test_emacs '(let ((max-lisp-eval-depth 10))
+                      (notmuch-unthreaded "subject:large-thread")
+                      (notmuch-test-wait)
+                      (notmuch-tree-show-message nil)
+                      (notmuch-test-wait)
+                      "SUCCESS")' )
+test_expect_equal "$output" '"SUCCESS"'
+
+add_email_corpus
+test_begin_subtest "Functions in unthreaded-result-format"
+test_emacs '
+(let
+    ((notmuch-unthreaded-result-format
+     (quote (("date" . "%12s  ")
+            ("authors" . "%-20s")
+            ("subject" . "%-54s")
+            (notmuch-test-result-flags . "(%s)")))))
+  (notmuch-unthreaded "tag:inbox")
+  (notmuch-test-wait)
+  (test-output))
+'
+test_expect_equal_file $EXPECTED/result-format-function OUTPUT
+
+test_begin_subtest "notmuch-unthreaded with nonexistent CWD"
+test_emacs '(test-log-error
+             (let ((default-directory "/nonexistent"))
+               (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
index b2eb80b901027ae2931d06f6ef9be33dac04722c..8bddf3e744c7f58d4db165b15905ccc83921b12c 100755 (executable)
@@ -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
index d1c70cfaf5d5fca98e66b9e1c6a2709f55fb7731..3b6e48c42aefe040ec926c1e634bab3c9ffbc7a0 100755 (executable)
@@ -2,15 +2,17 @@
 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 ()
-{
+_date () {
     date -d "$*" +%s
 }
 
-_parse_time ()
-{
+_parse_time () {
     ${TEST_DIRECTORY}/parse-time --format=%s "$*"
 }
 
index 2859d29f37f7548c789e10c12311f406528b4bef..35f3ff8314bd714e19d45d1c0ab35d40396ba3da 100755 (executable)
@@ -10,6 +10,7 @@ test_description='test of proper handling of in-reply-to and references headers'
 # non-RFC-compliant headers'
 
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
 test_begin_subtest "Use References when In-Reply-To is broken"
 add_message '[id]="foo@one.com"' \
index 16222650eafc68b1d1f816a8cdda96bc3b0ce23a..6bcf109c76160c32fa0380fc6d2b68deb4d37474 100755 (executable)
@@ -3,6 +3,13 @@ test_description='"notmuch show"'
 
 . $(dirname "$0")/test-lib.sh || exit 1
 
+test_query_syntax () {
+    test_begin_subtest "sexpr query: $1"
+    sexp=$(notmuch show --format=json --query=sexp "$1")
+    infix=$(notmuch show --format=json "$2")
+    test_expect_equal_json "$sexp" "$infix"
+}
+
 add_email_corpus
 
 test_begin_subtest "exit code for show invalid query"
@@ -10,4 +17,68 @@ notmuch show foo..
 exit_code=$?
 test_expect_equal 1 $exit_code
 
+test_begin_subtest "notmuch show --sort=newest-first"
+notmuch show --entire-thread=true '*' > EXPECTED
+notmuch show --entire-thread=true --sort=newest-first '*' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch show --sort=oldest-first"
+notmuch show --entire-thread=true '*' | grep ^depth:0 > EXPECTED
+notmuch show --entire-thread=true --sort=oldest-first '*' | grep ^depth:0 > OLDEST
+perl -e 'print reverse<>' OLDEST > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch show --sort for single thread"
+QUERY="id:yun1vjwegii.fsf@aiko.keithp.com"
+notmuch show --entire-thread=true --sort=newest-first $QUERY > EXPECTED
+notmuch show --entire-thread=true --sort=oldest-first $QUERY > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+
+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'
+    test_query_syntax 'wizard' 'wizard'
+    test_query_syntax 'Wizard' 'Wizard'
+    test_query_syntax '(attachment notmuch-help.patch)' 'attachment:notmuch-help.patch'
+
+fi
+
+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
index cce29f4580855044f51ea23cb341191f19e0436f..5f0de2edb772743aa2065b7e50dd4bf03a91df9c 100755 (executable)
@@ -54,4 +54,23 @@ for key in 'from/subject/message-ID in database' \
     restore_database
 done
 
+test_begin_subtest "upgrade with configured backup dir"
+notmuch config set database.backup_dir ${HOME}/backups
+delete_feature 'modification tracking'
+notmuch new | grep Backing | notmuch_dir_sanitize | sed 's/dump-[0-9T]*/dump-XXX/' > OUTPUT
+cat <<EOF > EXPECTED
+Backing up tags to CWD/home/backups/dump-XXX.gz...
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "upgrade with relative configured backup dir"
+notmuch config set database.backup_dir ${HOME}/backups
+delete_feature 'modification tracking'
+notmuch new | grep Backing | notmuch_dir_sanitize | sed 's/dump-[0-9T]*/dump-XXX/' > OUTPUT
+cat <<EOF > EXPECTED
+Backing up tags to CWD/home/backups/dump-XXX.gz...
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+
 test_done
index 9d5a9e70a598089793d5d06b136dcbd0ae428ced..3048c7c4a5534c6a092836754ca35b33bc55fe26 100755 (executable)
@@ -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/')
index 89447e9a073da5d74342e32eba259d6a666e67d9..78cae1cd8ee6f93da9412ae1b8d71b9f4226d9f5 100755 (executable)
@@ -16,13 +16,17 @@ 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
 == stdout ==
 == stderr ==
-Error: Cannot open database at CWD/home/.local/share/notmuch/default: No such file or directory.
+Error: could not locate database.
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
@@ -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: Cannot open database at CWD/home/.local/share/notmuch/default: No such file or directory.
 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}
index 769fe86e6f8a347d86c9d49af17c26fd31ae94d5..fedfc9ed15730a7c8dfbd5b5463032daa4bda8df 100755 (executable)
@@ -9,17 +9,17 @@ test_begin_subtest "building database"
 test_expect_success "NOTMUCH_NEW"
 
 cat <<EOF > c_head
-#include <stdio.h>
-#include <notmuch.h>
 #include <notmuch-test.h>
-#include <talloc.h>
+
 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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
 
index 28325ff23605cae0ea644d13c69b2fc0b2bb4896..4711fcdfaa1f192c1d8d8c431558a23524fc5b94 100755 (executable)
@@ -9,10 +9,8 @@ test_begin_subtest "building database"
 test_expect_success "NOTMUCH_NEW"
 
 cat <<EOF > c_head
-#include <stdio.h>
-#include <notmuch.h>
 #include <notmuch-test.h>
-#include <talloc.h>
+
 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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
index 50b0a88e0793ada49f486cf463865ac064445085..53a63bf61ac29611d390a355e8ddf35f2b3b8f4d 100755 (executable)
@@ -9,17 +9,17 @@ test_begin_subtest "building database"
 test_expect_success "NOTMUCH_NEW"
 
 cat <<EOF > c_head
-#include <stdio.h>
-#include <notmuch.h>
 #include <notmuch-test.h>
-#include <talloc.h>
+
 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 (executable)
index 0000000..2a59f8d
--- /dev/null
@@ -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 <<EOF > c_head
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <talloc.h>
+#include <notmuch.h>
+
+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
index ee55ef291d7a44c23a2728cf26c9af36be8f1107..6905193788a4b22419897119d2e1d14d50c405bc 100755 (executable)
@@ -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 <<EOF > c_head0
-#include <stdio.h>
-#include <notmuch.h>
 #include <notmuch-test.h>
+
 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > EXPECTED
@@ -306,12 +311,126 @@ cat <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > EXPECTED
@@ -401,4 +520,31 @@ cat <<EOF > 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 <<EOF > 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
index 088e66dd937313bd1f9c44d60513f998f2b3dc7a..b4c24ca23b049b4ec8e06a67d28e9d7fb1bae407 100755 (executable)
@@ -24,9 +24,8 @@ cat <<'EOF' > c_tail
 EOF
 
 cat <<EOF > c_head
-#include <stdio.h>
-#include <notmuch.h>
 #include <notmuch-test.h>
+
 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);
index a59e7c980fc97c7c0fb4658cfe616da63d5a9e15..bcc97dd9396305c7b984d156e575bdde87198804 100755 (executable)
@@ -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
index 51dd29c845584fefbe4d8f089d440da4070ed8a1..9326ba3e3d72fa856a02458dfcfe0f67084ed1b4 100755 (executable)
@@ -5,9 +5,24 @@ test_description="library config API"
 
 add_email_corpus
 
+_libconfig_sanitize() {
+    ${NOTMUCH_PYTHON} /dev/fd/3 3<<'EOF'
+import os, sys, pwd, socket
+
+pw = pwd.getpwuid(os.getuid())
+user = pw.pw_name
+name = pw.pw_gecos.partition(",")[0]
+
+for l in sys.stdin:
+    if l[:4] == "08: ":
+        l = l.replace(user, "USERNAME", 1)
+    elif l[:4] == "10: ":
+        l = l.replace("'" + name, "'USER_FULL_NAME", 1)
+    sys.stdout.write(l)
+EOF
+}
+
 cat <<EOF > c_head
-#include <string.h>
-#include <stdlib.h>
 #include <notmuch-test.h>
 
 int main (int argc, char** argv)
@@ -255,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%
 {
@@ -380,26 +418,29 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} '' %NULL%
         key < NOTMUCH_CONFIG_LAST;
         key = (notmuch_config_key_t)(key + 1)) {
        const char *val = notmuch_config_get (db, key);
-        printf("%s\n", val ? val : "NULL" );
+       printf("%02d: '%s'\n", key, val ? val : "NULL" );
     }
 }
 EOF
 
-notmuch_passwd_sanitize < OUTPUT > OUTPUT.clean
+_libconfig_sanitize < OUTPUT > OUTPUT.clean
 
 cat <<'EOF' >EXPECTED
 == stdout ==
-MAIL_DIR
-MAIL_DIR
-MAIL_DIR/.notmuch/hooks
-MAIL_DIR/.notmuch/backups
-
-unread;inbox
-
-true
-USERNAME@FQDN
-NULL
-USER_FULL_NAME
+00: 'MAIL_DIR'
+01: 'MAIL_DIR'
+02: 'MAIL_DIR/.notmuch/hooks'
+03: 'MAIL_DIR/.notmuch/backups'
+04: ''
+05: 'unread;inbox'
+06: ''
+07: 'true'
+08: 'USERNAME@localhost'
+09: 'NULL'
+10: 'USER_FULL_NAME'
+11: '8000'
+12: 'NULL'
+13: ''
 == stderr ==
 EOF
 unset MAILDIR
@@ -443,7 +484,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
 }
 EOF
 rm -f ${ovconfig}
-NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
 cat <<'EOF' >EXPECTED
 == stdout ==
 test.key1 = overridden-home
@@ -470,7 +511,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
 }
 EOF
 rm -f ${ovconfig}
-NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
 cat <<'EOF' >EXPECTED
 == stdout ==
 test.key1 = overridden-xdg
@@ -497,7 +538,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% work
 }
 EOF
 rm -f ${ovconfig}
-NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
 cat <<'EOF' >EXPECTED
 == stdout ==
 test.key1 = overridden-xdg-profile
@@ -523,7 +564,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% work
 }
 EOF
 #rm -f ${ovconfig}
-NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
 cat <<'EOF' >EXPECTED
 == stdout ==
 test.key1 = overridden-profile
@@ -541,13 +582,13 @@ cat c_head - c_tail <<'EOF' | test_C %NULL% '' %NULL%
    printf("NOT RUN");
 }
 EOF
-NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
 cat <<'EOF' >EXPECTED
 == stdout ==
 == stderr ==
 error opening database
-Something went wrong trying to read or write a file
-Error: Cannot open database at CWD/home/.local/share/notmuch/default: No such file or directory.
+No database found
+Error: could not locate database.
 
 EOF
 test_expect_equal_file EXPECTED OUTPUT
@@ -586,7 +627,7 @@ cat c_head - c_tail <<'EOF' | test_C %NULL% '' %NULL%
    printf("test.key2 = %s\n", val);
 }
 EOF
-NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
 unset NOTMUCH_DATABASE
 cat <<'EOF' >EXPECTED
 == stdout ==
@@ -598,8 +639,6 @@ cp notmuch-config.bak notmuch-config
 test_expect_equal_file EXPECTED OUTPUT
 
 cat <<EOF > c_head2
-#include <string.h>
-#include <stdlib.h>
 #include <notmuch-test.h>
 
 int main (int argc, char** argv)
@@ -687,6 +726,7 @@ test_expect_equal_file EXPECTED OUTPUT
 test_begin_subtest "list by keys (ndlc)"
 notmuch config set search.exclude_tags "foo;bar;fub"
 notmuch config set new.ignore "sekrit_junk"
+notmuch config set index.as_text "text/"
 cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
 {
     notmuch_config_key_t key;
@@ -694,23 +734,26 @@ cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
         key < NOTMUCH_CONFIG_LAST;
         key = (notmuch_config_key_t)(key + 1)) {
        const char *val = notmuch_config_get (db, key);
-        printf("%s\n", val ? val : "NULL" );
+       printf("%02d: '%s'\n", key, val ? val : "NULL" );
     }
 }
 EOF
 cat <<'EOF' >EXPECTED
 == stdout ==
-MAIL_DIR
-MAIL_DIR
-MAIL_DIR/.notmuch/hooks
-MAIL_DIR/.notmuch/backups
-foo;bar;fub
-unread;inbox
-sekrit_junk
-true
-test_suite@notmuchmail.org
-test_suite_other@notmuchmail.org;test_suite@otherdomain.org
-Notmuch Test Suite
+00: 'MAIL_DIR'
+01: 'MAIL_DIR'
+02: 'MAIL_DIR/.notmuch/hooks'
+03: 'MAIL_DIR/.notmuch/backups'
+04: 'foo;bar;fub'
+05: 'unread;inbox'
+06: 'sekrit_junk'
+07: 'true'
+08: 'test_suite@notmuchmail.org'
+09: 'test_suite_other@notmuchmail.org;test_suite@otherdomain.org'
+10: 'Notmuch Test Suite'
+11: '8000'
+12: 'NULL'
+13: 'text/'
 == stderr ==
 EOF
 test_expect_equal_file EXPECTED OUTPUT
@@ -723,25 +766,29 @@ cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} /nonexistent %NULL%
         key < NOTMUCH_CONFIG_LAST;
         key = (notmuch_config_key_t)(key + 1)) {
        const char *val = notmuch_config_get (db, key);
-       printf("%s\n", val ? val : "NULL" );
+       printf("%02d: '%s'\n", key, val ? val : "NULL" );
     }
 }
 EOF
 
-notmuch_passwd_sanitize < OUTPUT > OUTPUT.clean
+_libconfig_sanitize < OUTPUT > OUTPUT.clean
+
 cat <<'EOF' >EXPECTED
 == stdout ==
-MAIL_DIR
-MAIL_DIR
-MAIL_DIR/.notmuch/hooks
-MAIL_DIR/.notmuch/backups
-
-unread;inbox
-
-true
-USERNAME@FQDN
-NULL
-USER_FULL_NAME
+00: 'MAIL_DIR'
+01: 'MAIL_DIR'
+02: 'MAIL_DIR/.notmuch/hooks'
+03: 'MAIL_DIR/.notmuch/backups'
+04: ''
+05: 'unread;inbox'
+06: ''
+07: 'true'
+08: 'USERNAME@localhost'
+09: 'NULL'
+10: 'USER_FULL_NAME'
+11: '8000'
+12: 'NULL'
+13: ''
 == stderr ==
 EOF
 test_expect_equal_file EXPECTED OUTPUT.clean
@@ -762,7 +809,7 @@ cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
 }
 EOF
 rm -f ${ovconfig}
-NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
 cat <<'EOF' >EXPECTED
 == stdout ==
 test.key1 = overridden-home
@@ -808,15 +855,18 @@ EOF
 cat <<'EOF' >EXPECTED
 == stdout ==
 aaabefore beforeval
+database.autocommit 8000
 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.exclude_tags foo;bar;fub
+show.extra_headers (null)
 test.key1 testvalue1
 test.key2 testvalue2
 user.name Notmuch Test Suite
@@ -827,4 +877,169 @@ zzzafter afterval
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+cat <<EOF > c_head3
+#include <notmuch-test.h>
+int main (int argc, char **argv) {
+  notmuch_status_t stat;
+  notmuch_database_t *db = NULL;
+EOF
+
+cat <<EOF > 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 <<EOF> 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 <<EOF> 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 <<EOF> 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 <<EOF> 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 <<EOF> 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 <<EOF> 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 <<EOF> 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 <<EOF> 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 <<EOF > 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 <<EOF> 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/T590-thread-breakage.sh b/test/T590-thread-breakage.sh
deleted file mode 100755 (executable)
index aeb82cf..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/usr/bin/env bash
-#
-# Copyright (c) 2016 Daniel Kahn Gillmor
-#
-
-test_description='thread breakage during reindexing'
-
-# notmuch uses ghost documents to track messages we have seen references
-# to but have never seen.  Regardless of the order of delivery, message
-# deletion, and reindexing, the list of ghost messages for a given
-# stored corpus should not vary, so that threads can be reassmebled
-# cleanly.
-#
-# In practice, we accept a small amount of variation (and therefore
-# traffic pattern metadata leakage to be stored in the index) for the
-# sake of efficiency.
-#
-# This test also embeds some subtests to ensure that indexing actually
-# works properly and attempted fixes to threading issues do not break
-# the expected contents of the index.
-
-. $(dirname "$0")/test-lib.sh || exit 1
-
-message_a() {
-    mkdir -p ${MAIL_DIR}/cur
-    cat > ${MAIL_DIR}/cur/a <<EOF
-Subject: First message
-Message-ID: <a@example.net>
-From: Alice <alice@example.net>
-To: Bob <bob@example.net>
-Date: Thu, 31 Mar 2016 20:10:00 -0400
-
-This is the first message in the thread.
-Apple
-EOF
-}
-
-message_b() {
-    mkdir -p ${MAIL_DIR}/cur
-    cat > ${MAIL_DIR}/cur/b <<EOF
-Subject: Second message
-Message-ID: <b@example.net>
-In-Reply-To: <a@example.net>
-References: <a@example.net>
-From: Bob <bob@example.net>
-To: Alice <alice@example.net>
-Date: Thu, 31 Mar 2016 20:15:00 -0400
-
-This is the second message in the thread.
-Banana
-EOF
-}
-
-
-test_content_count() {
-    test_begin_subtest "${3:-looking for $2 instance of '$1'}"
-    count=$(notmuch count --output=threads "$1")
-    test_expect_equal "$count" "$2"
-}
-
-test_thread_count() {
-    test_begin_subtest "${2:-Expecting $1 thread(s)}"
-    count=$(notmuch count --output=threads)
-    test_expect_equal "$count" "$1"
-}
-
-test_ghost_count() {
-    test_begin_subtest "${2:-Expecting $1 ghosts(s)}"
-    ghosts=$($NOTMUCH_BUILDDIR/test/ghost-report ${MAIL_DIR}/.notmuch/xapian)
-    test_expect_equal "$ghosts" "$1"
-}
-
-notmuch new >/dev/null
-
-test_thread_count 0 'There should be no threads initially'
-test_ghost_count 0 'There should be no ghosts initially'
-
-message_a
-notmuch new >/dev/null
-test_thread_count 1 'One message in: one thread'
-test_content_count apple 1
-test_content_count banana 0
-test_ghost_count 0
-
-message_b
-notmuch new >/dev/null
-test_thread_count 1 'Second message in the same thread: one thread'
-test_content_count apple 1
-test_content_count banana 1
-test_ghost_count 0
-
-rm -f ${MAIL_DIR}/cur/a
-notmuch new >/dev/null
-test_thread_count 1 'First message removed: still only one thread'
-test_content_count apple 0
-test_content_count banana 1
-test_ghost_count 1 'should be one ghost after first message removed'
-
-message_a
-notmuch new >/dev/null
-test_thread_count 1 'First message reappears: should return to the same thread'
-test_content_count apple 1
-test_content_count banana 1
-test_ghost_count 0
-
-rm -f ${MAIL_DIR}/cur/b
-notmuch new >/dev/null
-test_thread_count 1 'Removing second message: still only one thread'
-test_content_count apple 1
-test_content_count banana 0
-test_begin_subtest 'No ghosts should remain after deletion of second message'
-# this is known to fail; we are leaking ghost messages deliberately
-test_subtest_known_broken
-ghosts=$($NOTMUCH_BUILDDIR/test/ghost-report ${MAIL_DIR}/.notmuch/xapian)
-test_expect_equal "$ghosts" "0"
-
-rm -f ${MAIL_DIR}/cur/a
-notmuch new >/dev/null
-test_thread_count 0 'All messages gone: no threads'
-test_content_count apple 0
-test_content_count banana 0
-test_ghost_count 0 'No ghosts should remain after full thread deletion'
-
-test_done
diff --git a/test/T592-thread-breakage.sh b/test/T592-thread-breakage.sh
new file mode 100755 (executable)
index 0000000..2334fca
--- /dev/null
@@ -0,0 +1,128 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2016 Daniel Kahn Gillmor
+#
+
+test_description='thread breakage during reindexing'
+
+# notmuch uses ghost documents to track messages we have seen references
+# to but have never seen.  Regardless of the order of delivery, message
+# deletion, and reindexing, the list of ghost messages for a given
+# stored corpus should not vary, so that threads can be reassmebled
+# cleanly.
+#
+# In practice, we accept a small amount of variation (and therefore
+# traffic pattern metadata leakage to be stored in the index) for the
+# sake of efficiency.
+#
+# This test also embeds some subtests to ensure that indexing actually
+# works properly and attempted fixes to threading issues do not break
+# the expected contents of the index.
+
+. $(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 <<EOF
+Subject: First message
+Message-ID: <a@example.net>
+From: Alice <alice@example.net>
+To: Bob <bob@example.net>
+Date: Thu, 31 Mar 2016 20:10:00 -0400
+
+This is the first message in the thread.
+Apple
+EOF
+}
+
+message_b () {
+    mkdir -p ${MAIL_DIR}/cur
+    cat > ${MAIL_DIR}/cur/b <<EOF
+Subject: Second message
+Message-ID: <b@example.net>
+In-Reply-To: <a@example.net>
+References: <a@example.net>
+From: Bob <bob@example.net>
+To: Alice <alice@example.net>
+Date: Thu, 31 Mar 2016 20:15:00 -0400
+
+This is the second message in the thread.
+Banana
+EOF
+}
+
+
+test_content_count () {
+    test_begin_subtest "${3:-looking for $2 instance of '$1'}"
+    count=$(notmuch count --output=threads "$1")
+    test_expect_equal "$count" "$2"
+}
+
+test_thread_count () {
+    test_begin_subtest "${2:-Expecting $1 thread(s)}"
+    count=$(notmuch count --output=threads)
+    test_expect_equal "$count" "$1"
+}
+
+test_ghost_count () {
+    test_begin_subtest "${2:-Expecting $1 ghosts(s)}"
+    ghosts=$($NOTMUCH_BUILDDIR/test/ghost-report ${MAIL_DIR}/.notmuch/xapian)
+    test_expect_equal "$ghosts" "$1"
+}
+
+notmuch new >/dev/null
+
+test_thread_count 0 'There should be no threads initially'
+test_ghost_count 0 'There should be no ghosts initially'
+
+message_a
+notmuch new >/dev/null
+test_thread_count 1 'One message in: one thread'
+test_content_count apple 1
+test_content_count banana 0
+test_ghost_count 0
+
+message_b
+notmuch new >/dev/null
+test_thread_count 1 'Second message in the same thread: one thread'
+test_content_count apple 1
+test_content_count banana 1
+test_ghost_count 0
+
+rm -f ${MAIL_DIR}/cur/a
+notmuch new >/dev/null
+test_thread_count 1 'First message removed: still only one thread'
+test_content_count apple 0
+test_content_count banana 1
+test_ghost_count 1 'should be one ghost after first message removed'
+
+message_a
+notmuch new >/dev/null
+test_thread_count 1 'First message reappears: should return to the same thread'
+test_content_count apple 1
+test_content_count banana 1
+test_ghost_count 0
+
+rm -f ${MAIL_DIR}/cur/b
+notmuch new >/dev/null
+test_thread_count 1 'Removing second message: still only one thread'
+test_content_count apple 1
+test_content_count banana 0
+test_begin_subtest 'No ghosts should remain after deletion of second message'
+# this is known to fail; we are leaking ghost messages deliberately
+test_subtest_known_broken
+ghosts=$($NOTMUCH_BUILDDIR/test/ghost-report ${MAIL_DIR}/.notmuch/xapian)
+test_expect_equal "$ghosts" "0"
+
+rm -f ${MAIL_DIR}/cur/a
+notmuch new >/dev/null
+test_thread_count 0 'All messages gone: no threads'
+test_content_count apple 0
+test_content_count banana 0
+test_ghost_count 0 'No ghosts should remain after full thread deletion'
+
+test_done
index 7375e2acefbdb7ce947acc475e0af68778f4d600..1a517423e3e476a2acaa4c8b871badadfb7084f8 100755 (executable)
@@ -6,8 +6,6 @@ test_description="library reopen API"
 add_email_corpus
 
 cat <<EOF > c_head
-#include <string.h>
-#include <stdlib.h>
 #include <notmuch-test.h>
 
 int main (int argc, char** argv)
index d0e52f4ac46cbb481b76197682f43ecb3adce728..a7cbe04842a78a20827c06d4b5d5f825cf0807c7 100755 (executable)
@@ -6,17 +6,13 @@ test_description="message property API"
 add_email_corpus
 
 cat <<EOF > c_head
-#include <stdio.h>
-#include <string.h>
-#include <stdlib.h>
-#include <talloc.h>
 #include <notmuch-test.h>
 
 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 <<EOF > 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 <<EOF > 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
@@ -328,7 +324,6 @@ EOF
 cat <<'EOF' > EXPECTED
 testkey1 = alice
 testkey1 = bob
-testkey1 = testvalue1
 testkey1 = testvalue2
 EOF
 test_expect_equal_file EXPECTED OUTPUT
@@ -344,7 +339,6 @@ EOF
 cat <<'EOF' > EXPECTED
 testkey1 = alice
 testkey1 = bob
-testkey1 = testvalue1
 testkey1 = testvalue2
 testkey3 = alice3
 testkey3 = bob3
@@ -362,4 +356,49 @@ for (key,val) in msg.get_properties("testkey",True):
 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
index 7aaaff2ababea2bc9ef6a6cd7bc9844782a30f38..99cc70103d07105f8cee6acbe19df2f50612f461 100755 (executable)
@@ -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 <unistd.h>
-#include <stdlib.h>
-#include <sys/wait.h>
 #include <notmuch-test.h>
 
 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));
index d7903ce799da7ac846fce8c50f0c521f70650adc..c443f417c40824786b3fdd38bf7f6f5e2e509039 100755 (executable)
@@ -1,7 +1,9 @@
 #!/usr/bin/env bash
 test_description="Emacs Draft Handling"
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
 
+test_require_emacs
 add_email_corpus
 
 notmuch config set search.exclude_tags deleted
index 274105c708fa2d4c73429520e3811fcdce6debff..2c3fa7356134664e37fe04f8148656f495c9ae42 100755 (executable)
@@ -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} <<EOF
-#include <unistd.h>
-#include <stdlib.h>
 #include <notmuch-test.h>
-#include <talloc.h>
-#include <assert.h>
+
 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));
 
index 55dc6c8870d3a1fd8eccbf280bc79628e9b36aec..a9844501edf4b60f7c2652838a26ef7926363a4c 100755 (executable)
@@ -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
index 4e5672abc4e880a1b6c536366d5936d03d7c373b..8fec291ee97a6e8158c20797129465e5930d2033 100755 (executable)
@@ -2,10 +2,25 @@
 test_description="duplicate message ids"
 . $(dirname "$0")/test-lib.sh || exit 1
 
+test_require_external_prereq xapian-delve
+
 add_message '[id]="duplicate"' '[subject]="message 1" [filename]=copy1'
 add_message '[id]="duplicate"' '[subject]="message 2" [filename]=copy2'
 
 add_message '[id]="duplicate"' '[subject]="message 0" [filename]=copy0'
+
+test_begin_subtest 'at most 1 thread-id per xapian document'
+db=${MAIL_DIR}/.notmuch/xapian
+for doc in $(xapian-delve -1 -t '' "$db" | grep '^[1-9]'); do
+    xapian-delve -1 -r "$doc" "$db" | grep -c '^G'
+done > OUTPUT.raw
+sort -u < OUTPUT.raw > OUTPUT
+cat <<EOF > EXPECTED
+0
+1
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
 test_begin_subtest 'search: first indexed subject preserved'
 cat <<EOF > EXPECTED
 thread:XXX   2001-01-05 [1/1(3)] Notmuch Test Suite; message 1 (inbox unread)
index f51130e8f86d4ed8c4a80698b971305c34b4ae91..af34ad7cb9e756636377604c3f01adff88d0bf6f 100755 (executable)
@@ -4,10 +4,25 @@ test_description='reindexing messages'
 
 add_email_corpus
 
+
+if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then
+
+    count=$(notmuch count --lastmod '*' | cut -f 3)
+    for query in '()' '(not)' '(and)' '(or ())' '(or (not))' '(or (and))' \
+               '(or (and) (or) (not (and)))'; do
+       test_begin_subtest "reindex all messages: $query"
+       notmuch reindex --query=sexp "$query"
+       output=$(notmuch count --lastmod '*' | cut -f 3)
+       count=$((count + 1))
+       test_expect_equal "$output" "$count"
+    done
+
+fi
+
 notmuch tag +usertag1 '*'
 
-notmuch search '*' | notmuch_search_sanitize > initial-threads
-notmuch search --output=messages '*' > initial-message-ids
+notmuch search '*' 2>1 | notmuch_search_sanitize > initial-threads
+notmuch search --output=messages '*' 2>/dev/null > initial-message-ids
 notmuch dump > initial-dump
 
 test_begin_subtest 'reindex preserves threads'
@@ -41,6 +56,7 @@ notmuch dump > OUTPUT
 notmuch tag -attachment2 -encrypted2 -signed2 '*'
 test_expect_equal_file EXPECTED OUTPUT
 
+backup_database
 test_begin_subtest 'reindex moves a message between threads'
 notmuch search --output=threads id:87iqd9rn3l.fsf@vertex.dottedmag > EXPECTED
 # re-parent
@@ -48,7 +64,19 @@ sed -i 's/1258471718-6781-1-git-send-email-dottedmag@dottedmag.net/87iqd9rn3l.fs
 notmuch reindex id:1258471718-6781-2-git-send-email-dottedmag@dottedmag.net
 notmuch search --output=threads id:1258471718-6781-2-git-send-email-dottedmag@dottedmag.net > OUTPUT
 test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest 'reindex detects removal of all files'
+notmuch search --output=messages not id:20091117232137.GA7669@griffis1.net> EXPECTED
+# remove both copies
+mv $MAIL_DIR/cur/51:2,* duplicate-message-2.eml
+notmuch reindex id:20091117232137.GA7669@griffis1.net
+notmuch search --output=messages '*' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
 
+backup_database
 test_begin_subtest 'reindex detects removal of all files'
 notmuch search --output=messages not id:20091117232137.GA7669@griffis1.net> EXPECTED
 # remove both copies
@@ -56,6 +84,7 @@ mv $MAIL_DIR/cur/51:2,* duplicate-message-2.eml
 notmuch reindex id:20091117232137.GA7669@griffis1.net
 notmuch search --output=messages '*' > OUTPUT
 test_expect_equal_file EXPECTED OUTPUT
+restore_database
 
 test_begin_subtest "reindex preserves properties"
 cat <<EOF > prop-dump
@@ -83,4 +112,11 @@ notmuch reindex '*'
 notmuch search '*' | notmuch_search_sanitize > OUTPUT
 test_expect_equal_file EXPECTED OUTPUT
 
+
+test_begin_subtest "reindex after removing corpus"
+tar cf backup.tar mail/cur
+find mail/cur -type f -delete
+test_expect_success "notmuch reindex '*'"
+tar xf backup.tar
+
 test_done
index 5129d84c544dc5f0b101fed30a51da0e5cd7c590..a2d8ec71ecd12c043217bc09b122d3a8a3e02dfc 100755 (executable)
@@ -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 <<EOF >OUTPUT
 <018b1a8f2d1df62e804ce88b65401304832dfbbf.1346614915.git.jani@nikula.org>
index c8d2bcc241a6b4f2141bb0001cf36bed80b12c70..6e03f39d3437d40b3495ce44e6d170de98c174cc 100755 (executable)
@@ -2,6 +2,9 @@
 
 test_description="emacs attachment 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-attachment-warning part 1"
 test_emacs_expect_t '(notmuch-test-attachment-warning-1)'
index 3d94d4df9d243a32871c04ad44d82ac3f282f3b5..e5afeaa253ae8bf323167dcd8992ee0e48770c3f 100755 (executable)
@@ -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);
index 45e61568ec62540b03094562dfb3aa1c0def08f8..7b6ebf15b9b60e875dfba1904e23b3de4885dfcb 100755 (executable)
@@ -2,6 +2,9 @@
 
 test_description="emacs forwarding"
 . $(dirname "$0")/test-lib.sh || exit 1
+. $NOTMUCH_SRCDIR/test/test-lib-emacs.sh || exit 1
+
+test_require_emacs
 
 test_begin_subtest "Forward setting the correct references header"
 # Check that, when forwarding a message, the new message has
index 4408d085a58031046d81148375695d55ce5eef25..5648896fba377b231e7cb688a316ccb06787049c 100755 (executable)
@@ -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 <<EOF > 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
 
index 05f80885eec5d23360f282187bc62a33acd51f07..03c436561601800983f4a0b059d42181b7d1cce5 100755 (executable)
@@ -4,8 +4,8 @@ test_description='indexing user specified headers'
 
 add_email_corpus
 
-notmuch search '*' | notmuch_search_sanitize > initial-threads
-notmuch search --output=messages '*' > initial-message-ids
+notmuch search '*' 2>1 | notmuch_search_sanitize > initial-threads
+notmuch search --output=messages '*' 2>/dev/null > initial-message-ids
 notmuch dump > initial-dump
 
 test_begin_subtest "adding illegal prefix name, bad utf8"
diff --git a/test/T760-as-text.sh b/test/T760-as-text.sh
new file mode 100755 (executable)
index 0000000..744567f
--- /dev/null
@@ -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<<EOF > EXPECTED
+Error in index.as_text: Invalid regular expression: [
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T800-asan.sh b/test/T800-asan.sh
new file mode 100755 (executable)
index 0000000..9ce6baa
--- /dev/null
@@ -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} <<EOF
+#include <notmuch.h>
+#include <stdio.h>
+
+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 <<EOF > 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 (executable)
index 0000000..4071e29
--- /dev/null
@@ -0,0 +1,92 @@
+#!/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.
+
+. "$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 <<EOF
+#include <notmuch-test.h>
+#include <pthread.h>
+
+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 <<EOF > 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 <<EOF
+#include <notmuch-test.h>
+#include <pthread.h>
+
+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 <<EOF > EXPECTED
+== stdout ==
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T810-tsan.suppressions b/test/T810-tsan.suppressions
new file mode 100644 (file)
index 0000000..80dc062
--- /dev/null
@@ -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 (executable)
index 0000000..831e467
--- /dev/null
@@ -0,0 +1,402 @@
+#!/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 <<EOF>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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > EXPECTED
++inbox +signed +test2 +test3 +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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 <<EOF > 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
index 75400e6e3e3f6f14dbecff9902df84ec5aaf26f9..6845fcf0b2ae06b55e8a94e926c1f30484e3f14d 100755 (executable)
@@ -8,9 +8,16 @@ failed=0
 broken=0
 total=0
 all_skipped=0
+rep_failed=0
 
 for file
 do
+       if [ ! -f "$file" ]; then
+               echo "'$file' does not exist!"
+               rep_failed=$((rep_failed + 1))
+               continue
+       fi
+       has_total=0
        while read type value
        do
                case $type in
@@ -24,18 +31,23 @@ do
                        broken=$((broken + value)) ;;
                total)
                        total=$((total + value))
+                       has_total=1
                        if [ "$value" -eq 0 ]; then
                                all_skipped=$((all_skipped + 1))
                        fi
                esac
        done <"$file"
+       if [ "$has_total" -eq 0 ]; then
+               echo "'$file' lacks 'total ...'; results may be inconsistent."
+               failed=$((failed + 1))
+       fi
 done
 
 pluralize_s () { [ "$1" -eq 1 ] && s='' || s='s'; }
 
 echo "Notmuch test suite complete."
 
-if [ "$fixed" -eq 0 ] && [ "$failed" -eq 0 ]; then
+if [ "$fixed" -eq 0 ] && [ "$failed" -eq 0 ] && [ "$rep_failed" -eq 0 ]; then
        pluralize_s "$total"
        printf "All $total test$s "
        if [ "$broken" -eq 0 ]; then
@@ -70,10 +82,16 @@ if [ "$all_skipped" -ne 0 ]; then
        echo "All tests in $all_skipped file$s skipped."
 fi
 
+if [ "$rep_failed" -ne 0 ]; then
+       pluralize_s "$rep_failed"
+       echo "$rep_failed test$s failed to report results."
+fi
+
 # Note that we currently do not consider skipped tests as failing the
 # build.
 
-if [ "$success" -gt 0 ] && [ "$fixed" -eq 0 ] && [ "$failed" -eq 0 ]
+if [ "$success" -gt 0 ] && [ "$fixed" -eq 0 ] &&
+       [ "$failed" -eq 0 ] && [ "$rep_failed" -eq 0 ]
 then
        exit 0
 else
diff --git a/test/corpora/attachment/x-gtar-compressed.eml b/test/corpora/attachment/x-gtar-compressed.eml
new file mode 100644 (file)
index 0000000..258a74d
--- /dev/null
@@ -0,0 +1,136 @@
+Return-path: <anarcat@orangeseeds.org>
+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 <anarcat@orangeseeds.org>)
+       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?= <anarcat@orangeseeds.org>
+To: David Bremner <david@tethera.net>, 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
+
+--=-=-=--
index 1ba4698a25eccca10f8d3104d4d939b37d8c6eac..b139a735431d10c4f1200f5964e8c4747da1405d 100644 (file)
@@ -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
new file mode 100644 (file)
index 0000000..e7bad9b
--- /dev/null
@@ -0,0 +1,47 @@
+Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="===============9060418334135509864=="
+MIME-Version: 1.0
+From: Notmuch test suite <test_suite@notmuchmail.org>
+To: test_suite@notmuchmail.org
+Subject: testing encrypted rfc822 attachments
+Date: Sat, 03 Jul 2021 16:00:02 -0300
+Message-ID: <encrypted-rfc822-attachment@crypto.notmuchmail.org>
+User-Agent: alot/0.9.1
+
+--===============9060418334135509864==
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Type: application/pgp-encrypted; charset="us-ascii"
+
+Version: 1
+--===============9060418334135509864==
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Type: application/octet-stream; charset="us-ascii"
+
+-----BEGIN PGP MESSAGE-----
+
+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==--
index 0345e3e94036ac218975fe2fe246b040fb763331..f5d5d1203870666c5b4986eb6804bf175f9ded45 100644 (file)
@@ -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-----
 --=-=-=--
index 75b05fa4e252c2d955b6738ab25a8dcef60b6427..ce174b52a4caae3ff92dfca7ff8a913076a7bb98 100644 (file)
@@ -63,7 +63,7 @@ and <a href=3D"http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg01680=
 
 --0016e687869333b14e0478963d33--
 --0016e687869333b1570478963d35
-Content-Type: application/octet-stream; 
+Content-Type: text/x-diff;
        name="0001-Deal-with-situation-where-sysconf-_SC_GETPW_R_SIZE_M.patch"
 Content-Disposition: attachment; 
        filename="0001-Deal-with-situation-where-sysconf-_SC_GETPW_R_SIZE_M.patch"
diff --git a/test/corpora/duplicate/msg-1-1:2, b/test/corpora/duplicate/msg-1-1:2,
new file mode 100644 (file)
index 0000000..bc7da06
--- /dev/null
@@ -0,0 +1,85 @@
+Return-path: <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>
+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 <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       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 <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       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 <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       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 <gladky-anton-guest@alioth.debian.org>)
+       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 <gladky-anton-guest@vasks.debian.org>)
+       id 1TdqIc-0003j1-DE; Wed, 28 Nov 2012 22:40:10 +0000
+Date: Wed, 28 Nov 2012 22:40:10 +0000
+From: Anton Gladky <gladky.anton@gmail.com>
+To: 691896@bugs.debian.org, control@bugs.debian.org,
+       691896-submitter@bugs.debian.org
+Message-ID: <debian/2.6.1.dfsg-4-1-g87ea161@87ea161e851dfb1ea324af00e4ecfccc18875e15>
+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
+       <debian-science-maintainers.lists.alioth.debian.org>
+List-Unsubscribe: <http://lists.alioth.debian.org/cgi-bin/mailman/options/debian-science-maintainers>,
+       <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=unsubscribe>
+List-Archive: <http://lists.alioth.debian.org/pipermail/debian-science-maintainers>
+List-Post: <mailto:debian-science-maintainers@lists.alioth.debian.org>
+List-Help: <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=help>
+List-Subscribe: <http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-science-maintainers>,
+       <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=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 <gladky.anton@gmail.com> 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 (file)
index 0000000..dc0476e
--- /dev/null
@@ -0,0 +1,113 @@
+Return-path: <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>
+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 <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       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 <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       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 <debian-science-maintainers-bounces+david=tethera.net@lists.alioth.debian.org>)
+       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 <debbugs@buxtehude.debian.org>) 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 <debbugs@buxtehude.debian.org>)
+       id 1TdqKR-0001dH-UL; Wed, 28 Nov 2012 22:42:03 +0000
+X-Loop: owner@bugs.debian.org
+Resent-From: Anton Gladky <gladky.anton@gmail.com>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Science Maintainers
+       <debian-science-maintainers@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Wed, 28 Nov 2012 22:42:02 +0000
+Resent-Message-ID: <handler.691896.B691896.13541424145158@bugs.debian.org>
+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 <gladky-anton-guest@alioth.debian.org>)
+       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 <gladky-anton-guest@alioth.debian.org>)
+       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 <gladky-anton-guest@vasks.debian.org>)
+       id 1TdqIc-0003j1-DE; Wed, 28 Nov 2012 22:40:10 +0000
+Date: Wed, 28 Nov 2012 22:40:10 +0000
+From: Anton Gladky <gladky.anton@gmail.com>
+To: 691896@bugs.debian.org, control@bugs.debian.org,
+       691896-submitter@bugs.debian.org
+Message-ID: <debian/2.6.1.dfsg-4-1-g87ea161@87ea161e851dfb1ea324af00e4ecfccc18875e15>
+X-PTS-Approved: Yes
+Resent-Sender: Debian BTS <debbugs@buxtehude.debian.org>
+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 <gladky.anton@gmail.com>, 691896@bugs.debian.org
+List-Id: Mailing list for maintainer discussions and BTS messages
+       <debian-science-maintainers.lists.alioth.debian.org>
+List-Unsubscribe: <http://lists.alioth.debian.org/cgi-bin/mailman/options/debian-science-maintainers>,
+       <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=unsubscribe>
+List-Archive: <http://lists.alioth.debian.org/pipermail/debian-science-maintainers>
+List-Post: <mailto:debian-science-maintainers@lists.alioth.debian.org>
+List-Help: <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=help>
+List-Subscribe: <http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/debian-science-maintainers>,
+       <mailto:debian-science-maintainers-request@lists.alioth.debian.org?subject=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 <gladky.anton@gmail.com> 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 (file)
index 0000000..e118d78
--- /dev/null
@@ -0,0 +1,43 @@
+From: David Bremner <bremner@debian.org>
+To: Samuel Bronson <naesten@gmail.com>, 695159@bugs.debian.org, Debian Bug Tracking System <submit@bugs.debian.org>
+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 <naesten@gmail.com> 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 (file)
index 0000000..a7549c4
--- /dev/null
@@ -0,0 +1,140 @@
+Return-path: <bounces+20181025-bremner=debian.org@tracker.debian.org>
+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 <bounces+20181025-bremner=debian.org@tracker.debian.org>)
+       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 <bounces+20181025-bremner=debian.org@tracker.debian.org>)
+       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 <bounces+20181025-bremner=debian.org@tracker.debian.org>)
+       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 <debbugs@buxtehude.debian.org>)
+       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 <debbugs@buxtehude.debian.org>)
+       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 <debbugs@buxtehude.debian.org>)
+       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 <debbugs@buxtehude.debian.org>)
+       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 <debbugs@buxtehude.debian.org>)
+       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 <bremner@debian.org>, 695159@bugs.debian.org
+Resent-From: David Bremner <bremner@debian.org>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Emacsen team <debian-emacsen@lists.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 25 Oct 2018 13:45:01 +0000
+Resent-Message-ID: <handler.695159.B.154047490313415@bugs.debian.org>
+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 <bremner@debian.org>)
+       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 <bremner@debian.org>)
+       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 <bremner@debian.org>
+To: Samuel Bronson <naesten@gmail.com>, 695159@bugs.debian.org, Debian Bug Tracking System <submit@bugs.debian.org>
+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: <emacs-goodies-el.tracker.debian.org>
+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 <naesten@gmail.com> 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 (file)
index 0000000..8bb6a8c
--- /dev/null
@@ -0,0 +1,183 @@
+Return-path: <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+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 <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       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 <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       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 <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       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 <debbugs@buxtehude.debian.org>) 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 <debbugs@buxtehude.debian.org>)
+ id 1ga32F-0005PP-9m; Thu, 20 Dec 2018 18:27:03 +0000
+X-Loop: owner@bugs.debian.org
+Resent-From: Sean Whitton <spwhitton@spwhitton.name>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Emacs addons team
+ <pkg-emacsen-addons@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 20 Dec 2018 18:27:02 +0000
+Resent-Message-ID: <handler.916805.B916805.154533033319811@bugs.debian.org>
+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 <spwhitton@spwhitton.name>)
+ 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: <xms:mt4bXHFFQvAGVxMhHQP5DBif075kRubHE1KJQrR0OsDN2ClFFtlRXw>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod
+ ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu
+ fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg
+ gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi
+ thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh
+ hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr
+ ufhiiigvpedt
+X-ME-Proxy: <xmx:mt4bXGqtKYX8StS4oLrucVpNHp3EoaoH6jbQkd2mTFzupx7FwIJ2dQ>
+ <xmx:mt4bXH457xpOO1PnlqWQoPt1r7kL_P9ta064wZO_JDW1QAycFDjIsg>
+ <xmx:mt4bXPQaOrPjDseNXftuMgX1Y9gyHDbVUCYSYNdX6oXwBQhAGwzqIw>
+ <xmx:mt4bXLbmA2mCpvakWE26qsCvS2IOX4eN0KdeU2tQi-SXYEBMLVpE3A>
+From: Sean Whitton <spwhitton@spwhitton.name>
+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
+ <pkg-emacsen-addons.alioth-lists.debian.net>
+List-Unsubscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/options/pkg-emacsen-addons>, 
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=unsubscribe>
+List-Archive: <http://alioth-lists.debian.net/pipermail/pkg-emacsen-addons/>
+List-Post: <mailto:pkg-emacsen-addons@alioth-lists.debian.net>
+List-Help: <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=help>
+List-Subscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/pkg-emacsen-addons>,
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=subscribe>
+Reply-To: Sean Whitton <spwhitton@spwhitton.name>, 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"
+ <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+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 (file)
index 0000000..292d515
--- /dev/null
@@ -0,0 +1,184 @@
+Return-path: <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+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 <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       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 <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       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 <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       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 <debbugs@buxtehude.debian.org>) 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 <debbugs@buxtehude.debian.org>)
+ id 1ga32K-0005Sn-7B; Thu, 20 Dec 2018 18:27:08 +0000
+X-Loop: owner@bugs.debian.org
+Resent-From: Sean Whitton <spwhitton@spwhitton.name>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Emacs addons team
+ <pkg-emacsen-addons@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 20 Dec 2018 18:27:06 +0000
+Resent-Message-ID: <handler.916808.B916808.154533033319830@bugs.debian.org>
+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 <spwhitton@spwhitton.name>)
+ 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: <xms:mt4bXHFFQvAGVxMhHQP5DBif075kRubHE1KJQrR0OsDN2ClFFtlRXw>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod
+ ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu
+ fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg
+ gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi
+ thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh
+ hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr
+ ufhiiigvpedt
+X-ME-Proxy: <xmx:mt4bXGqtKYX8StS4oLrucVpNHp3EoaoH6jbQkd2mTFzupx7FwIJ2dQ>
+ <xmx:mt4bXH457xpOO1PnlqWQoPt1r7kL_P9ta064wZO_JDW1QAycFDjIsg>
+ <xmx:mt4bXPQaOrPjDseNXftuMgX1Y9gyHDbVUCYSYNdX6oXwBQhAGwzqIw>
+ <xmx:mt4bXLbmA2mCpvakWE26qsCvS2IOX4eN0KdeU2tQi-SXYEBMLVpE3A>
+From: Sean Whitton <spwhitton@spwhitton.name>
+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
+ <pkg-emacsen-addons.alioth-lists.debian.net>
+List-Unsubscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/options/pkg-emacsen-addons>, 
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=unsubscribe>
+List-Archive: <http://alioth-lists.debian.net/pipermail/pkg-emacsen-addons/>
+List-Post: <mailto:pkg-emacsen-addons@alioth-lists.debian.net>
+List-Help: <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=help>
+List-Subscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/pkg-emacsen-addons>,
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=subscribe>
+Reply-To: Sean Whitton <spwhitton@spwhitton.name>, 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"
+ <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+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 (file)
index 0000000..bff2473
--- /dev/null
@@ -0,0 +1,178 @@
+Return-path: <bounces+20181220-bremner=debian.org@tracker.debian.org>
+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 <bounces+20181220-bremner=debian.org@tracker.debian.org>)
+       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 <bounces+20181220-bremner=debian.org@tracker.debian.org>)
+       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 <bounces+20181220-bremner=debian.org@tracker.debian.org>)
+       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 <debbugs@buxtehude.debian.org>)
+       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 <debbugs@buxtehude.debian.org>)
+       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 <debbugs@buxtehude.debian.org>)
+       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 <debbugs@buxtehude.debian.org>)
+       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 <debbugs@buxtehude.debian.org>)
+       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 <spwhitton@spwhitton.name>, 916807@bugs.debian.org
+Resent-From: Sean Whitton <spwhitton@spwhitton.name>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Emacs addons team <pkg-emacsen-addons@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 20 Dec 2018 18:27:04 +0000
+Resent-Message-ID: <handler.916807.B916807.154533033319820@bugs.debian.org>
+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 <spwhitton@spwhitton.name>)
+       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: <xms:mt4bXHFFQvAGVxMhHQP5DBif075kRubHE1KJQrR0OsDN2ClFFtlRXw>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod
+    ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu
+    fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg
+    gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi
+    thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh
+    hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr
+    ufhiiigvpedt
+X-ME-Proxy: <xmx:mt4bXGqtKYX8StS4oLrucVpNHp3EoaoH6jbQkd2mTFzupx7FwIJ2dQ>
+    <xmx:mt4bXH457xpOO1PnlqWQoPt1r7kL_P9ta064wZO_JDW1QAycFDjIsg>
+    <xmx:mt4bXPQaOrPjDseNXftuMgX1Y9gyHDbVUCYSYNdX6oXwBQhAGwzqIw>
+    <xmx:mt4bXLbmA2mCpvakWE26qsCvS2IOX4eN0KdeU2tQi-SXYEBMLVpE3A>
+From: Sean Whitton <spwhitton@spwhitton.name>
+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: <haskell-mode.tracker.debian.org>
+X-Debian: tracker.debian.org
+X-Debian-Package: haskell-mode
+X-PTS-Package: haskell-mode
+X-PTS-Keyword: bts
+Precedence: list
+List-Unsubscribe: <mailto:control@tracker.debian.org?body=unsubscribe%20haskell-mode>
+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 (file)
index 0000000..861719d
--- /dev/null
@@ -0,0 +1,184 @@
+Return-path: <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+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 <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       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 <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       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 <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>)
+       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 <debbugs@buxtehude.debian.org>) 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 <debbugs@buxtehude.debian.org>)
+ id 1ga32O-0005Te-Sa; Thu, 20 Dec 2018 18:27:12 +0000
+X-Loop: owner@bugs.debian.org
+Resent-From: Sean Whitton <spwhitton@spwhitton.name>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Emacs addons team
+ <pkg-emacsen-addons@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 20 Dec 2018 18:27:11 +0000
+Resent-Message-ID: <handler.916811.B916811.154533033319851@bugs.debian.org>
+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 <spwhitton@spwhitton.name>)
+ 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: <xms:mt4bXHFFQvAGVxMhHQP5DBif075kRubHE1KJQrR0OsDN2ClFFtlRXw>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod
+ ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu
+ fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg
+ gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi
+ thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh
+ hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr
+ ufhiiigvpedt
+X-ME-Proxy: <xmx:mt4bXGqtKYX8StS4oLrucVpNHp3EoaoH6jbQkd2mTFzupx7FwIJ2dQ>
+ <xmx:mt4bXH457xpOO1PnlqWQoPt1r7kL_P9ta064wZO_JDW1QAycFDjIsg>
+ <xmx:mt4bXPQaOrPjDseNXftuMgX1Y9gyHDbVUCYSYNdX6oXwBQhAGwzqIw>
+ <xmx:mt4bXLbmA2mCpvakWE26qsCvS2IOX4eN0KdeU2tQi-SXYEBMLVpE3A>
+From: Sean Whitton <spwhitton@spwhitton.name>
+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
+ <pkg-emacsen-addons.alioth-lists.debian.net>
+List-Unsubscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/options/pkg-emacsen-addons>, 
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=unsubscribe>
+List-Archive: <http://alioth-lists.debian.net/pipermail/pkg-emacsen-addons/>
+List-Post: <mailto:pkg-emacsen-addons@alioth-lists.debian.net>
+List-Help: <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=help>
+List-Subscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/pkg-emacsen-addons>,
+ <mailto:pkg-emacsen-addons-request@alioth-lists.debian.net?subject=subscribe>
+Reply-To: Sean Whitton <spwhitton@spwhitton.name>, 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"
+ <pkg-emacsen-addons-bounces+bremner=debian.org@alioth-lists.debian.net>
+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 (file)
index 0000000..af26374
--- /dev/null
@@ -0,0 +1,179 @@
+Return-path: <debian-science-maintainers-bounces+david=tethera.net@alioth-lists.debian.net>
+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 <debian-science-maintainers-bounces+david=tethera.net@alioth-lists.debian.net>)
+       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 <debian-science-maintainers-bounces+david=tethera.net@alioth-lists.debian.net>)
+       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 <debbugs@buxtehude.debian.org>) 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 <debbugs@buxtehude.debian.org>)
+ 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 <spwhitton@spwhitton.name>
+Resent-To: debian-bugs-dist@lists.debian.org
+Resent-CC: Debian Science Maintainers
+ <debian-science-maintainers@lists.alioth.debian.org>
+X-Loop: owner@bugs.debian.org
+Resent-Date: Thu, 20 Dec 2018 18:27:13 +0000
+Resent-Message-ID: <handler.916867.B916867.154533033319861@bugs.debian.org>
+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 <spwhitton@spwhitton.name>)
+ 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: <xms:mt4bXHFFQvAGVxMhHQP5DBif075kRubHE1KJQrR0OsDN2ClFFtlRXw>
+X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedtkedrudejfedguddugecutefuodetggdotefrod
+ ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfhuthenuceurghilhhouhhtmecu
+ fedttdenucfuohhrthgvugcurhgvtghiphhsucdlgedtmdenucfjughrpefhvffufffkgg
+ gtsehgtderredttddtnecuhfhrohhmpefuvggrnhcuhghhihhtthhonhcuoehsphifhhhi
+ thhtohhnsehsphifhhhithhtohhnrdhnrghmvgeqnecurfgrrhgrmhepmhgrihhlfhhroh
+ hmpehsphifhhhithhtohhnsehsphifhhhithhtohhnrdhnrghmvgenucevlhhushhtvghr
+ ufhiiigvpedt
+X-ME-Proxy: <xmx:mt4bXGqtKYX8StS4oLrucVpNHp3EoaoH6jbQkd2mTFzupx7FwIJ2dQ>
+ <xmx:mt4bXH457xpOO1PnlqWQoPt1r7kL_P9ta064wZO_JDW1QAycFDjIsg>
+ <xmx:mt4bXPQaOrPjDseNXftuMgX1Y9gyHDbVUCYSYNdX6oXwBQhAGwzqIw>
+ <xmx:mt4bXLbmA2mCpvakWE26qsCvS2IOX4eN0KdeU2tQi-SXYEBMLVpE3A>
+From: Sean Whitton <spwhitton@spwhitton.name>
+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
+ <debian-science-maintainers.alioth-lists.debian.net>
+List-Unsubscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/options/debian-science-maintainers>,
+ <mailto:debian-science-maintainers-request@alioth-lists.debian.net?subject=unsubscribe>
+List-Archive: <http://alioth-lists.debian.net/pipermail/debian-science-maintainers/>
+List-Post: <mailto:debian-science-maintainers@alioth-lists.debian.net>
+List-Help: <mailto:debian-science-maintainers-request@alioth-lists.debian.net?subject=help>
+List-Subscribe: <https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/debian-science-maintainers>,
+ <mailto:debian-science-maintainers-request@alioth-lists.debian.net?subject=subscribe>
+Reply-To: Sean Whitton <spwhitton@spwhitton.name>, 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"
+ <debian-science-maintainers-bounces+david=tethera.net@alioth-lists.debian.net>
+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 (file)
index 0000000..1361c6f
--- /dev/null
@@ -0,0 +1,282 @@
+From mboxrd@z Thu Jan  1 00:00:00 1970
+Return-Path: <SRS0=/pzd=DH=vger.kernel.org=linux-man-owner@kernel.org>
+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 <linux-man@archiver.kernel.org>; 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 <linux-man@archiver.kernel.org>; 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 <rfc822;linux-man@archiver.kernel.org>);
+        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
+        <rfc822;linux-man@vger.kernel.org>); 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 <linux-man@vger.kernel.org>; Wed, 30 Sep 2020 03:12:20 -0700 (PDT)
+Received: by mail-pf1-x443.google.com with SMTP id b124so832681pfg.13
+        for <linux-man@vger.kernel.org>; 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" <g.branden.robinson@gmail.com>
+To:     "Michael Kerrisk (man-pages)" <mtk.manpages@gmail.com>
+Cc:     Jakub Wilk <jwilk@jwilk.net>, 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: <linux-man.vger.kernel.org>
+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 <g.branden.robinson@gmail.com>
+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 (file)
index 0000000..60a7a47
--- /dev/null
@@ -0,0 +1,11 @@
+From: David Bremner <david@tethera.net>
+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 (file)
index 0000000..98a8fc9
--- /dev/null
@@ -0,0 +1,83 @@
+From david@tethera.net  Sat Feb  5 09:19:10 2022
+From: David Bremner <david@tethera.net>
+To: David Bremner <david@tethera.net>
+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 <david@tethera.net>
+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
+
+
+--=-=-=--
index a09f6191954ac5079c406e4699c0c8e2c2e1b4eb..d4702798c0f4346020ed362e1bf9267560499469 100644 (file)
@@ -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==
 --=-=-=--
index b05cb545cb201927f1657270eb6d50d55b68c540..115019b0f17dc329fd038ed3d6412994d66bb65b 100644 (file)
@@ -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-----
 --=-=-=--
index eea66a947c7323a2f4303f512d2ed25f7e548f0c..d43331bde5e888ac2055fa37a5d55c8080000524 100644 (file)
@@ -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-----
 --=-=-=--
index 8dfd7c399437d7ac9900e46fdd54d861d49d1679..9f0e51b96b786bb9ec2a9ced96a9d67e99c3d08e 100644 (file)
@@ -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-----
 --=-=-=--
index c97d8c3cafbed58a287daa7ed4150de5d6e45611..f64b99bfecb2b742f94e076deb936c1cd2f91385 100644 (file)
@@ -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-----
 --=-=-=--
index f1a72f0dd9ebcda0ce5d92761e98d574bcee3842..c93ce0f7feea52841b39029d7bad6cdbefcde8c5 100644 (file)
@@ -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-----
 --=-=-=--
index 059783cef4b9b13a378c60fb76158280f592ecad..4d5073bb1f470a82ccd510f89195a08da002f871 100644 (file)
@@ -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-----
 --=-=-=--
index 880f60e33642e3b3d12472ca69622a1edbaa0175..cf786c024985bab0bb8554c18aaeb765d80cfbe7 100644 (file)
@@ -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-----
 --=-=-=--
index 15dc08ab8a22ac43ff01b7ecef0d99611b502fb2..d0e11a105d4c0d4b84158b1df283480153cafa0d 100644 (file)
@@ -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-----
 --=-=-=--
index dec822c2bb7d26cd9bd6d3927ce17521cabc908e..a10612f98dd9293115022d08afdd4c4311a148f7 100644 (file)
@@ -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-----
 --=-=-=--
index 8c5dd251a59c2c35859aaeb3028ba0683e776d0b..9a3c7644ce098c4cefaabdf5c9a771cac6aa275b 100644 (file)
@@ -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-----
 
 --=-=-=--
index c3a21b851cd6b5aca8861e7b321ef171f86b58c2..f5efaff0086732177591fa10fec28dbb155ad5fc 100644 (file)
@@ -6,7 +6,7 @@ Message-ID: <signed-protected-header@crypto.notmuchmail.org>
 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-----
 --=-=-=--
index ebf4b786dd615a6a098683fae72439f236836644..cd1e9fc34c628d466ef6f5537007ada51d607235 100644 (file)
@@ -6,7 +6,7 @@ Message-ID: <simple-signed-mail@crypto.notmuchmail.org>
 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-----
 --=-=-=--
index 7163b9ae344ff40bddb6be74ad32dcee6834d64b..8c7fb3ebadb68bd8a00309a3e6b0fc742c7b6b61 100644 (file)
@@ -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-----
 --=-=-=--
index 9a3c13848194391cc039e77c4b593803b6e86131..87f3f8da961eeb400be0d4987f7ecdbfb5921163 100644 (file)
@@ -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 (file)
index 0000000..ce1d711
--- /dev/null
@@ -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 (file)
index 0000000..8a87432
--- /dev/null
@@ -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 (file)
index 0000000..5c6b2d7
--- /dev/null
@@ -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 <stdint.h> 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            ╰─►[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-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            ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (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  ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (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 (file)
index 0000000..55806d1
--- /dev/null
@@ -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 <stdint.h> 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            ╰─►[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-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            ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (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  ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (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 (file)
index 0000000..d55818e
--- /dev/null
@@ -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 <stdint.h> 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 (file)
index 0000000..80c67d0
--- /dev/null
@@ -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 <stdint.h> 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 (file)
index 0000000..836f77b
--- /dev/null
@@ -0,0 +1,21 @@
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Sean Whitton <spwhitton@spwhitton.name>, 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 <spwhitton@spwhitton.name> 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 (file)
index 0000000..8299519
--- /dev/null
@@ -0,0 +1,44 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (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 <dottedmag@dottedmag.net> (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 <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed (hidden) ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (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 <keithp@keithp.com> (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 <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    Cc: notmuch@notmuchmail.org
+    Date: Tue, 17 Nov 2009 19:50:40 -0500
+
+    [ multipart/mixed (hidden) ]
+ Carl Worth <cworth@cworth.org> (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 (file)
index 0000000..e7c376b
--- /dev/null
@@ -0,0 +1,119 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (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 <dottedmag@dottedmag.net> (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 <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed (hidden) ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (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 <keithp@keithp.com> (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 <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    Cc: notmuch@notmuchmail.org
+    Date: Tue, 17 Nov 2009 19:50:40 -0500
+
+    [ multipart/mixed (hidden) ]
+ Carl Worth <cworth@cworth.org> (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 <lars at
+ seas.harvard.edu> 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 (file)
index 0000000..6bf49d8
--- /dev/null
@@ -0,0 +1,20 @@
+Sean Whitton <spwhitton@spwhitton.name> (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 (file)
index 0000000..d646353
--- /dev/null
@@ -0,0 +1,97 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (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 <dottedmag@dottedmag.net> (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 <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  Cc: notmuch@notmuchmail.org
+  Date: Tue, 17 Nov 2009 15:33:01 -0500
+
+  [ multipart/mixed (hidden) ]
+   Mikhail Gusarov <dottedmag@dottedmag.net> (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 <keithp@keithp.com> (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 <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    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 <stdint.h>" (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 <cworth@cworth.org> (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 <lars at
+ seas.harvard.edu> 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 (file)
index 0000000..e2951d2
--- /dev/null
@@ -0,0 +1,62 @@
+Alex Botero-Lowry <alex.boterolowry@gmail.com> (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 <alex.boterolowry@gmail.com>
+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 <cworth@cworth.org> (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 (file)
index 0000000..cdde467
--- /dev/null
@@ -0,0 +1,64 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (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 <dottedmag@dottedmag.net> (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 <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  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 <dottedmag@dottedmag.net> (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 <keithp@keithp.com> (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 <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    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 <cworth@cworth.org> (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 (file)
index 0000000..ec34612
--- /dev/null
@@ -0,0 +1,89 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (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 <dottedmag@dottedmag.net> (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 <lars@seas.harvard.edu> (2009-11-17) (inbox signed)
+  Subject: Re: [notmuch] Working with Maildir storage?
+  To: Mikhail Gusarov <dottedmag@dottedmag.net>
+  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 <dottedmag@dottedmag.net> (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 <keithp@keithp.com> (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 <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+    Subject: Re: [notmuch] Working with Maildir storage?
+    To: Keith Packard <keithp@keithp.com>
+    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 <stdint.h>" (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 <cworth@cworth.org> (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-tree.expected-output/inbox-outline b/test/emacs-tree.expected-output/inbox-outline
new file mode 100644 (file)
index 0000000..9119a91
--- /dev/null
@@ -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-tag-inbox-oldest-first b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-oldest-first
new file mode 100644 (file)
index 0000000..588fc58
--- /dev/null
@@ -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 <stdint.h> 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            ╰─►[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-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            ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (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  ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (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/result-format-function b/test/emacs-tree.expected-output/result-format-function
new file mode 100644 (file)
index 0000000..7eb2469
--- /dev/null
@@ -0,0 +1,53 @@
+  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-17  Ingmar Vanhassel      ┬►[notmuch] [PATCH] Typsos                            (  ui)
+  2009-11-18  Carl Worth            ╰─► ...                                               (  ui)
+  2009-11-17  Adrian Perez de Cast  ┬►[notmuch] Introducing myself                        ( =ui)
+  2009-11-18  Keith Packard         ├─► ...                                               (  ui)
+  2009-11-18  Carl Worth            ╰─► ...                                               (  ui)
+  2009-11-17  Israel Herraiz        ┬►[notmuch] New to the list                           (  ui)
+  2009-11-18  Keith Packard         ├─► ...                                               (  ui)
+  2009-11-18  Carl Worth            ╰─► ...                                               (  ui)
+  2009-11-17  Jan Janak             ┬►[notmuch] What a great idea!                        (  ui)
+  2009-11-17  Jan Janak             ├─► ...                                               (  ui)
+  2009-11-18  Carl Worth            ╰─► ...                                               (  ui)
+  2009-11-17  Jan Janak             ┬►[notmuch] [PATCH] Older versions of install do not support -C. (  ui)
+  2009-11-18  Carl Worth            ╰─► ...                                               (  ui)
+  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  Lars Kellogg-Stedman  ┬►[notmuch] Working with Maildir storage?             ( = i)
+  2009-11-17  Mikhail Gusarov       ├┬► ...                                               ( =ui)
+  2009-11-17  Lars Kellogg-Stedman  │╰┬► ...                                              ( =ui)
+  2009-11-17  Mikhail Gusarov       │ ├─► ...                                             (  ui)
+  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 <stdint.h> 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  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  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  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)
+  2009-11-17  Alex Botero-Lowry     ┬►[notmuch] preliminary FreeBSD support               (& ui)
+  2009-11-17  Carl Worth            ╰─► ...                                               (  ui)
+End of search results.
diff --git a/test/emacs-unthreaded.expected-output/result-format-function b/test/emacs-unthreaded.expected-output/result-format-function
new file mode 100644 (file)
index 0000000..bcb10b9
--- /dev/null
@@ -0,0 +1,53 @@
+  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  Carl Worth          [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] Typsos                              (  ui)
+  2009-11-18  Carl Worth          [notmuch] Introducing myself                          (  ui)
+  2009-11-18  Carl Worth          [notmuch] New to the list                             (  ui)
+  2009-11-18  Carl Worth          [notmuch] What a great idea!                          (  ui)
+  2009-11-18  Carl Worth          [notmuch] [PATCH] Older versions of install do not support -C.(  ui)
+  2009-11-18  Carl Worth          [notmuch] archive                                     (  ui)
+  2009-11-18  Carl Worth          [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags(  ui)
+  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  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  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  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  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-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)
+  2009-11-17  Israel Herraiz      [notmuch] New to the list                             (  ui)
+  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] 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)
+  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] preliminary FreeBSD support                 (  ui)
+  2009-11-17  Alex Botero-Lowry   [notmuch] preliminary FreeBSD support                 (& ui)
+  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 <stdint.h> 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 (file)
index 0000000..65e479f
--- /dev/null
@@ -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.
index 75b05fa4e252c2d955b6738ab25a8dcef60b6427..ce174b52a4caae3ff92dfca7ff8a913076a7bb98 100644 (file)
@@ -63,7 +63,7 @@ and <a href=3D"http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg01680=
 
 --0016e687869333b14e0478963d33--
 --0016e687869333b1570478963d35
-Content-Type: application/octet-stream; 
+Content-Type: text/x-diff;
        name="0001-Deal-with-situation-where-sysconf-_SC_GETPW_R_SIZE_M.patch"
 Content-Disposition: attachment; 
        filename="0001-Deal-with-situation-where-sysconf-_SC_GETPW_R_SIZE_M.patch"
diff --git a/test/emacs.expected-output/search-result-format-function b/test/emacs.expected-output/search-result-format-function
new file mode 100644 (file)
index 0000000..08b4bee
--- /dev/null
@@ -0,0 +1,25 @@
+  ui   2010-12-29     [1/1] François Boulogne              [aur-general] Guidelines: cp, mkdir vs install (inbox unread)
+  ui   2010-12-16     [1/1] Olivier Berger                 Essai accentué (inbox unread)
+  ui   2009-11-18     [1/1] Chris Wilson                   [notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread)
+& ui   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)
+  ui   2009-11-18     [2/2] Ingmar Vanhassel, Carl Worth   [notmuch] [PATCH] Typsos (inbox unread)
+ =ui   2009-11-18     [3/3] Adrian Perez de Castro, Keith Packard, Carl Worth     [notmuch] Introducing myself (inbox signed unread)
+  ui   2009-11-18     [3/3] Israel Herraiz, Keith Packard, Carl Worth             [notmuch] New to the list (inbox unread)
+  ui   2009-11-18     [3/3] Jan Janak, Carl Worth          [notmuch] What a great idea! (inbox unread)
+  ui   2009-11-18     [2/2] Jan Janak, Carl Worth          [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+  ui   2009-11-18     [3/3] Aron Griffis, Keith Packard, Carl Worth               [notmuch] archive (inbox unread)
+  ui   2009-11-18     [2/2] Keith Packard, Carl Worth      [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread)
+ =ui   2009-11-18     [7/7] Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth       [notmuch] Working with Maildir storage? (inbox signed unread)
+  ui   2009-11-18     [5/5] Mikhail Gusarov, Carl Worth, Keith Packard            [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread)
+  ui   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)
+  ui   2009-11-18     [1/1] Alexander Botero-Lowry         [notmuch] request for pull (inbox unread)
+  ui   2009-11-18     [4/4] Jjgod Jiang, Alexander Botero-Lowry                [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)
+  ui   2009-11-18     [1/1] Rolland Santimano              [notmuch] Link to mailing list archives ? (inbox unread)
+  ui   2009-11-18     [1/1] Jan Janak                      [notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread)
+  ui   2009-11-18     [1/1] Stewart Smith                  [notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread)
+  ui   2009-11-18     [1/1] Stewart Smith                  [notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox unread)
+  ui   2009-11-18     [1/1] Stewart Smith                  [notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread)
+&=ui   2009-11-18     [2/2] Lars Kellogg-Stedman           [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread)
+  ui   2009-11-17     [1/1] Mikhail Gusarov                [notmuch] [PATCH] Handle rename of message file (inbox unread)
+& ui   2009-11-17     [2/2] Alex Botero-Lowry, Carl Worth  [notmuch] preliminary FreeBSD support (attachment inbox unread)
+End of search results.
index 0578b1e50194d308d42f782556c951630a702bf1..9bc99a2cf3588fcb2dbc3ac32933b9e425ebd7f6 100644 (file)
@@ -9,8 +9,7 @@ if [[ -z "${NOTMUCH_SRCDIR}" ]]; then
        export NOTMUCH_SRCDIR="$(cd "$(dirname "$0")"/.. && pwd)"
 fi
 
-find_builddir()
-{
+find_builddir () {
        local dir="$1"
 
        while [[ -n "$dir" ]] && [[ "$dir" != "/" ]]; do
@@ -25,7 +24,7 @@ find_builddir()
 if [[ -z "${NOTMUCH_BUILDDIR}" ]]; then
        export NOTMUCH_BUILDDIR="$(find_builddir "$(pwd)")"
 
-       if [[ -z "${NOTMUCH_BUILDDIR}" ]]; then
+       if [ -z "${NOTMUCH_BUILDDIR}" -a "${NOTMUCH_TEST_INSTALLED-0}" = "0" ]; then
                echo "Run tests in a subdir of built notmuch tree." >&2
                exit 1
        fi
diff --git a/test/gnupg-secret-key.NOTE b/test/gnupg-secret-key.NOTE
deleted file mode 100644 (file)
index 604496c..0000000
+++ /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 (file)
index 6431b56..0000000
+++ /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-----
index b58fd3b36f942a91fbe0ac9708d53aef9fdf4e1e..5d27e4d1d143c74daa4b3921a151876e8d441ce0 100755 (executable)
@@ -18,12 +18,22 @@ fi
 # Ensure NOTMUCH_SRCDIR and NOTMUCH_BUILDDIR are set.
 . $(dirname "$0")/export-dirs.sh || exit 1
 
+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
+for test in ${NOTMUCH_TESTS-}; do
     TESTS="$TESTS $NOTMUCH_SRCDIR/test/$test"
 done
 
-if [[ -z "$TESTS" ]]; then
+if [ -z "$TESTS" ]; then
     TESTS="$NOTMUCH_SRCDIR/test/T[0-9][0-9][0-9]-*.sh"
 fi
 
@@ -44,43 +54,46 @@ else
     TEST_TIMEOUT_CMD=""
 fi
 
-trap 'e=$?; kill $!; exit $e' HUP INT TERM
-
 META_FAILURE=
+RES=0
 # Run the tests
-if test -z "$NOTMUCH_TEST_SERIALIZE" && command -v parallel >/dev/null ; then
+if test -z "${NOTMUCH_TEST_SERIALIZE-}" && command -v parallel >/dev/null ; then
     test -t 1 && export COLORS_WITHOUT_TTY=t || :
-    if parallel -h | grep -q GNU ; then
+    if parallel --minversion 0 >/dev/null 2>&1 ; then
         echo "INFO: running tests with GNU parallel"
-        printf '%s\n' $TESTS | $TEST_TIMEOUT_CMD parallel
+        printf '%s\n' $TESTS | $TEST_TIMEOUT_CMD parallel || RES=$?
     else
         echo "INFO: running tests with moreutils parallel"
-        $TEST_TIMEOUT_CMD parallel -- $TESTS
+        $TEST_TIMEOUT_CMD parallel -- $TESTS || RES=$?
     fi
-    RES=$?
-    if [[ $RES != 0 ]]; then
+    if [ $RES != 0 ]; then
         META_FAILURE="parallel test suite returned error code $RES"
     fi
 else
+    trap 'e=$?; trap - 0; kill ${!-}; exit $e' 0 HUP INT TERM
     for test in $TESTS; do
         $TEST_TIMEOUT_CMD $test "$@" &
-        wait $!
-        # If the test failed without producing results, then it aborted,
-        # so we should abort, too.
-        RES=$?
-        testname=$(basename $test .sh)
-        if [[ $RES != 0 && ! -e "$NOTMUCH_BUILDDIR/test/test-results/$testname" ]]; then
-            META_FAILURE="Aborting on $testname (returned $RES)"
-            break
-        fi
+        wait $! && ev=0 || ev=$?
+        test $ev = 0 || RES=$ev
     done
+    trap - 0 HUP INT TERM
+    if [ $RES != 0 ]; then
+        META_FAILURE="some tests failed; first failed returned error code $RES"
+    fi
 fi
-trap - HUP INT TERM
 
 # Report results
+RESULT_FILES=
+for file in $TESTS
+do
+    file=${file##*/} # drop leading path components
+    file=${file%.sh} # drop trailing '.sh'
+    RESULT_FILES="$RESULT_FILES $TEST_DIRECTORY/test-results/$file"
+done
+
 echo
-$NOTMUCH_SRCDIR/test/aggregate-results.sh $NOTMUCH_BUILDDIR/test/test-results/*
-ev=$?
+$NOTMUCH_SRCDIR/test/aggregate-results.sh $RESULT_FILES && ev=0 || ev=$?
+
 if [ -n "$META_FAILURE" ]; then
     printf 'ERROR: %s\n' "$META_FAILURE"
     if [ $ev = 0 ]; then
@@ -89,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
index 34dbb8e0403feefc4be402178943d8576a8d9c01..ed713099b1d3b435024abae6d100d6c1ddf75a4d 100644 (file)
@@ -1,7 +1,21 @@
 #ifndef _NOTMUCH_TEST_H
 #define _NOTMUCH_TEST_H
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <assert.h>
+#include <dlfcn.h>
+#include <fcntl.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <talloc.h>
+#include <unistd.h>
+
 #include <notmuch.h>
 
 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 (file)
index 0000000..182a824
--- /dev/null
@@ -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 (file)
index 0000000..4693768
--- /dev/null
@@ -0,0 +1,5 @@
+The OpenPGPv4 secret key for the crypto tests was generated using:
+
+$ gpg --quick-generate-key \
+  'Notmuch Test Suite (INSECURE!) <test_suite@notmuchmail.org>' \
+  future-default default never
index 7cde22c5aed34d9dfe3061bcda96bae6714b4306..8ae08971478a2a639b249a08b1fd96c1c432b8ec 100644 (file)
@@ -122,7 +122,8 @@ const notmuch_opt_desc_t notmuch_shared_options[] = {
 const char *notmuch_requested_db_uuid = NULL;
 
 void
-notmuch_process_shared_options (unused (const char *dummy))
+notmuch_process_shared_options (unused (notmuch_database_t *notmuch),
+                               unused (const char *dummy))
 {
 }
 
index 56c628e56b29f5007f49e78908238f8370d872cd..d925acea352e41dd3133d4062cdf4ac025a075e8 100644 (file)
@@ -1,7 +1,6 @@
 # .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
@@ -12,7 +11,6 @@
 #
 [database]
 path=/path/to/maildir
-
 # User configuration
 #
 # Here is where you can let notmuch know how you would like to be
@@ -31,8 +29,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 +46,6 @@ other_email=another.suite@example.com;
 #
 [new]
 tags=foo;bar;
-
 # Search configuration
 #
 # The following option is supported here:
@@ -60,8 +56,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/smime/0xE0972A47.p12 b/test/smime/0xE0972A47.p12
new file mode 100644 (file)
index 0000000..2c4a6d1
--- /dev/null
@@ -0,0 +1,62 @@
+Issuer ...: /CN=Notmuch Test Suite
+Serial ...: 6F748C94BD0C67A9
+Subject ..: /CN=Notmuch Test Suite
+    aka ..: test_suite@notmuchmail.org
+Keygrip ..: 1727B9C7108D50333614F3B1DD0807F624B31130
+
+-----BEGIN PKCS12-----
+MIIJ+AIBAzCCCb4GCSqGSIb3DQEHAaCCCa8EggmrMIIJpzCCBAcGCSqGSIb3DQEH
+BqCCA/gwggP0AgEAMIID7QYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIcfMY
+MS7tOpcCAggAgIIDwFu7ZRNrXCb0eKei44aeBZPRs9YI/5EpMcFuc8j4/8T1HkIt
+GuRe/HzRmoiLZcAMOzGC/hF8TkHlNeUZ7rOSpCg4UlBVWJS6avTMHHsakDvTV/7q
+X5VNi4pLUuyEToGTAPHV+s5P/gYYG6mFPkwG/pDDlAcgMhgtuPY/lQp6IS/E6CaR
+fhcnQiPq9ySTqO7UNwIyMwtAtSHkgBaje8UbOkQch4lg51i97rm9m4EMvklKtjXc
+Ud4aTEuoZguPmdBdLvF5QxqJf6Bm9lHa1Awhru2gBWQf9TjX8bwK9Xsv8G6gPOwc
+LVpIR9fMZtgBbc+heeJTjfn6VqEy881ckbkz+38hiN3pbLMuATM7QAY3u3N4whM6
+Hmfyl3iqba84Pl93zaUzqazAUeFdqcqSpAUGkS4gU6klr9qi3NicaGbry1DySYU7
+2h4xy3j7eiHxqdWaibdPoBC8CEbPaFj2qnOVsZykxG6zPvbEB+5sJ/a+T6xm1Btx
+N6vXR7ObbXlpC4pRkS32ehuRbY6wc6H2KKepOMCu7x10tN0Up5ccNxvkT26QIrEE
+LW296ijCLbsRhWymDtopWAZHcXXIu0fJ4tocSp2c3lojSEYu1jlMXR+Pa4R8EtgZ
+lb5+NqISxjUlMMWzGDyhrp9ImcsZmpv6N8zPcZVyU+M1/h+p9ur/IOVZU9P1vIKy
+kcM4pslr0JhLfnZCLZ+3Ux1yKAcndGZFPb1vZ83jyZKR38BVSGu53ODaBJBqSMHu
+Mv2Na/qzvQBSVJuWF9cAhiVd7v9R/EvT0zmljN4w7l4EXsB5wRsO1wvlL+MhwaET
+dIHbRH2GD3gERX6oTc3t3cgritVePk70rCxQDxn5zUbjW7dNIlIobAumLHBfgSxR
+QCE6gxdTm5MW2O9hnfTSQvliVaGU1gd0M3BRiqeNpPPxnloGKnOEODM381F4HxyR
+CzO2r/2aKJP+U5HxSf4cljp3/Lripxykzfqc9/xZshl+jGixsSSm+Ul916Hpj2Rt
+j9vHg4H9YfJTGdvzxZcvZCvNSy3ygtjx0++SrI5hGHKjpVJIK2/9Wi39q5s6LkiA
+RCjvuoBBcQXm++69X7QGWSsGFtwerCGnq3nAxGpHVKVGTvFYMAg6y1RR0zvE0SuM
+MZegD8w45QyrmiPqSRM7/RtqVdA+r/wiJwWerUBq+mrCvJHB2NRcjiUiCJY1bjRU
+ATMfB0uZaNInUXiLDGxp2mdBgdFVq7sYTbq+OvprzxeAjIvodxl3J9ThvJnt1fzK
+RPCJw5COI60ibE3XTTCCBZgGCSqGSIb3DQEHAaCCBYkEggWFMIIFgTCCBX0GCyqG
+SIb3DQEMCgECoIIE7jCCBOowHAYKKoZIhvcNAQwBAzAOBAiEe8CcxIIv9wICCAAE
+ggTIujut93lYPUsKc/JNhZhUWS/RHHog6d8ZAjpFvXpyD8Z2z4A4PpgIn8eUSRW5
+Gwp8izR+16Tj3ht52pJ5Y1x27/S3l3sDlekEZ/33X/AdLFWAXbcibmwtRea1ucKZ
+ze3DJM7CvuRvVSBG8XubPGi3pZkEjHBGQqgtsTnxlBp0PXl7wxfyT7F6gOH2DGYP
+bYzNa2fnY8twEcUYhuksI/eh9Zwj9TrF0HWq1hwp0tDCfqutzshSX2GQ/p0raL3B
+C2stHBjl0OVUfDHpqQ5OJWbQvGcJntECqu4gmSJohunObaUKcN8xs+FzB5czpmsT
+W/pyR58nc8QhTttByqZN3EerhEogWDZj4tQ6dK8p6bqLO/0qqBehZGchfof5Evwj
+VFsvVGD8xVLQWWAFnrQs5+U56NQEbmZzN5RCI7FEK2VVOeG03dpXyoAQyxuYrsYU
+3znmoSleIqDDBFD21YePUcJZ0R8AQsvgV11tdwPWqr1hk0bIazLQ9rappGrTgkK8
+DFdQKSH1dRvjqtbuDyY7j5PXXJTXthVv9T9N7Vp6qU+pWBQ1Mz30J+fHX2ilEnbi
+tQ49hwt1+/2Zkmwz3reoEnxYOKzCg/ySIpQ27/Hx4xZ+ecEzX/0IxCkHeAV3V3bB
+1z8wFxWEh1s9hL6C8lRk/wQ9KsKaxM7BdLw7RjiqEwR4HgeCqMPdCVQQpILARDC8
+Poz8xUmjv7HyIvvyBUP12YdIj74Jjj0Mm2r/FDj7nsXxkjXMZEMMKK3oVaAMq8Bd
+cO4VQXDd7bgNzLF9PKxWNjoCuQcPJXwMPqlFoc06BLPstEaR4enafv0Pd4l0pyME
+YgezyVW+3yFEsbbB2UUs0r7oqxsDFU9/iHf8O3nu3NuKTJkux4uMlOTBKsm6sY7k
+GduP2UA+WU27jHrf4zQQbkDLG1lJFfcaKzlcOmz5B9iZwugBz9Y28w5f2/12Kqrh
+4tibFBUG0E85KAb1wnFUNUx06OMX229U1M0E1LHbcUJ9mcRipONPVn0FRi8XzaLK
+023XRoihuoWhVUiB1OJ2eZW1JnUYRztfa3nfmGjXv4VGkxYlnTkE9z0PAAhf6t5A
+7Ir0y1JUeOlBITTcojOp6qQ8tMQQ5wRk1oncHiw3WwJvFN6fOa9Q/+4ZmULHz0vV
+Xl+Qio8B7/4jqZoT4e/gK6U/zHriznLzqp63LjP47eFRXTfuXslaCt7YF75Mq2J6
+VPA+qfYRw0K5BvDUkr8c+nLP2AiDaEYVBHGdBRTlWO9UkcB1F4cuZZiU5MZbxVrb
+Db+zGWW6AT+4XTO4z9KmAqgTTv1+BQrLxNI+RG8JfQapUKQyB794F4kXK2yhd1P3
+XS9cwh24COiqbOpI1nB5qn7cn4RRHW156LWGF+VJFdxR6Wu3vZx/kZGevG9o1ARF
+z1l9mbGyhwnUJO1EQwjbppvRou1bZuNbuRgLmHKEVPAv+J+7hLXZAnRdwoV0x91t
+bpmy4qyxA/90DHguIhRVcKsYBrdShY7LXdZArECBhMY9R41D6v1yyhC6fL6PKR5g
+DaluN2K9TBALzZH7NnNdE14l+56+kLc9Fq8JXsq3rxdeBTsNl09fHPf9w5VLkq4I
+doNcPPlta0Q0xJNa/RYENCJpAMZdMFIJ558uMXwwVQYJKoZIhvcNAQkUMUgeRgBH
+AG4AdQBQAEcAIABlAHgAcABvAHIAdABlAGQAIABjAGUAcgB0AGkAZgBpAGMAYQB0
+AGUAIABlADAAOQA3ADIAYQA0ADcwIwYJKoZIhvcNAQkVMRYEFGFvRs1zg0xjhHdW
+rw37ZKbglypHMDEwITAJBgUrDgMCGgUABBSluQBa+tVpYVYmB/zAZuPE9NnargQI
+XWSQTDEONWgCAggA
+-----END PKCS12-----
index 9d73a571a71e5f733ca09c0cca57725a4dda7de3..9e956ddf228b6dc87482fa559625deec04a62252 100644 (file)
@@ -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,
-                                      &notmuch, &message)) {
+    if (notmuch_database_open_with_config (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY,
+                                          "",
+                                          NULL,
+                                          &notmuch, &message)) {
        if (message) {
            fputs (message, stderr);
            free (message);
index 2f7950ac75b7b6c61777560bb933d847b9a7e3f7..f5d72e12c5c4873700c7814bf550cd0510c1a15c 100644 (file)
 #
 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
@@ -105,8 +121,7 @@ fi
 gen_msg_cnt=0
 gen_msg_filename=""
 gen_msg_id=""
-generate_message ()
-{
+generate_message () {
     # This is our (bash-specific) magic for doing named parameters
     local -A template="($@)"
     local additional_headers
@@ -225,8 +240,7 @@ EOF
 #
 # All of the arguments and return values supported by generate_message
 # are also supported here, so see that function for details.
-add_message ()
-{
+add_message () {
     generate_message "$@" &&
     notmuch new > /dev/null
 }
@@ -303,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
@@ -311,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
new file mode 100644 (file)
index 0000000..0ab58fc
--- /dev/null
@@ -0,0 +1,226 @@
+#
+# Copyright (c) 2010-2020 Notmuch Developers
+#
+# 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 2 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/ .
+
+test_require_emacs () {
+    local ret=0
+    test_require_external_prereq "$TEST_EMACS" || ret=1
+    test_require_external_prereq "$TEST_EMACSCLIENT" || ret=1
+    test_require_external_prereq dtach || ret=1
+    return $ret
+}
+
+# Deliver a message with emacs and add it to the database
+#
+# Uses emacs to generate and deliver a message to the mail store.
+# Accepts arbitrary extra emacs/elisp functions to modify the message
+# before sending, which is useful to doing things like attaching files
+# 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
+    # before we can send a message, we have to prepare the FCC maildir
+    mkdir -p "$MAIL_DIR"/sent/{cur,new,tmp}
+    # eval'ing smtp-dummy --background will set smtp_dummy_pid and -_port
+    smtp_dummy_pid= smtp_dummy_port=
+    eval `$TEST_DIRECTORY/smtp-dummy --background sent_message`
+    test -n "$smtp_dummy_pid" || return 1
+    test -n "$smtp_dummy_port" || return 1
+
+    test_emacs \
+       "(let ((message-send-mail-function 'message-smtpmail-send-it)
+              (mail-host-address \"example.com\")
+              (smtpmail-smtp-server \"localhost\")
+              (smtpmail-smtp-service \"${smtp_dummy_port}\"))
+          (notmuch-mua-mail)
+          (message-goto-to)
+          (insert \"test_suite@notmuchmail.org\nDate: 01 Jan 2000 12:00:00 -0000\")
+          (message-goto-subject)
+          (insert \"${subject}\")
+          (message-goto-body)
+          (insert \"${body}\")
+          $*
+          (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.
+    kill -9 $smtp_dummy_pid
+    notmuch new >/dev/null
+}
+
+# Pretend to deliver a message with emacs. Really save it to a file
+# and add it to the database
+#
+# Uses emacs to generate and deliver a message to the mail store.
+# Accepts arbitrary extra emacs/elisp functions to modify the message
+# before sending, which is useful to doing things like attaching files
+# to the message and encrypting/signing.
+#
+# If any GNU-style long-arguments (like --quiet or --decrypt=true) are
+# at the head of the argument list, they are sent directly to "notmuch
+# new" after message delivery
+emacs_fcc_message () {
+    local nmn_args subject body
+    nmn_args=''
+    while [[ "$1" =~ ^-- ]]; do
+       nmn_args="$nmn_args $1"
+       shift
+    done
+    subject="$1"
+    body="$2"
+    shift 2
+    # before we can send a message, we have to prepare the FCC maildir
+    mkdir -p "$MAIL_DIR"/sent/{cur,new,tmp}
+
+    test_emacs \
+       "(let ((message-send-mail-function (lambda () t))
+              (mail-host-address \"example.com\"))
+          (notmuch-mua-mail)
+          (message-goto-to)
+          (insert \"test_suite@notmuchmail.org\nDate: 01 Jan 2000 12:00:00 -0000\")
+          (message-goto-subject)
+          (insert \"${subject}\")
+          (message-goto-body)
+          (insert \"${body}\")
+          $*
+          (let ((mml-secure-smime-sign-with-sender t)
+                (mml-secure-openpgp-sign-with-sender t))
+            (notmuch-mua-send-and-exit)))" || return 1
+    notmuch new $nmn_args >/dev/null
+}
+
+test_emacs_expect_t () {
+       local result
+       test "$#" = 1 ||
+       error "bug in the test script: not 1 parameter to test_emacs_expect_t"
+       if [ -z "$inside_subtest" ]; then
+               error "bug in the test script: test_emacs_expect_t without test_begin_subtest"
+       fi
+
+       # Run the test.
+       if ! test_skip "$test_subtest_name"
+       then
+               test_emacs "(notmuch-test-run $1)" >/dev/null
+
+               # Restore state after the test.
+               exec 1>&6 2>&7          # Restore stdout and stderr
+               inside_subtest=
+
+               # test_emacs may update missing external prerequisites
+               test_check_missing_external_prereqs_ "$test_subtest_name" && return
+
+               # Report success/failure.
+               result=$(cat OUTPUT)
+               if [ "$result" = t ]
+               then
+                       test_ok_
+               else
+                       test_failure_ "${result}"
+               fi
+       else
+               # Restore state after the (non) test.
+               exec 1>&6 2>&7          # Restore stdout and stderr
+               inside_subtest=
+       fi
+}
+
+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 <<EOF >"$TMP_DIRECTORY/run_emacs"
+#!/bin/sh
+export PATH=$PATH
+export NOTMUCH_CONFIG=$NOTMUCH_CONFIG
+
+# Here's what we are using here:
+#
+# --quick              Use minimal customization. This implies --no-init-file,
+#                      --no-site-file and (emacs 24) --no-site-lisp
+#
+# --directory          Ensure that the local elisp sources are found
+#
+# --load               Force loading of notmuch.el and test-lib.el
+
+exec ${TEST_EMACS} ${find_notmuch_el} --quick \
+       ${EXTRA_DIR} --load notmuch.el \
+       --directory "$NOTMUCH_SRCDIR/test" --load test-lib.el \
+       "\$@"
+EOF
+       chmod a+x "$TMP_DIRECTORY/run_emacs"
+}
+
+test_emacs () {
+       # test dependencies beforehand to avoid the waiting loop below
+       test_require_emacs || return
+
+       if [ -z "$EMACS_SERVER" ]; then
+               emacs_tests="$NOTMUCH_SRCDIR/test/${this_test_bare}.el"
+               if [ -f "$emacs_tests" ]; then
+                       load_emacs_tests="--eval '(load \"$emacs_tests\")'"
+               else
+                       load_emacs_tests=
+               fi
+               server_name="notmuch-test-suite-$$"
+               # start a detached session with an emacs server
+               # user's TERM (or 'vt100' in case user's TERM is known dumb
+               # or unknown) is given to dtach which assumes a minimally
+               # VT100-compatible terminal -- and emacs inherits that
+               TERM=$SMART_TERM dtach -n "$TEST_TMPDIR/emacs-dtach-socket.$$" \
+                       sh -c "stty rows 24 cols 80; exec '$TMP_DIRECTORY/run_emacs' \
+                               --no-window-system \
+                               $load_emacs_tests \
+                               --eval '(setq server-name \"$server_name\")' \
+                               --eval '(server-start)' \
+                               --eval '(orphan-watchdog $$)'" || return
+               EMACS_SERVER="$server_name"
+               # wait until the emacs server is up
+               until test_emacs '()' >/dev/null 2>/dev/null; do
+                       sleep 1
+               done
+       fi
+
+       # Clear test-output output file.  Most Emacs tests end with a
+       # call to (test-output).  If the test code fails with an
+       # exception before this call, the output file won't get
+       # updated.  Since we don't want to compare against an output
+       # file from another test, so start out with an empty file.
+       rm -f OUTPUT
+       touch OUTPUT
+
+       ${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
index 4de5b292c6071cb54e3dcf89e4c06fb64288fa27..4cfb8ef163aac196558c81adae112091661036ff 100644 (file)
 
 ;;; 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)
 
 ;; Ensure that the dynamic variables that are defined by this library
@@ -99,6 +108,9 @@ running, quit if it terminated."
 (add-hook 'notmuch-hello-refresh-hook
          (lambda () (cl-incf notmuch-hello-refresh-hook-counter)))
 
+(defvar notmuch-test-tag-hook-output nil)
+(defun notmuch-test-tag-hook () (push (cons query tag-changes) notmuch-test-tag-hook-output))
+
 (defun notmuch-test-mark-links ()
   "Enclose links in the current buffer with << and >>."
   ;; Links are often created by jit-lock functions
@@ -156,6 +168,38 @@ running, quit if it terminated."
         (lambda (x) `(prog1 ,x (notmuch-post-command)))
         body)))
 
+;; For testing functions in
+;; notmuch-{search,tree,unsorted}-result-format
+(defun notmuch-test-result-flags (format-string result)
+  (let ((tags-to-letters (quote (("attachment" . "&")
+                                ("signed" . "=")
+                                ("unread" . "u")
+                                ("inbox" . "i"))))
+       (tags (plist-get result :tags)))
+    (format format-string
+           (mapconcat (lambda (t2l)
+                        (if (member (car t2l) tags)
+                            (cdr t2l)
+                          " "))
+                      tags-to-letters ""))))
+
+;; Log any signalled error (and other messages) to MESSAGES
+;; Log "COMPLETE" if forms complete without error.
+(defmacro test-log-error (&rest body)
+  `(progn
+     (with-current-buffer "*Messages*"
+       (let ((inhibit-read-only t)) (erase-buffer)))
+     (condition-case err
+       (progn ,@body
+         (message "COMPLETE"))
+       (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
@@ -170,3 +214,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:"))
index 1f438042909d7115778ef19efe47c37ef709b0c4..059e110c87d540bb5bdd8baca0ef6e2140a1bfc4 100644 (file)
@@ -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,59 +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
+. "$NOTMUCH_SRCDIR/test/test-vars.sh" || exit 1
 
-# 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 emacsclient
-unset ALTERNATE_EDITOR
-
-# for reproducibility
-unset EMAIL
-
-add_gnupg_home ()
-{
+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
@@ -126,23 +81,23 @@ 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 <test_suite@notmuchmail.org> (INSECURE!)"
+    FINGERPRINT="9A3AFE6C60065A148FD4B58A7E6ABE924645CC60"
+    SELF_USERID="Notmuch Test Suite (INSECURE!) <test_suite@notmuchmail.org>"
+    SELF_EMAIL="test_suite@notmuchmail.org"
     printf '%s:6:\n' "$FINGERPRINT" | gpg --quiet --batch --no-tty --import-ownertrust
 }
 
-add_gpgsm_home ()
-{
+add_gpgsm_home () {
+    test_require_external_prereq openssl
+
     local fpr
     [ -e "$GNUPGHOME/gpgsm.conf" ] && return
     _gnupg_exit () { gpgconf --kill all 2>/dev/null || true; }
     at_exit_function _gnupg_exit
     mkdir -p -m 0700 "$GNUPGHOME"
-    openssl pkcs12 -export -passout pass: -inkey "$NOTMUCH_SRCDIR/test/smime/key+cert.pem" \
-       < "$NOTMUCH_SRCDIR/test/smime/test.crt" | \
-       gpgsm --batch --no-tty --no-common-certs-import --pinentry-mode=loopback --passphrase-fd 3 \
-             --disable-dirmngr --import  >"$GNUPGHOME"/import.log 2>&1 3<<<''
-    fpr=$(gpgsm --batch --list-key test_suite@notmuchmail.org | sed -n 's/.*fingerprint: //p')
+    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 --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"
@@ -191,56 +146,53 @@ do
 done
 
 if test -n "$debug"; then
-    print_subtest () {
-       printf " %-4s" "[$((test_count - 1))]"
-    }
+       fmt_subtest () {
+               printf -v $1 " %-4s" "[$((test_count - 1))]"
+       }
 else
-    print_subtest () {
-       true
-    }
+       fmt_subtest () {
+               printf -v $1 ''
+       }
 fi
 
 test -n "$COLORS_WITHOUT_TTY" || [ -t 1 ] || color=
 
-if [ -n "$color" ] && [ "$ORIGINAL_TERM" != 'dumb' ] && (
-               TERM=$ORIGINAL_TERM &&
-               export TERM &&
-               tput bold
-               tput setaf
-               tput sgr0
-       ) >/dev/null 2>&1
+if [ -n "$color" ] && [ "$ORIGINAL_TERM" != 'dumb' ] &&
+       tput -T "$ORIGINAL_TERM" -S <<<$'bold\nsetaf\nsgr0\n' >/dev/null 2>&1
 then
        color=t
 else
        color=
 fi
 
-if test -n "$color"; then
+if test -n "$color"
+then
+       # _tput run in subshell (``) only
+       _tput () { exec tput -T "$ORIGINAL_TERM" "$@"; }
+       unset BOLD RED GREEN BROWN SGR0
        say_color () {
-               (
-               TERM=$ORIGINAL_TERM
-               export TERM
                case "$1" in
-                       error) tput bold; tput setaf 1;; # bold red
-                       skip)  tput bold; tput setaf 2;; # bold green
-                       pass)  tput setaf 2;;            # green
-                       info)  tput setaf 3;;            # brown
-                       *) test -n "$quiet" && return;;
+                       error)  b=${BOLD=`_tput bold`}
+                               c=${RED=`_tput setaf 1`}   ;; # bold red
+                       skip)   b=${BOLD=`_tput bold`}
+                               c=${GREEN=`_tput setaf 2`} ;; # bold green
+                       pass)   b= c=${GREEN=`_tput setaf 2`} ;; # green
+                       info)   b= c=${BROWN=`_tput setaf 3`} ;; # brown
+                       *) b= c=; test -n "$quiet" && return ;;
                esac
-               shift
-               printf " "
-               printf "$@"
-               tput sgr0
-               print_subtest
-               )
+               f=$2
+               shift 2
+               sgr0=${SGR0=`_tput sgr0`}
+               fmt_subtest st
+               printf " ${b}${c}${f}${sgr0}${st}" "$@"
        }
 else
        say_color() {
                test -z "$1" && test -n "$quiet" && return
-               shift
-               printf " "
-               printf "$@"
-               print_subtest
+               f=$2
+               shift 2
+               fmt_subtest st
+               printf " ${f}${st}" "$@"
        }
 fi
 
@@ -264,8 +216,7 @@ then
 fi
 
 test_description_printed=
-print_test_description ()
-{
+print_test_description () {
        test -z "$test_description_printed" || return 0
        echo
        echo $this_test: "Testing ${test_description}"
@@ -331,98 +282,9 @@ 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
 
-# Deliver a message with emacs and add it to the database
-#
-# Uses emacs to generate and deliver a message to the mail store.
-# Accepts arbitrary extra emacs/elisp functions to modify the message
-# before sending, which is useful to doing things like attaching files
-# to the message and encrypting/signing.
-emacs_deliver_message ()
-{
-    local subject body smtp_dummy_pid smtp_dummy_port
-    subject="$1"
-    body="$2"
-    shift 2
-    # before we can send a message, we have to prepare the FCC maildir
-    mkdir -p "$MAIL_DIR"/sent/{cur,new,tmp}
-    # eval'ing smtp-dummy --background will set smtp_dummy_pid and -_port
-    smtp_dummy_pid= smtp_dummy_port=
-    eval `$TEST_DIRECTORY/smtp-dummy --background sent_message`
-    test -n "$smtp_dummy_pid" || return 1
-    test -n "$smtp_dummy_port" || return 1
-
-    test_emacs \
-       "(let ((message-send-mail-function 'message-smtpmail-send-it)
-              (mail-host-address \"example.com\")
-              (smtpmail-smtp-server \"localhost\")
-              (smtpmail-smtp-service \"${smtp_dummy_port}\"))
-          (notmuch-mua-mail)
-          (message-goto-to)
-          (insert \"test_suite@notmuchmail.org\nDate: 01 Jan 2000 12:00:00 -0000\")
-          (message-goto-subject)
-          (insert \"${subject}\")
-          (message-goto-body)
-          (insert \"${body}\")
-          $*
-          (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.
-    kill -9 $smtp_dummy_pid
-    notmuch new >/dev/null
-}
-
-# Pretend to deliver a message with emacs. Really save it to a file
-# and add it to the database
-#
-# Uses emacs to generate and deliver a message to the mail store.
-# Accepts arbitrary extra emacs/elisp functions to modify the message
-# before sending, which is useful to doing things like attaching files
-# to the message and encrypting/signing.
-#
-# If any GNU-style long-arguments (like --quiet or --decrypt=true) are
-# at the head of the argument list, they are sent directly to "notmuch
-# new" after message delivery
-emacs_fcc_message ()
-{
-    local nmn_args subject body
-    nmn_args=''
-    while [[ "$1" =~ ^-- ]]; do
-       nmn_args="$nmn_args $1"
-       shift
-    done
-    subject="$1"
-    body="$2"
-    shift 2
-    # before we can send a message, we have to prepare the FCC maildir
-    mkdir -p "$MAIL_DIR"/sent/{cur,new,tmp}
-
-    test_emacs \
-       "(let ((message-send-mail-function (lambda () t))
-              (mail-host-address \"example.com\"))
-          (notmuch-mua-mail)
-          (message-goto-to)
-          (insert \"test_suite@notmuchmail.org\nDate: 01 Jan 2000 12:00:00 -0000\")
-          (message-goto-subject)
-          (insert \"${subject}\")
-          (message-goto-body)
-          (insert \"${body}\")
-          $*
-          (let ((mml-secure-smime-sign-with-sender t)
-                (mml-secure-openpgp-sign-with-sender t))
-            (notmuch-mua-send-and-exit)))" || return 1
-    notmuch new $nmn_args >/dev/null
-}
-
 # Add an existing, fixed corpus of email to the database.
 #
 # $1 is the corpus dir under corpora to add, using "default" if unset.
@@ -431,8 +293,7 @@ emacs_fcc_message ()
 # history of the notmuch mailing list, which allows for reliably
 # testing commands that need to operate on a not-totally-trivial
 # number of messages.
-add_email_corpus ()
-{
+add_email_corpus () {
     local corpus
     corpus=${1:-default}
 
@@ -441,8 +302,7 @@ add_email_corpus ()
     notmuch new >/dev/null || die "'notmuch new' failed while adding email corpus"
 }
 
-test_begin_subtest ()
-{
+test_begin_subtest () {
     if [ -n "$inside_subtest" ]; then
        exec 1>&6 2>&7          # Restore stdout and stderr
        error "bug in test script: Missing test_expect_equal in ${BASH_SOURCE[1]}:${BASH_LINENO[0]}"
@@ -462,8 +322,7 @@ test_begin_subtest ()
 # not accept a test name. Instead, the caller should call
 # test_begin_subtest before calling this function in order to set the
 # name.
-test_expect_equal ()
-{
+test_expect_equal () {
        local output expected testname
        exec 1>&6 2>&7          # Restore stdout and stderr
        if [ -z "$inside_subtest" ]; then
@@ -488,33 +347,78 @@ test_expect_equal ()
     fi
 }
 
-# Like test_expect_equal, but takes two filenames.
-test_expect_equal_file ()
-{
-       local file1 file2 testname basename1 basename2
-       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"
+test_diff_file_ () {
+    local file1 file2 testname basename1 basename2
+    file1="$1"
+    file2="$2"
+    if ! test_skip "$test_subtest_name"
+    then
+       if diff -q "$file1" "$file2" >/dev/null ; then
+           test_ok_
+       else
+           testname=$this_test.$test_count
+           basename1=`basename "$file1"`
+           basename2=`basename "$file2"`
+           cp "$file1" "$testname.$basename1"
+           cp "$file2" "$testname.$basename2"
+           test_failure_ "$(diff -u "$testname.$basename1" "$testname.$basename2")"
        fi
-       inside_subtest=
-       test "$#" = 2 ||
+    fi
+}
+
+# Like test_expect_equal, but takes two filenames.
+test_expect_equal_file () {
+    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
+    inside_subtest=
+    test "$#" = 2 ||
        error "bug in the test script: not 2 parameters to test_expect_equal_file"
 
-       file1="$1"
-       file2="$2"
-       if ! test_skip "$test_subtest_name"
-       then
-               if diff -q "$file1" "$file2" >/dev/null ; then
-                       test_ok_
-               else
-                       testname=$this_test.$test_count
-                       basename1=`basename "$file1"`
-                       basename2=`basename "$file2"`
-                       cp "$file1" "$testname.$basename1"
-                       cp "$file2" "$testname.$basename2"
-                       test_failure_ "$(diff -u "$testname.$basename1" "$testname.$basename2")"
-               fi
+    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
+    if [ -z "$inside_subtest" ]; then
+       error "bug in the test script: test_expect_equal_file_nonempty without test_begin_subtest"
+    fi
+    inside_subtest=
+    test "$#" = 2 ||
+       error "bug in the test script: not 2 parameters to test_expect_equal_file_nonempty"
+
+    for file in "$1" "$2"; do
+       if [ ! -s "$file" ]; then
+           test_failure_ "Missing or zero length file: $file"
+           return $?
+       fi
+    done
+
+    test_diff_file_ "$1" "$2"
 }
 
 # Like test_expect_equal, but arguments are JSON expressions to be
@@ -571,51 +475,16 @@ test_json_nodes () {
        fi
 }
 
-test_emacs_expect_t () {
-       local result
-       test "$#" = 1 ||
-       error "bug in the test script: not 1 parameter to test_emacs_expect_t"
-       if [ -z "$inside_subtest" ]; then
-               error "bug in the test script: test_emacs_expect_t without test_begin_subtest"
-       fi
-
-       # Run the test.
-       if ! test_skip "$test_subtest_name"
-       then
-               test_emacs "(notmuch-test-run $1)" >/dev/null
-
-               # Restore state after the test.
-               exec 1>&6 2>&7          # Restore stdout and stderr
-               inside_subtest=
-
-               # Report success/failure.
-               result=$(cat OUTPUT)
-               if [ "$result" = t ]
-               then
-                       test_ok_
-               else
-                       test_failure_ "${result}"
-               fi
-       else
-               # Restore state after the (non) test.
-               exec 1>&6 2>&7          # Restore stdout and stderr
-               inside_subtest=
-       fi
-}
-
-NOTMUCH_NEW ()
-{
+NOTMUCH_NEW () {
     notmuch new "${@}" | grep -v -E -e '^Processed [0-9]*( total)? file|Found [0-9]* total file'
 }
 
-NOTMUCH_DUMP_TAGS ()
-{
+NOTMUCH_DUMP_TAGS () {
     # this relies on the default format being batch-tag, otherwise some tests will break
     notmuch dump --include=tags "${@}" | sed '/^#/d' | sort
 }
 
-notmuch_drop_mail_headers ()
-{
+notmuch_drop_mail_headers () {
     $NOTMUCH_PYTHON -c '
 import email, sys
 msg = email.message_from_file(sys.stdin)
@@ -624,94 +493,99 @@ print(msg.as_string(False))
 ' "$@"
 }
 
-notmuch_exception_sanitize ()
-{
-    perl -pe 's/(A Xapian exception occurred at .*[.]cc?):([0-9]*)/\1:XXX/'
+notmuch_debug_sanitize () {
+    grep -v '^D.:'
+}
+
+notmuch_exception_sanitize () {
+    perl -pe 's,(A Xapian exception occurred at) .*?([^/]*[.]cc?):([0-9]*),\1 \2:XXX,'
 }
 
-notmuch_search_sanitize ()
-{
-    perl -pe 's/("?thread"?: ?)("?)................("?)/\1\2XXX\3/'
+notmuch_search_sanitize () {
+    notmuch_debug_sanitize | perl -pe 's/("?thread"?: ?)("?)................("?)/\1\2XXX\3/'
 }
 
-notmuch_search_files_sanitize ()
-{
-    notmuch_dir_sanitize
+notmuch_search_files_sanitize () {
+    notmuch_dir_sanitize |  sed 's/msg-[0-9][0-9][0-9]/msg-XXX/'
 }
 
-notmuch_dir_sanitize ()
-{
+notmuch_dir_sanitize () {
     sed -e "s,$MAIL_DIR,MAIL_DIR," -e "s,${PWD},CWD,g" "$@"
 }
 
 NOTMUCH_SHOW_FILENAME_SQUELCH='s,filename:.*/mail,filename:/XXX/mail,'
-notmuch_show_sanitize ()
-{
+notmuch_show_sanitize () {
     sed -e "$NOTMUCH_SHOW_FILENAME_SQUELCH"
 }
-notmuch_show_sanitize_all ()
-{
+notmuch_show_sanitize_all () {
+    notmuch_debug_sanitize | \
     sed \
        -e 's| filename:.*| filename:XXXXX|' \
        -e 's| id:[^ ]* | id:XXXXX |' | \
        notmuch_date_sanitize
 }
 
-notmuch_json_show_sanitize ()
-{
+notmuch_json_show_sanitize () {
     sed \
        -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_emacs_error_sanitize ()
-{
+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
     shift
     for file in "$@"; do
        echo "=== $file ==="
-       cat "$file"
+       notmuch_debug_sanitize < "$file"
     done | sed \
-       -e 's/^\[.*\]$/[XXX]/' \
+       -e '/^$/d' \
+       -e '/^\[.*\]$/d' \
        -e "s|^\(command: \)\{0,1\}/.*/$command|\1YYY/$command|"
 }
 
-notmuch_date_sanitize ()
-{
+notmuch_date_sanitize () {
     sed \
        -e 's/^Date: Fri, 05 Jan 2001 .*0000/Date: GENERATED_DATE/'
 }
 
-notmuch_uuid_sanitize ()
-{
+# 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'
 }
 
-notmuch_built_with_sanitize ()
-{
+notmuch_built_with_sanitize () {
     sed 's/^built_with[.]\(.*\)=.*$/built_with.\1=something/'
 }
 
-notmuch_passwd_sanitize ()
-{
-    local user=$(id -un)
-    local fqdn=$(hostname -f)
-    local full_name=$(getent passwd $user | cut -d: -f 5 | cut -d, -f1)
-    sed -e "s/$user/USERNAME/" -e "s/$fqdn/FQDN/" -e "s/$full_name/USER_FULL_NAME/"
-}
-
-notmuch_config_sanitize ()
-{
+notmuch_config_sanitize () {
     notmuch_dir_sanitize | notmuch_built_with_sanitize
 }
 
-notmuch_show_part ()
-{
+notmuch_show_part () {
     awk '/^\014part}/{ f=0 }; { if (f) { print $0 } } /^\014part{ ID: '"$1"'/{ f=1 }'
 }
 
@@ -757,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.
 
@@ -888,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
@@ -902,7 +774,7 @@ test_expect_success () {
                test_run_ "$1"
                run_ret="$?"
                # test_run_ may update missing external prerequisites
-               test_check_missing_external_prereqs_ "$@" ||
+               test_check_missing_external_prereqs_ "$test_subtest_name" ||
                if [ "$run_ret" = 0 -a "$eval_ret" = 0 ]
                then
                        test_ok_
@@ -926,7 +798,7 @@ test_expect_code () {
                test_run_ "$2"
                run_ret="$?"
                # test_run_ may update missing external prerequisites,
-               test_check_missing_external_prereqs_ "$@" ||
+               test_check_missing_external_prereqs_ "$test_subtest_name" ||
                if [ "$run_ret" = 0 -a "$eval_ret" = "$1" ]
                then
                        test_ok_
@@ -966,7 +838,7 @@ test_must_fail () {
 # - cmp's output is not nearly as easy to read as diff -u
 # - not all diff versions understand "-u"
 
-test_cmp() {
+test_cmp () {
        $GIT_TEST_CMP "$@"
 }
 
@@ -1001,12 +873,13 @@ test_done () {
        mkdir -p "$test_results_dir"
        test_results_path="$test_results_dir/$this_test"
 
-       echo "total $test_count" >> $test_results_path
-       echo "success $test_success" >> $test_results_path
-       echo "fixed $test_fixed" >> $test_results_path
-       echo "broken $test_broken" >> $test_results_path
-       echo "failed $test_failure" >> $test_results_path
-       echo "" >> $test_results_path
+       printf %s\\n \
+               "success $test_success" \
+               "fixed $test_fixed" \
+               "broken $test_broken" \
+               "failed $test_failure" \
+               "total $test_count" \
+           > $test_results_path
 
        [ -n "$EMACS_SERVER" ] && test_emacs '(kill-emacs)'
 
@@ -1020,88 +893,13 @@ test_done () {
        fi
 }
 
-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).
-       cat <<EOF >"$TMP_DIRECTORY/run_emacs"
-#!/bin/sh
-export PATH=$PATH
-export NOTMUCH_CONFIG=$NOTMUCH_CONFIG
-
-# Here's what we are using here:
-#
-# --quick              Use minimal customization. This implies --no-init-file,
-#                      --no-site-file and (emacs 24) --no-site-lisp
-#
-# --directory          Ensure that the local elisp sources are found
-#
-# --load               Force loading of notmuch.el and test-lib.el
-
-exec ${TEST_EMACS} --quick \
-       --directory "$NOTMUCH_BUILDDIR/emacs" --load notmuch.el \
-       --directory "$NOTMUCH_SRCDIR/test" --load test-lib.el \
-       "\$@"
-EOF
-       chmod a+x "$TMP_DIRECTORY/run_emacs"
-}
-
-test_emacs () {
-       # test dependencies beforehand to avoid the waiting loop below
-       missing_dependencies=
-       test_require_external_prereq dtach || missing_dependencies=1
-       test_require_external_prereq emacs || missing_dependencies=1
-       test_require_external_prereq ${TEST_EMACSCLIENT} || missing_dependencies=1
-       test -z "$missing_dependencies" || return
-
-       if [ -z "$EMACS_SERVER" ]; then
-               emacs_tests="$NOTMUCH_SRCDIR/test/${this_test_bare}.el"
-               if [ -f "$emacs_tests" ]; then
-                       load_emacs_tests="--eval '(load \"$emacs_tests\")'"
-               else
-                       load_emacs_tests=
-               fi
-               server_name="notmuch-test-suite-$$"
-               # start a detached session with an emacs server
-               # user's TERM (or 'vt100' in case user's TERM is known dumb
-               # or unknown) is given to dtach which assumes a minimally
-               # VT100-compatible terminal -- and emacs inherits that
-               TERM=$SMART_TERM dtach -n "$TEST_TMPDIR/emacs-dtach-socket.$$" \
-                       sh -c "stty rows 24 cols 80; exec '$TMP_DIRECTORY/run_emacs' \
-                               --no-window-system \
-                               $load_emacs_tests \
-                               --eval '(setq server-name \"$server_name\")' \
-                               --eval '(server-start)' \
-                               --eval '(orphan-watchdog $$)'" || return
-               EMACS_SERVER="$server_name"
-               # wait until the emacs server is up
-               until test_emacs '()' >/dev/null 2>/dev/null; do
-                       sleep 1
-               done
-       fi
-
-       # Clear test-output output file.  Most Emacs tests end with a
-       # call to (test-output).  If the test code fails with an
-       # exception before this call, the output file won't get
-       # updated.  Since we don't want to compare against an output
-       # file from another test, so start out with an empty file.
-       rm -f OUTPUT
-       touch OUTPUT
-
-       ${TEST_EMACSCLIENT} --socket-name="$EMACS_SERVER" --eval "(notmuch-test-progn $*)"
-}
-
-test_python() {
+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:$NOTMUCH_SRCDIR/bindings/python${PYTHONPATH:+:$PYTHONPATH}" \
        $NOTMUCH_PYTHON -B - > OUTPUT
 }
 
-test_ruby() {
-    MAIL_DIR=$MAIL_DIR $NOTMUCH_RUBY -I "$NOTMUCH_BUILDDIR/bindings/ruby"> OUTPUT
-}
-
 test_C () {
     local exec_file test_file
     exec_file="test${test_count}"
@@ -1111,7 +909,20 @@ test_C () {
     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 > OUTPUT
+    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 <notmuch-private.h>' > ${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 () {
@@ -1124,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=./${shim_file}${LD_PRELOAD:+:$LD_PRELOAD} 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
@@ -1180,13 +996,14 @@ 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
 
-emacs_generate_script
-
-
 # Use -P to resolve symlinks in our working directory so that the cwd
 # in subprocesses like git equals our $PWD (for pathname comparisons).
 cd -P "$TMP_DIRECTORY" || error "Cannot set up test environment"
diff --git a/test/test-vars.sh b/test/test-vars.sh
new file mode 100644 (file)
index 0000000..02d60f8
--- /dev/null
@@ -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"
index 7ef029a566d2067c357d72cb5f39da1537de7583..8a0b9bc3777ca9487ed9a3854a91c0ad77a91395 100644 (file)
@@ -6,7 +6,7 @@ extra_cflags += -I$(srcdir)/$(dir)
 libnotmuch_util_c_srcs := $(dir)/xutil.c $(dir)/error_util.c $(dir)/hex-escape.c \
                  $(dir)/string-util.c $(dir)/talloc-extra.c $(dir)/zlib-extra.c \
                $(dir)/util.c $(dir)/gmime-extra.c $(dir)/crypto.c \
-               $(dir)/repair.c \
+               $(dir)/repair.c $(dir)/path-util.c \
                $(dir)/unicode-util.c
 
 libnotmuch_util_modules := $(libnotmuch_util_c_srcs:.c=.o)
index 81a5b1743b7bee88ff05277021a6195a20c85bff..192cb07812b38cb29649e68c494a7347bccffd73 100644 (file)
@@ -107,6 +107,21 @@ g_mime_certificate_get_valid_userid (GMimeCertificate *cert)
     return NULL;
 }
 
+const char *
+g_mime_certificate_get_valid_email (GMimeCertificate *cert)
+{
+    /* output e-mail address only if validity is FULL or ULTIMATE. */
+    const char *email = g_mime_certificate_get_email(cert);
+
+    if (email == NULL)
+       return email;
+    GMimeValidity validity = g_mime_certificate_get_id_validity (cert);
+
+    if (validity == GMIME_VALIDITY_FULL || validity == GMIME_VALIDITY_ULTIMATE)
+       return email;
+    return NULL;
+}
+
 const char *
 g_mime_certificate_get_fpr16 (GMimeCertificate *cert)
 {
index 094309ec26041e4af3e6356087166a1873e594a6..889e91f3e8c0e000ec9ab3660851a755ce9d2507 100644 (file)
@@ -69,6 +69,10 @@ gint64 g_mime_utils_header_decode_date_unix (const char *date);
  * Return string for valid User ID (or NULL if no valid User ID exists)
  */
 const char *g_mime_certificate_get_valid_userid (GMimeCertificate *cert);
+/**
+ * Return string for valid e-mail address (or NULL if no valid e-mail address exists)
+ */
+const char *g_mime_certificate_get_valid_email (GMimeCertificate *cert);
 
 #ifdef __cplusplus
 }
index 8703334cda8d4836660bed1f8cf41c65e2fec023..83a4c6f146fe15387b0ef8c151ae6e5b4dcbc766 100644 (file)
@@ -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/path-util.c b/util/path-util.c
new file mode 100644 (file)
index 0000000..3267a96
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define _GNU_SOURCE
+
+#include "path-util.h"
+
+#include <limits.h>
+#include <stdlib.h>
+
+
+char *
+notmuch_canonicalize_file_name (const char *path)
+{
+#if HAVE_CANONICALIZE_FILE_NAME
+    return canonicalize_file_name (path);
+#elif defined(PATH_MAX)
+    char *resolved_path =  malloc (PATH_MAX + 1);
+    if (resolved_path == NULL)
+       return NULL;
+
+    return realpath (path, resolved_path);
+#else
+#error undefined PATH_MAX _and_ missing canonicalize_file_name not supported
+#endif
+}
diff --git a/util/path-util.h b/util/path-util.h
new file mode 100644 (file)
index 0000000..ac85f69
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef NOTMUCH_UTIL_PATH_UTIL_H_
+#define NOTMUCH_UTIL_PATH_UTIL_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+char *
+notmuch_canonicalize_file_name (const char *path);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NOTMUCH_UTIL_PATH_UTIL_H_ */
index 9c46a81a58e20030b586953e577efd777ef2af17..03d7648d329a2e7b3667a894b558e93d440d1a6a 100644 (file)
@@ -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;
 }
 
index 32d1e6ef693ae4cc5267b09d3dbf6641db03c2f0..1bb9336a2ed6c615dd8aacce4618800000527691 100644 (file)
@@ -4,9 +4,16 @@
 #include <stdbool.h>
 #include <gmodule.h>
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 /* The utf8 encoded string would tokenize as a single word, according
  * to xapian. */
 bool unicode_word_utf8 (const char *str);
 typedef gunichar notmuch_unichar;
 
+#ifdef __cplusplus
+}
+#endif
 #endif
diff --git a/util/xapian-extra.h b/util/xapian-extra.h
new file mode 100644 (file)
index 0000000..39c7f48
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef _XAPIAN_EXTRA_H
+#define _XAPIAN_EXTRA_H
+
+#include <string>
+#include <xapian.h>
+
+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
index a8a0217298153523be41c08773ecf133ba4ce380..f2687f32e8878bb36f76e6cb14a4b09169e25f8b 100644 (file)
@@ -1 +1 @@
-0.31.4
+0.38.3
index c137bacdc0f4f1e3af78cb9617d5fe92f527c53f..777c20c0e6f0b8a695fe2d997ddb7284660e5de2 100644 (file)
@@ -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")',
        \ }
index 43741022e13580cafd7f1190a601608b14afcafe..c98f2b53d80df5c50fa13142a73ebc53b50e5075 100644 (file)
@@ -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'
index ad8b7c800cd5cca1b9b4c23fe226d57c4e414191..c1c2f63d20178dab69f92c9ff9aab4631dda6ee5 100644 (file)
@@ -317,6 +317,9 @@ ruby << EOF
        $curbuf.render do |b|
                q = $curbuf.query(get_cur_view)
                q.sort = Notmuch::SORT_OLDEST_FIRST
+               $exclude_tags.each { |t|
+                       q.add_tag_exclude(t)
+               }
                msgs = q.search_messages
                msgs.each do |msg|
                        m = Mail.read(msg.filename)
@@ -666,7 +669,7 @@ ruby << EOF
                                date = Time.at(e.newest_date).strftime(date_fmt)
                                subject = e.messages.first['subject']
                                if $mail_installed
-                                       subject = Mail::Field.new("Subject: " + subject).to_s
+                                       subject = Mail::Field.parse("Subject: " + subject).to_s
                                else
                                        subject = subject.force_encoding('utf-8')
                                end