]> git.cworth.org Git - notmuch/commitdiff
Import notmuch_0.38.2.orig.tar.xz
authorDavid Bremner <bremner@debian.org>
Fri, 1 Dec 2023 11:51:09 +0000 (07:51 -0400)
committerDavid Bremner <bremner@debian.org>
Fri, 1 Dec 2023 11:51:09 +0000 (07:51 -0400)
[dgit import orig notmuch_0.38.2.orig.tar.xz]

930 files changed:
.dir-locals.el [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.mailmap [new file with mode: 0644]
.travis.yml [new file with mode: 0644]
AUTHORS [new file with mode: 0644]
COPYING [new file with mode: 0644]
COPYING-GPL-3 [new file with mode: 0644]
INSTALL [new file with mode: 0644]
Makefile [new file with mode: 0644]
Makefile.global [new file with mode: 0644]
Makefile.local [new file with mode: 0644]
NEWS [new file with mode: 0644]
README [new file with mode: 0644]
README.rst [new file with mode: 0644]
bindings/Makefile [new file with mode: 0644]
bindings/Makefile.local [new file with mode: 0644]
bindings/python-cffi/MANIFEST.in [new file with mode: 0644]
bindings/python-cffi/notmuch2/__init__.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_base.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_build.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_config.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_database.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_errors.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_message.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_query.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_tags.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_thread.py [new file with mode: 0644]
bindings/python-cffi/setup.py [new file with mode: 0644]
bindings/python-cffi/tests/conftest.py [new file with mode: 0644]
bindings/python-cffi/tests/test_base.py [new file with mode: 0644]
bindings/python-cffi/tests/test_config.py [new file with mode: 0644]
bindings/python-cffi/tests/test_database.py [new file with mode: 0644]
bindings/python-cffi/tests/test_errors.py [new file with mode: 0644]
bindings/python-cffi/tests/test_message.py [new file with mode: 0644]
bindings/python-cffi/tests/test_tags.py [new file with mode: 0644]
bindings/python-cffi/tests/test_thread.py [new file with mode: 0644]
bindings/python-cffi/tox.ini [new file with mode: 0644]
bindings/python/.gitignore [new file with mode: 0644]
bindings/python/MANIFEST.in [new file with mode: 0644]
bindings/python/README [new file with mode: 0644]
bindings/python/docs/COPYING [new file with mode: 0644]
bindings/python/docs/Makefile [new file with mode: 0644]
bindings/python/docs/source/conf.py [new file with mode: 0644]
bindings/python/docs/source/database.rst [new file with mode: 0644]
bindings/python/docs/source/filesystem.rst [new file with mode: 0644]
bindings/python/docs/source/index.rst [new file with mode: 0644]
bindings/python/docs/source/message.rst [new file with mode: 0644]
bindings/python/docs/source/messages.rst [new file with mode: 0644]
bindings/python/docs/source/notes.rst [new file with mode: 0644]
bindings/python/docs/source/query.rst [new file with mode: 0644]
bindings/python/docs/source/quickstart.rst [new file with mode: 0644]
bindings/python/docs/source/status_and_errors.rst [new file with mode: 0644]
bindings/python/docs/source/tags.rst [new file with mode: 0644]
bindings/python/docs/source/thread.rst [new file with mode: 0644]
bindings/python/docs/source/threads.rst [new file with mode: 0644]
bindings/python/notmuch/__init__.py [new file with mode: 0644]
bindings/python/notmuch/compat.py [new file with mode: 0644]
bindings/python/notmuch/database.py [new file with mode: 0644]
bindings/python/notmuch/directory.py [new file with mode: 0644]
bindings/python/notmuch/errors.py [new file with mode: 0644]
bindings/python/notmuch/filenames.py [new file with mode: 0644]
bindings/python/notmuch/globals.py [new file with mode: 0644]
bindings/python/notmuch/message.py [new file with mode: 0644]
bindings/python/notmuch/messages.py [new file with mode: 0644]
bindings/python/notmuch/query.py [new file with mode: 0644]
bindings/python/notmuch/tag.py [new file with mode: 0644]
bindings/python/notmuch/thread.py [new file with mode: 0644]
bindings/python/notmuch/threads.py [new file with mode: 0644]
bindings/python/notmuch/version.py [new file with mode: 0644]
bindings/python/setup.py [new file with mode: 0644]
bindings/ruby/.gitignore [new file with mode: 0644]
bindings/ruby/README [new file with mode: 0644]
bindings/ruby/database.c [new file with mode: 0644]
bindings/ruby/defs.h [new file with mode: 0644]
bindings/ruby/directory.c [new file with mode: 0644]
bindings/ruby/extconf.rb [new file with mode: 0644]
bindings/ruby/filenames.c [new file with mode: 0644]
bindings/ruby/init.c [new file with mode: 0644]
bindings/ruby/message.c [new file with mode: 0644]
bindings/ruby/messages.c [new file with mode: 0644]
bindings/ruby/query.c [new file with mode: 0644]
bindings/ruby/rdoc.sh [new file with mode: 0755]
bindings/ruby/status.c [new file with mode: 0644]
bindings/ruby/tags.c [new file with mode: 0644]
bindings/ruby/thread.c [new file with mode: 0644]
bindings/ruby/threads.c [new file with mode: 0644]
command-line-arguments.c [new file with mode: 0644]
command-line-arguments.h [new file with mode: 0644]
compat/.gitignore [new file with mode: 0644]
compat/Makefile [new file with mode: 0644]
compat/Makefile.local [new file with mode: 0644]
compat/README [new file with mode: 0644]
compat/check_asctime.c [new file with mode: 0644]
compat/check_getpwuid.c [new file with mode: 0644]
compat/compat.h [new file with mode: 0644]
compat/function-attributes.h [new file with mode: 0644]
compat/gen_zlib_pc.c [new file with mode: 0644]
compat/getdelim.c [new file with mode: 0644]
compat/getline.c [new file with mode: 0644]
compat/have_canonicalize_file_name.c [new file with mode: 0644]
compat/have_d_type.c [new file with mode: 0644]
compat/have_getline.c [new file with mode: 0644]
compat/have_strcasestr.c [new file with mode: 0644]
compat/have_strsep.c [new file with mode: 0644]
compat/have_timegm.c [new file with mode: 0644]
compat/strcasestr.c [new file with mode: 0644]
compat/strsep.c [new file with mode: 0644]
compat/timegm.c [new file with mode: 0644]
completion/Makefile [new file with mode: 0644]
completion/Makefile.local [new file with mode: 0644]
completion/README [new file with mode: 0644]
completion/notmuch-completion.bash [new file with mode: 0644]
completion/zsh/_email-notmuch [new file with mode: 0644]
completion/zsh/_notmuch [new file with mode: 0644]
configure [new file with mode: 0755]
contrib/go/.gitignore [new file with mode: 0644]
contrib/go/LICENSE [new file with mode: 0644]
contrib/go/Makefile [new file with mode: 0644]
contrib/go/README [new file with mode: 0644]
contrib/go/src/notmuch-addrlookup/addrlookup.go [new file with mode: 0644]
contrib/go/src/notmuch/notmuch.go [new file with mode: 0644]
contrib/notmuch-mutt/.gitignore [new file with mode: 0644]
contrib/notmuch-mutt/Makefile [new file with mode: 0644]
contrib/notmuch-mutt/README [new file with mode: 0644]
contrib/notmuch-mutt/notmuch-mutt [new file with mode: 0755]
contrib/notmuch-mutt/notmuch-mutt.rc [new file with mode: 0644]
debian/.gitignore [new file with mode: 0644]
debian/NEWS [new file with mode: 0644]
debian/changelog [new file with mode: 0644]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/elpa-notmuch.elpa [new file with mode: 0644]
debian/elpa-notmuch.info [new file with mode: 0644]
debian/elpa-notmuch.lintian-overrides [new file with mode: 0644]
debian/elpa-test [new file with mode: 0644]
debian/gbp.conf [new file with mode: 0644]
debian/libnotmuch-dev.install [new file with mode: 0644]
debian/libnotmuch-dev.manpages [new file with mode: 0644]
debian/libnotmuch5.install [new file with mode: 0644]
debian/libnotmuch5.symbols [new file with mode: 0644]
debian/not-installed [new file with mode: 0644]
debian/notmuch-doc.install [new file with mode: 0644]
debian/notmuch-emacs.README.Debian [new file with mode: 0644]
debian/notmuch-emacs.maintscript [new file with mode: 0644]
debian/notmuch-git.install [new file with mode: 0644]
debian/notmuch-git.manpages [new file with mode: 0644]
debian/notmuch-mutt.docs [new file with mode: 0644]
debian/notmuch-mutt.install [new file with mode: 0644]
debian/notmuch-mutt.manpages [new file with mode: 0644]
debian/notmuch-vim.README.Debian [new file with mode: 0644]
debian/notmuch-vim.dirs [new file with mode: 0644]
debian/notmuch-vim.docs [new file with mode: 0644]
debian/notmuch-vim.install [new file with mode: 0644]
debian/notmuch.dirs [new file with mode: 0644]
debian/notmuch.docs [new file with mode: 0644]
debian/notmuch.install [new file with mode: 0644]
debian/notmuch.maintscript [new file with mode: 0644]
debian/notmuch.manpages [new file with mode: 0644]
debian/ruby-notmuch.install [new file with mode: 0644]
debian/rules [new file with mode: 0755]
debian/source/format [new file with mode: 0644]
debian/source/options [new file with mode: 0644]
debian/tests/control [new file with mode: 0644]
debian/upstream/metadata [new file with mode: 0644]
debugger.c [new file with mode: 0644]
devel/RELEASING [new file with mode: 0644]
devel/STYLE [new file with mode: 0644]
devel/TODO [new file with mode: 0644]
devel/author-scan.sh [new file with mode: 0644]
devel/check-notmuch-commit [new file with mode: 0755]
devel/check-out-of-tree-build.sh [new file with mode: 0755]
devel/emacs-keybindings.org [new file with mode: 0644]
devel/man-to-mdwn.pl [new file with mode: 0755]
devel/news2wiki.pl [new file with mode: 0755]
devel/nmbug/doc/.gitignore [new file with mode: 0644]
devel/nmbug/doc/Makefile [new file with mode: 0644]
devel/nmbug/doc/conf.py [new file with mode: 0644]
devel/nmbug/doc/index.rst [new file with mode: 0644]
devel/nmbug/doc/man1/notmuch-report.1.rst [new file with mode: 0644]
devel/nmbug/doc/man5/notmuch-report.json.5.rst [new file with mode: 0644]
devel/nmbug/notmuch-report [new file with mode: 0755]
devel/nmbug/notmuch-report.json [new file with mode: 0644]
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 [new file with mode: 0755]
devel/schemata [new file with mode: 0644]
devel/try-emacs-mua [new file with mode: 0755]
devel/uncrustify.cfg [new file with mode: 0644]
doc/.gitignore [new file with mode: 0644]
doc/INSTALL [new file with mode: 0644]
doc/Makefile [new file with mode: 0644]
doc/Makefile.local [new file with mode: 0644]
doc/command-line.rst [new file with mode: 0644]
doc/conf.py [new file with mode: 0644]
doc/doxygen.cfg [new file with mode: 0644]
doc/elisp.py [new file with mode: 0644]
doc/index.rst [new file with mode: 0644]
doc/man1/notmuch-address.rst [new file with mode: 0644]
doc/man1/notmuch-compact.rst [new file with mode: 0644]
doc/man1/notmuch-config.rst [new file with mode: 0644]
doc/man1/notmuch-count.rst [new file with mode: 0644]
doc/man1/notmuch-dump.rst [new file with mode: 0644]
doc/man1/notmuch-emacs-mua.rst [new file with mode: 0644]
doc/man1/notmuch-git.rst [new file with mode: 0644]
doc/man1/notmuch-insert.rst [new file with mode: 0644]
doc/man1/notmuch-new.rst [new file with mode: 0644]
doc/man1/notmuch-reindex.rst [new file with mode: 0644]
doc/man1/notmuch-reply.rst [new file with mode: 0644]
doc/man1/notmuch-restore.rst [new file with mode: 0644]
doc/man1/notmuch-search.rst [new file with mode: 0644]
doc/man1/notmuch-show.rst [new file with mode: 0644]
doc/man1/notmuch-tag.rst [new file with mode: 0644]
doc/man1/notmuch.rst [new file with mode: 0644]
doc/man5/notmuch-hooks.rst [new file with mode: 0644]
doc/man7/notmuch-properties.rst [new file with mode: 0644]
doc/man7/notmuch-search-terms.rst [new file with mode: 0644]
doc/man7/notmuch-sexp-queries.rst [new file with mode: 0644]
doc/notmuch-emacs.rst [new file with mode: 0644]
doc/python-bindings.rst [new file with mode: 0644]
doc/queries.rst [new file with mode: 0644]
emacs/.gitignore [new file with mode: 0644]
emacs/Makefile [new file with mode: 0644]
emacs/Makefile.local [new file with mode: 0644]
emacs/coolj.el [new file with mode: 0644]
emacs/make-deps.el [new file with mode: 0644]
emacs/notmuch-address.el [new file with mode: 0644]
emacs/notmuch-company.el [new file with mode: 0644]
emacs/notmuch-compat.el [new file with mode: 0644]
emacs/notmuch-crypto.el [new file with mode: 0644]
emacs/notmuch-draft.el [new file with mode: 0644]
emacs/notmuch-emacs-mua [new file with mode: 0755]
emacs/notmuch-emacs-mua.desktop [new file with mode: 0644]
emacs/notmuch-hello.el [new file with mode: 0644]
emacs/notmuch-jump.el [new file with mode: 0644]
emacs/notmuch-lib.el [new file with mode: 0644]
emacs/notmuch-logo.svg [new file with mode: 0644]
emacs/notmuch-maildir-fcc.el [new file with mode: 0644]
emacs/notmuch-message.el [new file with mode: 0644]
emacs/notmuch-mua.el [new file with mode: 0644]
emacs/notmuch-parser.el [new file with mode: 0644]
emacs/notmuch-pkg.el.tmpl [new file with mode: 0644]
emacs/notmuch-print.el [new file with mode: 0644]
emacs/notmuch-query.el [new file with mode: 0644]
emacs/notmuch-show.el [new file with mode: 0644]
emacs/notmuch-tag.el [new file with mode: 0644]
emacs/notmuch-tree.el [new file with mode: 0644]
emacs/notmuch-version.el.tmpl [new file with mode: 0644]
emacs/notmuch-wash.el [new file with mode: 0644]
emacs/notmuch.el [new file with mode: 0644]
emacs/rstdoc.el [new file with mode: 0644]
emacs/rstdoc.rsti [new file with mode: 0644]
gmime-filter-reply.c [new file with mode: 0644]
gmime-filter-reply.h [new file with mode: 0644]
hooks.c [new file with mode: 0644]
lib/Makefile [new file with mode: 0644]
lib/Makefile.local [new file with mode: 0644]
lib/add-message.cc [new file with mode: 0644]
lib/built-with.c [new file with mode: 0644]
lib/config.cc [new file with mode: 0644]
lib/database-private.h [new file with mode: 0644]
lib/database.cc [new file with mode: 0644]
lib/directory.cc [new file with mode: 0644]
lib/features.cc [new file with mode: 0644]
lib/filenames.c [new file with mode: 0644]
lib/index.cc [new file with mode: 0644]
lib/indexopts.c [new file with mode: 0644]
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 [new file with mode: 0644]
lib/message-id.c [new file with mode: 0644]
lib/message-private.h [new file with mode: 0644]
lib/message-property.cc [new file with mode: 0644]
lib/message.cc [new file with mode: 0644]
lib/messages.c [new file with mode: 0644]
lib/notmuch-private.h [new file with mode: 0644]
lib/notmuch.h [new file with mode: 0644]
lib/notmuch.sym [new file with mode: 0644]
lib/open.cc [new file with mode: 0644]
lib/parse-sexp.cc [new file with mode: 0644]
lib/parse-time-vrp.cc [new file with mode: 0644]
lib/parse-time-vrp.h [new file with mode: 0644]
lib/prefix.cc [new file with mode: 0644]
lib/query-fp.cc [new file with mode: 0644]
lib/query-fp.h [new file with mode: 0644]
lib/query.cc [new file with mode: 0644]
lib/regexp-fields.cc [new file with mode: 0644]
lib/regexp-fields.h [new file with mode: 0644]
lib/sexp-fp.cc [new file with mode: 0644]
lib/sexp-fp.h [new file with mode: 0644]
lib/sha1.c [new file with mode: 0644]
lib/string-list.c [new file with mode: 0644]
lib/string-map.c [new file with mode: 0644]
lib/tags.c [new file with mode: 0644]
lib/thread-fp.cc [new file with mode: 0644]
lib/thread-fp.h [new file with mode: 0644]
lib/thread.cc [new file with mode: 0644]
mime-node.c [new file with mode: 0644]
notmuch-client-init.c [new file with mode: 0644]
notmuch-client.h [new file with mode: 0644]
notmuch-compact.c [new file with mode: 0644]
notmuch-config.c [new file with mode: 0644]
notmuch-count.c [new file with mode: 0644]
notmuch-dump.c [new file with mode: 0644]
notmuch-git.py [new file with mode: 0644]
notmuch-insert.c [new file with mode: 0644]
notmuch-new.c [new file with mode: 0644]
notmuch-reindex.c [new file with mode: 0644]
notmuch-reply.c [new file with mode: 0644]
notmuch-restore.c [new file with mode: 0644]
notmuch-search.c [new file with mode: 0644]
notmuch-setup.c [new file with mode: 0644]
notmuch-show.c [new file with mode: 0644]
notmuch-tag.c [new file with mode: 0644]
notmuch-time.c [new file with mode: 0644]
notmuch.c [new file with mode: 0644]
packaging/debian [new file with mode: 0644]
packaging/fedora/notmuch.spec [new file with mode: 0644]
parse-time-string/Makefile [new file with mode: 0644]
parse-time-string/Makefile.local [new file with mode: 0644]
parse-time-string/README [new file with mode: 0644]
parse-time-string/parse-time-string.c [new file with mode: 0644]
parse-time-string/parse-time-string.h [new file with mode: 0644]
performance-test/.gitignore [new file with mode: 0644]
performance-test/M00-new.sh [new file with mode: 0755]
performance-test/M01-dump-restore.sh [new file with mode: 0755]
performance-test/M02-show.sh [new file with mode: 0755]
performance-test/M03-search.sh [new file with mode: 0755]
performance-test/M04-reply.sh [new file with mode: 0755]
performance-test/M05-reindex.sh [new file with mode: 0755]
performance-test/M06-insert.sh [new file with mode: 0755]
performance-test/Makefile [new file with mode: 0644]
performance-test/Makefile.local [new file with mode: 0644]
performance-test/README [new file with mode: 0644]
performance-test/T00-new.sh [new file with mode: 0755]
performance-test/T01-dump-restore.sh [new file with mode: 0755]
performance-test/T02-tag.sh [new file with mode: 0755]
performance-test/T03-reindex.sh [new file with mode: 0755]
performance-test/T04-thread-subquery.sh [new file with mode: 0755]
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/.gitignore [new file with mode: 0644]
performance-test/download/notmuch-email-corpus-0.3.tar.xz.asc [new file with mode: 0644]
performance-test/download/notmuch-email-corpus-0.4.tar.xz.asc [new file with mode: 0644]
performance-test/download/notmuch-email-corpus-0.5.tar.xz.asc [new file with mode: 0644]
performance-test/notmuch-memory-test [new file with mode: 0755]
performance-test/notmuch-time-test [new file with mode: 0755]
performance-test/perf-test-lib.sh [new file with mode: 0644]
performance-test/version.sh [new file with mode: 0644]
query-string.c [new file with mode: 0644]
sprinter-json.c [new file with mode: 0644]
sprinter-sexp.c [new file with mode: 0644]
sprinter-text.c [new file with mode: 0644]
sprinter.h [new file with mode: 0644]
status.c [new file with mode: 0644]
tag-util.c [new file with mode: 0644]
tag-util.h [new file with mode: 0644]
test/.gitignore [new file with mode: 0644]
test/Makefile [new file with mode: 0644]
test/Makefile.local [new file with mode: 0644]
test/README [new file with mode: 0644]
test/T000-basic.sh [new file with mode: 0755]
test/T010-help-test.sh [new file with mode: 0755]
test/T020-compact.sh [new file with mode: 0755]
test/T030-config.sh [new file with mode: 0755]
test/T035-read-config.sh [new file with mode: 0755]
test/T040-setup.sh [new file with mode: 0755]
test/T050-new.sh [new file with mode: 0755]
test/T051-new-renames.sh [new file with mode: 0755]
test/T055-path-config.sh [new file with mode: 0755]
test/T060-count.sh [new file with mode: 0755]
test/T070-insert.sh [new file with mode: 0755]
test/T080-search.sh [new file with mode: 0755]
test/T081-sexpr-search.sh [new file with mode: 0755]
test/T090-search-output.sh [new file with mode: 0755]
test/T095-address.sh [new file with mode: 0755]
test/T100-search-by-folder.sh [new file with mode: 0755]
test/T110-search-position-overlap-bug.sh [new file with mode: 0755]
test/T120-search-insufficient-from-quoting.sh [new file with mode: 0755]
test/T130-search-limiting.sh [new file with mode: 0755]
test/T131-show-limiting.sh [new file with mode: 0755]
test/T140-excludes.sh [new file with mode: 0755]
test/T150-tagging.sh [new file with mode: 0755]
test/T160-json.sh [new file with mode: 0755]
test/T170-sexp.sh [new file with mode: 0755]
test/T180-text.sh [new file with mode: 0755]
test/T190-multipart.sh [new file with mode: 0755]
test/T200-thread-naming.sh [new file with mode: 0755]
test/T205-author-naming.sh [new file with mode: 0755]
test/T210-raw.sh [new file with mode: 0755]
test/T220-reply.sh [new file with mode: 0755]
test/T230-reply-to-sender.sh [new file with mode: 0755]
test/T240-dump-restore.sh [new file with mode: 0755]
test/T250-uuencode.sh [new file with mode: 0755]
test/T260-thread-order.sh [new file with mode: 0755]
test/T270-author-order.sh [new file with mode: 0755]
test/T280-from-guessing.sh [new file with mode: 0755]
test/T290-long-id.sh [new file with mode: 0755]
test/T300-encoding.sh [new file with mode: 0755]
test/T310-emacs.sh [new file with mode: 0755]
test/T315-emacs-tagging.sh [new file with mode: 0755]
test/T320-emacs-large-search-buffer.sh [new file with mode: 0755]
test/T330-emacs-subject-to-filename.sh [new file with mode: 0755]
test/T340-maildir-sync.sh [new file with mode: 0755]
test/T350-crypto.sh [new file with mode: 0755]
test/T351-pgpmime-mangling.sh [new file with mode: 0755]
test/T355-smime.sh [new file with mode: 0755]
test/T356-protected-headers.sh [new file with mode: 0755]
test/T357-index-decryption.sh [new file with mode: 0755]
test/T358-emacs-protected-headers.sh [new file with mode: 0755]
test/T360-symbol-hiding.sh [new file with mode: 0755]
test/T370-search-folder-coherence.sh [new file with mode: 0755]
test/T380-atomicity.sh [new file with mode: 0755]
test/T385-transactions.sh [new file with mode: 0755]
test/T390-python.sh [new file with mode: 0755]
test/T391-python-cffi.sh [new file with mode: 0755]
test/T392-python-cffi-notmuch.sh [new file with mode: 0755]
test/T395-ruby.sh [new file with mode: 0755]
test/T400-hooks.sh [new file with mode: 0755]
test/T405-external.sh [new file with mode: 0755]
test/T410-argument-parsing.sh [new file with mode: 0755]
test/T420-emacs-test-functions.sh [new file with mode: 0755]
test/T430-emacs-address-cleaning.sh [new file with mode: 0755]
test/T440-emacs-hello.sh [new file with mode: 0755]
test/T450-emacs-show.sh [new file with mode: 0755]
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 [new file with mode: 0755]
test/T460-emacs-tree.sh [new file with mode: 0755]
test/T465-emacs-unthreaded.sh [new file with mode: 0755]
test/T470-missing-headers.sh [new file with mode: 0755]
test/T480-hex-escaping.sh [new file with mode: 0755]
test/T490-parse-time-string.sh [new file with mode: 0755]
test/T500-search-date.sh [new file with mode: 0755]
test/T510-thread-replies.sh [new file with mode: 0755]
test/T520-show.sh [new file with mode: 0755]
test/T530-upgrade.sh [new file with mode: 0755]
test/T550-db-features.sh [new file with mode: 0755]
test/T560-lib-error.sh [new file with mode: 0755]
test/T562-lib-database.sh [new file with mode: 0755]
test/T563-lib-directory.sh [new file with mode: 0755]
test/T564-lib-query.sh [new file with mode: 0755]
test/T565-lib-tags.sh [new file with mode: 0755]
test/T566-lib-message.sh [new file with mode: 0755]
test/T568-lib-thread.sh [new file with mode: 0755]
test/T570-revision-tracking.sh [new file with mode: 0755]
test/T580-thread-search.sh [new file with mode: 0755]
test/T585-thread-subquery.sh [new file with mode: 0755]
test/T590-libconfig.sh [new file with mode: 0755]
test/T592-thread-breakage.sh [new file with mode: 0755]
test/T595-reopen.sh [new file with mode: 0755]
test/T600-named-queries.sh [new file with mode: 0755]
test/T610-message-property.sh [new file with mode: 0755]
test/T620-lock.sh [new file with mode: 0755]
test/T630-emacs-draft.sh [new file with mode: 0755]
test/T640-database-modified.sh [new file with mode: 0755]
test/T650-regexp-query.sh [new file with mode: 0755]
test/T660-bad-date.sh [new file with mode: 0755]
test/T670-duplicate-mid.sh [new file with mode: 0755]
test/T680-html-indexing.sh [new file with mode: 0755]
test/T690-command-line-args.sh [new file with mode: 0755]
test/T700-reindex.sh [new file with mode: 0755]
test/T710-message-id.sh [new file with mode: 0755]
test/T720-emacs-attachment-warnings.sh [new file with mode: 0755]
test/T720-lib-lifetime.sh [new file with mode: 0755]
test/T730-emacs-forwarding.sh [new file with mode: 0755]
test/T740-body.sh [new file with mode: 0755]
test/T750-gzip.sh [new file with mode: 0755]
test/T750-user-header.sh [new file with mode: 0755]
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 [new file with mode: 0755]
test/arg-test.c [new file with mode: 0644]
test/atomicity.py [new file with mode: 0644]
test/corpora/README [new file with mode: 0644]
test/corpora/attachment/x-gtar-compressed.eml [new file with mode: 0644]
test/corpora/broken/broken-cc [new file with mode: 0644]
test/corpora/broken/loop/loop-12 [new file with mode: 0644]
test/corpora/broken/loop/loop-21 [new file with mode: 0644]
test/corpora/crypto/basic-encrypted.eml [new file with mode: 0644]
test/corpora/crypto/encrypted-rfc822-attachment [new file with mode: 0644]
test/corpora/crypto/encrypted-signed.eml [new file with mode: 0644]
test/corpora/crypto/simple-encrypted [new file with mode: 0644]
test/corpora/default/01:2, [new file with mode: 0644]
test/corpora/default/02:2, [new file with mode: 0644]
test/corpora/default/bar/17:2, [new file with mode: 0644]
test/corpora/default/bar/18:2, [new file with mode: 0644]
test/corpora/default/bar/baz/05:2, [new file with mode: 0644]
test/corpora/default/bar/baz/23:2, [new file with mode: 0644]
test/corpora/default/bar/baz/24:2, [new file with mode: 0644]
test/corpora/default/bar/baz/cur/25:2, [new file with mode: 0644]
test/corpora/default/bar/baz/cur/26:2, [new file with mode: 0644]
test/corpora/default/bar/baz/new/27:2, [new file with mode: 0644]
test/corpora/default/bar/baz/new/28:2, [new file with mode: 0644]
test/corpora/default/bar/cur/19:2, [new file with mode: 0644]
test/corpora/default/bar/cur/20:2, [new file with mode: 0644]
test/corpora/default/bar/new/21:2, [new file with mode: 0644]
test/corpora/default/bar/new/22:2, [new file with mode: 0644]
test/corpora/default/cur/29:2, [new file with mode: 0644]
test/corpora/default/cur/30:2, [new file with mode: 0644]
test/corpora/default/cur/31:2, [new file with mode: 0644]
test/corpora/default/cur/32:2, [new file with mode: 0644]
test/corpora/default/cur/33:2, [new file with mode: 0644]
test/corpora/default/cur/34:2, [new file with mode: 0644]
test/corpora/default/cur/35:2, [new file with mode: 0644]
test/corpora/default/cur/36:2, [new file with mode: 0644]
test/corpora/default/cur/37:2, [new file with mode: 0644]
test/corpora/default/cur/38:2, [new file with mode: 0644]
test/corpora/default/cur/39:2, [new file with mode: 0644]
test/corpora/default/cur/40:2, [new file with mode: 0644]
test/corpora/default/cur/41:2, [new file with mode: 0644]
test/corpora/default/cur/42:2, [new file with mode: 0644]
test/corpora/default/cur/43:2, [new file with mode: 0644]
test/corpora/default/cur/44:2, [new file with mode: 0644]
test/corpora/default/cur/45:2, [new file with mode: 0644]
test/corpora/default/cur/46:2, [new file with mode: 0644]
test/corpora/default/cur/47:2, [new file with mode: 0644]
test/corpora/default/cur/48:2, [new file with mode: 0644]
test/corpora/default/cur/49:2, [new file with mode: 0644]
test/corpora/default/cur/50:2, [new file with mode: 0644]
test/corpora/default/cur/51:2, [new file with mode: 0644]
test/corpora/default/cur/52:2, [new file with mode: 0644]
test/corpora/default/cur/53:2, [new file with mode: 0644]
test/corpora/default/foo/06:2, [new file with mode: 0644]
test/corpora/default/foo/baz/11:2, [new file with mode: 0644]
test/corpora/default/foo/baz/12:2, [new file with mode: 0644]
test/corpora/default/foo/baz/cur/13:2, [new file with mode: 0644]
test/corpora/default/foo/baz/cur/14:2, [new file with mode: 0644]
test/corpora/default/foo/baz/new/15:2, [new file with mode: 0644]
test/corpora/default/foo/baz/new/16:2, [new file with mode: 0644]
test/corpora/default/foo/cur/07:2, [new file with mode: 0644]
test/corpora/default/foo/cur/08:2, [new file with mode: 0644]
test/corpora/default/foo/new/03:2, [new file with mode: 0644]
test/corpora/default/foo/new/09:2, [new file with mode: 0644]
test/corpora/default/foo/new/10:2, [new file with mode: 0644]
test/corpora/default/new/04:2, [new file with mode: 0644]
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/html/attribute-text [new file with mode: 0644]
test/corpora/html/embedded-image [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/lkml/cur/1354585346.000260:2, [new file with mode: 0644]
test/corpora/lkml/cur/1354585346.000261:2, [new file with mode: 0644]
test/corpora/lkml/cur/1354585346.000265:2, [new file with mode: 0644]
test/corpora/lkml/cur/1354585346.000323:2, [new file with mode: 0644]
test/corpora/lkml/cur/1354585346.000324:2, [new file with mode: 0644]
test/corpora/lkml/cur/1354585346.000325:2, [new file with mode: 0644]
test/corpora/lkml/cur/1354585346.000539:2, [new file with mode: 0644]
test/corpora/lkml/cur/1354585346.000541:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001724:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001730:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001731:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001732:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001733:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001734:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001735:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001736:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001738:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001739:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001740:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001887:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001892:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.001970:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002189:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002193:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002194:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002195:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002196:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002197:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002201:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002228:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002878:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002912:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002915:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002917:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.002997:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.003106:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.003112:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.003117:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.003118:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.003171:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.003317:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.003318:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.003486:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.004581:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298587.004582:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001724:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001730:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001731:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001732:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001733:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001734:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001735:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001736:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001738:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001739:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001740:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001887:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.001892:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002189:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002191:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002193:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002194:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002195:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002196:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002197:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002201:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002878:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002879:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002911:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002912:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002915:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002917:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002930:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.002997:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.003106:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.003117:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.003118:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.003171:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.003317:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.003318:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.003486:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.003499:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.004581:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298770.004582:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298775.002830:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298775.002978:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298775.002992:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298775.002999:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298775.003976:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298775.004354:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298775.004363:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298775.004374:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002253:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002254:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002255:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002256:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002257:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002258:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002259:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002260:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002261:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002262:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002263:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002264:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002265:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002266:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002267:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002268:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002269:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002270:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002271:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002272:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002273:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002274:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002275:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002276:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002277:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002278:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002279:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002280:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002281:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002282:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002283:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002284:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002285:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002286:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002287:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002288:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002289:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002290:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002292:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002293:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002294:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002296:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002297:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002298:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002299:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002302:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002309:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002329:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002340:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002400:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002432:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002468:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002543:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002557:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002575:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002576:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002639:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002642:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002661:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002662:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002663:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002664:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002665:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002666:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002667:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002668:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002669:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002670:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002671:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002679:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002688:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.002699:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003013:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003145:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003148:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003216:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003231:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003278:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003295:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003316:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003334:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003340:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003448:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003459:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003462:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003468:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003471:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003472:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003478:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003497:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003501:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003503:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.003971:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.004059:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.004091:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298793.004190:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298795.000299:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298795.001362:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298795.002635:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298796.001941:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004526:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004551:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004613:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004614:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004615:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004617:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004618:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004619:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004636:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004638:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004639:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004640:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004642:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004653:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004665:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004680:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004688:2, [new file with mode: 0644]
test/corpora/lkml/cur/1382298805.004906:2, [new file with mode: 0644]
test/corpora/mangling/mixed-up.eml [new file with mode: 0644]
test/corpora/pkcs7/smime-onepart-signed.eml [new file with mode: 0644]
test/corpora/protected-headers/double-wrapped-with-phony-protected-header.eml [new file with mode: 0644]
test/corpora/protected-headers/encrypted-message-with-forwarded-attachment.eml [new file with mode: 0644]
test/corpora/protected-headers/encrypted-signed-not-masked.eml [new file with mode: 0644]
test/corpora/protected-headers/encrypted-signed.eml [new file with mode: 0644]
test/corpora/protected-headers/misplaced-protected-header.eml [new file with mode: 0644]
test/corpora/protected-headers/nested-rfc822-message.eml [new file with mode: 0644]
test/corpora/protected-headers/no-protected-header-attribute.eml [new file with mode: 0644]
test/corpora/protected-headers/phony-protected-header-bad-encryption.eml [new file with mode: 0644]
test/corpora/protected-headers/protected-header.eml [new file with mode: 0644]
test/corpora/protected-headers/protected-with-legacy-display.eml [new file with mode: 0644]
test/corpora/protected-headers/signed-protected-header.eml [new file with mode: 0644]
test/corpora/protected-headers/simple-signed-mail.eml [new file with mode: 0644]
test/corpora/protected-headers/smime-enc+legacy-disp.eml [new file with mode: 0644]
test/corpora/protected-headers/smime-multipart-signed.eml [new file with mode: 0644]
test/corpora/protected-headers/smime-onepart-signed.eml [new file with mode: 0644]
test/corpora/protected-headers/smime-sign+enc+legacy-disp.eml [new file with mode: 0644]
test/corpora/protected-headers/smime-sign+enc.eml [new file with mode: 0644]
test/corpora/protected-headers/subjectless-protected-header.eml [new file with mode: 0644]
test/corpora/protected-headers/wrapped-protected-header.eml [new file with mode: 0644]
test/corpora/threading/ghost-root/1529425589.M615261P21663.len:2,S [new file with mode: 0644]
test/corpora/threading/ghost-root/1532672447.R3166642290392477575.len:2,S [new file with mode: 0644]
test/corpora/threading/ghost-root/1532672447.R6968667928580738175.len:2,S [new file with mode: 0644]
test/corpora/threading/ghost-root/child [new file with mode: 0644]
test/corpora/threading/ghost-root/fake-root [new file with mode: 0644]
test/corpora/threading/ghost-root/grand-child [new file with mode: 0644]
test/corpora/threading/ghost-root/grand-child2 [new file with mode: 0644]
test/corpora/threading/ghost-root/great-grand-child [new file with mode: 0644]
test/corpora/threading/ghost-root/real-root [new file with mode: 0644]
test/corpora/threading/parent-priority/cur/child [new file with mode: 0644]
test/corpora/threading/parent-priority/cur/grand-child [new file with mode: 0644]
test/corpora/threading/parent-priority/cur/root [new file with mode: 0644]
test/database-test.c [new file with mode: 0644]
test/database-test.h [new file with mode: 0644]
test/emacs-address-cleaning.el [new file with mode: 0644]
test/emacs-attachment-warnings.el [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-decrypted-message [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-decrypted-message-no-crypto [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-elide-non-matching-messages-off [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-on [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-indent-thread-content-off [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-process-crypto-mime-parts-off [new file with mode: 0644]
test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-on [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-show.expected-output/notmuch-show-undecryptable-message [new file with mode: 0644]
test/emacs-tree.expected-output/inbox-outline [new file with mode: 0644]
test/emacs-tree.expected-output/notmuch-tree-show-window [new file with mode: 0644]
test/emacs-tree.expected-output/notmuch-tree-single-thread [new file with mode: 0644]
test/emacs-tree.expected-output/notmuch-tree-tag-inbox [new file with mode: 0644]
test/emacs-tree.expected-output/notmuch-tree-tag-inbox-tagged [new file with mode: 0644]
test/emacs-tree.expected-output/notmuch-tree-tag-inbox-thread-tagged [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/attachment [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-all-tags [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-empty-custom-queries-section [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-empty-custom-tags-section [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-long-names [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-new-section [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-no-saved-searches [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-section-counts [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-section-hidden-tag [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-section-with-empty [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-view-inbox [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-with-empty [new file with mode: 0644]
test/emacs.expected-output/notmuch-search-tag-inbox [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-message-with-headers-hidden [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-message-with-headers-visible [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-thread-maildir-storage [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-thread-maildir-storage-with-fourfold-indentation [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-thread-maildir-storage-without-indentation [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-thread-with-all-messages-collapsed [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-thread-with-all-messages-uncollapsed [new file with mode: 0644]
test/emacs.expected-output/notmuch-show-thread-with-hidden-messages [new file with mode: 0644]
test/emacs.expected-output/raw-message-cf0c4d-52ad0a [new file with mode: 0644]
test/emacs.expected-output/search-result-format-function [new file with mode: 0644]
test/export-dirs.sh [new file with mode: 0644]
test/gen-threads.py [new file with mode: 0644]
test/ghost-report.cc [new file with mode: 0644]
test/hex-xcode.c [new file with mode: 0644]
test/json_check_nodes.py [new file with mode: 0755]
test/make-db-version.cc [new file with mode: 0644]
test/message-id-parse.c [new file with mode: 0644]
test/notmuch-test [new file with mode: 0755]
test/notmuch-test.h [new file with mode: 0644]
test/openpgp4-secret-key.asc [new file with mode: 0644]
test/openpgp4-secret-key.asc.NOTE [new file with mode: 0644]
test/parse-time.c [new file with mode: 0644]
test/random-corpus.c [new file with mode: 0644]
test/setup.expected-output/config-with-comments [new file with mode: 0644]
test/smime/0xE0972A47.p12 [new file with mode: 0644]
test/smime/README [new file with mode: 0644]
test/smime/bob.p12 [new file with mode: 0644]
test/smime/ca.crt [new file with mode: 0644]
test/smime/key+cert.pem [new file with mode: 0644]
test/smime/test.crt [new file with mode: 0644]
test/smtp-dummy.c [new file with mode: 0644]
test/symbol-test.cc [new file with mode: 0644]
test/test-lib-FREEBSD.sh [new file with mode: 0644]
test/test-lib-common.sh [new file with mode: 0644]
test/test-lib-emacs.sh [new file with mode: 0644]
test/test-lib.el [new file with mode: 0644]
test/test-lib.sh [new file with mode: 0644]
test/test-vars.sh [new file with mode: 0644]
test/test-verbose [new file with mode: 0755]
test/test.expected-output/test-verbose-no [new file with mode: 0644]
test/test.expected-output/test-verbose-yes [new file with mode: 0644]
test/valgrind/suppressions [new file with mode: 0644]
test/valgrind/valgrind.sh [new file with mode: 0755]
util/Makefile [new file with mode: 0644]
util/Makefile.local [new file with mode: 0644]
util/crypto.c [new file with mode: 0644]
util/crypto.h [new file with mode: 0644]
util/error_util.c [new file with mode: 0644]
util/error_util.h [new file with mode: 0644]
util/gmime-extra.c [new file with mode: 0644]
util/gmime-extra.h [new file with mode: 0644]
util/hex-escape.c [new file with mode: 0644]
util/hex-escape.h [new file with mode: 0644]
util/path-util.c [new file with mode: 0644]
util/path-util.h [new file with mode: 0644]
util/repair.c [new file with mode: 0644]
util/repair.h [new file with mode: 0644]
util/string-util.c [new file with mode: 0644]
util/string-util.h [new file with mode: 0644]
util/talloc-extra.c [new file with mode: 0644]
util/talloc-extra.h [new file with mode: 0644]
util/unicode-util.c [new file with mode: 0644]
util/unicode-util.h [new file with mode: 0644]
util/util.c [new file with mode: 0644]
util/util.h [new file with mode: 0644]
util/xapian-extra.h [new file with mode: 0644]
util/xutil.c [new file with mode: 0644]
util/xutil.h [new file with mode: 0644]
util/zlib-extra.c [new file with mode: 0644]
util/zlib-extra.h [new file with mode: 0644]
version.txt [new file with mode: 0644]
vim/Makefile [new file with mode: 0644]
vim/README [new file with mode: 0644]
vim/notmuch.txt [new file with mode: 0644]
vim/notmuch.vim [new file with mode: 0644]
vim/notmuch.yaml [new file with mode: 0644]
vim/syntax/notmuch-compose.vim [new file with mode: 0644]
vim/syntax/notmuch-folders.vim [new file with mode: 0644]
vim/syntax/notmuch-git-diff.vim [new file with mode: 0644]
vim/syntax/notmuch-search.vim [new file with mode: 0644]
vim/syntax/notmuch-show.vim [new file with mode: 0644]

diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644 (file)
index 0000000..b3ddffe
--- /dev/null
@@ -0,0 +1,25 @@
+;; emacs local configuration settings for notmuch source
+;; surmised by dkg on 2010-11-23 13:43:18-0500
+;; amended by amdragon on 2011-06-06
+
+((c-mode
+  (indent-tabs-mode . t)
+  (tab-width . 8)
+  (c-basic-offset . 4)
+  (c-file-style . "linux"))
+ (c++-mode
+  (indent-tabs-mode . t)
+  (tab-width . 8)
+  (c-basic-offset . 4)
+  (c-file-style . "linux"))
+ (emacs-lisp-mode
+  (indent-tabs-mode . t)
+  (tab-width . 8))
+ (sh-mode
+  (indent-tabs-mode . t)
+  (tab-width . 8)
+  (sh-basic-offset . 4)
+  (sh-indentation . 4))
+ (nil
+  (fill-column . 70))
+ )
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..f94d148
--- /dev/null
@@ -0,0 +1,23 @@
+*.[ao]
+*.stamp
+*cscope*
+*~
+.*.swp
+/.deps
+/.first-build-message
+/.stamps
+/Makefile.config
+/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
diff --git a/.mailmap b/.mailmap
new file mode 100644 (file)
index 0000000..935c6eb
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,8 @@
+Peter Feigl <craven@gmx.net>
+Nate <nstraz@redhat.com>
+Ali Polatel <alip@penguen.ev>
+Stefan <aeuii@posteo.de>
+Patrick Totzke <patricktotzke@googlemail.com>
+Patrick Totzke <patricktotzke@gmail.com>
+Patrick Totzke <p.totzke@ed.ac.uk>
+Mark Walters <markwalters1009@gmail.com>
diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..5bb03de
--- /dev/null
@@ -0,0 +1,29 @@
+language: c
+
+dist: bionic
+
+addons:
+  apt:
+    sources:
+    - sourceline: 'ppa:xapian-backports/ppa'
+    packages:
+    - dtach
+    - libxapian-dev
+    - libgmime-3.0-dev
+    - libtalloc-dev
+    - python3-sphinx
+    - python3-cffi
+    - python3-pytest
+    - python3-setuptools
+    - libpython3-all-dev
+    - gpgsm
+
+script:
+  - ./configure
+  - make test
+
+notifications:
+  irc:
+    channels:
+      - "irc.libera.chat#notmuch"
+    on_success: change
diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..6e87208
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,129 @@
+Carl Worth <cworth@cworth.org> was the original author of Notmuch.
+David Bremner has maintained Notmuch since release 0.6 (2011).  But
+there's really not much that they've done. There's been a lot of
+standing on shoulders here:
+
+William Morgan deserves credit for providing the primary inspiration
+for Notmuch with his program Sup (https://sup-heliotrope.github.io/).
+
+Some people have contributed code that has made it into Notmuch
+without their specific knowledge (but with their full permission
+thanks to the GNU General Public License). This includes:
+
+Brian Gladman (with Mikhail Gusarov <dottedmag@dottedmag.net>)
+       Implementation of SHA-1 (nice and small) (libsha1.c)
+
+Please see the various files in the Notmuch distribution for
+individual copyright statements.
+
+And of course, though their code isn't distributed here, Notmuch would
+be not much of anything without the contributors to Xapian, the search
+engine that does the really heavy lifting, as well as the various
+system libraries, compilers, and the kernel that make it all work
+(thanks GNU, thanks Linux). Thanks to everyone who has played a part!
+
+The following list of people have at least 15 lines of code in the
+Notmuch 0.31 release (calculated by devel/author-scan.sh).
+
+ David Bremner
+ Carl Worth
+ Jani Nikula
+ Austin Clements
+ Daniel Kahn Gillmor
+ Mark Walters
+ Floris Bruynooghe
+ David Edmondson
+ Tomi Ollila
+ Sebastian Spaeth
+ Ali Polatel
+ Michal Sojka
+ Justus Winter
+ Sebastien Binet
+ W. Trevor King
+ Jameson Graef Rollins
+ Felipe Contreras
+ Jonas Bernoulli
+ Pieter Praet
+ Peter Feigl
+ Dmitry Kurochkin
+ Peter Wang
+ Gregor Zattler
+ Daniel Schoepe
+ Keith Packard
+ Adam Wolfe Gordon
+ Stefano Zacchiroli
+ Vincent Breitmoser
+ laochailan
+ Ben Gamari
+ Aaron Ecay
+ l-m-h@web.de
+ Thomas Jost
+ Jesse Rosenthal
+ Dirk Hohndel
+ Blake Jones
+ Damien Cassou
+ Anton Khirnov
+ Matt Armstrong
+ Vladimir Panteleev
+ William Casarin
+ Örjan Ekeberg
+ Jan Janak
+ Patrick Totzke
+ Ruben Pollan
+ rhn
+ Ioan-Adrian Ratiu
+ Ethan Glasser-Camp
+ Chunyang Xu
+ Todd
+ Chris Wilson
+ Yuri Volchkov
+ Cédric Cabessa
+ Mark Anderson
+ Jed Brown
+ Maxime Coste
+ Ludovic LANGE
+ Sebastian Poeplau
+ Mikhail
+ Keith Amidon
+ Gaute Hope
+ martin f. krafft
+ Jeffrey C. Ollie
+ Jameson Rollins
+ Scott Henson
+ Bart Trojanowski
+ Vladimir Marek
+ Servilio Afre Puentes
+ Tomas Carnecky
+ Kevin McCarthy
+ Kevin J. McCarthy
+ Scott Robinson
+ Wael M. Nasreddine
+ Charles Celerier
+ Olly Betts
+ Istvan Marko
+ Florian Klink
+ Thibaut Horel
+ Joel Borggrén-Franck
+ Ingmar Vanhassel
+ Olivier Taïbi
+ Ian Main
+ Alexander Botero-Lowry
+ Luis Ressel
+ Sergei Shilovsky
+ Trevor Jim
+ Uli Scholler
+ Matthew Lear
+ Jinwoo Lee
+ Amadeusz Żołnowski
+
+Here is an incomplete list of other people that have made
+contributions to Notmuch (whether by code, bug reporting/fixes,
+ideas, inspiration, testing or feedback):
+
+ Martin Krafft
+ Jamey Sharp
+
+The Notmuch project acknowledges the contributions of the following
+organizations via their employees
+
+ Google LLC
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..4e744d2
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,15 @@
+Notmuch is free software.
+
+You can redistribute it and/or modify it under the terms of the GNU
+General Public License as published by the Free Software Foundation,
+either version 3 of the License, or (at your option) any later
+version.
+
+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, (in the COPYING-GPL-3 file in this
+directory). If not, see https://www.gnu.org/licenses/
diff --git a/COPYING-GPL-3 b/COPYING-GPL-3
new file mode 100644 (file)
index 0000000..4c49354
--- /dev/null
@@ -0,0 +1,676 @@
+
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                      TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+  
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/philosophy/why-not-lgpl.html>.
+
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..8054faf
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,106 @@
+Build and install instructions for Notmuch.
+
+Compilation commands
+--------------------
+The process for compiling and installing Notmuch is the very standard
+sequence of:
+
+       ./configure
+       make
+       sudo make install
+
+In fact, if you don't plan to pass any arguments to the configure
+script, then you can skip that step and just start with "make", (which
+will call configure for you). See this command:
+
+       ./configure --help
+
+for detailed documentation of the things you can control at the
+configure stage.
+
+Dependencies
+------------
+Notmuch depends on four libraries: Xapian, GMime 3.0,
+Talloc, and zlib which are each described below:
+
+       Xapian
+       ------
+       Xapian is the search-engine library underlying Notmuch.
+
+       It provides all the real machinery of indexing and searching,
+       (including the very nice parsing of the query string).
+
+       Xapian is available from https://xapian.org
+
+       GMime
+       -----
+       GMime provides decoding of MIME email messages for Notmuch.
+
+       Without GMime, Notmuch would not be able to extract and index
+       the actual text from email message encoded as BASE64, etc.
+
+       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.
+
+       Talloc is an extremely lightweight and easy-to-use tool for
+       allocating memory in a hierarchical fashion and then freeing
+       it with a single call of the top-level handle. Using it has
+       made development of Notmuch much easier and much less prone to
+       memory leaks.
+
+       Talloc is available from https://talloc.samba.org/
+
+       zlib
+       ----
+
+       zlib is an extremely popular compression library. It is used
+       by Xapian, so if you installed that you will already have
+       zlib. You may need to install the zlib headers separately.
+
+       Notmuch needs the transparent write feature of zlib introduced
+       in version 1.2.5.2 (Dec. 2011).
+
+       zlib is available from https://zlib.net
+
+Building Documentation
+----------------------
+
+To build the documentation for notmuch you need at least version 1.0
+of sphinx (Jul. 2010).
+
+Sphinx is available from www.sphinx-doc.org.
+
+To install the documentation as "info" pages, you will need the
+additional tools makeinfo and install-info.
+
+Installing Dependencies from Packages
+-------------------------------------
+
+On a modern, package-based operating system you can install all of the
+dependencies with a single simple command line. For example:
+
+  For Debian and similar:
+
+        sudo apt-get install libxapian-dev libgmime-3.0-dev libtalloc-dev zlib1g-dev python3-sphinx texinfo install-info
+
+  For Fedora and similar:
+
+       sudo dnf install xapian-core-devel gmime30-devel libtalloc-devel zlib-devel python3-sphinx texinfo info
+
+On other systems, a similar command can be used, but the details of
+the package names may be different.
+
+       
+
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..d2010fe
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,71 @@
+# We want the all target to be the implicit target (if no target is
+# given explicitly on the command line) so mention it first.
+all:
+
+# Sub-directory Makefile.local fragments can append to these variables
+# to have directory-specific cflags as necessary.
+
+extra_cflags :=
+extra_cxxflags :=
+
+# Get settings from the output of configure by running it to generate
+# Makefile.config if it doesn't exist yet.
+
+# If Makefile.config doesn't exist, then srcdir won't be
+# set. Conditionally set it (assuming a plain srcdir build) so that
+# the rule to generate Makefile.config can actually work.
+srcdir ?= .
+
+include Makefile.config
+
+# We make all targets depend on the Makefiles themselves.
+global_deps = Makefile Makefile.config Makefile.local \
+       $(subdirs:%=%/Makefile) $(subdirs:%=%/Makefile.local)
+
+INCLUDE_MORE := yes
+ifneq ($(filter clean distclean dataclean, $(word 1, $(MAKECMDGOALS))),)
+CLEAN_GOAL := $(word 1, $(MAKECMDGOALS))
+
+# If there are more goals following CLEAN_GOAL, run $(MAKE)s in parts.
+ifneq ($(word 2, $(MAKECMDGOALS)),)
+INCLUDE_MORE := no
+FOLLOWING_GOALS := $(wordlist 2, 99, $(MAKECMDGOALS))
+
+.PHONY: $(FOLLOWING_GOALS) make_in_parts
+$(FOLLOWING_GOALS):
+       @true
+$(CLEAN_GOAL): make_in_parts
+make_in_parts:
+       $(MAKE) $(CLEAN_GOAL)
+       $(MAKE) $(FOLLOWING_GOALS) configure_options="$(configure_options)"
+endif
+
+else
+CLEAN_GOAL :=
+endif
+
+# Potentially speedup make clean, distclean and dataclean ; avoid
+# re-creating Makefile.config if it exists but configure is newer.
+ifneq ($(CLEAN_GOAL),)
+Makefile.config: | $(srcdir)/configure
+else
+Makefile.config: $(srcdir)/configure
+endif
+ifeq ($(configure_options),)
+       @echo ""
+       @echo "Note: Calling ./configure with no command-line arguments. This is often fine,"
+       @echo "      but if you want to specify any arguments (such as an alternate prefix"
+       @echo "      into which to install), call ./configure explicitly and then make again."
+       @echo "      See \"./configure --help\" for more details."
+       @echo ""
+endif
+       $(srcdir)/configure $(configure_options)
+
+ifeq ($(INCLUDE_MORE),yes)
+# runtime variable definitions available in all subdirs
+include $(srcdir)/Makefile.global
+# Finally, include all of the Makefile.local fragments where all the
+# real work is done.
+
+include $(subdirs:%=%/Makefile.local) Makefile.local
+endif
diff --git a/Makefile.global b/Makefile.global
new file mode 100644 (file)
index 0000000..7a7a3c6
--- /dev/null
@@ -0,0 +1,65 @@
+# -*- makefile-gmake -*-
+# Here's the (hopefully simple) versioning scheme.
+#
+# Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We
+# increment the second digit for each release and increment the first
+# digit when we reach particularly major milestones of usability.
+#
+# Between releases, (such as when compiling notmuch from the git
+# repository), we let git append identification of the actual commit.
+PACKAGE=notmuch
+
+IS_GIT:=$(if $(wildcard ${srcdir}/.git),yes,no)
+
+ifeq ($(IS_GIT),yes)
+DATE:=$(shell git --git-dir=${srcdir}/.git log --date=short -1 --pretty=format:%cd)
+else
+DATE:=$(shell date +%F)
+endif
+
+VERSION:=$(shell cat ${srcdir}/version.txt)
+ELPA_VERSION:=$(subst ~,_,$(VERSION))
+ifeq ($(filter release release-message pre-release update-versions,$(MAKECMDGOALS)),)
+ifeq ($(IS_GIT),yes)
+VERSION:=$(shell git --git-dir=${srcdir}/.git describe --abbrev=7 --match '[0-9.]*'|sed -e s/_/~/ -e s/-/+/ -e s/-/~/)
+# drop the ~g$sha1 part
+ELPA_VERSION:=$(word 1,$(subst ~, ,$(VERSION)))
+# convert git version to package.el friendly form
+ELPA_VERSION:=$(subst +,snapshot,$(ELPA_VERSION))
+
+# Write the file 'version.stamp' in case its contents differ from $(VERSION)
+FILE_VERSION:=$(shell test -f version.stamp && read vs < version.stamp || vs=; echo $$vs)
+ifneq ($(FILE_VERSION),$(VERSION))
+       $(shell echo "$(VERSION)" > version.stamp)
+endif
+endif
+endif
+
+UPSTREAM_TAG=$(subst ~,_,$(VERSION))
+
+RELEASE_HOST=notmuchmail.org
+RELEASE_DIR=/srv/notmuchmail.org/www/releases
+DOC_DIR=/srv/notmuchmail.org/www/doc/latest
+RELEASE_URL=https://notmuchmail.org/releases
+TAR_FILE=$(PACKAGE)-$(VERSION).tar.xz
+ELPA_FILE:=$(PACKAGE)-emacs-$(ELPA_VERSION).tar
+DEB_TAR_FILE=$(PACKAGE)_$(VERSION).orig.tar.xz
+SHA256_FILE=$(TAR_FILE).sha256.asc
+DETACHED_SIG_FILE=$(TAR_FILE).asc
+
+PV_FILE=bindings/python/notmuch/version.py
+
+# Smash together user's values with our extra values
+FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(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
+FINAL_NOTMUCH_LDFLAGS += $(AS_NEEDED_LDFLAGS) $(GMIME_LDFLAGS) $(TALLOC_LDFLAGS) $(ZLIB_LDFLAGS)
+FINAL_NOTMUCH_LINKER = CC
+ifneq ($(LINKER_RESOLVES_LIBRARY_DEPENDENCIES),1)
+FINAL_NOTMUCH_LDFLAGS += $(CONFIGURE_LDFLAGS)
+FINAL_NOTMUCH_LINKER = CXX
+endif
+FINAL_LIBNOTMUCH_LDFLAGS = $(LDFLAGS) $(AS_NEEDED_LDFLAGS) $(CONFIGURE_LDFLAGS)
diff --git a/Makefile.local b/Makefile.local
new file mode 100644 (file)
index 0000000..7699c20
--- /dev/null
@@ -0,0 +1,324 @@
+# -*- makefile-gmake -*-
+
+.PHONY: all
+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
+       @echo ""
+       @echo "Compilation of notmuch is now complete. You can install notmuch with:"
+       @echo ""
+       @echo " make install"
+       @echo ""
+       @echo "Note that depending on the prefix to which you are installing"
+       @echo "you may need root permission (such as \"sudo make install\")."
+       @echo "See \"./configure --help\" for help on setting an alternate prefix."
+       @echo Printed > .first-build-message
+endif
+endif
+
+# Depend (also) on the file 'version'. In case of ifeq ($(IS_GIT),yes)
+# this file may already have been updated.
+version.stamp: $(srcdir)/version.txt
+       echo $(VERSION) > $@
+
+$(TAR_FILE):
+       if git tag -v $(UPSTREAM_TAG) >/dev/null 2>&1; then \
+           ref=$(UPSTREAM_TAG); \
+        else \
+           ref="HEAD" ; \
+          echo "Warning: No signed tag for $(VERSION)"; \
+       fi ; \
+       git archive --format=tar --prefix=$(PACKAGE)-$(VERSION)/ $$ref > $(TAR_FILE).tmp
+       echo $(VERSION) > version.txt.tmp
+       ct=`git --no-pager log -1 --pretty=format:%ct $$ref` ; \
+       tar --owner root --group root --append -f $(TAR_FILE).tmp \
+               --transform s_^_$(PACKAGE)-$(VERSION)/_  \
+               --transform 's_.tmp$$__' --mtime=@$$ct version.txt.tmp
+       rm version.txt.tmp
+       xz -C sha256 -9 < $(TAR_FILE).tmp > $(TAR_FILE)
+       @echo "Source is ready for release in $(TAR_FILE)"
+
+$(SHA256_FILE): $(TAR_FILE)
+       sha256sum $^ | gpg --clear-sign --output $@ -
+
+$(DETACHED_SIG_FILE): $(TAR_FILE)
+       gpg --armor --detach-sign $^
+
+CLEAN := $(CLEAN) notmuch-git
+notmuch-git: notmuch-git.py
+       cp $< $@
+       chmod ugo+x $@
+
+CLEAN := $(CLEAN) nmbug
+nmbug: notmuch-git
+       ln -s $< $@
+
+.PHONY: dist
+dist: $(TAR_FILE)
+
+.PHONY: update-versions
+
+update-versions:
+       sed -i -e "s/^__VERSION__[[:blank:]]*=.*$$/__VERSION__ = \'${VERSION}\'/" \
+           -e "s/^SOVERSION[[:blank:]]*=.*$$/SOVERSION = \'${LIBNOTMUCH_VERSION_MAJOR}\'/" \
+           ${PV_FILE}
+
+# We invoke make recursively only to force ordering of our phony
+# targets in the case of parallel invocation of make (-j).
+#
+# We carefully ensure that our VERSION variable is passed down to any
+# sub-ordinate make invocations (which won't otherwise know that they
+# are part of the release and need to take the version from the
+# version file).
+.PHONY: release
+release: verify-source-tree-and-version
+       $(MAKE) VERSION=$(VERSION) verify-newer
+       $(MAKE) VERSION=$(VERSION) clean
+       $(MAKE) VERSION=$(VERSION) sphinx-html
+       $(MAKE) VERSION=$(VERSION) test
+       git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG)
+       $(MAKE) VERSION=$(VERSION) $(SHA256_FILE) $(DETACHED_SIG_FILE)
+       ln -sf $(TAR_FILE) $(DEB_TAR_FILE)
+       pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG)
+       mkdir -p 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
+       cd releases && scp $(TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) $(RELEASE_HOST):$(RELEASE_DIR)
+       ssh $(RELEASE_HOST) "rm -f $(RELEASE_DIR)/LATEST-$(PACKAGE)-* ; ln -s $(TAR_FILE) $(RELEASE_DIR)/LATEST-$(TAR_FILE)"
+       rsync --verbose --delete --recursive doc/_build/html/ $(RELEASE_HOST):$(DOC_DIR)
+endif
+       @echo "Please send a release announcement using $(PACKAGE)-$(VERSION).announce as a template."
+
+.PHONY: pre-release
+pre-release:
+       $(MAKE) VERSION=$(VERSION) clean
+       $(MAKE) VERSION=$(VERSION) test
+       git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG)
+       $(MAKE) VERSION=$(VERSION) $(SHA256_FILE) $(DETACHED_SIG_FILE)
+       ln -sf $(TAR_FILE) $(DEB_TAR_FILE)
+       pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG)
+       mkdir -p 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:
+       make VERSION=$(VERSION) clean
+       RETVAL=0 &&                                             \
+         TMPFILE=$$(mktemp /tmp/notmuch.XXXXXX) &&             \
+         cp debian/changelog $${TMPFILE} &&                    \
+         (EDITOR=/bin/true dch -b -v $(VERSION)+1              \
+           -D UNRELEASED 'test build, not for upload' &&       \
+         echo '3.0 (native)' > debian/source/format &&         \
+         debuild -us -uc); RETVAL=$$?                          \
+         mv -f $${TMPFILE} debian/changelog;                   \
+         echo '3.0 (quilt)' > debian/source/format;            \
+         exit $$RETVAL
+
+.PHONY: release-message
+release-message:
+       @echo "To: notmuch@notmuchmail.org"
+       @echo "Subject: $(PACKAGE) release $(VERSION) now available"
+       @echo ""
+       @echo "Where to obtain notmuch $(VERSION)"
+       @echo "==========================="
+       @echo "  $(RELEASE_URL)/$(TAR_FILE)"
+       @echo ""
+       @echo "Which can be verified with:"
+       @echo ""
+       @echo "  $(RELEASE_URL)/$(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`)"
+       @echo ""
+       @echo "What's new in notmuch $(VERSION)"
+       @echo "========================="
+       @sed -ne '/^[Nn]otmuch $(VERSION)/{n;n;b NEWS}; d; :NEWS /^===/q; {p;n;b NEWS}' < NEWS | head -n -2
+       @echo ""
+       @echo "What is notmuch"
+       @echo "==============="
+       @echo "Notmuch is a system for indexing, searching, reading, and tagging"
+       @echo "large collections of email messages in maildir or mh format. It uses"
+       @echo "the Xapian library to provide fast, full-text search with a convenient"
+       @echo "search syntax."
+       @echo ""
+       @echo "For more about notmuch, see https://notmuchmail.org"
+
+# This is a chain of dependencies rather than a simple list simply to
+# avoid the messages getting interleaved in the case of a parallel
+# make invocation.
+.PHONY: verify-source-tree-and-version
+verify-source-tree-and-version: verify-no-dirty-code
+
+.PHONY: verify-no-dirty-code
+verify-no-dirty-code: release-checks
+ifeq ($(IS_GIT),yes)
+       @printf "Checking that source tree is clean..."
+ifneq ($(shell git --git-dir=${srcdir}/.git ls-files -m),)
+       @echo "No"
+       @echo "The following files have been modified since the most recent git commit:"
+       @echo ""
+       @git --git-dir=${srcdir}/.git ls-files -m
+       @echo ""
+       @echo "The release will be made from the committed state, but perhaps you meant"
+       @echo "to commit this code first? Please clean this up to make it more clear."
+       @false
+else
+       @echo "Good"
+endif
+endif
+
+.PHONY: release-checks
+release-checks:
+       devel/release-checks.sh
+
+.PHONY: verify-newer
+verify-newer:
+       @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." ;; \
+          0) echo "Ouch."; \
+            echo "Found: $(RELEASE_URL)/$(TAR_FILE)"; \
+            echo "Refusing to replace an existing release."; \
+            echo "Don't forget to update \"version\" as described in RELEASING before release." ; \
+            false ;; \
+         *) echo "An unexpected error occurred"; \
+            false;; esac
+
+# The user has not set any verbosity, default to quiet mode and inform the
+# user how to enable verbose compiles.
+ifeq ($(V),)
+quiet_DOC := "Use \"$(MAKE) V=1\" to see the verbose compile lines.\n"
+quiet = @printf $(quiet_DOC)$(eval quiet_DOC:=)"$(1) $(or $(2),$@)\n"; $($(word 1, $(1)))
+endif
+# The user has explicitly enabled quiet compilation.
+ifeq ($(V),0)
+quiet = @printf "$(1) $(or $(2),$@)\n"; $($(word 1, $(1)))
+endif
+# Otherwise, print the full command line.
+quiet ?= $($(word 1, $(1)))
+
+%.o: %.cc $(global_deps)
+       @mkdir -p $(patsubst %/.,%,.deps/$(@D))
+       $(call quiet,CXX $(CPPFLAGS) $(CXXFLAGS)) -c $(FINAL_CXXFLAGS) $< -o $@ -MD -MP -MF .deps/$*.d
+
+%.o: %.c $(global_deps)
+       @mkdir -p $(patsubst %/.,%,.deps/$(@D))
+       $(call quiet,CC $(CPPFLAGS) $(CFLAGS)) -c $(FINAL_CFLAGS) $< -o $@ -MD -MP -MF .deps/$*.d
+
+CPPCHECK=cppcheck
+.stamps/cppcheck/%: %
+       @mkdir -p $(@D)
+       $(call quiet,CPPCHECK,$<) --template=gcc --error-exitcode=1 --quiet $<
+       @touch $@
+
+CLEAN := $(CLEAN) .stamps
+
+.PHONY : clean
+clean:
+       rm -rf $(CLEAN)
+
+.PHONY: distclean
+distclean: clean
+       rm -rf $(DISTCLEAN)
+
+.PHONY: dataclean
+dataclean: distclean
+       rm -rf $(DATACLEAN)
+
+notmuch_client_srcs =          \
+       $(notmuch_compat_srcs)  \
+       command-line-arguments.c\
+       debugger.c              \
+       status.c                \
+       gmime-filter-reply.c    \
+       hooks.c                 \
+       notmuch.c               \
+       notmuch-client-init.c   \
+       notmuch-compact.c       \
+       notmuch-config.c        \
+       notmuch-count.c         \
+       notmuch-dump.c          \
+       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-time.c          \
+       sprinter-json.c         \
+       sprinter-sexp.c         \
+       sprinter-text.c         \
+       query-string.c          \
+       mime-node.c             \
+       tag-util.c
+
+notmuch_client_modules = $(notmuch_client_srcs:.c=.o)
+
+notmuch.o: version.stamp
+
+notmuch: $(notmuch_client_modules) lib/libnotmuch.a util/libnotmuch_util.a parse-time-string/libparse-time-string.a
+       $(call quiet,CXX $(CFLAGS)) $^ $(FINAL_LIBNOTMUCH_LDFLAGS) -o $@
+
+notmuch-shared: $(notmuch_client_modules) lib/$(LINKER_NAME)
+       $(call quiet,$(FINAL_NOTMUCH_LINKER) $(CFLAGS)) $(notmuch_client_modules) $(FINAL_NOTMUCH_LDFLAGS) -o $@
+
+.PHONY: install
+install: all install-man install-info
+       mkdir -p "$(DESTDIR)$(prefix)/bin/"
+       install notmuch-shared "$(DESTDIR)$(prefix)/bin/notmuch"
+ifeq ($(MAKECMDGOALS), install)
+       @echo ""
+       @echo "Notmuch is now installed to $(DESTDIR)$(prefix)"
+       @echo ""
+       @echo "New users should simply run \"notmuch\" to be guided"
+       @echo "through the process of configuring notmuch and creating"
+       @echo "a database of existing email messages. The \"notmuch\""
+       @echo "command will also offer some sample search commands."
+ifeq ($(WITH_EMACS), 1)
+       @echo ""
+       @echo "Beyond the command-line interface, notmuch also offers"
+       @echo "a full-featured interface for reading and writing mail"
+       @echo "within emacs. To use this, each user should add the"
+       @echo "following line to the ~/.emacs file:"
+       @echo ""
+       @echo " (require 'notmuch)"
+       @echo ""
+       @echo "And then run emacs as \"emacs -f notmuch\" or invoke"
+       @echo "the command \"M-x notmuch\" from within emacs."
+endif
+endif
+
+SRCS  := $(SRCS) $(notmuch_client_srcs)
+CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules)
+CLEAN := $(CLEAN) version.stamp notmuch-*.tar.gz.tmp
+CLEAN := $(CLEAN) .deps
+
+DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config sh.config sphinx.config
+
+CPPCHECK_STAMPS := $(SRCS:%=.stamps/cppcheck/%)
+.PHONY: cppcheck
+ifeq ($(HAVE_CPPCHECK),1)
+cppcheck: ${CPPCHECK_STAMPS}
+else
+cppcheck:
+       @echo "No cppcheck found during configure; skipping static checking"
+endif
+
+
+DEPS := $(SRCS:%.c=.deps/%.d)
+DEPS := $(DEPS:%.cc=.deps/%.d)
+-include $(DEPS)
+
+.SUFFIXES: # Delete the default suffixes. Old-Fashioned Suffix Rules not used.
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..315f413
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,5120 @@
+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
+appropropriate 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 documention 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 punctation 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 formating 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)
+===========================
+
+Library
+-------
+
+Fix include bug triggered by glib 2.67.
+
+Test
+----
+
+Fix race condition in T568-lib-thread.
+
+Notmuch 0.31.3 (2020-12-25)
+===========================
+
+Bindings
+--------
+
+Fix for exclude tags in notmuch2 bindings.
+
+Build
+-----
+
+Portability update for T360-symbol-hiding.
+
+Library
+-------
+
+Fix for memory error in notmuch_database_get_config_list.
+
+Notmuch 0.31.2 (2020-11-08)
+===========================
+
+Build
+-----
+
+Catch one more occurence of "version" in the build system, which
+caused the file to be regenerated in the release tarball.
+
+Notmuch 0.31.1 (2020-11-08)
+===========================
+
+Library
+-------
+
+Fix a memory initialization bug in notmuch_database_get_config_list.
+
+Build
+-----
+
+Rename file 'version' to 'version.txt'. The old file name conflicted
+with a C++ header for some compilers.
+
+Replace use of coreutils `realpath` in configure.
+
+Notmuch 0.31 (2020-09-05)
+=========================
+
+Emacs
+-----
+
+Notmuch now supports Emacs 27.1. You may need to set
+`mml-secure-openpgp-sign-with-sender` and/or
+`mml-secure-smime-sign-with-sender` to continue signing messages.
+
+The minimum supported major version of GNU Emacs is now 25.1.
+
+Add support for moving between threads after notmuch-tree-from-search-thread.
+
+New `notmuch-unthreaded` mode (added in Notmuch 0.30)
+
+  Unthreaded view is a mode where each matching message is shown on a
+  separate line.
+
+  The main key entries to unthreaded view are
+
+  'u' enter a query to view in unthreaded mode (works in hello,
+      search, show and tree mode)
+
+  'U' view the current query in unthreaded mode (works from search,
+      show and tree)
+
+  Saved searches can also specify that they should open in unthreaded
+  view.
+
+  Currently it is not possible to specify the sort order: it will
+  always be newest first.
+
+Notmuch-Mutt
+------------
+
+The shell pipeline executed by notmuch-mutt, which symlinked matched
+files to a maildir for mutt to access is replaced with internal perl
+processing. This search operation is now more portable, and somewhat
+faster.
+
+Library
+-------
+
+Improve exception handling in the library. This should
+largely eliminate terminations inside the library due to uncaught
+exceptions or internal errors.  No doubt there are a few uncovered
+code paths still; please report them as bugs.
+
+Add `notmuch_message_get_flag_st` and
+`notmuch_message_has_maildir_flag_st`, and deprecate the existing
+non-status providing versions.
+
+Move memory de-allocation from `notmuch_database_close` to
+`notmuch_database_destroy`.
+
+Handle relative filenames in `notmuch_database_index_file`, as
+promised in the documentation.
+
+Python Bindings
+---------------
+
+Documentation for the python bindings is merged into the main
+sphinx-doc documentation tree. The merged documentation can be built
+with e.g. `make sphinx-html`
+
+Dependencies
+------------
+
+We now support building notmuch against Xapian 1.5 (the current
+development version).
+
+Test Suite
+----------
+
+Test suite fixes for compatibility with Emacs 27.1.
+
+Build System
+------------
+
+Man pages are now compressed reproducibly.
+
+Notmuch 0.30 (2020-07-10)
+=========================
+
+S/MIME
+------
+
+Handle S/MIME (PKCS#7) messages -- one-part signed messages, encrypted
+messages, and multilayer messages. Treat them symmetrically to
+OpenPGP messages. This includes handling protected headers
+gracefully.
+
+If you're using Notmuch with S/MIME, you currently need to configure
+gpgsm appropriately.
+
+Mixed-up MIME Repair
+--------------------
+
+Detect and automatically repair a common form of message mangling
+created by Microsoft Exchange (see index.repaired=mixedup in
+notmuch-properties(7)).
+
+Protected Headers
+-----------------
+
+Avoid indexing the legacy-display part of an encrypted message that
+has protected headers (see
+index.repaired=skip-protected-headers-legacy-display in
+notmuch-properties(7)).
+
+Python
+------
+
+Drop support for python2, focus on python3.
+
+Introduce new CFFI-based python bindings in the python module named
+"notmuch2".  Officially deprecate (but still support) the older
+"notmuch" module.
+
+Dependencies
+------------
+
+Support for Xapian 1.2 is removed. The minimum supported version of
+Xapian is now 1.4.0.
+
+Notmuch 0.29.3 (2019-11-27)
+===========================
+
+General
+-------
+
+Fix for use-after-free in notmuch_config_list_{key,val}.
+
+Fix for double close of file in notmuch-dump.
+
+Debian
+------
+
+Drop python2 support from shipped debian packaging.
+
+Notmuch 0.29.2 (2019-10-19)
+===========================
+
+General
+-------
+
+Fix for file descriptor leak when opening gzipped mail files. Thanks
+to James Troup for the bug report and the fix.
+
+Notmuch 0.29.1 (2019-06-11)
+===========================
+
+Build
+-----
+
+Fix for installation failure with `configure --without-emacs`.
+
+Notmuch 0.29 (2019-06-07)
+=========================
+
+General
+-------
+
+Add "body:" field to allow searching for terms that occur only in the
+message body. Users will need to reindex their mail to take advantage
+of this feature.
+
+Add support for indexing user specified headers (e.g. List-Id). See
+notmuch-config(1) for details. This requires reindexing after changing
+the set of headers to be indexed.
+
+Fix bug for searching in some headers for Xapian keywords in quoted
+strings.
+
+Add support for gzip compressed mail messages (/not/ multi-message
+mboxes); e.g. `gzip -9 $MAIL/archive/giant-message && notmuch new`
+should work. Note that maildir flag syncing for gzipped messages is
+currently untested.
+
+Notmuch is now capable of indexing, searching and rendering
+cryptographically-protected Subject: headers of the form produced by
+Enigmail and K-9 mail in encrypted messages.
+
+Command Line Interface
+----------------------
+
+`notmuch show` now supports --body=false and --include-html with
+--format=text.
+
+Fix several performance problems with `notmuch reindex`.
+
+`notmuch show` and `notmuch reply` now emit per-message cryptographic
+status in their json and sexp output formats.  See devel/schemata for
+more details about what is included there.  This status includes
+information about cryptographic protections for the Subject header.
+
+Emacs
+-----
+
+Optionally check for missing attachments in outgoing messages (see
+function `notmuch-mua-attachment-check`).
+
+Bind `B` to browse URLs in current message.
+
+Bind `g` to refresh the current notmuch buffer.
+
+Editing a message as new now includes an FCC header.
+
+Forwarded messages are now tagged as +forwarded (customizable).
+
+Add references header to link forwarded message to thread of original
+message.
+
+The minimum supported major version of Emacs is now 24.
+
+Support for GNU Emacs older than 25.1 is deprecated with this release,
+and may be removed in a future release.
+
+Notmuch-emacs documentation is somewhat expanded. More contributions
+are very welcome.
+
+Build System
+------------
+
+Notmuch release tarballs are now compressed with `xz`.
+
+We now provide conventional detached signatures of the release
+tarballs in addition to the signed `sha256sum` files.
+
+Dependencies
+------------
+
+Support for GMime 2.6 is removed. The minimum supported version of
+GMime is now 3.0.3.  GMime also needs to have been compiled with
+cryptography support.
+
+Test Suite
+----------
+
+If either GNU parallel or moreutils parallel is installed, the tests
+in the test suite will now be run in parallel (one per available
+core).  This can be disabled with NOTMUCH_TEST_SERIALIZE=1.
+
+Notmuch 0.28.4 (2019-05-05)
+===========================
+
+Command line interface
+----------------------
+
+Fix a spurious error when using `notmuch show --raw` on messages whose
+size is a multiple of the internal buffer size.
+
+Notmuch 0.28.3 (2019-03-05)
+===========================
+
+Library
+-------
+
+Fix a bug with the internal data structure _notmuch_string_map_t used
+by message properties.
+
+Build System
+------------
+
+Serialize calls to sphinx-build to avoid race condition.
+
+Notmuch 0.28.2 (2019-02-17)
+===========================
+
+Emacs
+-----
+
+Invoke gpg with --batch and --no-tty.
+
+Python Bindings
+---------------
+
+Fix documentation build with Python 3.7. Note that Python >= 3.3 is
+now needed to build this documentation.
+
+Notmuch 0.28.1 (2019-02-01)
+===========================
+
+Build System
+------------
+
+`configure` no longer uses the special variable BASH, as this causes
+problems on systems where /bin/sh is bash.
+
+Notmuch 0.28 (2018-10-12)
+=========================
+
+General
+-------
+
+Improve threading
+
+  The threading algorithm has been updated to consider all references,
+  not just the heuristically chosen parent (e.g. when that parent is
+  not in the database). The heuristic for choosing a parent message
+  has also been updated to again consider the In-Reply-To header, if
+  it looks sensible. Re-indexing might be needed to take advantage of
+  the latter change.
+
+Handle mislabelled Windows-1252 parts
+
+  Messages that contain Windows-1252 are apparently frequently
+  mislabelled as ISO 8859-1. Use GMime functionality to apply the
+  correct encoding for such messages.
+
+Command Line Interface
+----------------------
+
+Support relative database paths
+
+  Database paths (i.e. parameters to `notmuch config set
+  database.path`) without a leading `/` are now interpreted relative
+  to $HOME of the invoking user.
+
+Emacs
+-----
+
+Improve stderr handling
+
+  Add a real sentinel process to clean up stderr buffer. This is
+  needed on e.g. macOS.
+
+Call `notmuch-mua-send-hook` hooks when sending a message
+
+  This hook was documented, but not functional for a very long time.
+
+Completion
+----------
+
+The zsh completion has been updated to cover most of the notmuch
+CLI. Internally it uses regexp searching, so needs at least Notmuch
+0.24.
+
+Build System
+------------
+
+The build system now installs notmuch-mutt and notmuch-emacs-mua with
+absolute shebangs, following the conventions of most Linux
+distributions.
+
+Test Suite
+----------
+
+Fix certain tests that were failing with GMime 2.6. Users are reminded
+that support for versions of GMime before 3.0.3 has been deprecated
+since Notmuch 0.25.
+
+Notmuch 0.27 (2018-06-13)
+=========================
+
+General
+-------
+
+Add support for thread:{} queries
+
+  Queries of the form `thread:{foo} and thread:{bar}` match threads
+  containing (possibly distinct) messages matching foo and bar. See
+  `notmuch-search-terms(7)` for details.
+
+Command Line Interface
+----------------------
+
+Add the --full-scan option to `notmuch new`
+
+  This option disables mtime based optimization of scanning for new mail.
+
+Add new --decrypt=stash option for `notmuch show`
+
+  This facilitates a workflow for encrypted messages where message
+  cleartext are indexed on first read, but the user's decryption key
+  does not have to be available during message receipt.
+
+Documentation
+-------------
+
+An initial manual for `notmuch-emacs` is now installed by default (in
+`info` format).
+
+Dependencies
+------------
+
+As of this release, support for versions of Xapian before 1.4.0 is
+deprecated, and may disappear in a future release of notmuch.
+
+Notmuch 0.26.2 (2018-04-28)
+===========================
+
+Library Changes
+---------------
+
+Work around Xapian bug with `get_mset(0,0, x)`
+
+  This causes aborts in `_notmuch_query_count_documents` on
+  e.g. Fedora 28.  The underlying bug is fixed in Xapian commit
+  f92e2a936c1592, and will be fixed in Xapian 1.4.6.
+
+Make thread indexing more robust against reference loops
+
+  Choose a thread root by date in case of reference loops. Fix a
+  related abort in `notmuch show`.
+
+Notmuch 0.26.1 (2018-04-02)
+===========================
+
+Library Changes
+---------------
+
+Bump the library minor version. This should have happened in 0.26, but
+better late than never.
+
+
+Notmuch 0.26 (2018-01-09)
+=========================
+
+Command Line Interface
+----------------------
+
+Support for re-indexing existing messages
+
+  There is a new subcommand, `notmuch reindex`, which re-indexes all
+  messages matching supplied search terms.  This permits users to
+  change the way specific messages are indexed.
+
+  Note that for messages with multiple variants in the message
+  archive, the recorded Subject: of may change upon reindexing,
+  depending on the order in which the variants are indexed.
+
+Improved error reporting in notmuch new
+
+  Give more details when reporting certain Xapian exceptions.
+
+Support maildir synced tags in `new.tags`
+
+  Tags `draft`, `flagged`, `passed`, and `replied` are now supported
+  in `new.tags`. The tag `unread` is still special in the presence of
+  maildir syncing, and will be added for files in `new/` regardless of
+  the setting of `new.tags`.
+
+Support /regex/ in new.ignore
+
+  Files and directories may be ignored based on regular expressions.
+
+Allow `notmuch insert --folder=""`
+
+  This inserts into the top level folder.
+
+Strip trailing '/' from folder path for notmuch insert
+
+  This prevents a potential problem with duplicated database records.
+
+New option --output=address for notmuch address
+
+Make `notmuch show` more robust against deleting duplicate files
+
+The option --decrypt now takes an explicit argument
+
+  The --decrypt option to `notmuch show` and `notmuch reply` now takes
+  an explicit argument.  If you were used to invoking `notmuch show
+  --decrypt`, you should switch to `notmuch show --decrypt=true`.
+
+Boolean and keyword arguments now take a `--no-` prefix
+
+Encrypted Mail
+--------------
+
+Indexing cleartext of encrypted e-mails
+
+  It's now possible to include the cleartext of encrypted e-mails in
+  the notmuch index.  This makes it possible to search your encrypted
+  e-mails with the same ease as searching cleartext.  This can be done
+  on a per-message basis by passing --decrypt=true to indexing
+  commands (new, insert, reindex), or by default by running "notmuch
+  config set index.decrypt true".
+
+  Encrypted messages whose cleartext is indexed will typically also
+  have their session keys stashed as properties associated with the
+  message.  Stashed session keys permit rapid rendering of long
+  encrypted threads, and disposal of expired encryption-capable keys.
+  If for some reason you want cleartext indexing without stashed
+  session keys, use --decrypt=nostash for your indexing commands (or
+  run "notmuch config set index.decrypt nostash"). See `index.decrypt`
+  in notmuch-config(1) for more details.
+
+  Note that stashed session keys permit reconstruction of the
+  cleartext of the encrypted message itself, and the contents of the
+  index are roughly equivalent to the cleartext as well.  DO NOT USE
+  this feature without considering the security of your index.
+
+Emacs
+-----
+
+Guard against concurrent searches in notmuch-tree
+
+Use make-process when available
+
+  This allows newer Emacs to separate stdout and stderr from the
+  notmuch command without using temporary files.
+
+Library Changes
+---------------
+
+Indexing files with duplicate message-id
+
+  Files with duplicate message-id's are now indexed, and searchable
+  via terms and phrases. There are known issues related to
+  presentation of results and regular-expression search, but in
+  principle no mail file should be completely unsearchable now.
+
+New functions to count files
+
+  Two new functions in the libnotmuch API:
+  `notmuch_message_count_files`, and `notmuch_thread_get_total_files`.
+
+New function to remove properties
+
+  A new function was added to the libnotmuch API to make it easier to
+  drop all properties with a common pattern:
+  `notmuch_message_remove_all_properties_with_prefix`
+
+Change of return value of `notmuch_thread_get_authors`
+
+  In certain corner cases, `notmuch_thread_get_authors` previously
+  returned NULL.  This has been replaced by an empty string, since the
+  possibility of NULL was not documented.
+
+Transition `notmuch_database_add_message` to `notmuch_database_index_file`
+
+  When indexing an e-mail message, the new
+  `notmuch_database_index_file` function is the preferred form, and
+  the old `notmuch_database_add_message` is deprecated.  The new form
+  allows passing a set of options to the indexing engine, which the
+  operator may decide to change from message to message.
+
+Test Suite
+----------
+
+Out-of-tree builds
+
+  The test suite now works properly with out-of-tree builds, i.e. with
+  separate source and build directories. The --root option to tests
+  has been dropped. The same can now be achieved more reliably using
+  out-of-tree builds.
+
+Python Bindings
+---------------
+
+Python bindings specific Debian packaging is removed
+
+  The bindings have been build by the top level Debian packaging for a
+  long time, and `bindings/python/debian` has bit-rotted.
+
+Open mail files in binary mode when using Python 3
+
+  This avoids certain encoding related crashes under Python 3.
+
+Add python bindings for `notmuch_database_{get,set}_config*`
+
+Optional `decrypt_policy` flag is available for notmuch.database().index_file()
+
+nmbug
+-----
+
+nmbug's internal version increases to 0.3 in this notmuch release.
+User-facing changes with this notmuch release:
+
+* Accept failures to unset `core.worktree` in `clone`, which allows
+  nmbug to be used with Git 2.11.0 and later.
+* Auto-checkout in `clone` if it wouldn't clobber existing content,
+  which makes the initial clone more convenient.
+* Only error for invalid diff lines in `tags/`, which allows for
+  `README`s and similar in nmbug repositories.
+
+Documentation
+-------------
+
+New man page: notmuch-properties(7)
+
+  This new page to the manual describes common conventions for how
+  properties are used by libnotmuch, the CLI, and associated programs.
+  External projects that use properties are encouraged to claim their
+  properties and conventions here to avoid collisions.
+
+Notmuch 0.25.3 (2017-12-08)
+===========================
+
+Emacs
+-----
+
+Extend mitigation (disabling handling x-display in text/enriched) for
+Emacs bug #28350 to Emacs versions before 24.4 (i.e. without
+`advice-add`).
+
+Command Line Interface
+----------------------
+
+Correctly report userid validity. Fix test suite failure for GMime >=
+3.0.3. This change raises the minimum supported version of GMime 3.x
+to 3.0.3.
+
+Notmuch 0.25.2 (2017-11-05)
+===========================
+
+Command Line Interface
+----------------------
+
+Fix segfault in notmuch-show crypto handling when compiled against
+GMime 2.6; this was a regression in 0.25.
+
+General
+-------
+
+Support for GMime before 3.0 is now deprecated, and will be removed in
+a future release.
+
+Notmuch 0.25.1 (2017-09-11)
+===========================
+
+Emacs
+-----
+
+Disable handling x-display in text/enriched messages. Mitigation for
+Emacs bug #28350.
+
+Notmuch 0.25 (2017-07-25)
+=========================
+
+General
+-------
+
+Add regexp searching for mid, paths, and tags.
+
+Skip HTML tags when indexing
+
+  In particular this avoids indexing large inline images.
+
+Command Line Interface
+----------------------
+
+Bash completion is now installed to /usr/share by default.
+
+Allow space as separator for keyword arguments.
+
+Emacs
+-----
+
+Support for stashing message timestamp in show and tree views
+
+  Invoking `notmuch-show-stash-date` with a prefix argument
+  stashes the unix timestamp of the current message instead of
+  the date string.
+
+Don't use 'function' as variable name, workaround emacs bug 26406.
+
+Library Changes
+---------------
+
+Add workaround for date parsing of bad input in older GMime
+
+  In certain circumstances, older GMime libraries could return
+  negative numbers when parsing syntactically invalid dates.
+
+Replace deprecated functions with status returning versions
+
+  API of notmuch_query_{search,count}_{messages,threads} has
+  changed.  notmuch_query_add_tag_exclude now returns a status
+  value.
+
+Add support for building against GMime 3.0.
+
+Rename libutil.a to libnotmuch_util.a.
+
+libnotmuch SONAME is incremented to libnotmuch.so.5.
+
+Notmuch 0.24.2 (2017-06-01)
+===========================
+
+Command Line Interface
+----------------------
+
+Fix output from `notmuch dump --include=properties` to not include tags.
+
+Emacs
+-----
+
+Fix filename stashing in tree view.
+
+Notmuch 0.24.1 (2017-04-01)
+===========================
+
+General
+-------
+
+Fix regressions in non-regexp search for `from:` and `subject:`
+
+  The regexp search code in 0.24 introduced a regression in the
+  handling of empty queries and wildcards. These are both corrected in
+  this release.
+
+Command Line Interface
+----------------------
+
+Fix several memory leaks in `notmuch show`
+
+Update NEWS for 0.24 to mention schema changes
+
+Fix bug in dump header
+
+  The previous version of the dump header failed to mention the
+  inclusion of tags. This fix bumps the version number of the dump
+  format to 3. There are no other changes to the format.
+
+Library Changes
+---------------
+
+Fix a read-after-free in the library.
+
+Notmuch 0.24 (2017-03-12)
+=========================
+
+General
+-------
+
+Regular expression searches supported for `from:` and `subject:`
+
+  This requires recent Xapian (1.4+) See notmuch-search-terms(7) for
+  details.
+
+Command Line Interface
+----------------------
+
+Run external `notmuch-` prefixed commands as subcommands
+
+  You can now add your own `notmuch-` prefixed commands in PATH, and
+  have notmuch run them as if they were notmuch commands. See the
+  `notmuch(1)` man page for details
+
+New default output format to 3
+
+  See devel/schemata for details. Users of the structured output
+  format are reminded of the `--format-version` argument to `notmuch
+  show` and `notmuch search` which can prevent breakage when the
+  default format changes.
+
+Emacs
+-----
+
+Postpone and resume messages in `notmuch-message-mode` (composition)
+
+  Notmuch now has built in support for postponing, saving and resuming
+  messages. The default bindings are C-x C-s to save a draft, C-c C-p
+  to postpone a draft (save and exit compose buffer), and "e" in show
+  or tree view to resume.
+
+  Draft messages are tagged with `notmuch-draft-tags` (draft by
+  default) so you may wish to add that to the excluded tags list. When
+  saving a previously saved draft message the earlier draft gets
+  tagged deleted.
+
+  Note that attachments added before postponing will be included as
+  they were when you postponed in the final message.
+
+Address Completion
+
+  It is now possible to save the list of address completions for
+  notmuch's internal completion between runs of emacs. This makes the
+  first calls to address completion much better and faster. For
+  privacy reasons it is disabled by default, to enable set or
+  customize `notmuch-address-save-filename`.
+
+Tag jump menu
+
+  It is now possible to configure tagging shortcuts (with an interface
+  like notmuch jump). For example (by default) k u will remove the
+  unread tag, and k s will add a tag "spam" and remove the inbox
+  tag. Pressing k twice will do the reverse operation so, for example,
+  k k s removes the spam tag and adds the inbox tag. See the customize
+  variable `notmuch-tagging-keys` for more information.
+
+Refresh all buffers
+
+  It is now possible to refresh all notmuch buffers to reflect the
+  current state of the database with a single command, `M-=`.
+
+Stop display of `application/*` parts
+
+  By default gnus displays all `application/*` parts such as
+  application/zip in the message buffer. This has several undesirable
+  effects for notmuch (security, triggering errors etc). Notmuch now
+  overrides this and does not display them by default. If you have
+  customized `mm-inline-override-types` then we assume you know what
+  you want and do not interfere; if you do want to stop the display of
+  `application/*` add `application/*` to your customization. If you want
+  to allow `application/*` then set `mm-inline-override-types` to
+  "non/existent".
+
+Small change in the api for notmuch-search-tag
+
+  When `notmuch-search-tag` is called non-interactively and the region
+  is set, then it only tags the threads in the region. (Previously it
+  only tagged the current thread.)
+
+Bugfix for sending messages with very long headers
+
+  Previously emacs didn't fold very long headers when sending which
+  could cause the MTA to refuse to send the message. This makes sure
+  it does fold any long headers so the message is RFC compliant.
+
+`notmuch emacs-mua` command installed with the Emacs interface
+
+  We've carried a `notmuch-emacs-mua` script in the source tree for
+  quite some time. It can be used to launch the Notmuch Emacs
+  interface from the command line in many different ways. Starting
+  with this release, it will be installed with the Emacs
+  interface. With the new external subcommand support, the script
+  transparently becomes a new notmuch command. See the
+  `notmuch-emacs-mua(1)` man page for details.
+
+Notmuch Emacs desktop integration
+
+  The desktop integration file will now be installed with the Notmuch
+  Emacs interface, adding a Notmuch menu item and configuration to
+  allow the user to set up Notmuch Emacs as the `mailto:` URL handler.
+
+Library changes
+---------------
+
+`notmuch_query_count_messages` is now non-destructive
+
+  Internally the implementation of excludes has changed to make this
+  possible.
+
+Improved handling of DatabaseModifiedError
+
+  Previously uncaught exceptions reading message metadata are now
+  handled.
+
+Notmuch 0.23.7 (2017-02-28)
+===========================
+
+Test Suite
+----------
+
+Drop use of gpgconf --create-socketdir. Move $GNUPGHOME to /tmp.
+
+  It turns out the hardcoded use of /run/user in gpg doesn't work out
+  that well in some environments. The more low tech fix is to move all
+  of $GNUPGHOME to somewhere where we can control the length of the
+  paths.
+
+Notmuch 0.23.6 (2017-02-27)
+===========================
+
+Command Line Interface
+----------------------
+
+Fix read-after-free bug in `notmuch new`.
+
+Test Suite
+----------
+
+Use gpgconf --create-socketdir if available.
+
+  GnuPG has a facility to use sockets in /run or /var/run to avoid
+  problems with long socket paths, but this is not enabled by default
+  for GNUPGHOME other than $HOME/.gnupg. Enable it, if possible.
+
+Notmuch 0.23.5 (2017-01-09)
+===========================
+
+Build system
+------------
+
+Fix quoting bug in configure. This had introduced a RUNPATH into the
+notmuch binary in cases where it was not not needed.
+
+Notmuch 0.23.4 (2016-12-24)
+===========================
+
+Command Line Interface
+----------------------
+
+Improve error handling in notmuch insert
+
+  Database lock errors no longer prevent message file delivery to the
+  filesystem.  Certain errors during `notmuch insert` most likely to
+  be temporary return EX_TEMPFAIL.
+
+Emacs
+-----
+
+Restore autoload cookie for notmuch-search.
+
+Notmuch 0.23.3 (2016-11-27)
+===========================
+
+Command Line Interface
+----------------------
+
+Treat disappearing files during notmuch new as non-fatal.
+
+Test Suite
+----------
+
+Fix incompatibility (related to signature size) with gnupg 2.1.16.
+
+Notmuch 0.23.2 (2016-11-20)
+===========================
+
+Emacs
+-----
+
+Fix notmuch-interesting-buffer and notmuch-cycle-notmuch-buffers.
+
+  notmuch-tree-mode and notmuch-message-mode buffers are now
+  considered interesting by `notmuch-interesting-buffer` and
+  `notmuch-cycle-notmuch-buffers`.
+
+Restore compatibility with Emacs 23.
+
+  Notmuch support for Emacs 23 is now deprecated.
+
+Notmuch 0.23.1 (2016-10-23)
+===========================
+
+General
+-------
+
+Require Xapian >= 1.2.6
+
+  The ifdef branch for older Xapian (pre-compact API) had bit-rotted.
+
+Emacs
+-----
+
+Fix default colours for unread and flagged messages
+
+  In 0.23 the default colours for unread and flagged messages in
+  search view were accidentally swapped. This release returns them to
+  the original colours.
+
+  A related change in 0.23 broke the customize widget for
+  notmuch-search-line-faces. This is now fixed.
+
+Fix test failure with Emacs 25.1
+
+  A previously undiscovered jit-lock related bug was exposed by Emacs
+  25, causing a notmuch-show mode test to fail. This release fixes the
+  bug, and hence the test.
+
+Notmuch 0.23 (2016-10-03)
+=========================
+
+General (Xapian 1.4+)
+---------------------
+
+Compiling against Xapian 1.4 enables several new features.
+
+Support for single argument date: queries
+
+  `date:<expr>` is equivalent to `date:<expr>..<expr>`.
+
+Support for blocking opens
+
+  When opening a database notmuch by default will wait for another
+  process to release a write lock, rather than returning an error.
+
+Support for named queries
+
+  Named queries (also known as 'saved searches') can be defined with a
+  `query:name` format. The expansion of these queries is stored in the
+  database and they can be used from any notmuch client.
+
+Library
+-------
+
+Message property API
+
+  libnotmuch now supports the attachment of arbitrary key-value pairs
+  to messages. These can be used by various tools to manage their
+  private data without polluting the user tag space. They also support
+  iteration of values with the same key or same key prefix.
+
+Bug fix for `notmuch_directory_set_mtime`
+
+  Update cached mtime to match on-disk mtime.
+
+CLI
+---
+
+Support for compile time options
+
+  A group of `built_with` keys is now supported for notmuch
+  config. Initial keys in this group are `compact`, `field_processor`,
+  and `retry_lock`.
+
+Dump/Restore support for configuration information and properties
+
+  Any configuration information stored in the database (initially just
+  named queries) is dumped and restored. Similarly any properties
+  attached to messages are also dumped and restored. Any new
+  information in the dump format is prefixed by '#' to allow existing
+  scripts to ignore it.
+
+Emacs
+-----
+
+Make notmuch-message-mode use insert for fcc
+
+  Notmuch-message-mode now defaults to using notmuch insert for
+  fcc. The old file based fcc behaviour can be restored by setting the
+  defcustom `notmuch-maildir-use-notmuch-insert` to nil.
+
+  When using notmuch insert, `notmuch-fcc-dirs` must be a subdirectory
+  of the mailstore (absolute paths are not permitted) followed by any
+  tag changes to be applied to the inserted message. The tag changes
+  are applied after the default tagging for new messages. For example
+  setting the header to "sentmail -inbox +sent" would insert the
+  message in the subdirectory sentmail of the mailstore, add the tag
+  "sent", and not add the (normally added) "inbox" tag.
+
+  Finally, if the insert fails (e.g. if the database is locked) the
+  user is presented with the option to retry, ignore, or edit the
+  header.
+
+Make internal address completion customizable
+
+  There is a new defcustom `notmuch-address-internal-completion` which
+  controls how the internal completion works: it allows the user to
+  choose whether to match on messages the user sent, or the user
+  received, and to filter the messages used for the match, for example
+  by date.
+
+Allow internal address completion on an individual basis
+
+  There is a new function `notmuch-address-toggle-internal-completion`
+  (by default it has no keybinding) which allows users who normally
+  use an external completion command to use the builtin internal
+  completion for the current buffer.
+
+  Alternatively, if the user has company-mode enabled, then the user
+  can use company mode commands such as `company-complete` to
+  activate the builtin completion for an individual completion.
+
+Resend messages
+
+  The function `notmuch-show-resend-message` (bound to `b` in show
+  and tree modes) will (attempt to) send current message to new
+  recipients. The headers of the message won't be altered (e.g. `To:`
+  may point to yourself). New `Resent-To:`, `Resent-From:` and so on
+  will be added instead.
+
+Face customization is easier
+
+  New faces `notmuch-tag-unread`, `notmuch-tag-flagged`,
+  `notmuch-tag-deleted`, `notmuch-tag-added`,
+  `notmuch-search-flagged-face` and `notmuch-search-unread-face` are
+  now used by default. Customize `notmuch-faces` to modify them.
+
+Omit User-Agent header by default when sending mail
+
+Ruby Bindings
+-------------
+
+Add support for `notmuch_database_get_all_tags`
+
+Go Bindings
+-----------
+
+Go bindings moved to contrib
+
+Add support for `notmuch_threads_t` and `notmuch_thread_t`
+
+Fixed constant values so they are not all zero anymore
+
+  Previously, it was impossible to open writable database handles,
+  because `DATABASE_MODE_READ_ONLY` and `DATABASE_MODE_READ_WRITE` were
+  both set to zero.
+  The same issue occurred with sort modes.
+
+Notmuch 0.22.2 (2016-09-08)
+===========================
+
+Test Suite
+----------
+
+Silence gdb more
+
+  Have gdb write to a log file instead of stdout, hiding some more
+  (harmless) stderr chatter which causes test failures.
+
+Hardcode fingerprint in PGP/MIME tests
+
+  Make the tests more robust against changing GnuPG output formats.
+
+Notmuch 0.22.1 (2016-07-19)
+===========================
+
+Library
+-------
+
+Correct the definition of `LIBNOTMUCH_CHECK_VERSION`.
+
+Document the (lack of) operations permitted on a closed database.
+
+Test Suite
+----------
+
+Fix race condition in dump / restore tests.
+
+Notmuch-Mutt
+------------
+
+Use `env` to locate perl.
+
+Emacs
+-----
+
+Tell `message-mode` mode that outgoing messages are mail
+
+  This makes message-mode configuration behave more predictably.
+
+Respect charset of MIME parts when reading them
+
+  Fix previous assumption that everyone uses UTF-8.
+
+Notmuch 0.22 (2016-04-26)
+=========================
+
+General
+-------
+
+Xapian 1.3 support
+
+  Notmuch should now build (and the test suite should pass) on recent
+  releases of Xapian 1.3.x. It has been tested with Xapian 1.3.5.
+
+Limited support for S/MIME messages
+
+  Signature verification is supported, but not decryption. S/MIME
+  signature creation and S/MIME encryption are supported via built-in
+  support in Emacs. S/MIME support is not extensively tested at this
+  time.
+
+Bug Fixes
+
+   Fix for threading bug involving deleting and re-adding
+   messages. Fix for case-sensitive content disposition headers. Fix
+   handling of 1 character directory names at top level.
+
+Command Line Interface
+----------------------
+
+`notmuch show` now supports verifying S/MIME signatures
+
+  This support relies on an appropriately configured `gpgsm`.
+
+Build System
+------------
+
+Drop dependency on "pkg-config emacs".
+
+Emacs Interface
+---------------
+
+Notmuch replies now include all parts shown in the show view
+
+  There are two main user visible changes. The first is that rfc822
+  parts are now included in replies.
+
+  The second change is that part headers are now included in the reply
+  buffer to provide visible separation of the parts. The choice of
+  which part headers to show is customizable via the variable
+  `notmuch-mua-reply-insert-header-p-function`.
+
+Filtering or Limiting messages is now bound to `l` in the search view
+
+  This binding now matches the analogous binding in show view.
+
+`F` forwards all open messages in a thread
+
+  When viewing a thread of messages, the new binding `F` can be used
+  to generate a new outgoing message which forwards all of the open
+  messages in the thread. This is analogous to the `f` binding, which
+  forwards only the current message.
+
+Preferred content type can be determined from the message content
+
+  More flexibility in choosing which sub-part of a
+  multipart/alternative part is initially shown is available by
+  setting `notmuch-multipart/alternative-discouraged` to a function
+  that returns a list of discouraged types. The function so specified
+  is passed the message as an argument and can examine the message
+  content to determine which content types should be discouraged. This
+  is in addition to the current capabilities (i.e. setting
+  `notmuch-multipart/alternative-discouraged` to a list of discouraged
+  types).
+
+When viewing a thread ("show" mode), queries that match no messages no
+longer generate empty buffers
+
+  Should an attempt be made to view the thread corresponding to a
+  query that matches no messages, a warning message is now displayed
+  and the terminal bell rung rather than displaying an empty buffer
+  (or, in some cases, displaying an empty buffer and throwing an
+  error). This also affects re-display of the current thread.
+
+Handle S/MIME signatures in emacs
+
+  The emacs interface is now capable making and verifying S/MIME
+  signatures.
+
+`notmuch-message-address-insinuate` is now a no-op
+
+  This reduces the amount of interference with non-notmuch uses of
+  message-mode.
+
+Address completion improvements
+
+  An external script is no longer needed for address completion; if
+  you previously configured one, customize the variable
+  `notmuch-address-command` to try the internal completion. If
+  `company-mode` is available, notmuch uses it by default for
+  interactive address completion.
+
+Test and experiment with the emacs MUA available in source tree
+
+  `./devel/try-emacs-mua` runs emacs and fills the window with
+  information how to try the MUA safely. Emacs is configured to use
+  the notmuch (lisp) files located in `./emacs` directory.
+
+Documentation
+-------------
+
+New `notmuch-report(1)` and `notmuch-report.json(5)` man pages
+describe `notmuch-report` and its JSON configuration file.  You can
+build these files by running `make` in the `devel/nmbug/doc`
+directory.
+
+notmuch-report
+--------------
+
+Renamed from `nmbug-status`.  This script generates reports based on
+notmuch queries, and doesn't really have anything to do with nmbug,
+except for sharing the `NMBGIT` environment variable.  The new name
+focuses on the script's action, instead of its historical association
+with the nmbug workflow.  This should make it more discoverable for
+users looking for generic notmuch reporting tools.
+
+The default configuration file name (extracted from the `config`
+branch of `NBMGIT` has changed from `status-config.json` to
+`notmuch-report.json` so it is more obviously associated with the
+report-generating script.  The configuration file also has a new
+`meta.message-url` setting, which is documented in
+`notmuch-report.json(5)`.
+
+`notmuch-report` now wraps query phrases in parentheses when and-ing
+them together, to avoid confusion about clause grouping.
+
+Notmuch 0.21 (2015-10-29)
+=========================
+
+General
+-------
+
+Notmuch now requires gmime >= 2.6.7. The gmime 2.4 series is no longer
+supported.
+
+Database revision tracking: `lastmod:` queries
+
+  Each message now has a metadata revision number that increases with
+  every tagging operation. See the discussion of `lastmod:` in
+  `notmuch-search-terms(7)` for more information.
+
+Date queries now support `date:<expr>..!` shorthand for
+`date:<expr>..<expr>`
+
+  You can use, for example, `date:yesterday..!` to match from the
+  beginning of yesterday to the end of yesterday. For further details,
+  please refer to the `notmuch-search-terms` manual page.
+
+Notmuch database upgrade to support `lastmod:` queries
+
+  The above mentioned `lastmod:` prefix. This will be done
+  automatically, without prompting on the next time `notmuch new` is
+  run after the upgrade. The upgrade is not reversible, and the
+  upgraded database will not be readable by older versions of
+  Notmuch. As a safeguard, a database dump will be created in the
+  `.notmuch` directory before upgrading.
+
+Build System
+------------
+
+The ruby bindings are now built as part of the main notmuch build
+process. This can be disabled with the `--without-ruby` option to
+configure.
+
+Building the documentation can be disabled with the `--without-docs`
+option to configure.
+
+Skipped individual tests are no longer considered as failures.
+
+Command Line Interface
+----------------------
+
+Database revision tracking
+
+  Two new options were added to support revision tracking. A global
+  option "--uuid" (`notmuch(1)`) was added for to detect counter
+  rollover and reinitialization, and `notmuch-count(1)` gained a
+  `--lastmod` option to query database revision tracking data.
+
+The `notmuch address` command supports new deduplication schemes
+
+  `notmuch address` has gained a new `--deduplicate` option to specify
+  how the results should be deduplicated, if at all. The alternatives
+  are `no` (do not deduplicate, useful for processing the results with
+  external tools), `mailbox` (deduplicate based on the full, case
+  sensitive name and email address), and `address` (deduplicate based
+  on the case insensitive address part). See the `notmuch-address`
+  manual page for further information.
+
+Emacs Interface
+---------------
+
+`notmuch-emacs-version` is used in `User-Agent` header
+
+  The value of recently introduced variable `notmuch-emacs-version` is
+  now used as a part of `User-Agent` header when sending emails.
+
+Removed `notmuch-version` function by renaming it to `notmuch-cli-version`
+
+  With existing variable `notmuch-emacs-version` the accompanied
+  function which retrieves the version of `notmuch-command` is
+  better named as `notmuch-cli-version`.
+
+Query input now supports completion for "is:<tag>"
+
+New message composition mode: `notmuch-compose-mode`
+
+  This is mainly to fix fcc handling, but may be useful for user
+  customization as well.
+
+Allow filtering of search results in `notmuch-show`
+
+Add function to rerun current tree-view search in search mode
+
+Bug fix for replying to encrypted messages in `notmuch-tree` mode
+
+Allow saved searched to specify tree view rather than search view
+
+  Applies to saved searches run from `notmuch-hello`, or by a keyboard
+  shortcut (`notmuch-jump`).  Can be set in the customize interface, or
+  by adding :search-type tree to the appropriate saved search plist in
+  `notmuch-saved-searches`.
+
+Increase maximum size of rendered text parts
+
+  The variable `notmuch-show-max-text-part-size` controls the maximum
+  size (in bytes) which is automatically rendered. This may make
+  rendering large threads slower. To get the previous behaviour set
+  this variable to 10000.
+
+Library
+-------
+
+The use of absolute paths is now enforced when calling
+`notmuch_database_{open, create}`
+
+New function `notmuch_directory_delete` to delete directory documents
+
+  Previously there was no way to delete directory documents from the
+  database, leading to confusing results when the "ghost" directory
+  document of a renamed or deleted filesystem directory was
+  encountered every time the parent directory was being scanned by
+  `notmuch new`. The mtime of the old directory document was also used
+  if a directory by the same name was added again in the filesystem,
+  potentially bypassing the scan for the directory. The issues are
+  fixed by providing a library call to delete directory documents, and
+  deleting the old documents in `notmuch new` on filesystem directory
+  removal or rename.
+
+Database revision tracking
+
+  Revision tracking is supported via a new prefix "lastmod:" in the
+  query parser and the new function
+  `notmuch_database_get_revision`. For the latter, see `notmuch(3)`.
+
+New status code returning API for n_query_count_{messages,threads}
+
+Deprecated functions
+
+  `notmuch_query_search_threads`, `notmuch_query_search_messages`,
+  `notmuch_query_count_messages`, and `notmuch_query_count_threads`
+  are all deprecated as of this release.  Clients are encouraged to
+  transition to the `_st` variants supporting better error reporting.
+
+nmbug-status
+------------
+
+`nmbug-status` now supports specifying the sort order for each view.
+
+Notmuch 0.20.2 (2015-06-27)
+===========================
+
+Emacs Interface
+---------------
+
+Bug fix for marking messages read in `notmuch-tree` mode.
+
+Notmuch 0.20.1 (2015-06-01)
+===========================
+
+Test Suite
+----------
+
+Work around apparent gdb bug on arm64.
+
+Notmuch 0.20 (2015-05-31)
+=========================
+
+Command-Line Interface
+----------------------
+
+There is a new `mimetype:` search prefix
+
+  The new `mimetype:` search prefix allows searching for the
+  content-type of attachments, which is now indexed. See the
+  `notmuch-search-terms` manual page for details.
+
+Path to gpg is now configurable
+
+  On systems with multiple versions of gpg, you can tell
+  notmuch which one to use by setting `crypto.gpg_path`
+
+Emacs
+-----
+
+Avoid rendering large text attachments.
+
+Improved rendering of CID references in HTML.
+
+Vim
+---
+
+Vim client now respects excluded tags.
+
+Notmuch-Mutt
+------------
+
+Support messages without Message-IDs.
+
+Library
+-------
+
+Undeprecate single message mboxes
+
+  It seems more trouble to remove this feature than expected, so
+  `notmuch new` will no longer nag about mboxes with a single message.
+
+New error logging facility
+
+  Clients should call `notmuch_database_status_string` to retrieve
+  output formerly printed to stderr.
+
+Several bug fixes related to stale iterators
+
+New status code returning API for n_query_search_{messages,thread}
+
+Fix for library `install_name` on Mac OS X
+
+Fix for rounding of seconds
+
+Documentation
+-------------
+
+Sphinx is now mandatory to build docs
+
+  Support for using rst2man in place of sphinx to build the
+  documentation has been removed.
+
+Improved notmuch-search-terms.7
+
+  The man page `notmuch-search-terms(7)` has been extended, merging
+  some material from the relicensed wiki.
+
+Contrib
+-------
+
+`notmuch-deliver` is removed. As far as we know, all functionality
+previously provided by `notmuch-deliver` should now be provided by
+`notmuch insert`, provided by the main notmuch binary.
+
+nmbug-status
+------------
+
+`nmbug-status` now only matches local branches when reading
+`status-config.json` from the `config` branch of the `NMBGIT`
+repository.  To help new users running `nmbug-status`, `nmbug clone`
+now creates a local `config` branch tracking `origin/config`.  Folks
+who use `nmbug-status` with an in-Git config (i.e. you don't use the
+`--config` option) who already have `NMBGIT` set up are encouraged to
+run:
+
+    git checkout config origin/config
+
+in their `NMBGIT` repository (usually `~/.nmbug`).
+
+Notmuch 0.19 (2014-11-14)
+=========================
+
+Overview
+--------
+
+This release improves the reliability of `notmuch dump` and the error
+handling for `notmuch insert`. The new `notmuch address` command is
+intended to make searching for email addresses more convenient. At the
+library level the revised handling of missing messages fixes at least
+one bug in threading. The release also includes several interface
+improvements to the emacs interface, most notably the ability to bind
+keyboard shortcuts to saved searches.
+
+Command-Line Interface
+----------------------
+
+Stopped `notmuch dump` failing if someone writes to the database
+
+  The dump command now takes the write lock when running. This
+  prevents other processes from writing to the database during the
+  dump which would cause the dump to fail. Note, if another notmuch
+  process already has the write lock the dump will not start, so
+  script callers should still check the return value.
+
+`notmuch insert` requires successful message indexing for success status
+
+  Previously the `notmuch insert` subcommand indicated success even if
+  the message indexing failed, as long as the message was delivered to
+  file system. This could have lead to delivered messages missing
+  tags, etc. `notmuch insert` is now more strict, also requiring
+  successful indexing for success status. Use the new `--keep` option
+  to revert to the old behaviour (keeping the delivered message file
+  and returning success even if indexing fails).
+
+`notmuch insert` has gained support for `post-insert` hook
+
+  The new `post-insert` hook is run after message delivery, similar to
+  `post-new`. There's also a new option `notmuch insert --no-hooks` to
+  skip the hook. See the notmuch-hooks(1) man page for details.
+
+`notmuch deliver` is deprecated
+
+  With this release we believe that `notmuch insert` has reached
+  parity with `notmuch deliver`. We recommend that all users of
+  `notmuch deliver` switch to `notmuch insert` as the former is
+  currently unmaintained.
+
+`notmuch search` now supports `--duplicate=N` option with `--output=messages`
+
+  Complementing the `notmuch search --duplicate=N --output=files`
+  options, the new `--duplicate=N --output=messages` combination
+  limits output of message IDs to messages matching search terms that
+  have at least `N` files associated with them.
+
+Added `notmuch address` subcommand
+
+  This new subcommand searches for messages matching the given search
+  terms, and prints the addresses from them. Duplicate addresses are
+  filtered out. The `--output` option controls which of the following
+  information is printed: sender addresses, recipient addresses and
+  count of duplicate addresses.
+
+Emacs Interface
+---------------
+
+Use the `j` key to access saved searches from anywhere in notmuch
+
+  `j` is now globally bound to `notmuch-jump`, which provides fast,
+  interactive keyboard shortcuts to saved searches.  For example,
+  with the default saved searches `j i` from anywhere in notmuch will
+  bring up the inbox.
+
+Improved handling of the unread tag
+
+  Notmuch now marks an open message read (i.e., removes the unread
+  tag) if point enters the message at any time in a show buffer
+  regardless of how point got there (mouse click, cursor command, page
+  up/down, notmuch commands such as n,N etc). This fixes various
+  anomalies or bugs in the previous handling. Additionally it is
+  possible to customize the mark read handling by setting
+  `notmuch-show-mark-read-function` to a custom function.
+
+Expanded default saved search settings
+
+  The default saved searches now include several more common searches,
+  as well as shortcut keys for `notmuch-jump`.
+
+Improved `q` binding in notmuch buffers
+
+  `q` will now bury rather than kill a notmuch search, show or tree
+  buffer if there are multiple windows showing the buffer. If only a
+  single window is showing the buffer, it is killed.
+
+`notmuch-show-stash-mlarchive-link-alist` now supports functions
+
+  Some list archives may use a more complicated scheme for referring
+  to messages than just concatenated URL and message ID. For example,
+  patchwork requires a query to translate message ID to a patchwork
+  patch ID. `notmuch-show-stash-mlarchive-link-alist` now supports
+  functions to better cover such cases. See the help documentation for
+  the variable for details.
+
+Library changes
+---------------
+
+Introduced database version 3 with support for "database features."
+
+  Features are independent aspects of the database schema.
+  Representing these independently of the database version number will
+  let us evolve the database format faster and more incrementally,
+  while maintaining better forwards and backwards compatibility.
+
+Library users are no longer required to call `notmuch_database_upgrade`
+
+  Previously, library users were required to call
+  `notmuch_database_needs_upgrade` and `notmuch_database_upgrade`
+  before using a writable database.  Even the CLI didn't get this
+  right, and it is no longer required.  Now, individual APIs may
+  return `NOTMUCH_STATUS_UPGRADE_REQUIRED` if the database format is
+  too out of date for that API.
+
+Library users can now abort an atomic section by closing the database
+
+  Previously there was no supported way to abort an atomic section.
+  Callers can now simply close the database, and any outstanding
+  atomic section will be aborted.
+
+Add return status to `notmuch_database_close` and
+`notmuch_database_destroy`
+
+Bug fixes and performance improvements for thread linking
+
+  The database now represents missing-but-referenced messages ("ghost
+  messages") similarly to how it represents regular messages.  This
+  enables an improved thread linking algorithm that performs better
+  and fixes a bug that sometimes prevented notmuch from linking
+  messages into the same thread.
+
+nmbug
+-----
+
+The Perl script has been translated to Python; you'll need Python 2.7
+or anything from the 3.x line.  Most of the user-facing interface is
+the same, but `nmbug help` is now `nmbug --help`, and the following nmbug
+commands have slightly different interfaces: `archive`, `commit`,
+`fetch`, `log`, `pull`, `push`, and `status`.  For details on the
+new interface for a given command, run `nmbug COMMAND --help`.
+
+nmbug-status
+------------
+
+`nmbug-status` can now optionally load header and footer templates
+from the config file.  Use something like:
+
+    {
+      "meta": {
+        "header": "<!DOCTYPE html>\n<html lang="en">\n...",
+        "footer": "</body></html>",
+         ...
+      },
+      ...
+    }
+
+Python Bindings
+---------------
+
+Add support for `notmuch_query_add_tag_exclude`
+
+Build System
+------------
+
+The notmuch binaries and libraries are now build with debugging symbols
+by default.  Users concerned with disk space should change the
+defaults when configuring or use the strip(1) command.
+
+Notmuch 0.18.2 (2014-10-25)
+===========================
+
+Test Suite
+----------
+
+Translate T380-atomicity to use gdb/python
+
+  The new version is compatible with gdb 7.8
+
+Emacs 24.4 related bug fixes
+
+  The Messages buffer became read-only, and the generated mime
+  structure for signatures changed slightly.
+
+Simplify T360-symbol-hiding
+
+   Replace the use of `objdump` on the object files with `nm` on the
+   resulting lib.
+
+Notmuch 0.18.1 (2014-06-25)
+===========================
+
+This is a bug fix and portability release.
+
+Build System
+------------
+
+Add a workaround for systems without zlib.pc
+
+Make emacs install robust against the non-existence of emacs
+
+Put notmuch lib directory first in RPATH
+
+Fix handling of `html_static_path` in sphinx
+
+  Both the python bindings and the main docs had spurious settings of
+  this variable.
+
+Test Suite
+----------
+
+Use --quick when starting emacs
+
+  This avoids a hang in the T160-json tests.
+
+Allow pending break points in atomicity script
+
+  This allows the atomicity tests to run on several more architectures/OSes.
+
+Command-Line Interface
+----------------------
+
+To improve portability use fsync instead of fdatasync in
+`notmuch-dump`. There should be no functional difference.
+
+Library changes
+---------------
+
+Resurrect support for single-message mbox files
+
+  The removal introduced a bug with previously indexed single-message
+  mboxes.  This support remains deprecated.
+
+Fix for phrase indexing
+
+  There were several bugs where words intermingled from different
+  headers and MIME parts could match a single phrase query.  This fix
+  will affect only newly indexed messages.
+
+Emacs Interface
+---------------
+
+Make sure tagging on an empty query is harmless
+
+  Previously tagging an empty query could lead to tags being
+  unintentionally removed.
+
+Notmuch 0.18 (2014-05-06)
+=========================
+
+Overview
+--------
+
+This new release includes some enhancements to searching for messages
+by filesystem location (`folder:` and `path:` prefixes under *General*
+below).  Saved searches in *Emacs* have also been enhanced to allow
+distinct search orders for each one.  Another enhancement to the
+*Emacs* interface is that replies to encrypted messages are now
+encrypted, reducing the risk of unintentional information disclosure.
+The default dump output format has changed to the more robust
+`batch-tag` format. The previously deprecated parsing of single
+message mboxes has been removed. For detailed release notes, see
+below.
+
+General
+-------
+
+The `folder:` search prefix now requires an exact match
+
+  The `folder:` prefix has been changed to search for email messages
+  by the exact, case sensitive maildir or MH folder name. Wildcard
+  matching (`folder:foo*`) is no longer supported. The new behaviour
+  allows for more accurate mail folder based searches, makes it
+  possible to search for messages in the top-level folder, and should
+  lead to less surprising results than the old behaviour. Users are
+  advised to see the `notmuch-search-terms` manual page for details,
+  and review how the change affects their existing `folder:` searches.
+
+There is a new `path:` search prefix
+
+  The new `path:` search prefix complements the `folder:` prefix. The
+  `path:` prefix searches for email messages that are in particular
+  directories within the mail store, optionally recursively using a
+  special syntax. See the `notmuch-search-terms` manual page for
+  details.
+
+Notmuch database upgrade due to `folder:` and `path:` changes
+
+  The above mentioned changes to the `folder:` prefix and the addition
+  of `path:` prefix require a Notmuch database upgrade. This will be
+  done automatically, without prompting on the next time `notmuch new`
+  is run after the upgrade. The upgrade is not reversible, and the
+  upgraded database will not be readable by older versions of
+  Notmuch. As a safeguard, a database dump will be created in the
+  `.notmuch` directory before upgrading.
+
+Library changes
+---------------
+
+Notmuch database upgrade
+
+  The libnotmuch consumers are reminded to handle database upgrades
+  properly, either by relying on running `notmuch new`, or checking
+  `notmuch_database_needs_upgrade()` and calling
+  `notmuch_database_upgrade()` as necessary. This has always been the
+  case, but in practise there have been no database upgrades in any
+  released version of Notmuch before now.
+
+Support for indexing mbox files has been dropped
+
+  There has never been proper support for mbox files containing
+  multiple messages, and the support for single-message mbox files has
+  been deprecated since Notmuch 0.15. The support has now been
+  dropped, and all mbox files will be rejected during indexing.
+
+Message header parsing changes
+
+  Notmuch previously had an internal parser for message headers. The
+  parser has now been dropped in favour of letting GMime parse both
+  the headers and the message MIME structure at the same pass. This is
+  mostly an internal change, but the GMime parser is stricter in its
+  interpretation of the headers. This may result in messages with
+  slightly malformed message headers being now rejected.
+
+Command-Line Interface
+----------------------
+
+`notmuch dump` now defaults to `batch-tag` format
+
+  The old format is still available with `--format=sup`.
+
+`notmuch new` has a --quiet option
+
+  This option suppresses the progress and summary reports.
+
+`notmuch insert` respects maildir.synchronize_flags config option
+
+  Do not synchronize tags to maildir flags in `notmuch insert` if the
+  user does not want it.
+
+The commands set consistent exit status codes on failures
+
+  The cli commands now consistently set exit status of 1 on failures,
+  except where explicitly otherwise noted. The notable exceptions are
+  the status codes for format version mismatches for commands that
+  support formatted output.
+
+Bug fix for checking configured new.tags for invalid tags
+
+  `notmuch new` and `notmuch insert` now check the user configured
+  new.tags for invalid tags, and refuse to apply them, similar to
+  `notmuch tag`. Invalid tags are currently the empty string and tags
+  starting with `-`.
+
+Emacs Interface
+---------------
+
+Init file
+
+  If the file pointed by new variable `notmuch-init-file` (typically
+  `~/.emacs.d/notmuch-config.el`) exists, it is loaded at the end of
+  `notmuch.el`. Users can put their personal notmuch emacs lisp based
+  configuration/customization items there instead of filling
+  `~/.emacs` with these.
+
+Changed format for saved searches
+
+  The format for `notmuch-saved-searches` has changed, but old style
+  saved searches are still supported. The new style means that a saved
+  search can store the desired sort order for the search, and it can
+  store a separate query to use for generating the count notmuch
+  shows.
+
+  The variable is fully customizable and any configuration done
+  through customize should *just work*, with the additional options
+  mentioned above. For manual customization see the documentation for
+  `notmuch-saved-searches`.
+
+  IMPORTANT: a new style notmuch-saved-searches variable will break
+  previous versions of notmuch-emacs (even search will not work); to
+  fix remove the customization for notmuch-saved-searches.
+
+  If you have a custom saved search sort function (not unsorted or
+  alphabetical) then the sort function will need to be
+  modified. Replacing (car saved-search) by (notmuch-saved-search-get
+  saved-search :name) and (cdr saved-search) by
+  (notmuch-saved-search-get saved-search :query) should be sufficient.
+
+The keys of `notmuch-tag-formats` are now regexps
+
+  Previously, the keys were literal strings.  Customized settings of
+  `notmuch-tag-formats` will continue to work as before unless tags
+  contain regexp special characters like `.` or `*`.
+
+Changed tags are now shown in the buffer
+
+  Previously tag changes made in a buffer were shown immediately. In
+  some cases (particularly automatic tag changes like marking read)
+  this made it hard to see what had happened (e.g., whether the
+  message had been unread).
+
+  The changes are now shown explicitly in the buffer: by default
+  deleted tags are displayed with red strike-through and added tags
+  are displayed underlined in green (inverse video is used for deleted
+  tags if the terminal does not support strike-through).
+
+  The variables `notmuch-tag-deleted-formats` and
+  `notmuch-tag-added-formats`, which have the same syntax as
+  `notmuch-tag-formats`, allow this to be customized.
+
+  Setting `notmuch-tag-deleted-formats` to `'((".*" nil))` and
+  `notmuch-tag-added-formats` to `'((".*" tag))` will give the old
+  behavior of hiding deleted tags and showing added tags identically
+  to tags already present.
+
+Version variable
+
+  The new, build-time generated variable `notmuch-emacs-version` is used
+  to distinguish between notmuch cli and notmuch emacs versions.
+  The function `notmuch-hello-versions` (bound to 'v' in notmuch-hello
+  window) prints both notmuch cli and notmuch emacs versions in case
+  these differ from each other.
+  This is especially useful when using notmuch remotely.
+
+Ido-completing-read initialization in Emacs 23
+
+  `ido-completing-read` in Emacs 23 versions 1 through 3 freezes unless
+  it is initialized. Defadvice-based *Ido* initialization is defined
+  for these Emacs versions.
+
+Bug fix for saved searches with newlines in them
+
+  Split lines confuse `notmuch count --batch`, so we remove embedded
+  newlines before calling notmuch count.
+
+Bug fixes for sender identities
+
+  Previously, Emacs would rewrite some sender identities in unexpected
+  and undesirable ways.  Now it will use identities exactly as
+  configured in `notmuch-identities`.
+
+Replies to encrypted messages will be encrypted by default
+
+  In the interest of maintaining confidentiality of communications,
+  the Notmuch Emacs interface now automatically adds the mml tag to
+  encrypt replies to encrypted messages. This should make it less
+  likely to accidentally reply to encrypted messages in plain text.
+
+Reply pushes mark before signature
+
+  We push mark and set point on reply so that the user can easily cut
+  the quoted text. The mark is now pushed before the signature, if
+  any, instead of end of buffer so the signature is preserved.
+
+Message piping uses the originating buffer's working directory
+
+  `notmuch-show-pipe-message` now uses the originating buffer's
+  current default directory instead of that of the `*notmuch-pipe*`
+  buffer's.
+
+nmbug
+-----
+
+nmbug adds a `clone` command for setting up the initial repository and
+uses `@{upstream}` instead of `FETCH_HEAD` to track upstream changes.
+
+  The `@{upstream}` change reduces ambiguity when fetching multiple
+  branches, but requires existing users update their `NMBGIT`
+  repository (usually `~/.nmbug`) to distinguish between local and
+  remote-tracking branches.  The easiest way to do this is:
+
+  1. If you have any purely local commits (i.e. they aren't in the
+     nmbug repository on nmbug.tethera.net), push them to a remote
+     repository.  We'll restore them from the backup in step 4.
+  2. Remove your `NMBGIT` repository (e.g. `mv .nmbug .nmbug.bak`).
+  3. Use the new `clone` command to create a fresh clone:
+
+        nmbug clone https://nmbug.notmuchmail.org/git/nmbug-tags.git
+
+  4. If you had local commits in step 1, add a remote for that
+     repository and fetch them into the new repository.
+
+Notmuch 0.17 (2013-12-30)
+=========================
+
+Incompatible change in SHA1 computation
+---------------------------------------
+
+Previously on big endian architectures like sparc and powerpc the
+computation of SHA1 hashes was incorrect. This meant that messages
+with overlong or missing message-ids were given different computed
+message-ids than on more common little endian architectures like i386
+and amd64.  If you use notmuch on a big endian architecture, you are
+strongly advised to make a backup of your tags using `notmuch dump`
+before this upgrade. You can locate the affected files using something
+like:
+
+    notmuch dump | \
+      awk '/^notmuch-sha1-[0-9a-f]{40} / \
+        {system("notmuch search --exclude=false --output=files id:" $1)}'
+
+Command-Line Interface
+----------------------
+
+New options to better support handling duplicate messages
+
+  If more than one message file is associated with a message-id,
+  `notmuch search --output=files` will print all of them. A new
+  `--duplicate=N` option can be used to specify which duplicate to
+  print for each message.
+
+  `notmuch count` now supports `--output=files` option to 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).
+
+Improved `notmuch new` performance for unchanged folders
+
+  `notmuch new` now skips over unchanged folders more efficiently,
+  which can substantially improve the performance of checking for new
+  mail in some situations (like NFS-mounted Maildirs).
+
+`notmuch reply --format=text` RFC 2047-encodes headers
+
+  Previously, this used a mix of standard MIME encoding for the reply
+  body and UTF-8 for the headers.  Now, the text format reply template
+  RFC 2047-encodes the headers, making the output a valid RFC 2822
+  message.  The JSON/sexp format is unchanged.
+
+`notmuch compact` command
+
+  The new `compact` command exposes Xapian's compaction
+  functionality through a more convenient interface than
+  `xapian-compact`. `notmuch compact` will compact the database to a
+  temporary location, optionally backup the original database, and
+  move the compacted database into place.
+
+Emacs Interface
+---------------
+
+`notmuch-tree` (formerly `notmuch-pick`) has been added to mainline
+
+  `notmuch-tree` is a threaded message view for the emacs
+  interface. Each message is one line in the results and the thread
+  structure is shown using UTF-8 box drawing characters (similar to
+  Mutt's threaded view). It comes between search and show in terms of
+  amount of output and can be useful for viewing both single threads
+  and multiple threads.
+
+  Using `notmuch-tree`
+
+  The main key entries to notmuch tree are
+
+  'z' enter a query to view using notmuch tree (works in hello,
+      search, show and tree mode itself)
+
+  'Z' view the current query in tree notmuch tree (works from search
+      and show)
+
+  Once in tree mode, keybindings are mostly in line with the rest of
+  notmuch and are all viewable with '?' as usual.
+
+  Customising `notmuch-tree`
+
+  `notmuch-tree` has several customisation variables. The most
+  significant is the first notmuch-tree-show-out which determines the
+  behaviour when selecting a message (with RET) in tree view. By
+  default tree view uses a split window showing the single message in
+  the bottom pane. However, if this option is set then it views the
+  whole thread in the complete window jumping to the selected message
+  in the thread. In either case command-prefix selects the other option.
+
+Tagging threads in search is now race-free
+
+  Previously, adding or removing a tag from a thread in a search
+  buffer would affect messages that had arrived after the search was
+  performed, resulting in, for example, archiving messages that were
+  never seen.  Tagging now affects only the messages that were in the
+  thread when the search was performed.
+
+`notmuch-hello` refreshes when switching to the buffer
+
+  The hello buffer now refreshes whenever you switch to the buffer,
+  regardless of how you get there.  You can disable automatic
+  refreshing by customizing `notmuch-hello-auto-refresh`.
+
+Specific mini-buffer prompts for tagging operations
+
+  When entering tags to add or remove, the mini-buffer prompt now
+  indicates what operation will be performed (e.g., "Tag thread", "Tag
+  message", etc).
+
+Built-in help improvements
+
+  Documentation for many commands has been improved, as displayed by
+  `notmuch-help` (usually bound to "?").  The bindings listed by
+  `notmuch-help` also now include descriptions of prefixed commands.
+
+Quote replies as they are displayed in show view
+
+  We now render the parts for reply quoting the same way they are
+  rendered for show. At this time, the notable change is that replies
+  to text/calendar are now pretty instead of raw vcalendar.
+
+Fixed inconsistent use of configured search order
+
+  All ways of interactively invoking search now honor the value of
+  `notmuch-search-oldest-first`.
+
+Common keymap for notmuch-wide bindings
+
+  Several key bindings have been moved from mode-specific keymaps to
+  the single `notmuch-common-keymap`, which is inherited by each
+  notmuch mode.  If you've customized your key bindings, you may want
+  to move some of them to the common keymap.
+
+The `notmuch-tag` function now requires a list of tag changes
+
+  For users who have scripted the Emacs interface: the `notmuch-tag`
+  API has changed.  Previously, it accepted either a list of tag
+  changes or a space-separated string of tag changes.  The latter is
+  no longer supported and the function now returns nothing.
+
+Fixed `notmuch-reply` putting reply in primary selection
+
+  On emacs 24 notmuch-reply used to put the cited text into the
+  primary selection (which could lead to inadvertently pasting this
+  cited text elsewhere). Now the primary-selection is not changed.
+
+Fixed `notmuch-show` invisible part handling
+
+  In some obscure cases part buttons and invisibility had strange
+  interactions: in particular, the default action for some parts gave
+  the wrong action. This has been fixed.
+
+Fixed `notmuch-show` attachment viewers and stderr
+
+  In emacs 24.3+ viewing an attachment could cause spurious text to
+  appear in the show buffer (any stderr or stdout the viewer
+  produced). By default this output is now discarded. For debugging,
+  setting `notmuch-show-attachment-debug` causes notmuch to keep the
+  viewer's stderr and stdout in a separate buffer.
+
+Fixed `notmuch-mua-reply` point placement when signature involved
+
+  By restricting cursor movement to body section for cursor placement
+  after signature is inserted, the cursor cannot "leak" to header
+  section anymore. Now inserted citation content will definitely go to
+  the body part of the message.
+
+Vim Interface
+-------------
+
+  It is now possible to compose new messages in the Vim interface, as
+  opposed reply to existing messages.  There is also support for
+  going straight to a search (bypassing the folders view).
+
+Notmuch 0.16 (2013-08-03)
+=========================
+
+Command-Line Interface
+----------------------
+
+Support for delivering messages to Maildir
+
+  There is a new command `insert` that adds a message to a Maildir
+  folder and notmuch index.
+
+`notmuch count --batch` option
+
+  `notmuch count` now supports batch operations similar to `notmuch
+  tag`. This is mostly an optimization for remote notmuch usage.
+
+`notmuch tag` option to remove all tags from matching messages
+
+  `notmuch tag --remove-all` option has been added to remove all tags
+  from matching messages. This can be combined with adding new tags,
+  resulting in setting (rather than modifying) the tags of the
+  messages.
+
+Decrypting commands explicitly expect a gpg-agent
+
+  Decryption in `notmuch show` and `notmuch reply` has only ever
+  worked with a functioning gpg-agent. This is now made explicit in
+  code and documentation. The functional change is that it's now
+  possible to have gpg-agent running, but gpg "use-agent"
+  configuration option disabled, not forcing the user to use the agent
+  everywhere.
+
+Configuration file saves follow symbolic links
+
+  The notmuch commands that save the configuration file now follow
+  symbolic links instead of overwrite them.
+
+Top level option to specify configuration file
+
+  It's now possible to specify the configuration file to use on the
+  command line using the `notmuch --config=FILE` option.
+
+Bash command-line completion
+
+  The notmuch command-line completion support for the bash shell has
+  been rewritten. Supported completions include all the notmuch
+  commands, command-line arguments, values for keyword arguments,
+  search prefixes (such as "subject:" or "from:") in all commands that
+  use search terms, tags after + and - in `notmuch tag`, tags after
+  "tag:" prefix, user's email addresses after "from:" and "to:"
+  prefixes, and config options (and some config option values) in
+  `notmuch config`. The new completion support depends on the
+  bash-completion package.
+
+Deprecated commands "part" and "search-tags" are removed.
+
+Emacs Interface
+---------------
+
+New keymap to view/save parts; removed s/v/o/| part button bindings
+
+  The commands to view, save, and open MIME parts are now prefixed
+  with "." (". s" to save, ". v" to view, etc) and can be invoked with
+  point anywhere in a part, unlike the old commands, which were
+  restricted to part buttons.  The old "s"/"v"/"o"/"|" commands on
+  part buttons have been removed since they clashed with other
+  bindings (notably "s" for search!) and could not be invoked when
+  there was no part button.  The new, prefixed bindings appear in
+  show's help, so you no longer have to memorize them.
+
+Default part save directory is now `mm-default-directory`
+
+  Previously, notmuch offered to save parts and attachments to a mix
+  of `mm-default-directory`, `mailcap-download-directory`, and `~/`.
+  This has been standardized on `mm-default-directory`.
+
+Key bindings for next/previous thread
+
+  Show view has new key bindings M-n and M-p to move to the next and
+  previous thread in the search results.
+
+Better handling of errors in search buffers
+
+  Instead of interleaving errors in search result buffers, search mode
+  now reports errors in the minibuffer.
+
+Faster search and show
+
+  Communication between Emacs and the notmuch CLI is now more
+  efficient because it uses the CLI's S-expression support.  As a
+  result, search mode should now fill search buffers faster and
+  threads should show faster.
+
+No Emacs 22 support
+
+  The Emacs 22 support added late 2010 was sufficient only for a short
+  period of time. After being incomplete for roughly 2 years the code
+  in question was now removed from this release.
+
+Vim Front-End
+-------------
+
+The vim based front-end has been replaced with a new one that uses the Ruby
+bindings. The old font-end is available in the contrib subfolder.
+
+Python Bindings
+---------------
+
+Fix loading of libnotmuch shared library on OS X (Darwin) systems.
+
+Notmuch 0.15.2 (2013-02-17)
+===========================
+
+Build fixes
+-----------
+
+Update dependencies to avoid problems when building in parallel.
+
+Internal test framework changes
+-------------------------------
+
+Adjust Emacs test watchdog mechanism to cope with `process-attributes`
+being unimplemented.
+
+Notmuch 0.15.1 (2013-01-24)
+===========================
+
+Internal test framework changes
+-------------------------------
+
+Set a default value for TERM when running tests. This fixes certain
+build failures in non-interactive environments.
+
+Notmuch 0.15 (2013-01-18)
+=========================
+
+General
+-------
+
+Date range search support
+
+  The `date:` prefix can now be used in queries to restrict the results
+  to only messages within a particular time range (based on the Date:
+  header) with a range syntax of `date:<since>..<until>`. Notmuch
+  supports a wide variety of expressions in `<since>` and
+  `<until>`. Please refer to the `notmuch-search-terms(7)` manual page
+  for details.
+
+Empty tag names and tags beginning with "-" are deprecated
+
+  Such tags have been a frequent source of confusion and cause
+  (sometimes unresolvable) conflicts with other syntax.  notmuch tag
+  no longer allows such tags to be added to messages.  Removing such
+  tags continues to be supported to allow cleanup of existing tags,
+  but may be removed in a future release.
+
+Command-Line Interface
+----------------------
+
+`notmuch new` no longer chokes on mboxes
+
+  `notmuch new` now rejects mbox files containing more than one
+  message, rather than treating the file as one giant message.
+
+Support for single message mboxes is deprecated
+
+  For historical reasons, `notmuch new` will index mbox files
+  containing a single message; however, this behavior is now
+  officially deprecated.
+
+Fixed `notmuch new` to skip ignored broken symlinks
+
+  `notmuch new` now correctly skips symlinks if they are in the
+  ignored files list.  Previously, it would abort when encountering
+  broken symlink, even if it was ignored.
+
+New dump/restore format and tagging interface
+
+  There is a new `batch-tag` format for dump and restore that is more
+  robust, particularly with respect to tags and message-ids containing
+  whitespace.
+
+  `notmuch tag` now supports the ability to read tag operations and
+  queries from an input stream, in a format compatible with the new
+  dump/restore format.
+
+Bcc and Reply-To headers are now available in notmuch show json output
+
+  The `notmuch show --format=json` now includes "Bcc" and "Reply-To" headers.
+  For example notmuch Emacs client can now have these headers visible
+  when the headers are added to the `notmuch-message-headers` variable.
+
+CLI callers can now request a specific output format version
+
+  `notmuch` subcommands that support structured output now support a
+  `--format-version` argument for requesting a specific version of the
+  structured output, enabling better compatibility and error handling.
+
+`notmuch search` has gained a null character separated text output format
+
+  The new --format=text0 output format for `notmuch search` prints
+  output separated by null characters rather than newline
+  characters. This is similar to the find(1) -print0 option, and works
+  together with the xargs(1) -0 option.
+
+Emacs Interface
+---------------
+
+Removal of the deprecated `notmuch-folders` variable
+
+  `notmuch-folders` has been deprecated since the introduction of saved
+  searches and the notmuch hello view in notmuch 0.3. `notmuch-folders`
+  has now been removed. Any remaining users should migrate to
+  `notmuch-saved-searches`.
+
+Visibility of MIME parts can be toggled
+
+  Each part of a multi-part MIME email can now be shown or hidden
+  using the button at the top of each part (by pressing RET on it or
+  by clicking).  For emails with multiple alternative formats (e.g.,
+  plain text and HTML), only the preferred format is shown initially,
+  but other formats can be shown using their part buttons.  To control
+  the behavior of this, see
+  `notmuch-multipart/alternative-discouraged` and
+  `notmuch-show-all-multipart/alternative-parts`.
+
+  Note notmuch-show-print-message (bound to '#' by default) will print
+  all parts of multipart/alternative message regardless of whether
+  they are currently hidden or shown in the buffer.
+
+Emacs now buttonizes mid: links
+
+  mid: links are a standardized way to link to messages by message ID
+  (see RFC 2392).  Emacs now hyperlinks mid: links to the appropriate
+  notmuch search.
+
+Handle errors from bodypart insertions
+
+  If displaying the text of a message in show mode causes an error (in
+  the `notmuch-show-insert-part-*` functions), notmuch no longer cuts
+  off thread display at the offending message.  The error is now
+  simply displayed in place of the message.
+
+Emacs now detects version mismatches with the notmuch CLI
+
+  Emacs now detects and reports when the Emacs interface version and
+  the notmuch CLI version are incompatible.
+
+Improved text/calendar content handling
+
+  Carriage returns in embedded text/calendar content caused insertion
+  of the calendar content fail. Now CRs are removed before calling icalendar
+  to extract icalendar data. In case icalendar extraction fails an error
+  is thrown for the bodypart insertion function to deal with.
+
+Disabled coding conversions when reading in `with-current-notmuch-show-message`
+
+  Depending on the user's locale, saving attachments containing 8-bit
+  data may have performed an unintentional encoding conversion,
+  corrupting the saved attachment.  This has been fixed by making
+  `with-current-notmuch-show-message` disable coding conversion.
+
+Fixed errors with HTML email containing images in Emacs 24
+
+  Emacs 24 ships with a new HTML renderer that produces better output,
+  but is slightly buggy.  We work around a bug that caused it to fail
+  for HTML email containing images.
+
+Fixed handling of tags with unusual characters in them
+
+  Emacs now handles tags containing spaces, quotes, and parenthesis.
+
+Fixed buttonization of id: links without quote characters
+
+  Emacs now correctly buttonizes id: links where the message ID is not
+  quoted.
+
+`notmuch-hello` refresh point placement improvements
+
+  Refreshing the `notmuch-hello` buffer does a better job of keeping
+  the point where it was.
+
+Automatic tag changes are now unified and customizable
+
+  All the automatic tag changes that the Emacs interface makes when
+  reading, archiving, or replying to messages, can now be
+  customized. Any number of tag additions and removals is supported
+  through the `notmuch-show-mark-read`, `notmuch-archive-tags`, and
+  `notmuch-message-replied-tags` customization variables.
+
+Support for stashing the thread id in show view
+
+  Invoking `notmuch-show-stash-message-id` with a prefix argument
+  stashes the (local and database specific) thread id of the current
+  thread instead of the message id.
+
+New add-on tool: notmuch-pick
+-----------------------------
+
+The new contrib/ tool `notmuch-pick` is an experimental threaded message
+view for the emacs interface. Each message is one line in the results
+and the thread structure is shown using UTF-8 box drawing characters
+(similar to Mutt's threaded view). It comes between search and show in
+terms of amount of output and can be useful for viewing both single
+threads and multiple threads. See the notmuch-pick README file for
+further details and installation.
+
+Portability
+-----------
+
+notmuch now builds on OpenBSD.
+
+Internal test framework changes
+-------------------------------
+
+The emacsclient binary is now user-configurable
+
+  The test framework now accepts `TEST_EMACSCLIENT` in addition to
+  `TEST_EMACS` for configuring the emacsclient to use.  This is
+  necessary to avoid using an old emacsclient with a new emacs, which
+  can result in buggy behavior.
+
+Notmuch 0.14 (2012-08-20)
+=========================
+
+General bug fixes
+-----------------
+
+Maildir tag synchronization
+
+  Maildir flag-to-tag synchronization now applies only to messages in
+  maildir-like directory structures.  Previously, it applied to any
+  message that had a maildir "info" part, which meant it could
+  incorrectly synchronize tags for non-maildir messages, while at the
+  same time failing to synchronize tags for newly received maildir
+  messages (typically causing new messages to not receive the "unread"
+  tag).
+
+Command-Line Interface
+----------------------
+
+  The deprecated positional output file argument to `notmuch dump` has
+  been replaced with an `--output` option. The input file positional
+  argument to `notmuch restore` has been replaced with an `--input`
+  option for consistency with dump.  These changes simplify the syntax
+  of dump/restore options and make them more consistent with other
+  notmuch commands.
+
+Emacs Interface
+---------------
+
+Search results now get re-colored when tags are updated
+
+The formatting of tags in search results can now be customized
+
+  Previously, attempting to change the format of tags in
+  `notmuch-search-result-format` would usually break tagging from
+  search-mode.  We no longer make assumptions about the format.
+
+Experimental support for multi-line search result formats
+
+  It is now possible to embed newlines in
+  `notmuch-search-result-format` to make individual search results
+  span multiple lines.
+
+Next/previous in search and show now move by boundaries
+
+  All "next" and "previous" commands in the search and show modes now
+  move to the next/previous result or message boundary.  This doesn't
+  change the behavior of "next", but "previous" commands will first
+  move to the beginning of the current result or message if point is
+  inside the result or message.
+
+Search now uses the JSON format internally
+
+  This should address problems with unusual characters in authors and
+  subject lines that could confuse the old text-based search parser.
+
+The date shown in search results is no longer padded before applying
+user-specified formatting
+
+  Previously, the date in the search results was padded to fixed width
+  before being formatted with `notmuch-search-result-format`.  It is
+  no longer padded.  The default format has been updated, but if
+  you've customized this variable, you may have to change your date
+  format from `"%s "` to `"%12s "`.
+
+The thread-id for the `target-thread` argument for `notmuch-search` should
+now be supplied without the "thread:" prefix.
+
+Notmuch 0.13.2 (2012-06-02)
+===========================
+
+Bug-fix release
+---------------
+
+Update `contrib/notmuch-deliver` for API changes in 0.13. This fixes a
+compilation error for this contrib package.
+
+Notmuch 0.13.1 (2012-05-29)
+===========================
+
+Bug-fix release
+---------------
+
+Fix inserting of UTF-8 characters from *text/plain* parts in reply
+
+  While notmuch gained ability to insert content from other than *text/plain*
+  parts of email whenever *text/plain* parts are not available (notably
+  HTML-only emails), replying to mails that do have *text/plain* the
+  non-ASCII characters were incorrectly decoded. This is now fixed.
+
+`notmuch_database_get_directory` and
+`notmuch_database_find_message_by_filename` now work on read-only
+databases
+
+  Previously, these functions attempted to create directory documents
+  that didn't exist and would return an error or abort when given a
+  read-only database.  Now they no longer create directory documents
+  and simply return a `NULL` object if the directory does not exist,
+  as documented.
+
+Fix compilation of ruby bindings
+
+  Revert to dynamic linking, since the statically linked bindings did
+  not work well.
+
+Notmuch 0.13 (2012-05-15)
+=========================
+
+Command-Line Interface
+----------------------
+
+JSON reply format
+
+  `notmuch reply` can now produce JSON output that contains the headers
+  for a reply message and full information about the original message
+  begin replied to. This allows MUAs to create replies intelligently.
+  For example, an MUA that can parse HTML might quote HTML parts.
+
+  Calling notmuch reply with `--format=json` imposes the restriction that
+  only a single message is returned by the search, as replying to
+  multiple messages does not have a well-defined behavior. The default
+  retains its current behavior for multiple message replies.
+
+Tag exclusion
+
+  Tags can be automatically excluded from search results by adding them
+  to the new `search.exclude_tags` option in the Notmuch config file.
+
+  This behaviour can be overridden by explicitly including an excluded
+  tag in your query, for example:
+
+        notmuch search $your_query and tag:$excluded_tag
+
+  Existing users will probably want to run `notmuch setup` again to add
+  the new well-commented [search] section to the configuration file.
+
+  For new configurations, accepting the default setting will cause the
+  tags "deleted" and "spam" to be excluded, equivalent to running:
+
+        notmuch config set search.exclude_tags deleted spam
+
+Raw show format changes
+
+  The output of show `--format=raw` has changed for multipart and
+  message parts.  Previously, the output was a mash of somewhat-parsed
+  headers and transfer-decoded bodies.  Now, such parts are reproduced
+  faithfully from the original source.  Message parts (which includes
+  part 0) output the full message, including the message headers (but
+  not the transfer headers).  Multipart parts output the part as
+  encoded in the original message, including the part's headers.  Leaf
+  parts, as before, output the part's transfer-decoded body.
+
+Listing configuration items
+
+  The new `config list` command prints out all configuration items and
+  their values.
+
+Emacs Interface
+---------------
+
+Changes to tagging interface
+
+  The user-facing tagging functions in the Emacs interface have been
+  normalized across all notmuch modes.  The tagging functions are now
+  notmuch-search-tag in search-mode, and notmuch-show-tag in
+  show-mode.  They accept a string representing a single tag change,
+  or a list of tag changes.  See 'M-x describe-function notmuch-tag'
+  for more information.
+
+  NOTE: This breaks compatibility with old tagging functions, so user
+  may need to update in custom configurations.
+
+Reply improvement using the JSON format
+
+  Emacs now uses the JSON reply format to create replies. It obeys
+  the customization variables message-citation-line-format and
+  message-citation-line-function when creating the first line of the
+  reply body, and it will quote HTML parts if no text/plain parts are
+  available.
+
+New add-on tool: notmuch-mutt
+-----------------------------
+
+The new contrib/ tool `notmuch-mutt` provides Notmuch integration for
+the Mutt mail user agent. Using it, Mutt users can perform mail
+search, thread reconstruction, and mail tagging/untagging without
+leaving Mutt.  notmuch-mutt, formerly distributed under the name
+`mutt-notmuch` by Stefano Zacchiroli, will be maintained as a notmuch
+contrib/ from now on.
+
+Library changes
+---------------
+
+The API changes detailed below break binary and source compatibility,
+so libnotmuch has been bumped to version 3.0.0.
+
+The function `notmuch_database_close` has been split into
+`notmuch_database_close` and `notmuch_database_destroy`
+
+  This makes it possible for long running programs to close the xapian
+  database and thus release the lock associated with it without
+  destroying the data structures obtained from it.
+
+`notmuch_database_open`, `notmuch_database_create`, and
+`notmuch_database_get_directory` now return errors
+
+  The type signatures of these functions have changed so that the
+  functions now return a `notmuch_status_t` and take an out-argument for
+  returning the new database object or directory object.
+
+Go bindings changes
+-------------------
+
+Go 1 compatibility
+
+  The go bindings and the `notmuch-addrlookup` utility are now
+  compatible with go 1.
+
+Notmuch 0.12 (2012-03-20)
+=========================
+
+Command-Line Interface
+----------------------
+
+Reply to sender
+
+  `notmuch reply` has gained the ability to create a reply template
+  for replying just to the sender of the message, in addition to reply
+  to all. The feature is available through the new command line option
+  `--reply-to=(all|sender)`.
+
+Mail store folder/file ignore
+
+  A new configuration option, `new.ignore`, lets users specify a
+  ;-separated list of file and directory names that will not be
+  searched for messages by `notmuch new`.
+
+  NOTE: *Every* file/directory that goes by one of those names will
+  be ignored, independent of its depth/location in the mail store.
+
+Unified help and manual pages
+
+  The notmuch help command now runs man for the appropriate page.  If
+  you install notmuch somewhere "unusual", you may need to update
+  MANPATH.
+
+Manual page for notmuch configuration options
+
+  The notmuch CLI configuration file options are now documented in the
+  notmuch-config(1) manual page in addition to the configuration file
+  itself.
+
+Emacs Interface
+---------------
+
+Reply to sender
+
+  The Emacs interface has, with the new CLI support, gained the
+  ability to reply to sender in addition to reply to all. In both show
+  and search modes, 'r' has been bound to reply to sender, replacing
+  reply to all, which now has key binding 'R'.
+
+More flexible and consistent tagging operations
+
+  All tagging operations ('+', '-', '*') now accept multiple tags with
+  '+' or '-' prefix, like '*' operation in notmuch-search view before.
+
+  '*' operation (`notmuch-show-tag-all`) is now available in
+  notmuch-show view.
+
+  `notmuch-show-{add,remove}-tag` functions no longer accept tag
+  argument, `notmuch-show-tag-message` should be used instead.  Custom
+  bindings using these functions should be updated, e.g.:
+
+        (notmuch-show-remove-tag "unread")
+
+  should be changed to:
+
+        (notmuch-show-tag-message "-unread")
+
+Refreshing the show view ('=' by default) no longer opens or closes messages
+
+  To get the old behavior of putting messages back in their initial
+  opened/closed state, use a prefix argument, e.g., 'C-u ='.
+
+Attachment buttons can be used to view or save attachments.
+
+  When the cursor is on an attachment button the key 's' can be used
+  to save the attachment, the key 'v' to view the attachment in the
+  default mailcap application, and the key 'o' prompts the user for an
+  application to use to open the attachment. By default Enter or mouse
+  button 1 saves the attachment but this is customisable (option
+  Notmuch Show Part Button Default Action).
+
+New functions
+
+  `notmuch-show-stash-mlarchive-link{,-and-go}` allow stashing and
+  optionally visiting a URI to the current message at one of a number
+  of Mailing List Archives.
+
+Fix MML tag quoting in replies
+
+  The MML tag quoting fix of 0.11.1 unintentionally quoted tags
+  inserted in `message-setup-hook`. Quoting is now limited to the
+  cited message.
+
+Show view archiving key binding changes
+
+  The show view archiving key bindings 'a' and 'x' now remove the
+  "inbox" tag from the current message only (instead of thread), and
+  move to the next message. At the last message, 'a' proceeds to the
+  next thread in search results, and 'x' returns to search
+  results. The thread archiving functions are now available in 'A' and
+  'X'.
+
+Support text/calendar MIME type
+
+  The text/calendar MIME type is now supported in addition to
+  text/x-vcalendar.
+
+Generate inline patch fake attachment file names from message subject
+
+  Use the message subject to generate file names for the inline patch
+  fake attachments. The names are now similar to the ones generated by
+  'git format-patch' instead of just "inline patch". See "Notmuch Show
+  Insert Text/Plain Hook" in the notmuch customize interface.
+
+Enable `notmuch-search-line-faces` by default
+
+  Make the `notmuch-search-line-faces` functionality more discoverable
+  for new users by showing "unread" messages bold and "flagged"
+  messages blue by default in the search view.
+
+Printing Support
+
+  notmuch-show mode now has simple printing support, bound to '#' by
+  default. You can customize the variable notmuch-print-mechanism.
+
+Library changes
+---------------
+
+New functions
+
+  `notmuch_query_add_tag_exclude` supports the new tag exclusion
+  feature.
+
+Python bindings changes
+-----------------------
+
+Python 3.2 compatibility
+
+  The python bindings are now compatible with both python 2.5+ and 3.2.
+
+Added missing unicode conversions
+
+  Python strings have to be encoded to and decoded from utf-8 when
+  calling libnotmuch functions. Porting the bindings to python 3.2
+  revealed a few function calls that were missing these conversions.
+
+Build fixes
+-----------
+
+Compatibility with GMime 2.6
+
+  It is now possible to build notmuch against both GMime 2.4 and 2.6.
+  However, a bug in GMime 2.6 before 2.6.5 causes notmuch not to
+  report signatures where the signer key is unavailable (GNOME bug
+  668085).  For compatibility with GMime 2.4's tolerance of "From "
+  headers we require GMime 2.6 >= 2.6.7.
+
+Notmuch 0.11.1 (2012-02-03)
+===========================
+
+Bug-fix release
+---------------
+
+Fix error handling in python bindings
+
+  The python bindings in 0.11 failed to detect NULL pointers being
+  returned from libnotmuch functions and thus failed to raise
+  exceptions to indicate the error condition. Any subsequent calls
+  into libnotmuch caused segmentation faults.
+
+Quote MML tags in replies
+
+  MML tags are text codes that Emacs uses to indicate attachments
+  (among other things) in messages being composed.  The Emacs
+  interface did not quote MML tags in the quoted text of a reply.
+  User could be tricked into replying to a maliciously formatted
+  message and not editing out the MML tags from the quoted text.  This
+  could lead to files from the user's machine being attached to the
+  outgoing message.  The Emacs interface now quotes these tags in
+  reply text, so that they do not effect outgoing messages.
+
+Notmuch 0.11 (2012-01-13)
+=========================
+
+Command-Line Interface
+----------------------
+
+Hooks
+
+  Hooks have been introduced to notmuch. Hooks are scripts that notmuch
+  invokes before and after certain actions. Initially, `notmuch new`
+  supports `pre-new` and `post-new` hooks that are run before and after
+  importing new messages into the database.
+
+`notmuch reply --decrypt bugfix`
+
+  The `notmuch reply` command with `--decrypt` argument had a rarely
+  occurring bug that caused an encrypted message not to be decrypted
+  sometimes. This is now fixed.
+
+Performance
+-----------
+
+Automatic tag query optimization
+
+  `notmuch tag` now automatically optimizes the user's query to
+  exclude messages whose tags won't change.  In the past, we've
+  suggested that people do this by hand; this is no longer necessary.
+
+Don't sort messages when creating a dump file
+
+  This speeds up tag dumps considerably, without any loss of
+  information. To replicate the old behavior of sorted output (for
+  example to compare two dump files), one can use e.g. `sort(1)`.
+
+Memory Management
+-----------------
+
+Reduction of memory leaks
+
+  Two memory leaks when searching and showing messages were identified
+  and fixed in this release.
+
+Emacs Interface
+---------------
+
+Bug fixes
+
+  notmuch-show-advance (bound to the spacebar in notmuch-show-mode) had
+  a bug that caused it to always jump to the next message, even if it
+  should have scrolled down to show more of the current message instead.
+  This is now fixed.
+
+Support `notmuch new` as a notmuch-poll-script
+
+  It's now possible to use `notmuch new` as a notmuch-poll-script
+  directly. This is also the new default. This allows taking better
+  advantage of the `notmuch new` hooks from emacs without intermediate
+  scripts.
+
+Improvements in saved search management
+
+  New saved searches are now appended to the list of saved searches,
+  not inserted in front. It's also possible to define a sort function
+  for displaying saved searches; alphabetical sort is provided.
+
+Hooks for notmuch-hello
+
+  Two new hooks have been added: "notmuch-hello-mode-hook" (called after
+  entering notmuch-hello-mode) and "notmuch-hello-refresh-hook" (called
+  after updating a notmuch-hello buffer).
+
+New face for crypto parts headers
+
+  Crypto parts used to be displayed with a hardcoded color. A new face
+  has been introduced to fix this: notmuch-crypto-part-header. It
+  defaults to the same value as before, but can be customized to match
+  other color themes.
+
+Use space as default thousands separator
+
+  Large numbers in notmuch-hello are now displayed using a space as
+  thousands separator (e.g. "123 456" instead of "123,456"). This can be
+  changed by customizing "notmuch-hello-thousands-separator".
+
+Call notmuch-show instead of notmuch-search when clicking on
+buttonized id: links
+
+New function notmuch-show-advance
+
+  This new function advances through just the current thread, and is
+  less invasive than notmuch-show-advance-and-archive.  It can easily
+  be bound to SPC with:
+
+        (define-key notmuch-show-mode-map " " 'notmuch-show-advance)
+
+Various performance improvements
+
+New add-on tool
+---------------
+
+The tool `contrib/notmuch-deliver` helps with initial delivery and
+tagging of mail (replacing running `notmuch new`).
+
+
+Notmuch 0.10.2 (2011-12-04)
+===========================
+
+Bug-fix release
+---------------
+
+Fix crash in python bindings
+
+  The python bindings did not call `g_type_init`, which caused crashes
+  for some, but not all users.
+
+Notmuch 0.10.1 (2011-11-25)
+===========================
+
+Bug-fix release
+---------------
+
+Fix `--help` argument
+
+  Argument processing changes in 0.10 introduced a bug where
+  `notmuch --help` crashed while `notmuch help` worked fine.
+  This is fixed in 0.10.1.
+
+Notmuch 0.10 (2011-11-23)
+=========================
+
+New build and testing features
+------------------------------
+
+Emacs tests are now done in `dtach`. This means that dtach is now
+needed to run the notmuch test suite, at least until the checking for
+prerequisites is improved.
+
+Full test coverage of the stashing feature in Emacs.
+
+New command-line features
+-------------------------
+
+Add `notmuch restore --accumulate` option
+
+  The `--accumulate` switch causes the union of the existing and new tags to
+  be applied, instead of replacing each message's tags as they are read in
+  from the dump file.
+
+Add search terms to `notmuch dump`
+
+  The dump command now takes an optional search term much like notmuch
+  search/show/tag. The output file argument of dump is deprecated in
+  favour of using stdout.
+
+Add `notmuch search` `--offset` and `--limit` options
+
+  The search command now takes options `--offset=[-]N` and `--limit=N` to
+  limit the number of results shown.
+
+Add `notmuch count --output` option
+
+  The count command is now capable of counting threads in addition to
+  messages. This is selected using the new `--output=(threads|messages)`
+  option.
+
+New emacs UI features
+---------------------
+
+Add tab-completion for `notmuch-search` and `notmuch-search-filter`
+
+  These functions now support completion tags for query parts
+  starting with "tag:".
+
+Turn "id:MSG-ID" links into buttons associated with notmuch searches
+
+  Text of the form "id:MSG-ID" in mails is now a clickable button that
+  opens a notmuch search for the given message id.
+
+Add keybinding ('c I') for stashing Message-ID's without an id: prefix
+
+  Reduces manual labor when stashing them for use outside notmuch.
+
+Do not query on `notmuch-search` exit
+
+  It is harmless to kill the external notmuch process, so the user
+  is no longer interrogated when they interrupt a search.
+
+Performance
+-----------
+
+Emacs now constructs large search buffers more efficiently
+
+Search avoids opening and parsing message files
+
+  We now store more information in the database so search no longer
+  has to open every message file to get basic headers.  This can
+  improve search speed by as much as 10X, but taking advantage of this
+  requires a database rebuild:
+
+        notmuch dump > notmuch.dump
+        # Backup, then remove notmuch database ($MAIL/.notmuch)
+        notmuch new
+        notmuch restore notmuch.dump
+
+New collection of add-on tools
+------------------------------
+
+The source directory "contrib" contains tools built on notmuch.  These
+tools are not part of notmuch, and you should check their individual
+licenses.  Feel free to report problems with them to the notmuch
+mailing list.
+
+nmbug - share tags with a given prefix
+
+  nmbug helps maintain a git repo containing all tags with a given
+  prefix (by default "notmuch::"). Tags can be shared by committing
+  them to git in one location and restoring in another.
+
+Notmuch 0.9 (2011-10-01)
+========================
+
+New, general features
+---------------------
+
+Correct handling of interruptions during `notmuch new`
+
+  `notmuch new` now operates as a series of small, self-consistent
+  transactions, so it can correctly resume after an interruption or
+  crash.  Previously, interruption could lose existing tags, fail to
+  detect messages on resume, or leave the database in a state
+  temporarily or permanently inconsistent with the mail store.
+
+Library changes
+---------------
+
+New functions
+
+  `notmuch_database_begin_atomic` and `notmuch_database_end_atomic`
+  allow multiple database operations to be performed atomically.
+
+  `notmuch_database_find_message_by_filename` does exactly what it says.
+
+API changes
+
+  `notmuch_database_find_message` (and `n_d_f_m_by_filename`) now return
+  a status indicator and uses an output parameter for the
+  message. This change required changing the SONAME of libnotmuch to
+  libnotmuch.so.2
+
+Python bindings changes
+-----------------------
+
+  - Re-encode python unicode objects to utf-8 before passing back to
+    libnotmuch.
+  - Support `Database().begin_atomic()/end_atomic()`
+  - Support `Database().find_message_by_filename()`
+    NB! This needs a db opened in READ-WRITE mode currently, or it will crash
+    the python process. The is a limitation (=bug) of the underlying libnotmuch.
+  - Fixes where we would not throw NotmuchErrors when we should (Justus Winter)
+  - Update for `n_d_find_message*` API changes (see above).
+
+Ruby bindings changes
+---------------------
+
+  - Wrap new library functions `notmuch_database_{begin,end}_atomic.`
+  - Add new exception `Notmuch::UnbalancedAtomicError.`
+  - Rename destroy to destroy! according to Ruby naming conventions.
+  - Update for `n_d_find_message*` API changes (see above).
+
+Emacs improvements
+------------------
+
+  * Add gpg callback to crypto sigstatus buttons to retrieve/refresh
+    signing key.
+  * Add `notmuch-show-refresh-view` function (and corresponding binding)
+    to refresh the view of a notmuch-show buffer.
+
+Reply formatting cleanup
+------------------------
+
+  `notmuch reply` no longer includes notification that non-leafnode
+  MIME parts are being suppressed.
+
+Notmuch 0.8 (2011-09-10)
+========================
+
+Improved handling of message/rfc822 parts
+
+  Both in the CLI and the emacs interface.  Output of rfc822 parts now
+  includes the primary headers, as well as the body and all subparts.
+  Output of the completely raw rfc822-formatted message, including all
+  headers, is unfortunately not yet supported (but hopefully will be
+  soon).
+
+Improved Build system portability
+
+  Certain parts of the shell script generating notmuch.sym were
+  specific to the GNU versions of sed and nm. The new version should
+  be more portable to e.g. OpenBSD.
+
+Documentation update for Ruby bindings
+
+  Added documentation, typo fixes, and improved support for rdoc.
+
+Unicode, iterator, PEP8 changes for python bindings
+
+  - PEP8 (code formatting) changes for python files.
+  - Remove `Tags.__len__` ; see 0.6 release notes for motivation.
+  - Decode headers as UTF8, encode (unicode) database paths as UTF8.
+
+Notmuch 0.7 (2011-08-01)
+========================
+
+Vim interface improvements
+--------------------------
+
+Jason Woofenden provided a number of bug fixes for the Vim interface
+
+  * fix citation/signature fold lengths
+  * fix cig/cit parsing within multipart/*
+  * fix on-screen instructions for show-signature
+  * fix from list reformatting in search view
+  * fix space key: now archives (did opposite)
+
+Uwe Kleine-König contributed
+
+  * use full path for sendmail/doc fix
+  * fix compose temp file name
+
+Python Bindings changes
+-----------------------
+
+Sebastian Spaeth contributed two changes related to unicode and UTF8:
+
+  * message tags are now explicitly unicode
+  * query string is encoded as a UTF8 byte string
+
+Build-System improvements
+-------------------------
+
+Generate notmuch.sym after the relevant object files
+
+  This fixes a bug in parallel building. Thanks to Thomas Jost for the
+  patch.
+
+Notmuch 0.6.1 (2011-07-17)
+==========================
+
+Bug-fix release
+---------------
+
+Re-export Xapian exception typeinfo symbols
+
+  It turned out our aggressive symbol hiding caused problems for
+  people running gcc 4.4.5.
+
+Notmuch 0.6 (2011-07-01)
+=======================
+
+New, general features
+---------------------
+
+Folder-based searching
+
+  Notmuch queries can now include a search term to match the
+  directories in which mail files are stored (within the mail
+  storage). The syntax is as follows:
+
+        folder:<path>
+
+  For example, one might use things such as:
+
+        folder:spam
+        folder:2011-*
+        folder:work/todo
+
+  to match any path containing a directory "spam", "work/todo", or
+  containing a directory starting with "2011-", respectively.
+
+  This feature is particularly useful for users of delivery-agent
+  software (such as procmail or maildrop) that is filtering mail and
+  delivering it to particular folders, or users of systems such as
+  Gmail that use filesystem directories to indicate message tags.
+
+  NOTE: Only messages that are newly indexed with this version of
+  notmuch will be searchable with folder: terms. In order to enable
+  this feature for all mail, the entire notmuch index will need to be
+  rebuilt as follows:
+
+        notmuch dump > notmuch.dump
+        # Backup, then remove notmuch database ($MAIL/.notmuch)
+        notmuch new
+        notmuch restore notmuch.dump
+
+Support for PGP/MIME
+
+  Both the command-line interface and the emacs-interface have new
+  support for PGP/MIME, detailed below. Thanks to Daniel Kahn Gillmor
+  and Jameson Graef Rollins for making this happen.
+
+New, automatic tags: "signed" and "encrypted"
+
+  These tags will automatically be applied to messages containing
+  multipart/signed and multipart/encrypted parts.
+
+  NOTE: Only messages that are newly indexed with this version of
+  notmuch will receive these tags.
+
+New command-line features
+-------------------------
+
+Add new "notmuch show --verify" option for signature verification
+
+  This option instruct notmuch to verify the signature of
+  PGP/MIME-signed parts.
+
+Add new "notmuch show --decrypt" and "notmuch reply --decrypt" options
+
+  This option instructs notmuch to decrypt PGP/MIME-encrypted parts.
+  Note that this feature currently requires gpg-agent and a passphrase entry
+  tool (e.g. pinentry-gtk or pinentry-curses).
+
+Proper nesting of multipart parts in "notmuch show" output
+
+  MIME parts are now display with proper nesting to reflect original
+  MIME hierarchy of a message. This allows clients to correctly
+  analyze the MIME structure, (such as, for example, determining to
+  which parts a signature part applies).
+
+Add new "notmuch show --part" option
+
+  This is a replacement for the older "notmuch part" command, (which
+  is now deprecated—it should still work as always, but is no longer
+  documented). Putting part output under "notmuch show" allows for all
+  of the "notmuch show" options to be applied when extracting a single
+  part, (such as --format=json for extracting a message part with JSON
+  formatting).
+
+Deprecate "notmuch search-tags" (in favor of "notmuch search --output=tags *")
+
+  The "notmuch search-tags" sub-command has been redundant since the
+  addition of the --output=tags option to "notmuch search". We now
+  make that more clear by deprecating "notmuch search-tags", (dropping
+  it from the documentation). We do continue to support the old syntax
+  by translating it internally to the new call.
+
+Performance improvements
+------------------------
+
+Faster searches (by doing fewer searches to construct threads)
+
+  Whenever a user asks for search results as threads, notmuch first
+  performs a search for messages matching the query, then performs
+  additional searches to find other messages in the resulting threads.
+
+  Removing inefficiencies and redundancies in these secondary searches
+  results in a measured speedups of 1.5x for a typical search.
+
+Faster searches (by doing fewer passes to gather message data)
+
+  Optimizing Xapian data access patterns (using a single pass to get
+  all message-document data rather than a pass for each data type)
+  results in a measured speedup of 1.7x for a typical search.
+
+  The benefits of this optimization combine with the preceding
+  optimization. With both in place, Austin Clements measured a speedup
+  of 2.5x for a search of all messages in his inbox (was 4.5s, now
+  1.8s). Thanks, Austin!
+
+Faster initial indexing
+
+  More efficient indexing of new messages results in a measured
+  speedup of 1.4x for the initial indexing of 3 GB of mail (1h 14m
+  rather than 1h 46m). Thanks to Austin Clements and Michal Sojka.
+
+Make "notmuch new" faster for unchanged directories
+
+  Optimizing to not do any further examinations of sub-directories
+  when the filesystem indicates that a directory is unchanged from the
+  last "notmuch new" results in measured speedups of 8.5 for the "No
+  new mail" case, (was 0.77s, now 0.09s). Thanks to Karel Zak.
+
+New emacs-interface features
+----------------------------
+
+Support for PGP/MIME (GnuPG)
+
+  Automatically indicate validity of signatures for multipart/signed
+  messages.  Automatically display decrypted content for
+  multipart/encrypted messages.  See the emacs variable
+  notmuch-crypto-process-mime for more information. Note that this
+  needs gpg-agent and a pinentry tool just as the command-line tools.
+  Also note there is no support SMIME yet.
+
+Output of pipe command is now displayed if pipe command fails
+
+  This is extremely useful in the common use case of piping a patch to
+  "git am". If git fails to cleanly merge the patch the error messages
+  from the failed merge are now clearly displayed to the user, (where
+  previously they were silently hidden from the user).
+
+User-selectable From address
+
+  A user can choose which configured email addresses should be used as
+  the From address whenever composing a new message. To do so, simply
+  press C-u before the command which will open a new message. Emacs
+  will prompt for the from address to use.
+
+  The user can customize the "Notmuch Identities" setting in the
+  notmuch customize group in order to use addresses other than those in
+  the notmuch configuration file if desired.
+
+  The user can also choose to always be prompted for the from address
+  when composing a new message (without having to use C-u) by setting
+  the "Notmuch Always Prompt For Sender" option in the notmuch
+  customize group.
+
+Hiding of repeated subjects in collapsed thread view
+
+  In notmuch-show mode, if a collapsed message has the same subject as
+  its parent, the subject is not shown.
+
+Automatic detection and hiding of original message in top-posted message
+
+  When a message contains a line looking something like:
+
+        ----- Original Message -----
+
+  emacs hides this and all subsequent lines as an "original message",
+  (allowing the user to click or press enter on the "original message"
+  button to display it again). This makes the handling of top-posted
+  citations work much like conventional citations.
+
+New hooks for running code when tags are modified
+
+  Some users want to perform additional actions whenever a particular
+  tag is added/removed from a message. This could be used to, for
+  example, interface with some external spam-recognition training
+  tool. To facilitate this, two new hooks are added which can be
+  modified in the following settings of the notmuch customize group:
+
+        Notmuch Before Tag Hook
+        Notmuch After Tag Hook
+
+New optional support for hiding some multipart/alternative parts
+
+  Many emails are sent with redundant content within a
+  multipart/alternative group (such as a text/plain part as well as a
+  text/html part). Users can configure the setting:
+
+        Notmuch Show All Multipart/Alternative Parts
+
+  to "off" in the notmuch customize group to have the interface
+  automatically hide some part alternatives (such as text/html
+  parts). This new part hiding is not configured by default yet
+  because there's not yet a simple way to re-display such a hidden
+  part if it is not actually redundant with a displayed part.
+
+Better rendering of text/x-vcalendar parts
+
+  These parts are now displayed in a format suitable for use with the
+  emacs diary.
+
+Avoid getting confused by Subject and Author fields with newline characters
+
+  Replacing all characters with ASCII code less than 32 with a question mark.
+
+Cleaner display of From line in email messages
+
+  Remove double quotes, and drop "name" if it's actually just a repeat of
+  the email address.
+
+Vim interface improvements
+--------------------------
+
+Felipe Contreras provided a number of updates for the vim interface:
+
+  * Using sendmail directly rather than mailx,
+  * Implementing archive in show view
+  * Add support to mark as read in show and search views
+  * Add delete commands
+  * Various cleanups.
+
+Bindings improvements
+---------------------
+
+Ruby bindings are now much more complete
+
+  Including `QUERY.sort`, `QUERY.to_s`, `MESSAGE.maildir_flags_to_tags`,
+  `MESSAGE.tags_to_maildir_flags`, and `MESSAGE.get_filenames`
+
+Python bindings have been updated and extended
+
+  (docs online at https://notmuch.readthedocs.io/)
+
+  New bindings:
+
+  - `Message().get_filenames()`, `Message().tags_to_maildir_flags()`,
+    `Message().maildir_flags_to_tags()`, `list(Threads())` and
+    `list(Messages)` works now
+  - `Message().__cmp__()` and `__hash__()`
+
+  These allow, for example:
+
+        if msg1 == msg2: ...
+
+  As well as set arithmetic on `Messages()`:
+
+        s1, s2 = set(msgs1), set(msgs2)
+        s1.union(s2)
+        s2 -= s1
+
+  Removed:
+
+  - `len(Messages())` as it exhausted the iterator
+
+  Use `len(list(Messages()))` or `Query.count_messages()`
+  to get the length.
+
+Added initial Go bindings in bindings/go
+
+New build-system features
+-------------------------
+
+Added support for building in a directory other than the source directory
+
+  This can be used with the widely-supported idiom of simply running
+  the configure script from some other directory:
+
+        mkdir build
+        cd build
+        ../configure
+        make
+
+Fix to save configure options for future, implicit runs of configure
+
+  When a user updates the source (such as with "git pull") calling
+  "make" may cause an automatic re-run of the configure script. When
+  this happens, the configure script will automatically be called with
+  the same options the user originally passed in the most-recent
+  manual invocation of configure.
+
+New test-suite feature
+----------------------
+
+Binary for bash for running test suite now located via PATH
+
+  The notmuch test suite requires a fairly recent version of bash (>=
+  bash 4). As some systems supply an older version of bash at
+  /bin/bash, the test suite is now updated to search $PATH to locate
+  the bash binary. This allows users of systems with old /bin/bash to
+  simply install bash >= 4 somewhere on $PATH before /bin and then use
+  the test suite.
+
+Support for testing output with a trailing newline
+
+  Previously, some tests would fail to notice a difference in the
+  presence/absence of a trailing newline in a program output, (which
+  has led to bugs in the past). Now, carefully-written tests (using
+  `test_expect_equal_file` rather than `test_expect_equal`) will detect
+  any change in the presence/absence of a trailing newline. Many tests
+  are updated to take advantage of this.
+
+Avoiding accessing user's $HOME while running test suite
+
+  The test suite now carefully creates its own HOME directory. This
+  allows the test suite to be run with no existing HOME directory, (as
+  some build systems apparently do), and avoids test-suite differences
+  due to configuration files in the users HOME directory.
+
+
+General bug fixes
+-----------------
+
+Output *all* files for "notmuch search --output=files"
+
+  For the cases where multiple files have the same Message ID,
+  previous versions of notmuch would output only one such file. This
+  command is now fixed to correctly output all files.
+
+Fixed spurious search results from "overlapped" indexing of addresses
+
+  This fixed a bug where a search for:
+
+        to:user@elsewhere.com
+
+  would incorrectly match a message sent:
+
+        To: user@example,com, someone@elsewhere.com
+
+Fix --output=json when search has no results
+
+  A bug present since notmuch 0.4 had caused searches with no results
+  to produce an invalid json object. This is now fixed to cleanly
+  return a valid json object representing an empty array "[]" as
+  expected.
+
+Fix the automatic detection of the From address for "notmuch reply"
+from the Received headers in some cases
+
+Fix core dump on DragonFlyBSD due to -1 return value from
+`sysconf(_SC_GETPW_R_SIZE_MAX)`
+
+Cleaned up several memory leaks
+
+Eliminated a few, rare segmentation faults and a double-free
+
+Fix libnotmuch library to only export notmuch API functions
+
+  Previous release of the notmuch library also exported some Xapian
+  C++ exception type symbols. These were never part of the library
+  interface and were never intended to be exported.
+
+Emacs-interface bug fixes
+-------------------------
+
+Display any unexpected output or errors from "notmuch search" invocations
+
+  Previously any misformatted output or trailing error messages were
+  silently ignored. This output is now clearly displayed. This fix was
+  very helpful in identifying and fixing the bug described below.
+
+Fix bug where some threads would be missing from large search results
+
+  When a search returned a "large" number of results, the emacs
+  interface was incorrectly dropping one thread every time the output
+  of the "notmuch search" process spanned the emacs read-buffer. This
+  is now fixed.
+
+Avoid re-compression of .gz files (and similar) when saving attachment
+
+  Emacs was being too clever for its own good and trying to
+  re-compress pre-compressed .gz files when saving such attachments
+  (potentially corrupting the attachment). The emacs interface is
+  fixed to avoid this bug.
+
+Fix hiding of a message when a previously-hidden citation is visible
+
+  Previously the citation would remain visible in this case. This is
+  fixed so that hiding a message hides all parts.
+
+Notmuch 0.5 (2010-11-11)
+========================
+
+New, general features
+---------------------
+
+Maildir-flag synchronization
+
+  Notmuch now knows how to synchronize flags in maildir filenames with
+  tags in the notmuch database. The following flag/tag mappings are
+  supported:
+
+        Flag <-> Tag
+        ----     -----
+        'D'      draft
+        'F'      flagged
+        'P'      passed
+        'R'      replied
+        'S'      unread (added when 'S' flag is not present)
+
+  The synchronization occurs in both directions, (for example, adding
+  the 'S' flag to a file will cause the "unread" tag to be added, and
+  adding the "replied" tag to a message will cause the file to be
+  renamed with an 'R' flag).
+
+  This synchronization is enabled by default for users of the
+  command-line interface, (though only files in directories named
+  "cur" or "new" will be renamed). It can be disabled by setting the
+  new `maildir.synchronize_flags` option in the configuration file. For
+  example:
+
+        notmuch config set maildir.synchronize_flags false
+
+  Users upgrading may also want to run "notmuch setup" once (just
+  accept the existing configuration) to get a new, nicely-commented
+  [maildir] section added to the configuration file.
+
+  For users of the notmuch library, the new synchronization
+  functionality is available with the following two new functions:
+
+        notmuch_message_maildir_flags_to_tags
+        notmuch_message_tags_to_maildir_flags
+
+  It is anticipated that future improvements to this support will
+  allow for safe synchronization of the 'T' flag with the "deleted"
+  tag, as well as support for custom flag/tag mappings.
+
+New library features
+--------------------
+
+Support for querying multiple filenames for a single message
+
+  It is common for the mailstore to contain multiple files with the
+  same message ID. Previously, notmuch would always hide these
+  duplicate files, (returning a single, arbitrary filename with
+  `notmuch_message_get_filename`).
+
+  With this release, library users can access all filenames for a
+  message with the new function:
+
+        notmuch_message_get_filenames
+
+  Together with `notmuch_filenames_valid`, `notmuch_filenames_get`,
+  and `notmuch_filenames_move_to_next` it is now possible to iterate
+  over all available filenames for a given message.
+
+New command-line features
+-------------------------
+
+New "notmuch show --format=raw" for getting at original email contents
+
+  This new feature allows for a fully-functional email client to be
+  built on top of the notmuch command-line without needing any direct
+  access to the mail store itself.
+
+  For example, it's now possible to run "emacs -f notmuch" on a local
+  machine with only ssh access to the mail store/notmuch database. To
+  do this, simply set the notmuch-command variable in emacs to the
+  name of a script containing:
+
+        ssh user@host notmuch "$@"
+
+  If the ssh client has enabled connection sharing (ControlMaster
+  option in OpenSSH), the emacs interface can be quite responsive this
+  way.
+
+General bug fixes
+-----------------
+
+Fix "notmuch search" to print nothing when nothing matches
+
+  The 0.4 release had a bug in which:
+
+        notmuch search <expression-with-no-matches>
+
+  would produce a single blank line of output, (where previous
+  versions would produce no output. This fix also causes a change in
+  the --format=json output, (which would previously produce "[]" and
+  now produces nothing).
+
+Emacs interface improvements
+----------------------------
+
+Fix to allow pipe ('|') command to work when using notmuch over ssh
+
+Fix count of lines in hidden signatures
+
+Omit repeated subject lines in (collapsed) thread display
+
+Display current thread subject in a header line
+
+Provide a "c i" binding to copy a thread ID from the search view
+
+Allow for notmuch-fcc-dirs to have a value of nil
+
+  Also, the more complex form of notmuch-fcc-dirs now has a slightly
+  different format. It no longer has a special first-element, fallback
+  string. Instead it's now a list of cons cells where the car of each
+  cell is a regular expression to be matched against the sender
+  address, and the cdr is the name of a folder to use for an FCC. So
+  the old fallback behavior can be achieved by including a final cell
+  of (".*" . "default-fcc-folder").
+
+Vim interface improvements
+--------------------------
+
+Felipe Contreras provided a number of updates for the vim interface
+
+  These include optimizations, support for newer versions of vim, fixed
+  support for sending mail on modern systems, new commands, and
+  various cleanups.
+
+New bindings
+------------
+
+Added initial ruby bindings in bindings/ruby
+
+Notmuch 0.4 (2010-11-01)
+========================
+
+New command-line features
+-------------------------
+
+`notmuch search --output=(summary|threads|messages|tags|files)`
+
+  This new option allows for particular items to be returned from
+  notmuch searches. The "summary" option is the default and behaves
+  just as "notmuch search" has historically behaved.
+
+  The new option values allow for thread IDs, message IDs, lists of
+  tags, and lists of filenames to be returned from searches. It is
+  expected that this new option will be very useful in shell
+  scripts. For example:
+
+        for file in $(notmuch search --output=files <search-terms>); do
+                <operations-on> "$file"
+        done
+
+`notmuch show --format=mbox <search-specification>`
+
+  This new option allows for the messages matching a search
+  specification to be presented as an mbox. Specifically the "mboxrd"
+  format is used which allows for reversible quoting of lines
+  beginning with "From ". A reader should remove a single '>' from the
+  beginning of all lines beginning with one or more '>' characters
+  followed by the 5 characters "From ".
+
+`notmuch config [get|set] <section>.<item> [value ...]`
+
+  The new top-level "config" command allows for any value in the
+  notmuch configuration file to be queried or set to a new value. Both
+  single-valued and multi-valued items are supported, as our any
+  custom items stored in the configuration file.
+
+Avoid setting Bcc header in "notmuch reply"
+
+  We decided that this was a bit heavy-handed as the actual mail
+  user-agent should be responsible for setting any Bcc option. Also,
+  see below for the notmuch/emacs user-agent now setting an Fcc by
+  default rather than Bcc.
+
+New library features
+--------------------
+
+Add `notmuch_query_get_query_string` and `notmuch_query_get_sort`
+
+  These are simply functions for querying properties of a
+  `notmuch_query_t` object.
+
+New emacs features
+------------------
+
+Enable Fcc of all sent messages by default (to "sent" directory)
+
+  All messages sent from the emacs interface will now be saved to the
+  notmuch mail store where they will be incorporated to the database
+  by the next "notmuch new". By default, messages are saved to the
+  "sent" directory at the top-level of the mail store. This directory
+  can be customized by means of the "Notmuch Fcc Dirs" option in the
+  notmuch customize interface.
+
+Ability to all open messages in a thread to a pipe
+
+  Historically, the '|' keybinding allows for piping a single message
+  to an external command. Now, by prefixing this key with a prefix
+  argument, (for example, by pressing "Control-U |"), all open
+  messages in the current thread will be sent to the external command.
+
+Optional support for detecting inline patches
+
+  This hook is disabled by default but can be enabled with a checkbox
+  under "Notmuch Show Insert Text/Plain Hook" in the notmuch customize
+  interface. It allows for inline patches to be detected and treated
+  as if they were attachments, (with context-sensitive highlighting).
+
+Automatically tag messages as "replied" when sending a reply
+
+  Messages replied to within the emacs interface will now be tagged as
+  "replied". This feature can easily be customized to add or remove
+  other tags as well. For example, a user might use a tag of
+  "needs-reply" and can configure this feature to automatically remove
+  that tag when replying. See "Notmuch Message Mark Replied" in the
+  notmuch customize interface.
+
+Allow search-result color specifications to overlay each other
+
+  For example, one tag can specify the background color of matching
+  lines, while another can specify the foreground. With this change,
+  both settings will now be visible simultaneously, (which was not the
+  case in previous releases). See "Notmuch Search Line Faces" in the
+  notmuch customize interface.
+
+Make hidden author names still available for incremental search
+
+  When there is insufficient space to display all authors of a thread
+  in search results, the names of hidden authors are now still made
+  available to emacs' incremental search commands. As the user
+  searches, matching lines will temporarily expand to show the hidden
+  names.
+
+New binding of Control-TAB (works like TAB in reverse)
+
+  Many notmuch nodes already use TAB to navigate forward through
+  various items allowing actions, (message headers, email attachments,
+  etc.). The new Control-TAB binding operates similarly but in the
+  opposite direction.
+
+New build-system features
+-------------------------
+
+Various portability fixes have been applied
+
+  These include fixes for build failures on at least Solaris, FreeBSD,
+  and Fedora systems. We're hopeful that the notmuch code base is now
+  more portable than ever before.
+
+Arrange for libnotmuch to be found automatically after make install
+
+  The notmuch build system is now careful to help the user avoid
+  errors of the form "libnotmuch.so could not be found" immediately
+  after installing. This support takes two forms:
+
+  1. If the library is installed to a system directory,
+     (configured in /etc/ld.so.conf), then "make install" will
+     automatically run ldconfig.
+
+  2. If the library is installed to a non-system directory, the
+     build system adds a `DR_RUNPATH` entry to the final binary
+     pointing to the directory to which the library is installed.
+
+  When this support works, the user should be able to run notmuch
+  immediately after "make install", without any errors trying to find
+  the notmuch library, and without having to manually set environment
+  variables such as `LD_LIBRARY_PATH`.
+
+Check compiler/linker options before using them
+
+  The configure script now carefully checks that any desired
+  compilation options, (whether for enabling compiler warnings, or for
+  embedding rpath, etc.), are supported. Only supported options are
+  used in the resulting Makefile.
+
+New test-suite features
+-----------------------
+
+New modularization of test suite
+
+  Thanks to a gracious relicensing of the test-suite infrastructure
+  from the git project, notmuch now has a modular test suite. This
+  provides the ability to run individual sections of the test suite
+  rather than the whole things. It also provides better summary of
+  test results, with support for tests that are expected to fail
+  (BROKEN and FIXED) in addition to PASS and FAIL. Finally, it makes
+  it easy to run the test suite within valgrind (pass --valgrind to
+  notmuch-test or to any sub-script) which has been very useful.
+
+New testing of emacs interface
+
+  The test suite has been augmented to allow automated testing of the
+  emacs interfaces. So far, this includes basic searches, display of
+  threads, and tag manipulation. This also includes a test that a new
+  message can successfully be sent out through a (dummy) SMTP server
+  and that said message is successfully integrated into the notmuch
+  database via the FCC setting.
+
+General bug fixes
+-----------------
+
+Fix potential corruption of database when "notmuch new" is interrupted
+
+  Previously, an interruption of "notmuch new" would (rarely) result
+  in a corrupt database. The corruption would manifest itself by a
+  persistent error of the form:
+
+        document ID of 1234 has no thread ID
+
+  The message-adding code has been carefully audited and reworked to
+  avoid this sort of corruption regardless of when it is interrupted.
+
+Fix failure with extremely long message ID headers
+
+  Previously, a message with an extremely long message ID, (say, more
+  than 300 characters), would fail to be added to notmuch, (triggering
+  Xapian exceptions). This has now been fixed.
+
+Fix for messages with "charset=unknown-8bit"
+
+  Previously, messages with this charset would cause notmuch to emit a
+  GMime warning, (which would then trip up emacs or other interfaces
+  parsing the notmuch results).
+
+Fix `notmuch_query_search_threads` function to return NULL on any exception
+
+Fix "notmuch search" to return non-zero if `notmuch_query_search_threads`
+fails
+
+  Previously, this command could confusingly report a Xapian
+  exception, yet still return an error code of 0. It now correctly
+  returns a failing error code of 1 in this case.
+
+Emacs bug fixes
+---------------
+
+Fix to handle a message with a subject containing, for example "[1234]"
+
+  Previously, a message subject containing a sequence of digits within
+  square brackets would cause the emacs interface to mis-parse the
+  output of "notmuch search". This would result in the message being
+  mis-displayed and prevent the user from manipulating the message in
+  the emacs interface.
+
+Fix to correctly handle message IDs containing ".."
+
+  The emacs interface now properly quotes message IDs to avoid a
+  Xapian bug in which the ".." within a message ID would be
+  misinterpreted as a numeric range specification.
+
+Python-binding fixes
+--------------------
+
+The python bindings for notmuch have been updated to work with python3.
+
+Debian-specific fixes
+---------------------
+
+Fix emacs initialization so "M-x notmuch" works for users by default
+
+  Now, a new Debian user can immediately run "emacs -f notmuch" after
+  "apt-get install notmuch". Previously, the user would have had to
+  edit the ~/.emacs file to add "(require 'notmuch)" before this would
+  work.
+
+Notmuch 0.3.1 (2010-04-27)
+==========================
+
+General bug fixes
+-----------------
+
+Fix an infinite loop in "notmuch reply"
+
+  This bug could be triggered by replying to a message where the
+  user's primary email address did not appear in the To: header and
+  the user had not configured any secondary email addresses. The bug
+  was a simple re-use of the same iterator variable in nested loops.
+
+Fix a potential SEGV in "notmuch search"
+
+  This bug could be triggered by an author name ending in a ','.
+  Admittedly - that's almost certainly a spam email, but we never
+  want notmuch to crash.
+
+Emacs bug fixes
+---------------
+
+Fix calculations for line wrapping in the primary "notmuch" view
+
+Fix Fcc support to prompt to create a directory if the specified Fcc
+directory does not exist
+
+Build fix
+---------
+
+Fix build on OpenSolaris (at least) due to missing 'extern "C"' block
+
+  Without this, the C++ sources could not find strcasestr and the
+  final linking of notmuch would fail.
+
+Notmuch 0.3 (2010-04-27)
+========================
+
+New command-line features
+-------------------------
+
+User-configurable tags for new messages
+
+  A new "new.tags" option is available in the configuration file to
+  determine which tags are applied to new messages. Run "notmuch
+  setup" to generate new documentation within ~/.notmuch-config on how
+  to specify this value.
+
+Threads search results named based on subjects that match search
+
+  This means that when new mails arrived to a thread you've previously
+  read, and the new mails have a new subject, you will see that
+  subject in the search results rather than the old subject.
+
+Faster operation of "notmuch tag" (avoid unneeded sorting)
+
+  Since the user just wants to tag all matching messages, we can make
+  things perform a bit faster by avoiding the sort.
+
+Even Better guessing of From: header for "notmuch reply"
+
+  Notmuch now looks at a number of headers when trying to figure out
+  the best From: header to use in a reply. This is helpful if you have
+  several configured email addresses, and you also subscribe to various
+  mailing lists with different addresses, (so that mails you are
+  replying to won't always include your subscribed address in the To:
+  header).
+
+Indication of author names that match a search
+
+  When notmuch displays threads as the result of a search, it now
+  lists the authors that match the search before listing the other
+  authors in the thread. It inserts a pipe '|' symbol between the last
+  matching and first non-matching author. This is especially useful in
+  a search that includes tag:unread. Now the authors of the unread
+  messages in the thread are listed first.
+
+New: Python bindings
+--------------------
+
+Sebastian Spaeth has contributed his python bindings for the notmuch
+library to the central repository. These bindings were previously
+known as "cnotmuch" within python but have now been renamed to be
+accessible with a simple, and more official-looking "import notmuch".
+
+The bindings have already proven very useful as people proficient in
+python have been able to easily develop programs to do notmuch-based
+searches for email-address completion, maildir-flag synchronization,
+and other tasks.
+
+These bindings are available within the bindings/python directory, but
+are not yet integrated into the top-level Makefiles, nor the top-level
+package-building scripts. Improvements are welcome.
+
+Emacs interface improvements
+----------------------------
+
+An entirely new initial view for notmuch, (friendly yet powerful)
+
+  Some of us call the new view "notmuch hello" but you can get at it
+  by simply calling "emacs -f notmuch". The new view provides a search
+  bar where new searches can be performed. It also displays a list of
+  recent searches, along with a button to save any of these, giving it
+  a new name as a "saved search". Many people find these "saved
+  searches" one of the most convenient ways of organizing their mail,
+  (providing all of the features of "folders" in other mail clients,
+  but without any of the disadvantages).
+
+  Finally, this view can also optionally display all of the tags that
+  exist in the database, along with a count for each tag, and a custom
+  search of messages with that tag that's simply a click (or keypress)
+  away.
+
+  NOTE: For users that liked the original mode of "emacs -f notmuch"
+  immediately displaying a particular search result, we recommend
+  instead running something like:
+
+        emacs --eval '(notmuch search "tag:inbox" t)'
+
+  The "t" means to sort the messages in an "oldest first" order,
+  (as notmuch would do previously by default). You can also
+  leave that off to have your search results in "newest first"
+  order.
+
+Full-featured "customize" support for configuring notmuch
+
+  Notmuch now plugs in well to the emacs "customize" mode to make it
+  much simpler to find things about the notmuch interface that can be
+  tweaked by the user.
+
+  You can get to this mode by starting at the main "Customize" menu in
+  emacs, then browsing through "Applications", "Mail", and
+  "Notmuch". Or you can go straight to "M-x customize-group"
+  "notmuch".
+
+  Once you're at the customize screen, you'll see a list of documented
+  options that can be manipulated along with checkboxes, drop-down
+  selectors, and text-entry boxes for configuring the various
+  settings.
+
+Support for doing tab-completion of email addresses
+
+  This support currently relies on an external program,
+  (notmuch-addresses), that is not yet shipped with notmuch
+  itself. But multiple, suitable implementations of this program have
+  already been written that generate address completions by doing
+  notmuch searches of your email collection. For example, providing
+  first those addresses that you have composed messages to in the
+  past, etc.
+
+  One such program (implemented in python with the python bindings to
+  notmuch) is available via:
+
+        git clone  http://jkr.acm.jhu.edu/git/notmuch_addresses.git
+
+  Install that program as notmuch-addresses on your PATH, and then
+  hitting TAB on a partial email address or name within the To: or Cc:
+  line of an email message will provide matching completions.
+
+Support for file-based (Fcc) delivery of sent messages to mail store
+
+  This isn't yet enabled by default. To enable this, one will have to
+  set the "Notmuch Fcc Dirs" setting within the notmuch customize
+  screen, (see its documentation there for details). We anticipate
+  making this automatic in a future release.
+
+New 'G' key binding to trigger mail refresh (G == "Get new mail")
+
+  The 'G' key works wherever '=' works. Before refreshing the screen
+  it calls an external program that can be used to poll email servers,
+  run notmuch new and set up specific tags for the new emails. The
+  script to be called should be configured with the "Notmuch Poll
+  Script" setting in the customize interface. This script will
+  typically invoke "notmuch new" and then perhaps several "notmuch
+  tag" commands.
+
+Implement emacs message display with the JSON output from notmuch
+
+  This is much more robust than the previous implementation, (where
+  some HTML mails and mail quoting the notmuch code with the delimiter
+  characters in it would cause the parser to fall over).
+
+Better handling of HTML messages and MIME attachments (inline images!)
+
+  Allow for any MIME parts that emacs can display to be displayed
+  inline. This includes inline viewing of image attachments, (provided
+  the window is large enough to fit the image at its natural size).
+
+  Much more robust handling of HTML messages. Currently both text/plain
+  and text/html alternates will be rendered next to each other. In a
+  future release, users will be able to decide to see only one or the
+  other representation.
+
+  Each attachment now has its own button so that attachments can be
+  saved individually (the 'w' key is still available to save all
+  attachments).
+
+Customizable support for tidying of text/plain message content
+
+  Many new functions are available for tidying up message
+  content. These include options such as wrapping long lines,
+  compressing duplicate blank lines, etc.
+
+  Most of these are disabled by default, but can easily be enabled by
+  clicking the available check boxes under the "Notmuch Show Insert
+  Text/Plain Hook" within the notmuch customize screen.
+
+New support for searchable citations (even when hidden)
+
+  When portions of overly-long citations are hidden, the contents of
+  these citations will still be available for emacs' standard
+  "incremental search" functions. When the search matches any portion
+  of a hidden citation, the citation will become visible temporarily
+  to display the search result.
+
+More flexible handling of header visibility
+
+  As an answer to complaints from many users, the To, Cc, and Date
+  headers of messages are no longer hidden by default. For those users
+  that liked that these were hidden, a new "Notmuch Messages Headers
+  Visible" option in the customize interface can be set to nil. The
+  visibility of headers can still be toggled on a per-message basis
+  with the 'h' keybinding.
+
+  For users that don't want to see some subset of those headers, the
+  new "Notmuch Message Headers" variable can be customized to list
+  only those headers that should be present in the display of a message.
+
+The Return key now toggles message visibility anywhere
+
+  Previously this worked only on the first summary-line of a message.
+
+Customizable formatting of search results
+
+  The user can easily customize the order, width, and formatting of
+  the various fields in a "notmuch search" buffer. See the "Notmuch
+  Search Result Format" section of the customize interface.
+
+Generate nicer names for search buffers when using a saved search
+
+Add a notmuch User-Agent header when sending mail from notmuch/emacs
+
+New keybinding (M-Ret) to open all collapsed messages in a thread
+
+New library feature
+-------------------
+
+Provide a new `NOTMUCH_SORT_UNSORTED` value for queries
+
+  This can be somewhat faster when sorting simply isn't desired. For
+  example when collecting a set of messages that will all be
+  manipulated identically, (adding a tag, removing a tag, deleting the
+  messages), then there's no advantage to sorting the messages by
+  date.
+
+Build fixes
+-----------
+
+Fix to compile against GMime 2.6
+
+  Previously notmuch insisted on being able to find GMime 2.4, (even
+  though GMime 2.6 would have worked all along).
+
+Fix configure script to accept (and ignore) various standard options
+
+  For example, those that the Gentoo build scripts expect configure to
+  accept are now all accepted.
+
+Test suite
+----------
+
+A large number of new tests for the many new features
+
+Better display of output from failed tests
+
+  Now shows failures with diff rather than forcing the user to gaze at
+  complete actual and expected output looking for deviation.
+
+Notmuch 0.2 (2010-04-16)
+========================
+
+This is the second release of the notmuch mail system, with actual
+detailed release notes this time!
+
+This release consists of a number of minor new features that make
+notmuch more pleasant to use, and a few fairly major bug fixes.
+
+We didn't quite hit our release target of "about a week" from the 0.1
+release, (0.2 is happening 11 days after 0.1), but we hope to do
+better for next week. Look forward to some major features coming to
+notmuch in subsequent releases.
+
+-Carl
+
+General features
+----------------
+
+Better guessing of From: header
+
+  Notmuch now tries harder to guess which configured address should be
+  used as the From: line in a "notmuch reply". It will examine the
+  Received: headers if it fails to find any configured address in To:
+  or Cc:. This allows it to often choose the correct address even when
+  replying to a message sent to a mailing list, and not directly to a
+  configured address.
+
+Make "notmuch count" with no arguments count all messages
+
+  Previously, it was hard to construct a search term that was
+  guaranteed to match all messages.
+
+Provide a new special-case search term of "*" to match all messages
+
+  This can be used in any command accepting a search term, such as
+  "notmuch search '*'". Note that you'll want to take care that the
+  shell doesn't expand * against the current files. And note that the
+  support for "*" is a special case. It's only meaningful as a single
+  search term and loses its special meaning when combined with any
+  other search terms.
+
+Automatically detect thread connections even when a parent message is
+missing
+
+  Previously, if two or more message were received with a common
+  parent, but that parent was not received, then these messages would
+  not be recognized as belonging to the same thread. This is now fixed
+  so that such messages are properly connected in a thread.
+
+General bug fixes
+-----------------
+
+Fix potential data loss in "notmuch new" with SIGINT
+
+  One code path in "notmuch new" was not properly handling
+  SIGINT. Previously, this could lead to messages being removed from
+  the database (and their tags being lost) if the user pressed
+  Control-C while "notmuch new" was working.
+
+Fix segfault when a message includes a MIME part that is empty
+
+Fix handling of non-ASCII characters with --format=json
+
+  Previously, characters outside the range of 7-bit ASCII were
+  silently dropped from the JSON output. This led to corrupted display
+  of utf-8 content in the upcoming notmuch web-based frontends.
+
+Fix headers to be properly decoded in "notmuch reply"
+
+  Previously, the user might see:
+
+        Subject: Re: =?iso-8859-2?q?Rozlu=E8ka?=
+
+  rather than:
+
+        Subject: Re: Rozlučka
+
+  The former text is properly encoded to be RFC-compliant SMTP, will
+  be sent correctly, and will be properly decoded by the
+  recipient. But the user trying to edit the reply would likely be
+  unable to read or edit that field in its encoded form.
+
+Emacs client features
+---------------------
+
+Show the last few lines of citations as well as the first few lines
+
+  It's often the case that the last sentence of a citation is what is
+  being replied to directly, so the last few lines are often much more
+  important. The number of lines shown at the beginning and end of any
+  citation can be configured, (notmuch-show-citation-lines-prefix and
+  notmuch-show-citation-lines-suffix).
+
+The '+' and '-' commands in the search view can now add and remove
+tags by region
+
+  Selective bulk tagging is now possible by selecting a region of
+  threads and then using either the '+' or '-' keybindings. Bulk
+  tagging is still available for all threads matching the current
+  search with the '*' binding.
+
+More meaningful buffer names for thread-view buffers
+
+  Notmuch now uses the Subject of the thread as the buffer
+  name. Previously it was using the thread ID, which is a meaningless
+  number to the user.
+
+Provide for customized colors of threads in search view based on tags
+
+  See the documentation of notmuch-search-line-faces, (or us "M-x
+  customize" and browse to the "notmuch" group within "Applications"
+  and "Mail"), for details on how to configure this colorization.
+
+Build-system features
+---------------------
+
+Add support to properly build libnotmuch on Darwin systems (OS X)
+
+Add support to configure for many standard options
+
+  We include actual support for:
+
+        --includedir --mandir --sysconfdir
+
+  And accept and silently ignore several more:
+
+        --build --infodir --libexecdir --localstatedir
+        --disable-maintainer-mode --disable-dependency-tracking
+
+Install emacs client in "make install" rather than requiring a
+separate "make install-emacs"
+
+Automatically compute versions numbers between releases
+
+  This support uses the git-describe notation, so a version such as
+  0.1-144-g43cbbfc indicates a version that is 144 commits since the
+  0.1 release and is available as git commit "43cbbfc".
+
+Add a new "make test" target to run the test suite and actually
+verify its results
+
+Notmuch 0.1 (2010-04-05)
+========================
+
+This is the first release of the notmuch mail system.
+
+It includes the libnotmuch library, the notmuch command-line
+interface, and an emacs-based interface to notmuch.
+
+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.
+
+
+<!--
+ Local variables:
+ mode: text
+ tab-width: 8
+ indent-tabs-mode: nil
+ End:
+ vi: sw=8 ts=8 et
+-->
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..03bbb57
--- /dev/null
+++ b/README
@@ -0,0 +1,77 @@
+Notmuch - 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 convenient
+search syntax.
+
+Notmuch is free software, released under the GNU General Public
+License version 3 (or later).
+
+Building notmuch
+----------------
+See the INSTALL file for notes on compiling and installing notmuch.
+
+Running notmuch
+---------------
+After installing notmuch, start by running "notmuch setup" which will
+interactively prompt for configuration information such as your name,
+email address, and the directory which contains your mail archive to
+be indexed. You can change any answers later by running "notmuch
+setup" again or by editing the .notmuch-config file in your home
+directory.
+
+With notmuch configured you should next run "notmuch new" which will
+index all of your existing mail. This can take a long time, (several
+hours) if you have a lot of email, (hundreds of thousands of
+files). When new mail is delivered to your mail archive in the future,
+you will want to run "notmuch new" again. These runs will be much
+faster as they will only index new messages.
+
+Finally, you can prove to yourself that things are working by running
+some command-line searches such as "notmuch search
+from:someone@example.com" or "notmuch search subject:topic". See
+"notmuch help search-terms" for more details on the available search
+syntax.
+
+The command-line search output is not expected to be particularly
+friendly for day-to-day usage. Instead, it is expected that you will
+use an email interface that builds on the notmuch command-line tool or
+the libnotmuch library.
+
+Notmuch installs a full-featured email interface for use within
+emacs. To use this, first add the following line to your .emacs file:
+
+       (autoload 'notmuch "notmuch" "Notmuch mail" t)
+
+Then, either run "emacs -f notmuch" or execute the command "M-x
+notmuch" from within a running emacs.
+
+If you're interested in a non-emacs-based interface to notmuch, then
+please join the notmuch community. Various other interfaces are
+already in progress, (an interface within vim, a curses interface,
+graphical interfaces based on evolution, and various web-based
+interfaces). The authors of these interfaces would love further
+testing or contribution. See contact information below.
+
+Contacting users and developers
+-------------------------------
+The website for Notmuch is:
+
+       https://notmuchmail.org
+
+The mailing list address for the notmuch community is:
+
+       notmuch@notmuchmail.org
+
+We welcome any sort of questions, comments, kudos, or code there.
+
+Subscription is not required, (but if you do subscribe you'll avoid
+any delay due to moderation). See the website for subscription
+information.
+
+There is also an IRC channel dedicated to talk about using and
+developing notmuch:
+
+       IRC server:     irc.libera.chat
+       Channel:        #notmuch
diff --git a/README.rst b/README.rst
new file mode 100644 (file)
index 0000000..7ff3198
--- /dev/null
@@ -0,0 +1,11 @@
+If you're reading this on https://github.com/notmuch/notmuch, this is a
+read-only mirror of the notmuch project.
+
+For more information about the project, see https://notmuchmail.org.
+
+Please don't send us pull requests on github. If you have a feature
+branch that you want us to look at, use ``git send-email`` to send it
+to notmuch@notmuchmail.org.
+
+For more information about contributing to the project, see
+https://notmuchmail.org/contributing/.
diff --git a/bindings/Makefile b/bindings/Makefile
new file mode 100644 (file)
index 0000000..de492a7
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/bindings/Makefile.local b/bindings/Makefile.local
new file mode 100644 (file)
index 0000000..9875123
--- /dev/null
@@ -0,0 +1,40 @@
+# -*- makefile-gmake -*-
+
+dir := bindings
+
+# force the shared library to be built
+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 CFLAGS="$(CFLAGS) -pipe -fno-plt -fPIC" && touch $@
+endif
+
+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 && \
+               touch ../python-cffi.stamp
+endif
+
+CLEAN += $(patsubst %,$(dir)/ruby/%, \
+       .RUBYARCHDIR.time \
+       Makefile database.o directory.o filenames.o\
+       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 $(dir)/ruby.stamp
+
+CLEAN += bindings/python-cffi/build $(dir)/python-cffi.stamp
+CLEAN += bindings/python-cffi/__pycache__
+
+DISTCLEAN += bindings/python-cffi/_notmuch_config.py \
+       bindings/python-cffi/notmuch2.egg-info
diff --git a/bindings/python-cffi/MANIFEST.in b/bindings/python-cffi/MANIFEST.in
new file mode 100644 (file)
index 0000000..9ef81f2
--- /dev/null
@@ -0,0 +1,2 @@
+include MANIFEST.in
+include tox.ini
diff --git a/bindings/python-cffi/notmuch2/__init__.py b/bindings/python-cffi/notmuch2/__init__.py
new file mode 100644 (file)
index 0000000..f281edc
--- /dev/null
@@ -0,0 +1,62 @@
+"""Pythonic API to the notmuch database.
+
+Creating Objects
+================
+
+Only the :class:`Database` object is meant to be created by the user.
+All other objects should be created from this initial object.  Users
+should consider their signatures implementation details.
+
+Errors
+======
+
+All errors occurring due to errors from the underlying notmuch database
+are subclasses of the :exc:`NotmuchError`.  Due to memory management
+it is possible to try and use an object after it has been freed.  In
+this case a :exc:`ObjectDestroyedError` will be raised.
+
+Memory Management
+=================
+
+Libnotmuch uses a hierarchical memory allocator, this means all
+objects have a strict parent-child relationship and when the parent is
+freed all the children are freed as well.  This has some implications
+for these Python bindings as parent objects need to be kept alive.
+This is normally schielded entirely from the user however and the
+Python objects automatically make sure the right references are kept
+alive.  It is however the reason the :class:`BaseObject` exists as it
+defines the API all Python objects need to implement to work
+correctly.
+
+Collections and Containers
+==========================
+
+Libnotmuch exposes nearly all collections of things as iterators only.
+In these python bindings they have sometimes been exposed as
+:class:`collections.abc.Container` instances or subclasses of this
+like :class:`collections.abc.Set` or :class:`collections.abc.Mapping`
+etc.  This gives a more natural API to work with, e.g. being able to
+treat tags as sets.  However it does mean that the
+:meth:`__contains__`, :meth:`__len__` and frieds methods on these are
+usually more and essentially O(n) rather than O(1) as you might
+usually expect from Python containers.
+"""
+
+from notmuch2 import _capi
+from notmuch2._base import *
+from notmuch2._database import *
+from notmuch2._errors import *
+from notmuch2._message import *
+from notmuch2._tags import *
+from notmuch2._thread import *
+
+
+NOTMUCH_TAG_MAX = _capi.lib.NOTMUCH_TAG_MAX
+del _capi
+
+
+# Re-home all the objects to the package.  This leaves __qualname__ intact.
+for x in locals().copy().values():
+    if hasattr(x, '__module__'):
+        x.__module__ = __name__
+del x
diff --git a/bindings/python-cffi/notmuch2/_base.py b/bindings/python-cffi/notmuch2/_base.py
new file mode 100644 (file)
index 0000000..1cf03c8
--- /dev/null
@@ -0,0 +1,238 @@
+import abc
+import collections.abc
+
+from notmuch2 import _capi as capi
+from notmuch2 import _errors as errors
+
+
+__all__ = ['NotmuchObject', 'BinString']
+
+
+class NotmuchObject(metaclass=abc.ABCMeta):
+    """Base notmuch object syntax.
+
+    This base class exists to define the memory management handling
+    required to use the notmuch library.  It is meant as an interface
+    definition rather than a base class, though you can use it as a
+    base class to ensure you don't forget part of the interface.  It
+    only concerns you if you are implementing this package itself
+    rather then using it.
+
+    libnotmuch uses a hierarchical memory allocator, where freeing the
+    memory of a parent object also frees the memory of all child
+    objects.  To make this work seamlessly in Python this package
+    keeps references to parent objects which makes them stay alive
+    correctly under normal circumstances.  When an object finally gets
+    deleted the :meth:`__del__` method will be called to free the
+    memory.
+
+    However during some peculiar situations, e.g. interpreter
+    shutdown, it is possible for the :meth:`__del__` method to have
+    been called, whele there are still references to an object.  This
+    could result in child objects asking their memory to be freed
+    after the parent has already freed the memory, making things
+    rather unhappy as double frees are not taken lightly in C.  To
+    handle this case all objects need to follow the same protocol to
+    destroy themselves, see :meth:`destroy`.
+
+    Once an object has been destroyed trying to use it should raise
+    the :exc:`ObjectDestroyedError` exception.  For this see also the
+    convenience :class:`MemoryPointer` descriptor in this module which
+    can be used as a pointer to libnotmuch memory.
+    """
+
+    @abc.abstractmethod
+    def __init__(self, parent, *args, **kwargs):
+        """Create a new object.
+
+        Other then for the toplevel :class:`Database` object
+        constructors are only ever called by internal code and not by
+        the user.  Per convention their signature always takes the
+        parent object as first argument.  Feel free to make the rest
+        of the signature match the object's requirement.  The object
+        needs to keep a reference to the parent, so it can check the
+        parent is still alive.
+        """
+
+    @property
+    @abc.abstractmethod
+    def alive(self):
+        """Whether the object is still alive.
+
+        This indicates whether the object is still alive.  The first
+        thing this needs to check is whether the parent object is
+        still alive, if it is not then this object can not be alive
+        either.  If the parent is alive then it depends on whether the
+        memory for this object has been freed yet or not.
+        """
+
+    def __del__(self):
+        self._destroy()
+
+    @abc.abstractmethod
+    def _destroy(self):
+        """Destroy the object, freeing all memory.
+
+        This method needs to destroy the object on the
+        libnotmuch-level.  It must ensure it's not been destroyed by
+        it's parent object yet before doing so.  It also must be
+        idempotent.
+        """
+
+
+class MemoryPointer:
+    """Data Descriptor to handle accessing libnotmuch pointers.
+
+    Most :class:`NotmuchObject` instances will have one or more CFFI
+    pointers to C-objects.  Once an object is destroyed this pointer
+    should no longer be used and a :exc:`ObjectDestroyedError`
+    exception should be raised on trying to access it.  This
+    descriptor simplifies implementing this, allowing the creation of
+    an attribute which can be assigned to, but when accessed when the
+    stored value is *None* it will raise the
+    :exc:`ObjectDestroyedError` exception::
+
+       class SomeOjb:
+           _ptr = MemoryPointer()
+
+           def __init__(self, ptr):
+               self._ptr = ptr
+
+           def destroy(self):
+               somehow_free(self._ptr)
+               self._ptr = None
+
+           def do_something(self):
+               return some_libnotmuch_call(self._ptr)
+    """
+
+    def __get__(self, instance, owner):
+        try:
+            val = getattr(instance, self.attr_name, None)
+        except AttributeError:
+            # We're not on 3.6+ and self.attr_name does not exist
+            self.__set_name__(instance, 'dummy')
+            val = getattr(instance, self.attr_name, None)
+        if val is None:
+            raise errors.ObjectDestroyedError()
+        return val
+
+    def __set__(self, instance, value):
+        try:
+            setattr(instance, self.attr_name, value)
+        except AttributeError:
+            # We're not on 3.6+ and self.attr_name does not exist
+            self.__set_name__(instance, 'dummy')
+            setattr(instance, self.attr_name, value)
+
+    def __set_name__(self, instance, name):
+        self.attr_name = '_memptr_{}_{:x}'.format(name, id(instance))
+
+
+class BinString(str):
+    """A str subclass with binary data.
+
+    Most data in libnotmuch should be valid ASCII or valid UTF-8.
+    However since it is a C library these are represented as
+    bytestrings instead which means on an API level we can not
+    guarantee that decoding this to UTF-8 will both succeed and be
+    lossless.  This string type converts bytes to unicode in a lossy
+    way, but also makes the raw bytes available.
+
+    This object is a normal unicode string for most intents and
+    purposes, but you can get the original bytestring back by calling
+    ``bytes()`` on it.
+    """
+
+    def __new__(cls, data, encoding='utf-8', errors='ignore'):
+        if not isinstance(data, bytes):
+            data = bytes(data, encoding=encoding)
+        strdata = str(data, encoding=encoding, errors=errors)
+        inst = super().__new__(cls, strdata)
+        inst._bindata = data
+        return inst
+
+    @classmethod
+    def from_cffi(cls, cdata):
+        """Create a new string from a CFFI cdata pointer."""
+        return cls(capi.ffi.string(cdata))
+
+    def __bytes__(self):
+        return self._bindata
+
+
+class NotmuchIter(NotmuchObject, collections.abc.Iterator):
+    """An iterator for libnotmuch iterators.
+
+    It is tempting to use a generator function instead, but this would
+    not correctly respect the :class:`NotmuchObject` memory handling
+    protocol and in some unsuspecting cornercases cause memory
+    trouble.  You probably want to sublcass this in order to wrap the
+    value returned by :meth:`__next__`.
+
+    :param parent: The parent object.
+    :type parent: NotmuchObject
+    :param iter_p: The CFFI pointer to the C iterator.
+    :type iter_p: cffi.cdata
+    :param fn_destory: The CFFI notmuch_*_destroy function.
+    :param fn_valid: The CFFI notmuch_*_valid function.
+    :param fn_get: The CFFI notmuch_*_get function.
+    :param fn_next: The CFFI notmuch_*_move_to_next function.
+    """
+    _iter_p = MemoryPointer()
+
+    def __init__(self, parent, iter_p,
+                 *, fn_destroy, fn_valid, fn_get, fn_next):
+        self._parent = parent
+        self._iter_p = iter_p
+        self._fn_destroy = fn_destroy
+        self._fn_valid = fn_valid
+        self._fn_get = fn_get
+        self._fn_next = fn_next
+
+    def __del__(self):
+        self._destroy()
+
+    @property
+    def alive(self):
+        if not self._parent.alive:
+            return False
+        try:
+            self._iter_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def _destroy(self):
+        if self.alive:
+            try:
+                self._fn_destroy(self._iter_p)
+            except errors.ObjectDestroyedError:
+                pass
+        self._iter_p = None
+
+    def __iter__(self):
+        """Return the iterator itself.
+
+        Note that as this is an iterator and not a container this will
+        not return a new iterator.  Thus any elements already consumed
+        will not be yielded by the :meth:`__next__` method anymore.
+        """
+        return self
+
+    def __next__(self):
+        if not self._fn_valid(self._iter_p):
+            self._destroy()
+            raise StopIteration()
+        obj_p = self._fn_get(self._iter_p)
+        self._fn_next(self._iter_p)
+        return obj_p
+
+    def __repr__(self):
+        try:
+            self._iter_p
+        except errors.ObjectDestroyedError:
+            return '<NotmuchIter (exhausted)>'
+        else:
+            return '<NotmuchIter>'
diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py
new file mode 100644 (file)
index 0000000..65d7dcb
--- /dev/null
@@ -0,0 +1,346 @@
+import cffi
+from _notmuch_config import *
+
+ffibuilder = cffi.FFI()
+ffibuilder.set_source(
+    'notmuch2._capi',
+    r"""
+    #include <stdlib.h>
+    #include <time.h>
+    #include <notmuch.h>
+
+    #if LIBNOTMUCH_MAJOR_VERSION < 5
+        #error libnotmuch version not supported by notmuch2 python bindings
+    #endif
+    #if LIBNOTMUCH_MINOR_VERSION < 1
+        #ERROR libnotmuch  version < 5.1 not supported
+    #endif
+    """,
+    include_dirs=[NOTMUCH_INCLUDE_DIR],
+    library_dirs=[NOTMUCH_LIB_DIR],
+    libraries=['notmuch'],
+)
+ffibuilder.cdef(
+    r"""
+    void free(void *ptr);
+    typedef int... time_t;
+
+    #define LIBNOTMUCH_MAJOR_VERSION ...
+    #define LIBNOTMUCH_MINOR_VERSION ...
+    #define LIBNOTMUCH_MICRO_VERSION ...
+
+    #define NOTMUCH_TAG_MAX ...
+
+    typedef enum _notmuch_status {
+        NOTMUCH_STATUS_SUCCESS = 0,
+        NOTMUCH_STATUS_OUT_OF_MEMORY,
+        NOTMUCH_STATUS_READ_ONLY_DATABASE,
+        NOTMUCH_STATUS_XAPIAN_EXCEPTION,
+        NOTMUCH_STATUS_FILE_ERROR,
+        NOTMUCH_STATUS_FILE_NOT_EMAIL,
+        NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID,
+        NOTMUCH_STATUS_NULL_POINTER,
+        NOTMUCH_STATUS_TAG_TOO_LONG,
+        NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
+        NOTMUCH_STATUS_UNBALANCED_ATOMIC,
+        NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
+        NOTMUCH_STATUS_UPGRADE_REQUIRED,
+        NOTMUCH_STATUS_PATH_ERROR,
+        NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
+        NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL,
+        NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION,
+        NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL,
+        NOTMUCH_STATUS_NO_CONFIG,
+        NOTMUCH_STATUS_NO_DATABASE,
+        NOTMUCH_STATUS_DATABASE_EXISTS,
+        NOTMUCH_STATUS_BAD_QUERY_SYNTAX,
+        NOTMUCH_STATUS_NO_MAIL_ROOT,
+        NOTMUCH_STATUS_CLOSED_DATABASE,
+        NOTMUCH_STATUS_LAST_STATUS
+    } notmuch_status_t;
+    typedef enum {
+        NOTMUCH_DATABASE_MODE_READ_ONLY = 0,
+        NOTMUCH_DATABASE_MODE_READ_WRITE
+    } notmuch_database_mode_t;
+    typedef int notmuch_bool_t;
+    typedef enum _notmuch_message_flag {
+        NOTMUCH_MESSAGE_FLAG_MATCH,
+        NOTMUCH_MESSAGE_FLAG_EXCLUDED,
+        NOTMUCH_MESSAGE_FLAG_GHOST,
+    } notmuch_message_flag_t;
+    typedef enum {
+        NOTMUCH_SORT_OLDEST_FIRST,
+        NOTMUCH_SORT_NEWEST_FIRST,
+        NOTMUCH_SORT_MESSAGE_ID,
+        NOTMUCH_SORT_UNSORTED
+    } notmuch_sort_t;
+    typedef enum {
+        NOTMUCH_EXCLUDE_FLAG,
+        NOTMUCH_EXCLUDE_TRUE,
+        NOTMUCH_EXCLUDE_FALSE,
+        NOTMUCH_EXCLUDE_ALL
+    } notmuch_exclude_t;
+    typedef enum {
+        NOTMUCH_DECRYPT_FALSE,
+        NOTMUCH_DECRYPT_TRUE,
+        NOTMUCH_DECRYPT_AUTO,
+        NOTMUCH_DECRYPT_NOSTASH,
+    } notmuch_decryption_policy_t;
+
+    // These are fully opaque types for us, we only ever use pointers.
+    typedef struct _notmuch_database notmuch_database_t;
+    typedef struct _notmuch_query notmuch_query_t;
+    typedef struct _notmuch_threads notmuch_threads_t;
+    typedef struct _notmuch_thread notmuch_thread_t;
+    typedef struct _notmuch_messages notmuch_messages_t;
+    typedef struct _notmuch_message notmuch_message_t;
+    typedef struct _notmuch_tags notmuch_tags_t;
+    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_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_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_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
+    notmuch_database_destroy (notmuch_database_t *database);
+    const char *
+    notmuch_database_get_path (notmuch_database_t *database);
+    unsigned int
+    notmuch_database_get_version (notmuch_database_t *database);
+    notmuch_bool_t
+    notmuch_database_needs_upgrade (notmuch_database_t *database);
+    notmuch_status_t
+    notmuch_database_begin_atomic (notmuch_database_t *notmuch);
+    notmuch_status_t
+    notmuch_database_end_atomic (notmuch_database_t *notmuch);
+    unsigned long
+    notmuch_database_get_revision (notmuch_database_t *notmuch,
+                                   const char **uuid);
+    notmuch_status_t
+    notmuch_database_index_file (notmuch_database_t *database,
+                                 const char *filename,
+                                 notmuch_indexopts_t *indexopts,
+                                 notmuch_message_t **message);
+    notmuch_status_t
+    notmuch_database_remove_message (notmuch_database_t *database,
+                                     const char *filename);
+    notmuch_status_t
+    notmuch_database_find_message (notmuch_database_t *database,
+                                   const char *message_id,
+                                   notmuch_message_t **message);
+    notmuch_status_t
+    notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
+                                               const char *filename,
+                                               notmuch_message_t **message);
+    notmuch_tags_t *
+    notmuch_database_get_all_tags (notmuch_database_t *db);
+
+    notmuch_query_t *
+    notmuch_query_create (notmuch_database_t *database,
+                          const char *query_string);
+    const char *
+    notmuch_query_get_query_string (const notmuch_query_t *query);
+    notmuch_database_t *
+    notmuch_query_get_database (const notmuch_query_t *query);
+    void
+    notmuch_query_set_omit_excluded (notmuch_query_t *query,
+                                     notmuch_exclude_t omit_excluded);
+    void
+    notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
+    notmuch_sort_t
+    notmuch_query_get_sort (const notmuch_query_t *query);
+    notmuch_status_t
+    notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag);
+    notmuch_status_t
+    notmuch_query_search_threads (notmuch_query_t *query,
+                                  notmuch_threads_t **out);
+    notmuch_status_t
+    notmuch_query_search_messages (notmuch_query_t *query,
+                                   notmuch_messages_t **out);
+    notmuch_status_t
+    notmuch_query_count_messages (notmuch_query_t *query, unsigned int *count);
+    notmuch_status_t
+    notmuch_query_count_threads (notmuch_query_t *query, unsigned *count);
+    void
+    notmuch_query_destroy (notmuch_query_t *query);
+
+    notmuch_bool_t
+    notmuch_threads_valid (notmuch_threads_t *threads);
+    notmuch_thread_t *
+    notmuch_threads_get (notmuch_threads_t *threads);
+    void
+    notmuch_threads_move_to_next (notmuch_threads_t *threads);
+    void
+    notmuch_threads_destroy (notmuch_threads_t *threads);
+
+    const char *
+    notmuch_thread_get_thread_id (notmuch_thread_t *thread);
+    notmuch_messages_t *
+    notmuch_message_get_replies (notmuch_message_t *message);
+    int
+    notmuch_thread_get_total_messages (notmuch_thread_t *thread);
+    notmuch_messages_t *
+    notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
+    notmuch_messages_t *
+    notmuch_thread_get_messages (notmuch_thread_t *thread);
+    int
+    notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
+    const char *
+    notmuch_thread_get_authors (notmuch_thread_t *thread);
+    const char *
+    notmuch_thread_get_subject (notmuch_thread_t *thread);
+    time_t
+    notmuch_thread_get_oldest_date (notmuch_thread_t *thread);
+    time_t
+    notmuch_thread_get_newest_date (notmuch_thread_t *thread);
+    notmuch_tags_t *
+    notmuch_thread_get_tags (notmuch_thread_t *thread);
+    void
+    notmuch_thread_destroy (notmuch_thread_t *thread);
+
+    notmuch_bool_t
+    notmuch_messages_valid (notmuch_messages_t *messages);
+    notmuch_message_t *
+    notmuch_messages_get (notmuch_messages_t *messages);
+    void
+    notmuch_messages_move_to_next (notmuch_messages_t *messages);
+    void
+    notmuch_messages_destroy (notmuch_messages_t *messages);
+    notmuch_tags_t *
+    notmuch_messages_collect_tags (notmuch_messages_t *messages);
+
+    const char *
+    notmuch_message_get_message_id (notmuch_message_t *message);
+    const char *
+    notmuch_message_get_thread_id (notmuch_message_t *message);
+    const char *
+    notmuch_message_get_filename (notmuch_message_t *message);
+    notmuch_filenames_t *
+    notmuch_message_get_filenames (notmuch_message_t *message);
+    notmuch_bool_t
+    notmuch_message_get_flag (notmuch_message_t *message,
+                              notmuch_message_flag_t flag);
+    void
+    notmuch_message_set_flag (notmuch_message_t *message,
+                              notmuch_message_flag_t flag,
+                              notmuch_bool_t value);
+    time_t
+    notmuch_message_get_date  (notmuch_message_t *message);
+    const char *
+    notmuch_message_get_header (notmuch_message_t *message,
+                                const char *header);
+    notmuch_tags_t *
+    notmuch_message_get_tags (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_add_tag (notmuch_message_t *message, const char *tag);
+    notmuch_status_t
+    notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);
+    notmuch_status_t
+    notmuch_message_remove_all_tags (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_maildir_flags_to_tags (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_tags_to_maildir_flags (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_freeze (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_thaw (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_get_property (notmuch_message_t *message,
+                                  const char *key, const char **value);
+    notmuch_status_t
+    notmuch_message_add_property (notmuch_message_t *message,
+                                  const char *key, const char *value);
+    notmuch_status_t
+    notmuch_message_remove_property (notmuch_message_t *message,
+                                     const char *key, const char *value);
+    notmuch_status_t
+    notmuch_message_remove_all_properties (notmuch_message_t *message,
+                                           const char *key);
+    notmuch_message_properties_t *
+    notmuch_message_get_properties (notmuch_message_t *message,
+                                    const char *key, notmuch_bool_t exact);
+    notmuch_bool_t
+    notmuch_message_properties_valid (notmuch_message_properties_t
+                                          *properties);
+    void
+    notmuch_message_properties_move_to_next (notmuch_message_properties_t
+                                                 *properties);
+    const char *
+    notmuch_message_properties_key (notmuch_message_properties_t *properties);
+    const char *
+    notmuch_message_properties_value (notmuch_message_properties_t
+                                          *properties);
+    void
+    notmuch_message_properties_destroy (notmuch_message_properties_t
+                                            *properties);
+    void
+    notmuch_message_destroy (notmuch_message_t *message);
+
+    notmuch_bool_t
+    notmuch_tags_valid (notmuch_tags_t *tags);
+    const char *
+    notmuch_tags_get (notmuch_tags_t *tags);
+    void
+    notmuch_tags_move_to_next (notmuch_tags_t *tags);
+    void
+    notmuch_tags_destroy (notmuch_tags_t *tags);
+
+    notmuch_bool_t
+    notmuch_filenames_valid (notmuch_filenames_t *filenames);
+    const char *
+    notmuch_filenames_get (notmuch_filenames_t *filenames);
+    void
+    notmuch_filenames_move_to_next (notmuch_filenames_t *filenames);
+    void
+    notmuch_filenames_destroy (notmuch_filenames_t *filenames);
+    notmuch_indexopts_t *
+    notmuch_database_get_default_indexopts (notmuch_database_t *db);
+    notmuch_status_t
+    notmuch_indexopts_set_decrypt_policy (notmuch_indexopts_t *indexopts,
+                                          notmuch_decryption_policy_t decrypt_policy);
+    notmuch_decryption_policy_t
+    notmuch_indexopts_get_decrypt_policy (const notmuch_indexopts_t *indexopts);
+    void
+    notmuch_indexopts_destroy (notmuch_indexopts_t *options);
+
+    notmuch_status_t
+    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_config_pairs_t *
+    notmuch_config_get_pairs (notmuch_database_t *db, const char *prefix);
+    notmuch_bool_t
+    notmuch_config_pairs_valid (notmuch_config_pairs_t *config_list);
+    const char *
+    notmuch_config_pairs_key (notmuch_config_pairs_t *config_list);
+    const char *
+    notmuch_config_pairs_value (notmuch_config_pairs_t *config_list);
+    void
+    notmuch_config_pairs_move_to_next (notmuch_config_pairs_t *config_list);
+    void
+    notmuch_config_pairs_destroy (notmuch_config_pairs_t *config_list);
+    """
+)
+
+
+if __name__ == '__main__':
+    ffibuilder.compile(verbose=True)
diff --git a/bindings/python-cffi/notmuch2/_config.py b/bindings/python-cffi/notmuch2/_config.py
new file mode 100644 (file)
index 0000000..603fdcb
--- /dev/null
@@ -0,0 +1,101 @@
+import collections.abc
+
+import notmuch2._base as base
+import notmuch2._capi as capi
+import notmuch2._errors as errors
+
+
+__all__ = ['ConfigMapping']
+
+
+class ConfigIter(base.NotmuchIter):
+
+    def __init__(self, parent, iter_p):
+        super().__init__(
+            parent, iter_p,
+            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):
+        # 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 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):
+        self._parent = parent
+        self._ptr = lambda: getattr(parent, ptr_name)
+
+    @property
+    def alive(self):
+        return self._parent.alive
+
+    def _destroy(self):
+        pass
+
+    def __getitem__(self, key):
+        if isinstance(key, str):
+            key = key.encode('utf-8')
+        val_pp = capi.ffi.new('char**')
+        ret = capi.lib.notmuch_database_get_config(self._ptr(), key, val_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        val = base.BinString.from_cffi(val_pp[0])
+        capi.lib.free(val_pp[0])
+        if val == '':
+            raise KeyError
+        return val
+
+    def __setitem__(self, key, val):
+        if isinstance(key, str):
+            key = key.encode('utf-8')
+        if isinstance(val, str):
+            val = val.encode('utf-8')
+        ret = capi.lib.notmuch_database_set_config(self._ptr(), key, val)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def __delitem__(self, key):
+        self[key] = ""
+
+    def __iter__(self):
+        """Return an iterator over the config items.
+
+        :raises NullPointerError: If the iterator can not be created.
+        """
+        config_pairs_p = capi.lib.notmuch_config_get_pairs(self._ptr(), b'')
+        if config_pairs_p == capi.ffi.NULL:
+            raise KeyError
+        return ConfigIter(self._parent, config_pairs_p)
+
+    def __len__(self):
+        return sum(1 for t in self)
diff --git a/bindings/python-cffi/notmuch2/_database.py b/bindings/python-cffi/notmuch2/_database.py
new file mode 100644 (file)
index 0000000..d7485b4
--- /dev/null
@@ -0,0 +1,856 @@
+import collections
+import configparser
+import enum
+import functools
+import os
+import pathlib
+import weakref
+
+import notmuch2._base as base
+import notmuch2._config as config
+import notmuch2._capi as capi
+import notmuch2._errors as errors
+import notmuch2._message as message
+import notmuch2._query as querymod
+import notmuch2._tags as tags
+
+
+__all__ = ['Database', 'AtomicContext', 'DbRevision']
+
+
+def _config_pathname():
+    """Return the path of the configuration file.
+
+    :rtype: pathlib.Path
+    """
+    cfgfname = os.getenv('NOTMUCH_CONFIG', '~/.notmuch-config')
+    return pathlib.Path(os.path.expanduser(cfgfname))
+
+
+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
+    NEWEST_FIRST = capi.lib.NOTMUCH_SORT_NEWEST_FIRST
+    MESSAGE_ID = capi.lib.NOTMUCH_SORT_MESSAGE_ID
+    UNSORTED = capi.lib.NOTMUCH_SORT_UNSORTED
+
+
+class QueryExclude(enum.Enum):
+    TRUE = capi.lib.NOTMUCH_EXCLUDE_TRUE
+    FLAG = capi.lib.NOTMUCH_EXCLUDE_FLAG
+    FALSE = capi.lib.NOTMUCH_EXCLUDE_FALSE
+    ALL = capi.lib.NOTMUCH_EXCLUDE_ALL
+
+
+class DecryptionPolicy(enum.Enum):
+    FALSE = capi.lib.NOTMUCH_DECRYPT_FALSE
+    TRUE = capi.lib.NOTMUCH_DECRYPT_TRUE
+    AUTO = capi.lib.NOTMUCH_DECRYPT_AUTO
+    NOSTASH = capi.lib.NOTMUCH_DECRYPT_NOSTASH
+
+
+class Database(base.NotmuchObject):
+    """Toplevel access to notmuch.
+
+    A :class:`Database` can be opened read-only or read-write.
+    Modifications are not atomic by default, use :meth:`begin_atomic`
+    for atomic updates.  If the underlying database has been modified
+    outside of this class a :exc:`XapianError` will be raised and the
+    instance must be closed and a new one created.
+
+    You can use an instance of this class as a context-manager.
+
+    :cvar MODE: The mode a database can be opened with, an enumeration
+       of ``READ_ONLY`` and ``READ_WRITE``
+    :cvar SORT: The sort order for search results, ``OLDEST_FIRST``,
+       ``NEWEST_FIRST``, ``MESSAGE_ID`` or ``UNSORTED``.
+    :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.
+       This is used to implement the ``ro`` and ``rw`` string
+       variants.
+
+    :ivar closed: Boolean indicating if the database is closed or
+       still open.
+
+    :param path: The directory of where the database is stored.  If
+       ``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
+       convenience you can also use the strings ``ro`` for
+       :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.
+    :raises configparser.Error: or subclasses if the configuration
+       file can not be parsed.
+    :raises NotmuchError: or subclasses for other failures.
+    """
+
+    MODE = Mode
+    SORT = QuerySortOrder
+    EXCLUDE = QueryExclude
+    CONFIG = ConfigFile
+    AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup'])
+    _db_p = base.MemoryPointer()
+    STR_MODE_MAP = {
+        'ro': MODE.READ_ONLY,
+        'rw': MODE.READ_WRITE,
+    }
+
+    @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
+
+        db_pp = capi.ffi.new('notmuch_database_t **')
+        cmsg = capi.ffi.new('char**')
+        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])
+        else:
+            msg = None
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret, msg)
+        self._db_p = db_pp[0]
+        self.closed = False
+
+    @classmethod
+    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 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
+           file can not be parsed.
+        :raises NotmuchError: if the config file does not have the
+           database.path setting.
+        :raises FileError: if the database already exists.
+
+        :returns: The newly created instance.
+        """
+
+        db_pp = capi.ffi.new('notmuch_database_t **')
+        cmsg = capi.ffi.new('char**')
+        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])
+        else:
+            msg = None
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret, msg)
+
+        # Now close the db and let __init__ open it.  Inefficient but
+        # creating is not a hot loop while this allows us to have a
+        # clean API.
+        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, config=config)
+
+    @staticmethod
+    def default_path(cfg_path=None):
+        """Return the path of the user's default database.
+
+        This reads the user's configuration file and returns the
+        default path of the database.
+
+        :param cfg_path: The pathname of the notmuch configuration file.
+           If not specified tries to use the pathname provided in the
+           :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
+           exists.
+        :rtype: pathlib.Path
+        :raises OSError: or subclasses if the configuration file can not
+           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
+           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()
+        if not hasattr(os, 'PathLike') and isinstance(cfg_path, pathlib.Path):
+            cfg_path = bytes(cfg_path)
+        parser = configparser.ConfigParser()
+        with open(cfg_path) as fp:
+            parser.read_file(fp)
+        try:
+            return pathlib.Path(parser.get('database', 'path'))
+        except configparser.Error:
+            raise errors.NotmuchError(
+                'No database.path setting in {}'.format(cfg_path))
+
+    def __del__(self):
+        self._destroy()
+
+    @property
+    def alive(self):
+        try:
+            self._db_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def _destroy(self):
+        try:
+            ret = capi.lib.notmuch_database_destroy(self._db_p)
+        except errors.ObjectDestroyedError:
+            ret = capi.lib.NOTMUCH_STATUS_SUCCESS
+        else:
+            self._db_p = None
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def close(self):
+        """Close the notmuch database.
+
+        Once closed most operations will fail.  This can still be
+        useful however to explicitly close a database which is opened
+        read-write as this would otherwise stop other processes from
+        reading the database while it is open.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_database_close(self._db_p)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        self.closed = True
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.close()
+
+    @property
+    def path(self):
+        """The pathname of the notmuch database.
+
+        This is returned as a :class:`pathlib.Path` instance.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        try:
+            return self._cache_path
+        except AttributeError:
+            ret = capi.lib.notmuch_database_get_path(self._db_p)
+            self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
+            return self._cache_path
+
+    @property
+    def version(self):
+        """The database format version.
+
+        This is a positive integer.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        try:
+            return self._cache_version
+        except AttributeError:
+            ret = capi.lib.notmuch_database_get_version(self._db_p)
+            self._cache_version = ret
+            return ret
+
+    @property
+    def needs_upgrade(self):
+        """Whether the database should be upgraded.
+
+        If *True* the database can be upgraded using :meth:`upgrade`.
+        Not doing so may result in some operations raising
+        :exc:`UpgradeRequiredError`.
+
+        A read-only database will never be upgradable.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_database_needs_upgrade(self._db_p)
+        return bool(ret)
+
+    def upgrade(self, progress_cb=None):
+        """Upgrade the database to the latest version.
+
+        Upgrade the database, optionally with a progress callback
+        which should be a callable which will be called with a
+        floating point number in the range of [0.0 .. 1.0].
+        """
+        raise NotImplementedError
+
+    def atomic(self):
+        """Return a context manager to perform atomic operations.
+
+        The returned context manager can be used to perform atomic
+        operations on the database.
+
+        .. note:: Unlinke a traditional RDBMS transaction this does
+           not imply durability, it only ensures the changes are
+           performed atomically.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ctx = AtomicContext(self, '_db_p')
+        return ctx
+
+    def revision(self):
+        """The currently committed revision in the database.
+
+        Returned as a ``(revision, uuid)`` namedtuple.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        raw_uuid = capi.ffi.new('char**')
+        rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
+        return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
+
+    def get_directory(self, path):
+        raise NotImplementedError
+
+    def default_indexopts(self):
+        """Returns default index options for the database.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+
+        :returns: :class:`IndexOptions`.
+        """
+        opts = capi.lib.notmuch_database_get_default_indexopts(self._db_p)
+        return IndexOptions(self, opts)
+
+    def add(self, filename, *, sync_flags=False, indexopts=None):
+        """Add a message to the database.
+
+        Add a new message to the notmuch database.  The message is
+        referred to by the pathname of the maildir file.  If the
+        message ID of the new message already exists in the database,
+        this adds ``pathname`` to the list of list of files for the
+        existing message.
+
+        :param filename: The path of the file containing the message.
+        :type filename: str, bytes, os.PathLike or pathlib.Path.
+        :param sync_flags: Whether to sync the known maildir flags to
+           notmuch tags.  See :meth:`Message.flags_to_tags` for
+           details.
+        :type sync_flags: bool
+        :param indexopts: The indexing options, see
+           :meth:`default_indexopts`.  Leave as `None` to use the
+           default options configured in the database.
+        :type indexopts: :class:`IndexOptions` or `None`
+
+        :returns: A tuple where the first item is the newly inserted
+           messages as a :class:`Message` instance, and the second
+           item is a boolean indicating if the message inserted was a
+           duplicate.  This is the namedtuple ``AddedMessage(msg,
+           dup)``.
+        :rtype: Database.AddedMessage
+
+        If an exception is raised, no message was added.
+
+        :raises XapianError: A Xapian exception occurred.
+        :raises FileError: The file referred to by ``pathname`` could
+           not be opened.
+        :raises FileNotEmailError: The file referreed to by
+           ``pathname`` is not recognised as an email message.
+        :raises ReadOnlyDatabaseError: The database is opened in
+           READ_ONLY mode.
+        :raises UpgradeRequiredError: The database must be upgraded
+           first.
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
+            filename = bytes(filename)
+        msg_pp = capi.ffi.new('notmuch_message_t **')
+        opts_p = indexopts._opts_p if indexopts else capi.ffi.NULL
+        ret = capi.lib.notmuch_database_index_file(
+            self._db_p, os.fsencode(filename), opts_p, msg_pp)
+        ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
+              capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
+        if ret not in ok:
+            raise errors.NotmuchError(ret)
+        msg = message.Message(self, msg_pp[0], db=self)
+        if sync_flags:
+            msg.tags.from_maildir_flags()
+        return self.AddedMessage(
+            msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
+
+    def remove(self, filename):
+        """Remove a message from the notmuch database.
+
+        Removing a message which is not in the database is just a
+        silent nop-operation.
+
+        :param filename: The pathname of the file containing the
+           message to be removed.
+        :type filename: str, bytes, os.PathLike or pathlib.Path.
+
+        :returns: True if the message is still in the database.  This
+           can happen when multiple files contain the same message ID.
+           The true/false distinction is fairly arbitrary, but think
+           of it as ``dup = db.remove_message(name); if dup: ...``.
+        :rtype: bool
+
+        :raises XapianError: A Xapian exception occurred.
+        :raises ReadOnlyDatabaseError: The database is opened in
+           READ_ONLY mode.
+        :raises UpgradeRequiredError: The database must be upgraded
+           first.
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
+            filename = bytes(filename)
+        ret = capi.lib.notmuch_database_remove_message(self._db_p,
+                                                       os.fsencode(filename))
+        ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
+              capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
+        if ret not in ok:
+            raise errors.NotmuchError(ret)
+        if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+            return True
+        else:
+            return False
+
+    def find(self, msgid):
+        """Return the message matching the given message ID.
+
+        If a message with the given message ID is found a
+        :class:`Message` instance is returned.  Otherwise a
+        :exc:`LookupError` is raised.
+
+        :param msgid: The message ID to look for.
+        :type msgid: str
+
+        :returns: The message instance.
+        :rtype: Message
+
+        :raises LookupError: If no message was found.
+        :raises OutOfMemoryError: When there is no memory to allocate
+           the message instance.
+        :raises XapianError: A Xapian exception occurred.
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        msg_pp = capi.ffi.new('notmuch_message_t **')
+        ret = capi.lib.notmuch_database_find_message(self._db_p,
+                                                     msgid.encode(), msg_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        msg_p = msg_pp[0]
+        if msg_p == capi.ffi.NULL:
+            raise LookupError
+        msg = message.Message(self, msg_p, db=self)
+        return msg
+
+    def get(self, filename):
+        """Return the :class:`Message` given a pathname.
+
+        If a message with the given pathname exists in the database
+        return the :class:`Message` instance for the message.
+        Otherwise raise a :exc:`LookupError` exception.
+
+        :param filename: The pathname of the message.
+        :type filename: str, bytes, os.PathLike or pathlib.Path
+
+        :returns: The message instance.
+        :rtype: Message
+
+        :raises LookupError: If no message was found.  This is also
+           a subclass of :exc:`KeyError`.
+        :raises OutOfMemoryError: When there is no memory to allocate
+           the message instance.
+        :raises XapianError: A Xapian exception occurred.
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
+            filename = bytes(filename)
+        msg_pp = capi.ffi.new('notmuch_message_t **')
+        ret = capi.lib.notmuch_database_find_message_by_filename(
+            self._db_p, os.fsencode(filename), msg_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        msg_p = msg_pp[0]
+        if msg_p == capi.ffi.NULL:
+            raise LookupError
+        msg = message.Message(self, msg_p, db=self)
+        return msg
+
+    @property
+    def tags(self):
+        """Return an immutable set with all tags used in this database.
+
+        This returns an immutable set-like object implementing the
+        collections.abc.Set Abstract Base Class.  Due to the
+        underlying libnotmuch implementation some operations have
+        different performance characteristics then plain set objects.
+        Mainly any lookup operation is O(n) rather then O(1).
+
+        Normal usage treats tags as UTF-8 encoded unicode strings so
+        they are exposed to Python as normal unicode string objects.
+        If you need to handle tags stored in libnotmuch which are not
+        valid unicode do check the :class:`ImmutableTagSet` docs for
+        how to handle this.
+
+        :rtype: ImmutableTagSet
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        try:
+            ref = self._cached_tagset
+        except AttributeError:
+            tagset = None
+        else:
+            tagset = ref()
+        if tagset is None:
+            tagset = tags.ImmutableTagSet(
+                self, '_db_p', capi.lib.notmuch_database_get_all_tags)
+            self._cached_tagset = weakref.ref(tagset)
+        return tagset
+
+    @property
+    def config(self):
+        """Return a mutable mapping with the settings stored in this database.
+
+        This returns an mutable dict-like object implementing the
+        collections.abc.MutableMapping Abstract Base Class.
+
+        :rtype: Config
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        try:
+            ref = self._cached_config
+        except AttributeError:
+            config_mapping = None
+        else:
+            config_mapping = ref()
+        if config_mapping is None:
+            config_mapping = config.ConfigMapping(self, '_db_p')
+            self._cached_config = weakref.ref(config_mapping)
+        return config_mapping
+
+    def _create_query(self, query, *,
+                      omit_excluded=EXCLUDE.TRUE,
+                      sort=SORT.UNSORTED,  # Check this default
+                      exclude_tags=None):
+        """Create an internal query object.
+
+        :raises OutOfMemoryError: if no memory is available to
+           allocate the query.
+        """
+        if isinstance(query, str):
+            query = query.encode('utf-8')
+        query_p = capi.lib.notmuch_query_create(self._db_p, query)
+        if query_p == capi.ffi.NULL:
+            raise errors.OutOfMemoryError()
+        capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value)
+        capi.lib.notmuch_query_set_sort(query_p, sort.value)
+        if exclude_tags is not None:
+            for tag in exclude_tags:
+                if isinstance(tag, str):
+                    tag = tag.encode('utf-8')
+                capi.lib.notmuch_query_add_tag_exclude(query_p, tag)
+        return querymod.Query(self, query_p)
+
+    def messages(self, query, *,
+                 omit_excluded=EXCLUDE.TRUE,
+                 sort=SORT.UNSORTED,  # Check this default
+                 exclude_tags=None):
+        """Search the database for messages.
+
+        :returns: An iterator over the messages found.
+        :rtype: MessageIter
+
+        :raises OutOfMemoryError: if no memory is available to
+           allocate the query.
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        query = self._create_query(query,
+                                   omit_excluded=omit_excluded,
+                                   sort=sort,
+                                   exclude_tags=exclude_tags)
+        return query.messages()
+
+    def count_messages(self, query, *,
+                       omit_excluded=EXCLUDE.TRUE,
+                       sort=SORT.UNSORTED,  # Check this default
+                       exclude_tags=None):
+        """Search the database for messages.
+
+        :returns: An iterator over the messages found.
+        :rtype: MessageIter
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        query = self._create_query(query,
+                                   omit_excluded=omit_excluded,
+                                   sort=sort,
+                                   exclude_tags=exclude_tags)
+        return query.count_messages()
+
+    def threads(self,  query, *,
+                omit_excluded=EXCLUDE.TRUE,
+                sort=SORT.UNSORTED,  # Check this default
+                exclude_tags=None):
+        query = self._create_query(query,
+                                   omit_excluded=omit_excluded,
+                                   sort=sort,
+                                   exclude_tags=exclude_tags)
+        return query.threads()
+
+    def count_threads(self, query, *,
+                      omit_excluded=EXCLUDE.TRUE,
+                      sort=SORT.UNSORTED,  # Check this default
+                      exclude_tags=None):
+        query = self._create_query(query,
+                                   omit_excluded=omit_excluded,
+                                   sort=sort,
+                                   exclude_tags=exclude_tags)
+        return query.count_threads()
+
+    def status_string(self):
+        raise NotImplementedError
+
+    def __repr__(self):
+        return 'Database(path={self.path}, mode={self.mode})'.format(self=self)
+
+
+class AtomicContext:
+    """Context manager for atomic support.
+
+    This supports the notmuch_database_begin_atomic and
+    notmuch_database_end_atomic API calls.  The object can not be
+    directly instantiated by the user, only via ``Database.atomic``.
+    It does keep a reference to the :class:`Database` instance to keep
+    the C memory alive.
+
+    :raises XapianError: When this is raised at enter time the atomic
+       section is not active.  When it is raised at exit time the
+       atomic section is still active and you may need to try using
+       :meth:`force_end`.
+    :raises ObjectDestroyedError: if used after destroyed.
+    """
+
+    def __init__(self, db, ptr_name):
+        self._db = db
+        self._ptr = lambda: getattr(db, ptr_name)
+        self._exit_fn = lambda: None
+
+    def __del__(self):
+        self._destroy()
+
+    @property
+    def alive(self):
+        return self.parent.alive
+
+    def _destroy(self):
+        pass
+
+    def __enter__(self):
+        ret = capi.lib.notmuch_database_begin_atomic(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        self._exit_fn = self._end_atomic
+        return self
+
+    def _end_atomic(self):
+        ret = capi.lib.notmuch_database_end_atomic(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self._exit_fn()
+
+    def force_end(self):
+        """Force ending the atomic section.
+
+        This can only be called once __exit__ has been called.  It
+        will attempt to close the atomic section (again).  This is
+        useful if the original exit raised an exception and the atomic
+        section is still open.  But things are pretty ugly by now.
+
+        :raises XapianError: If exiting fails, the atomic section is
+           not ended.
+        :raises UnbalancedAtomicError: If the database was currently
+           not in an atomic section.
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_database_end_atomic(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def abort(self):
+        """Abort the transaction.
+
+        Aborting a transaction will not commit any of the changes, but
+        will also implicitly close the database.
+        """
+        self._exit_fn = lambda: None
+        self._db.close()
+
+
+@functools.total_ordering
+class DbRevision:
+    """A database revision.
+
+    The database revision number increases monotonically with each
+    commit to the database.  Which means user-visible changes can be
+    ordered.  This object is sortable with other revisions.  It
+    carries the UUID of the database to ensure it is only ever
+    compared with revisions from the same database.
+    """
+
+    def __init__(self, rev, uuid):
+        self._rev = rev
+        self._uuid = uuid
+
+    @property
+    def rev(self):
+        """The revision number, a positive integer."""
+        return self._rev
+
+    @property
+    def uuid(self):
+        """The UUID of the database, consider this opaque."""
+        return self._uuid
+
+    def __eq__(self, other):
+        if isinstance(other, self.__class__):
+            if self.uuid != other.uuid:
+                return False
+            return self.rev == other.rev
+        else:
+            return NotImplemented
+
+    def __lt__(self, other):
+        if self.__class__ is other.__class__:
+            if self.uuid != other.uuid:
+                return False
+            return self.rev < other.rev
+        else:
+            return NotImplemented
+
+    def __repr__(self):
+        return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self)
+
+
+class IndexOptions(base.NotmuchObject):
+    """Indexing options.
+
+    This represents the indexing options which can be used to index a
+    message.  See :meth:`Database.default_indexopts` to create an
+    instance of this.  It can be used e.g. when indexing a new message
+    using :meth:`Database.add`.
+    """
+    _opts_p = base.MemoryPointer()
+
+    def __init__(self, parent, opts_p):
+        self._parent = parent
+        self._opts_p = opts_p
+
+    @property
+    def alive(self):
+        if not self._parent.alive:
+            return False
+        try:
+            self._opts_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def _destroy(self):
+        if self.alive:
+            capi.lib.notmuch_indexopts_destroy(self._opts_p)
+        self._opts_p = None
+
+    @property
+    def decrypt_policy(self):
+        """The decryption policy.
+
+        This is an enum from the :class:`DecryptionPolicy`.  See the
+        `index.decrypt` section in :man:`notmuch-config` for details
+        on the options.  **Do not set this to
+        :attr:`DecryptionPolicy.TRUE`** without considering the
+        security of your index.
+
+        You can change this policy by assigning a new
+        :class:`DecryptionPolicy` to this property.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+
+        :returns: A :class:`DecryptionPolicy` enum instance.
+        """
+        raw = capi.lib.notmuch_indexopts_get_decrypt_policy(self._opts_p)
+        return DecryptionPolicy(raw)
+
+    @decrypt_policy.setter
+    def decrypt_policy(self, val):
+        ret = capi.lib.notmuch_indexopts_set_decrypt_policy(
+            self._opts_p, val.value)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret, msg)
diff --git a/bindings/python-cffi/notmuch2/_errors.py b/bindings/python-cffi/notmuch2/_errors.py
new file mode 100644 (file)
index 0000000..17c3ad9
--- /dev/null
@@ -0,0 +1,124 @@
+from notmuch2 import _capi as capi
+
+
+class NotmuchError(Exception):
+    """Base exception for errors originating from the notmuch library.
+
+    Usually this will have two attributes:
+
+    :status: This is a numeric status code corresponding to the error
+       code in the notmuch library.  This is normally fairly
+       meaningless, it can also often be ``None``.  This exists mostly
+       to easily create new errors from notmuch status codes and
+       should not normally be used by users.
+
+    :message: A user-facing message for the error.  This can
+       occasionally also be ``None``.  Usually you'll want to call
+       ``str()`` on the error object instead to get a sensible
+       message.
+    """
+
+    @classmethod
+    def exc_type(cls, status):
+        """Return correct exception type for notmuch status."""
+        types = {
+            capi.lib.NOTMUCH_STATUS_OUT_OF_MEMORY:
+                OutOfMemoryError,
+            capi.lib.NOTMUCH_STATUS_READ_ONLY_DATABASE:
+                ReadOnlyDatabaseError,
+            capi.lib.NOTMUCH_STATUS_XAPIAN_EXCEPTION:
+                XapianError,
+            capi.lib.NOTMUCH_STATUS_FILE_ERROR:
+                FileError,
+            capi.lib.NOTMUCH_STATUS_FILE_NOT_EMAIL:
+                FileNotEmailError,
+            capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+                DuplicateMessageIdError,
+            capi.lib.NOTMUCH_STATUS_NULL_POINTER:
+                NullPointerError,
+            capi.lib.NOTMUCH_STATUS_TAG_TOO_LONG:
+                TagTooLongError,
+            capi.lib.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
+                UnbalancedFreezeThawError,
+            capi.lib.NOTMUCH_STATUS_UNBALANCED_ATOMIC:
+                UnbalancedAtomicError,
+            capi.lib.NOTMUCH_STATUS_UNSUPPORTED_OPERATION:
+                UnsupportedOperationError,
+            capi.lib.NOTMUCH_STATUS_UPGRADE_REQUIRED:
+                UpgradeRequiredError,
+            capi.lib.NOTMUCH_STATUS_PATH_ERROR:
+                PathError,
+            capi.lib.NOTMUCH_STATUS_ILLEGAL_ARGUMENT:
+                IllegalArgumentError,
+            capi.lib.NOTMUCH_STATUS_NO_CONFIG:
+                NoConfigError,
+            capi.lib.NOTMUCH_STATUS_NO_DATABASE:
+                NoDatabaseError,
+            capi.lib.NOTMUCH_STATUS_DATABASE_EXISTS:
+                DatabaseExistsError,
+            capi.lib.NOTMUCH_STATUS_BAD_QUERY_SYNTAX:
+                QuerySyntaxError,
+        }
+        return types[status]
+
+    def __new__(cls, *args, **kwargs):
+        """Return the correct subclass based on status."""
+        # This is simplistic, but the actual __init__ will fail if the
+        # signature is wrong anyway.
+        if args:
+            status = args[0]
+        else:
+            status = kwargs.get('status', None)
+        if status and cls == NotmuchError:
+            exc = cls.exc_type(status)
+            return exc.__new__(exc, *args, **kwargs)
+        else:
+            return super().__new__(cls)
+
+    def __init__(self, status=None, message=None):
+        self.status = status
+        self.message = message
+
+    def __str__(self):
+        if self.message:
+            return self.message
+        elif 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'
+
+
+class OutOfMemoryError(NotmuchError): pass
+class ReadOnlyDatabaseError(NotmuchError): pass
+class XapianError(NotmuchError): pass
+class FileError(NotmuchError): pass
+class FileNotEmailError(NotmuchError): pass
+class DuplicateMessageIdError(NotmuchError): pass
+class NullPointerError(NotmuchError): pass
+class TagTooLongError(NotmuchError): pass
+class UnbalancedFreezeThawError(NotmuchError): pass
+class UnbalancedAtomicError(NotmuchError): pass
+class UnsupportedOperationError(NotmuchError): pass
+class UpgradeRequiredError(NotmuchError): pass
+class PathError(NotmuchError): pass
+class IllegalArgumentError(NotmuchError): pass
+class NoConfigError(NotmuchError): pass
+class NoDatabaseError(NotmuchError): pass
+class DatabaseExistsError(NotmuchError): pass
+class QuerySyntaxError(NotmuchError): pass
+
+class ObjectDestroyedError(NotmuchError):
+    """The object has already been destroyed and it's memory freed.
+
+    This occurs when :meth:`destroy` has been called on the object but
+    you still happen to have access to the object.  This should not
+    normally occur since you should never call :meth:`destroy` by
+    hand.
+    """
+
+    def __str__(self):
+        if self.message:
+            return self.message
+        else:
+            return 'Memory already freed'
diff --git a/bindings/python-cffi/notmuch2/_message.py b/bindings/python-cffi/notmuch2/_message.py
new file mode 100644 (file)
index 0000000..d4b34e9
--- /dev/null
@@ -0,0 +1,724 @@
+import collections
+import contextlib
+import os
+import pathlib
+import weakref
+
+import notmuch2._base as base
+import notmuch2._capi as capi
+import notmuch2._errors as errors
+import notmuch2._tags as tags
+
+
+__all__ = ['Message']
+
+
+class Message(base.NotmuchObject):
+    """An email message stored in the notmuch database retrieved via a query.
+
+    This should not be directly created, instead it will be returned
+    by calling methods on :class:`Database`.  A message keeps a
+    reference to the database object since the database object can not
+    be released while the message is in use.
+
+    Note that this represents a message in the notmuch database.  For
+    full email functionality you may want to use the :mod:`email`
+    package from Python's standard library.  You could e.g. create
+    this as such::
+
+       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)
+
+    Most commonly the functionality provided by notmuch is sufficient
+    to read email however.
+
+    Messages are considered equal when they have the same message ID.
+    This is how libnotmuch treats messages as well, the
+    :meth:`pathnames` function returns multiple results for
+    duplicates.
+
+    :param parent: The parent object.  This is probably one off a
+       :class:`Database`, :class:`Thread` or :class:`Query`.
+    :type parent: NotmuchObject
+    :param db: The database instance this message is associated with.
+       This could be the same as the parent.
+    :type db: Database
+    :param msg_p: The C pointer to the ``notmuch_message_t``.
+    :type msg_p: <cdata>
+    :param dup: Whether the message was a duplicate on insertion.
+    :type dup: None or bool
+    """
+    _msg_p = base.MemoryPointer()
+
+    def __init__(self, parent, msg_p, *, db):
+        self._parent = parent
+        self._msg_p = msg_p
+        self._db = db
+
+    @property
+    def alive(self):
+        if not self._parent.alive:
+            return False
+        try:
+            self._msg_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def __del__(self):
+        self._destroy()
+
+    def _destroy(self):
+        if self.alive:
+            capi.lib.notmuch_message_destroy(self._msg_p)
+        self._msg_p = None
+
+    @property
+    def messageid(self):
+        """The message ID as a string.
+
+        The message ID is decoded with the ignore error handler.  This
+        is fine as long as the message ID is well formed.  If it is
+        not valid ASCII then this will be lossy.  So if you need to be
+        able to write the exact same message ID back you should use
+        :attr:`messageidb`.
+
+        Note that notmuch will decode the message ID value and thus
+        strip off the surrounding ``<`` and ``>`` characters.  This is
+        different from Python's :mod:`email` package behaviour which
+        leaves these characters in place.
+
+        :returns: The message ID.
+        :rtype: :class:`BinString`, this is a normal str but calling
+           bytes() on it will return the original bytes used to create
+           it.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
+        return base.BinString(capi.ffi.string(ret))
+
+    @property
+    def threadid(self):
+        """The thread ID.
+
+        The thread ID is decoded with the surrogateescape error
+        handler so that it is possible to reconstruct the original
+        thread ID if it is not valid UTF-8.
+
+        :returns: The thread ID.
+        :rtype: :class:`BinString`, this is a normal str but calling
+           bytes() on it will return the original bytes used to create
+           it.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
+        return base.BinString(capi.ffi.string(ret))
+
+    @property
+    def path(self):
+        """A pathname of the message as a pathlib.Path instance.
+
+        If multiple files in the database contain the same message ID
+        this will be just one of the files, chosen at random.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_message_get_filename(self._msg_p)
+        return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
+
+    @property
+    def pathb(self):
+        """A pathname of the message as a bytes object.
+
+        See :attr:`path` for details, this is the same but does return
+        the path as a bytes object which is faster but less convenient.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_message_get_filename(self._msg_p)
+        return capi.ffi.string(ret)
+
+    def filenames(self):
+        """Return an iterator of all files for this message.
+
+        If multiple files contained the same message ID they will all
+        be returned here.  The files are returned as instances of
+        :class:`pathlib.Path`.
+
+        :returns: Iterator yielding :class:`pathlib.Path` instances.
+        :rtype: iter
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
+        return PathIter(self, fnames_p)
+
+    def filenamesb(self):
+        """Return an iterator of all files for this message.
+
+        This is like :meth:`pathnames` but the files are returned as
+        byte objects instead.
+
+        :returns: Iterator yielding :class:`bytes` instances.
+        :rtype: iter
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
+        return FilenamesIter(self, fnames_p)
+
+    @property
+    def ghost(self):
+        """Indicates whether this message is a ghost message.
+
+        A ghost message if a message which we know exists, but it has
+        no files or content associated with it.  This can happen if
+        it was referenced by some other message.  Only the
+        :attr:`messageid` and :attr:`threadid` attributes are valid
+        for it.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_message_get_flag(
+            self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
+        return bool(ret)
+
+    @property
+    def excluded(self):
+        """Indicates whether this message was excluded from the query.
+
+        When a message is created from a search, sometimes messages
+        that where excluded by the search query could still be
+        returned by it, e.g. because they are part of a thread
+        matching the query.  the :meth:`Database.query` method allows
+        these messages to be flagged, which results in this property
+        being set to *True*.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_message_get_flag(
+            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.
+
+        The time the message was sent as an integer number of seconds
+        since the *epoch*, 1 Jan 1970.  This is derived from the
+        message's header, you can get the original header value with
+        :meth:`header`.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        return capi.lib.notmuch_message_get_date(self._msg_p)
+
+    def header(self, name):
+        """Return the value of the named header.
+
+        Returns the header from notmuch, some common headers are
+        stored in the database, others are read from the file.
+        Headers are returned with their newlines stripped and
+        collapsed concatenated together if they occur multiple times.
+        You may be better off using the standard library email
+        package's ``email.message_from_file(msg.path.open())`` if that
+        is not sufficient for you.
+
+        :param header: Case-insensitive header name to retrieve.
+        :type header: str or bytes
+
+        :returns: The header value, an empty string if the header is
+           not present.
+        :rtype: str
+
+        :raises LookupError: if the header is not present.
+        :raises NullPointerError: For unexpected notmuch errors.
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        # The returned is supposedly guaranteed to be UTF-8.  Header
+        # names must be ASCII as per RFC x822.
+        if isinstance(name, str):
+            name = name.encode('ascii')
+        ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
+        if ret == capi.ffi.NULL:
+            raise errors.NullPointerError()
+        hdr = capi.ffi.string(ret)
+        if not hdr:
+            raise LookupError
+        return hdr.decode(encoding='utf-8')
+
+    @property
+    def tags(self):
+        """The tags associated with the message.
+
+        This behaves as a set.  But removing and adding items to the
+        set removes and adds them to the message in the database.
+
+        :raises ReadOnlyDatabaseError: When manipulating tags on a
+           database opened in read-only mode.
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        try:
+            ref = self._cached_tagset
+        except AttributeError:
+            tagset = None
+        else:
+            tagset = ref()
+        if tagset is None:
+            tagset = tags.MutableTagSet(
+                self, '_msg_p', capi.lib.notmuch_message_get_tags)
+            self._cached_tagset = weakref.ref(tagset)
+        return tagset
+
+    @contextlib.contextmanager
+    def frozen(self):
+        """Context manager to freeze the message state.
+
+        This allows you to perform atomic tag updates::
+
+           with msg.frozen():
+               msg.tags.clear()
+               msg.tags.add('foo')
+
+        Using This would ensure the message never ends up with no tags
+        applied at all.
+
+        It is safe to nest calls to this context manager.
+
+        :raises ReadOnlyDatabaseError: if the database is opened in
+           read-only mode.
+        :raises UnbalancedFreezeThawError: if you somehow managed to
+           call __exit__ of this context manager more than once.  Why
+           did you do that?
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_message_freeze(self._msg_p)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        self._frozen = True
+        try:
+            yield
+        except Exception:
+            # Only way to "rollback" these changes is to destroy
+            # ourselves and re-create.  Behold.
+            msgid = self.messageid
+            self._destroy()
+            with contextlib.suppress(Exception):
+                new = self._db.find(msgid)
+                self._msg_p = new._msg_p
+                new._msg_p = None
+                del new
+            raise
+        else:
+            ret = capi.lib.notmuch_message_thaw(self._msg_p)
+            if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+                raise errors.NotmuchError(ret)
+            self._frozen = False
+
+    @property
+    def properties(self):
+        """A map of arbitrary key-value pairs associated with the message.
+
+        Be aware that properties may be used by other extensions to
+        store state in.  So delete or modify with care.
+
+        The properties map is somewhat special.  It is essentially a
+        multimap-like structure where each key can have multiple
+        values.  Therefore accessing a single item using
+        :meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
+        will only return you the *first* item if there are multiple
+        and thus are only recommended if you know there to be only one
+        value.
+
+        Instead the map has an additional :meth:`PropertiesMap.all`
+        method which can be used to retrieve all properties of a given
+        key.  This method also allows iterating of a a subset of the
+        keys starting with a given prefix.
+        """
+        try:
+            ref = self._cached_props
+        except AttributeError:
+            props = None
+        else:
+            props = ref()
+        if props is None:
+            props = PropertiesMap(self, '_msg_p')
+            self._cached_props = weakref.ref(props)
+        return props
+
+    def replies(self):
+        """Return an iterator of all replies to this message.
+
+        This method will only work if the message was created from a
+        thread.  Otherwise it will yield no results.
+
+        :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, msg_cls=OwnedMessage)
+
+    def __hash__(self):
+        return hash(self.messageid)
+
+    def __eq__(self, other):
+        if isinstance(other, self.__class__):
+            return self.messageid == other.messageid
+
+
+class OwnedMessage(Message):
+    """An email message owned by parent thread object.
+
+    This subclass of Message is used for messages that are retrieved
+    from the notmuch database via a parent :class:`notmuch2.Thread`
+    object, which "owns" this message.  This means that when this
+    message object is destroyed, by calling :func:`del` or
+    :meth:`_destroy` directly or indirectly, the message is not freed
+    in the notmuch API and the parent :class:`notmuch2.Thread` object
+    can return the same object again when needed.
+    """
+
+    @property
+    def alive(self):
+        return self._parent.alive
+
+    def _destroy(self):
+        pass
+
+
+class FilenamesIter(base.NotmuchIter):
+    """Iterator for binary filenames objects."""
+
+    def __init__(self, parent, iter_p):
+        super().__init__(parent, iter_p,
+                         fn_destroy=capi.lib.notmuch_filenames_destroy,
+                         fn_valid=capi.lib.notmuch_filenames_valid,
+                         fn_get=capi.lib.notmuch_filenames_get,
+                         fn_next=capi.lib.notmuch_filenames_move_to_next)
+
+    def __next__(self):
+        fname = super().__next__()
+        return capi.ffi.string(fname)
+
+
+class PathIter(FilenamesIter):
+    """Iterator for pathlib.Path objects."""
+
+    def __next__(self):
+        fname = super().__next__()
+        return pathlib.Path(os.fsdecode(fname))
+
+
+class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
+    """A mutable mapping to manage properties.
+
+    Both keys and values of properties are supposed to be UTF-8
+    strings in libnotmuch.  However since the uderlying API uses
+    bytestrings you can use either str or bytes to represent keys and
+    all returned keys and values use :class:`BinString`.
+
+    Also be aware that ``iter(this_map)`` will return duplicate keys,
+    while the :class:`collections.abc.KeysView` returned by
+    :meth:`keys` is a :class:`collections.abc.Set` subclass.  This
+    means the former will yield duplicate keys while the latter won't.
+    It also means ``len(list(iter(this_map)))`` could be different
+    than ``len(this_map.keys())``.  ``len(this_map)`` will correspond
+    with the length of the default iterator.
+
+    Be aware that libnotmuch exposes all of this as iterators, so
+    quite a few operations have O(n) performance instead of the usual
+    O(1).
+    """
+    Property = collections.namedtuple('Property', ['key', 'value'])
+    _marker = object()
+
+    def __init__(self, msg, ptr_name):
+        self._msg = msg
+        self._ptr = lambda: getattr(msg, ptr_name)
+
+    @property
+    def alive(self):
+        if not self._msg.alive:
+            return False
+        try:
+            self._ptr
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def _destroy(self):
+        pass
+
+    def __iter__(self):
+        """Return an iterator which iterates over the keys.
+
+        Be aware that a single key may have multiple values associated
+        with it, if so it will appear multiple times here.
+        """
+        iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+        return PropertiesKeyIter(self, iter_p)
+
+    def __len__(self):
+        iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+        it = base.NotmuchIter(
+            self, iter_p,
+            fn_destroy=capi.lib.notmuch_message_properties_destroy,
+            fn_valid=capi.lib.notmuch_message_properties_valid,
+            fn_get=capi.lib.notmuch_message_properties_key,
+            fn_next=capi.lib.notmuch_message_properties_move_to_next,
+        )
+        return len(list(it))
+
+    def __getitem__(self, key):
+        """Return **the first** peroperty associated with a key."""
+        if isinstance(key, str):
+            key = key.encode('utf-8')
+        value_pp = capi.ffi.new('char**')
+        ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        if value_pp[0] == capi.ffi.NULL:
+            raise KeyError
+        return base.BinString.from_cffi(value_pp[0])
+
+    def keys(self):
+        """Return a :class:`collections.abc.KeysView` for this map.
+
+        Even when keys occur multiple times this is a subset of set()
+        so will only contain them once.
+        """
+        return collections.abc.KeysView({k: None for k in self})
+
+    def items(self):
+        """Return a :class:`collections.abc.ItemsView` for this map.
+
+        The ItemsView treats a ``(key, value)`` pair as unique, so
+        dupcliate ``(key, value)`` pairs will be merged together.
+        However duplicate keys with different values will be returned.
+        """
+        items = set()
+        props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+        while capi.lib.notmuch_message_properties_valid(props_p):
+            key = capi.lib.notmuch_message_properties_key(props_p)
+            value = capi.lib.notmuch_message_properties_value(props_p)
+            items.add((base.BinString.from_cffi(key),
+                       base.BinString.from_cffi(value)))
+            capi.lib.notmuch_message_properties_move_to_next(props_p)
+        capi.lib.notmuch_message_properties_destroy(props_p)
+        return PropertiesItemsView(items)
+
+    def values(self):
+        """Return a :class:`collecions.abc.ValuesView` for this map.
+
+        All unique property values are included in the view.
+        """
+        values = set()
+        props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+        while capi.lib.notmuch_message_properties_valid(props_p):
+            value = capi.lib.notmuch_message_properties_value(props_p)
+            values.add(base.BinString.from_cffi(value))
+            capi.lib.notmuch_message_properties_move_to_next(props_p)
+        capi.lib.notmuch_message_properties_destroy(props_p)
+        return PropertiesValuesView(values)
+
+    def __setitem__(self, key, value):
+        """Add a key-value pair to the properties.
+
+        You may prefer to use :meth:`add` for clarity since this
+        method usually implies implicit overwriting of an existing key
+        if it exists, while for properties this is not the case.
+        """
+        self.add(key, value)
+
+    def add(self, key, value):
+        """Add a key-value pair to the properties."""
+        if isinstance(key, str):
+            key = key.encode('utf-8')
+        if isinstance(value, str):
+            value = value.encode('utf-8')
+        ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def __delitem__(self, key):
+        """Remove all properties with this key."""
+        if isinstance(key, str):
+            key = key.encode('utf-8')
+        ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def remove(self, key, value):
+        """Remove a key-value pair from the properties."""
+        if isinstance(key, str):
+            key = key.encode('utf-8')
+        if isinstance(value, str):
+            value = value.encode('utf-8')
+        ret = capi.lib.notmuch_message_remove_property(self._ptr(), key, value)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def pop(self, key, default=_marker):
+        try:
+            value = self[key]
+        except KeyError:
+            if default is self._marker:
+                raise
+            else:
+                return default
+        else:
+            self.remove(key, value)
+            return value
+
+    def popitem(self):
+        try:
+            key = next(iter(self))
+        except StopIteration:
+            raise KeyError
+        value = self.pop(key)
+        return (key, value)
+
+    def clear(self):
+        ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
+                                                             capi.ffi.NULL)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def getall(self, prefix='', *, exact=False):
+        """Return an iterator yielding all properties for a given key prefix.
+
+        The returned iterator yields all peroperties which start with
+        a given key prefix as ``(key, value)`` namedtuples.  If called
+        with ``exact=True`` then only properties which exactly match
+        the prefix are returned, those a key longer than the prefix
+        will not be included.
+
+        :param prefix: The prefix of the key.
+        """
+        if isinstance(prefix, str):
+            prefix = prefix.encode('utf-8')
+        props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
+                                                          prefix, exact)
+        return PropertiesIter(self, props_p)
+
+
+class PropertiesKeyIter(base.NotmuchIter):
+
+    def __init__(self, parent, iter_p):
+        super().__init__(
+            parent,
+            iter_p,
+            fn_destroy=capi.lib.notmuch_message_properties_destroy,
+            fn_valid=capi.lib.notmuch_message_properties_valid,
+            fn_get=capi.lib.notmuch_message_properties_key,
+            fn_next=capi.lib.notmuch_message_properties_move_to_next)
+
+    def __next__(self):
+        item = super().__next__()
+        return base.BinString.from_cffi(item)
+
+
+class PropertiesIter(base.NotmuchIter):
+
+    def __init__(self, parent, iter_p):
+        super().__init__(
+            parent,
+            iter_p,
+            fn_destroy=capi.lib.notmuch_message_properties_destroy,
+            fn_valid=capi.lib.notmuch_message_properties_valid,
+            fn_get=capi.lib.notmuch_message_properties_key,
+            fn_next=capi.lib.notmuch_message_properties_move_to_next,
+        )
+
+    def __next__(self):
+        if not self._fn_valid(self._iter_p):
+            self._destroy()
+            raise StopIteration
+        key = capi.lib.notmuch_message_properties_key(self._iter_p)
+        value = capi.lib.notmuch_message_properties_value(self._iter_p)
+        capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
+        return PropertiesMap.Property(base.BinString.from_cffi(key),
+                                      base.BinString.from_cffi(value))
+
+
+class PropertiesItemsView(collections.abc.Set):
+
+    __slots__ = ('_items',)
+
+    def __init__(self, items):
+        self._items = items
+
+    @classmethod
+    def _from_iterable(self, it):
+        return set(it)
+
+    def __len__(self):
+        return len(self._items)
+
+    def __contains__(self, item):
+        return item in self._items
+
+    def __iter__(self):
+        yield from self._items
+
+
+collections.abc.ItemsView.register(PropertiesItemsView)
+
+
+class PropertiesValuesView(collections.abc.Set):
+
+    __slots__ = ('_values',)
+
+    def __init__(self, values):
+        self._values = values
+
+    def __len__(self):
+        return len(self._values)
+
+    def __contains__(self, value):
+        return value in self._values
+
+    def __iter__(self):
+        yield from self._values
+
+
+collections.abc.ValuesView.register(PropertiesValuesView)
+
+
+class MessageIter(base.NotmuchIter):
+
+    def __init__(self, parent, msgs_p, *, db, msg_cls=Message):
+        self._db = db
+        self._msg_cls = msg_cls
+        super().__init__(parent, msgs_p,
+                         fn_destroy=capi.lib.notmuch_messages_destroy,
+                         fn_valid=capi.lib.notmuch_messages_valid,
+                         fn_get=capi.lib.notmuch_messages_get,
+                         fn_next=capi.lib.notmuch_messages_move_to_next)
+
+    def __next__(self):
+        msg_p = super().__next__()
+        return self._msg_cls(self, msg_p, db=self._db)
diff --git a/bindings/python-cffi/notmuch2/_query.py b/bindings/python-cffi/notmuch2/_query.py
new file mode 100644 (file)
index 0000000..1db6ec9
--- /dev/null
@@ -0,0 +1,83 @@
+from notmuch2 import _base as base
+from notmuch2 import _capi as capi
+from notmuch2 import _errors as errors
+from notmuch2 import _message as message
+from notmuch2 import _thread as thread
+
+
+__all__ = []
+
+
+class Query(base.NotmuchObject):
+    """Private, minimal query object.
+
+    This is not meant for users and is not a full implementation of
+    the query API.  It is only an intermediate used internally to
+    match libnotmuch's memory management.
+    """
+    _query_p = base.MemoryPointer()
+
+    def __init__(self, db, query_p):
+        self._db = db
+        self._query_p = query_p
+
+    @property
+    def alive(self):
+        if not self._db.alive:
+            return False
+        try:
+            self._query_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def __del__(self):
+        self._destroy()
+
+    def _destroy(self):
+        if self.alive:
+            capi.lib.notmuch_query_destroy(self._query_p)
+        self._query_p = None
+
+    @property
+    def query(self):
+        """The query string as seen by libnotmuch."""
+        q = capi.lib.notmuch_query_get_query_string(self._query_p)
+        return base.BinString.from_cffi(q)
+
+    def messages(self):
+        """Return an iterator over all the messages found by the query.
+
+        This executes the query and returns an iterator over the
+        :class:`Message` objects found.
+        """
+        msgs_pp = capi.ffi.new('notmuch_messages_t**')
+        ret = capi.lib.notmuch_query_search_messages(self._query_p, msgs_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        return message.MessageIter(self, msgs_pp[0], db=self._db)
+
+    def count_messages(self):
+        """Return the number of messages matching this query."""
+        count_p = capi.ffi.new('unsigned int *')
+        ret = capi.lib.notmuch_query_count_messages(self._query_p, count_p)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        return count_p[0]
+
+    def threads(self):
+        """Return an iterator over all the threads found by the query."""
+        threads_pp = capi.ffi.new('notmuch_threads_t **')
+        ret = capi.lib.notmuch_query_search_threads(self._query_p, threads_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        return thread.ThreadIter(self, threads_pp[0], db=self._db)
+
+    def count_threads(self):
+        """Return the number of threads matching this query."""
+        count_p = capi.ffi.new('unsigned int *')
+        ret = capi.lib.notmuch_query_count_threads(self._query_p, count_p)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        return count_p[0]
diff --git a/bindings/python-cffi/notmuch2/_tags.py b/bindings/python-cffi/notmuch2/_tags.py
new file mode 100644 (file)
index 0000000..ee5d2a3
--- /dev/null
@@ -0,0 +1,359 @@
+import collections.abc
+
+import notmuch2._base as base
+import notmuch2._capi as capi
+import notmuch2._errors as errors
+
+
+__all__ = ['ImmutableTagSet', 'MutableTagSet', 'TagsIter']
+
+
+class ImmutableTagSet(base.NotmuchObject, collections.abc.Set):
+    """The tags associated with a message thread or whole database.
+
+    Both a thread as well as the database expose the union of all tags
+    in messages associated with them.  This exposes these as a
+    :class:`collections.abc.Set` object.
+
+    Note that due to the underlying notmuch API the performance of the
+    implementation is not the same as you would expect from normal
+    sets.  E.g. the :meth:`__contains__` and :meth:`__len__` are O(n)
+    rather then O(1).
+
+    Tags are internally stored as bytestrings but normally exposed as
+    unicode strings using the UTF-8 encoding and the *ignore* decoder
+    error handler.  However the :meth:`iter` method can be used to
+    return tags as bytestrings or using a different error handler.
+
+    Note that when doing arithmetic operations on tags, this class
+    will return a plain normal set as it is no longer associated with
+    the message.
+
+    :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.
+    :param cffi_fn: the callable CFFI wrapper to retrieve the tags
+       iter.  This can be one of notmuch_database_get_all_tags,
+       notmuch_thread_get_tags or notmuch_message_get_tags.
+    """
+
+    def __init__(self, parent, ptr_name, cffi_fn):
+        self._parent = parent
+        self._ptr = lambda: getattr(parent, ptr_name)
+        self._cffi_fn = cffi_fn
+
+    def __del__(self):
+        self._destroy()
+
+    @property
+    def alive(self):
+        return self._parent.alive
+
+    def _destroy(self):
+        pass
+
+    @classmethod
+    def _from_iterable(cls, it):
+        return set(it)
+
+    def __iter__(self):
+        """Return an iterator over the tags.
+
+        Tags are yielded as unicode strings, decoded using the
+        "ignore" error handler.
+
+        :raises NullPointerError: If the iterator can not be created.
+        """
+        return self.iter(encoding='utf-8', errors='ignore')
+
+    def iter(self, *, encoding=None, errors='strict'):
+        """Aternate iterator constructor controlling string decoding.
+
+        Tags are stored as bytes in the notmuch database, in Python
+        it's easier to work with unicode strings and thus is what the
+        normal iterator returns.  However this method allows you to
+        specify how you would like to get the tags, defaulting to the
+        bytestring representation instead of unicode strings.
+
+        :param encoding: Which codec to use.  The default *None* does not
+           decode at all and will return the unmodified bytes.
+           Otherwise this is passed on to :func:`str.decode`.
+        :param errors: If using a codec, this is the error handler.
+           See :func:`str.decode` to which this is passed on.
+
+        :raises NullPointerError: When things do not go as planned.
+        """
+        # self._cffi_fn should point either to
+        # notmuch_database_get_all_tags, notmuch_thread_get_tags or
+        # notmuch_message_get_tags.  nothmuch.h suggests these never
+        # fail, let's handle NULL anyway.
+        tags_p = self._cffi_fn(self._ptr())
+        if tags_p == capi.ffi.NULL:
+            raise errors.NullPointerError()
+        tags = TagsIter(self, tags_p, encoding=encoding, errors=errors)
+        return tags
+
+    def __len__(self):
+        return sum(1 for t in self)
+
+    def __contains__(self, tag):
+        if isinstance(tag, str):
+            tag = tag.encode()
+        for msg_tag in self.iter():
+            if tag == msg_tag:
+                return True
+        else:
+            return False
+
+    def __eq__(self, other):
+        return tuple(sorted(self.iter())) == tuple(sorted(other.iter()))
+
+    def issubset(self, other):
+        return self <= other
+
+    def issuperset(self, other):
+        return self >= other
+
+    def union(self, other):
+        return self | other
+
+    def intersection(self, other):
+        return self & other
+
+    def difference(self, other):
+        return self - other
+
+    def symmetric_difference(self, other):
+        return self ^ other
+
+    def copy(self):
+        return set(self)
+
+    def __hash__(self):
+        return hash(tuple(self.iter()))
+
+    def __repr__(self):
+        return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
+            name=self.__class__.__name__,
+            addr=id(self),
+            tags=', '.join(repr(t) for t in self))
+
+
+class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
+    """The tags associated with a message.
+
+    This is a :class:`collections.abc.MutableSet` object which can be
+    used to manipulate the tags of a message.
+
+    Note that due to the underlying notmuch API the performance of the
+    implementation is not the same as you would expect from normal
+    sets.  E.g. the ``in`` operator and variants are O(n) rather then
+    O(1).
+
+    Tags are bytestrings and calling ``iter()`` will return an
+    iterator yielding bytestrings.  However the :meth:`iter` method
+    can be used to return tags as unicode strings, while all other
+    operations accept either byestrings or unicode strings.  In case
+    unicode strings are used they will be encoded using utf-8 before
+    being passed to notmuch.
+    """
+
+    # Since we subclass ImmutableTagSet we inherit a __hash__.  But we
+    # are mutable, setting it to None will make the Python machinery
+    # recognise us as unhashable.
+    __hash__ = None
+
+    def add(self, tag):
+        """Add a tag to the message.
+
+        :param tag: The tag to add.
+        :type tag: str or bytes.  A str will be encoded using UTF-8.
+
+        :param sync_flags: Whether to sync the maildir flags with the
+           new set of tags.  Leaving this as *None* respects the
+           configuration set in the database, while *True* will always
+           sync and *False* will never sync.
+        :param sync_flags: NoneType or bool
+
+        :raises TypeError: If the tag is not a valid type.
+        :raises TagTooLongError: If the added tag exceeds the maximum
+           length, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
+        :raises ReadOnlyDatabaseError: If the database is opened in
+           read-only mode.
+        """
+        if isinstance(tag, str):
+            tag = tag.encode()
+        if not isinstance(tag, bytes):
+            raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
+        ret = capi.lib.notmuch_message_add_tag(self._ptr(), tag)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def discard(self, tag):
+        """Remove a tag from the message.
+
+        :param tag: The tag to remove.
+        :type tag: str of bytes.  A str will be encoded using UTF-8.
+        :param sync_flags: Whether to sync the maildir flags with the
+           new set of tags.  Leaving this as *None* respects the
+           configuration set in the database, while *True* will always
+           sync and *False* will never sync.
+        :param sync_flags: NoneType or bool
+
+        :raises TypeError: If the tag is not a valid type.
+        :raises TagTooLongError: If the tag exceeds the maximum
+           length, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
+        :raises ReadOnlyDatabaseError: If the database is opened in
+           read-only mode.
+        """
+        if isinstance(tag, str):
+            tag = tag.encode()
+        if not isinstance(tag, bytes):
+            raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
+        ret = capi.lib.notmuch_message_remove_tag(self._ptr(), tag)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def clear(self):
+        """Remove all tags from the message.
+
+        :raises ReadOnlyDatabaseError: If the database is opened in
+           read-only mode.
+        """
+        ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def from_maildir_flags(self):
+        """Update the tags based on the state in the message's maildir flags.
+
+        This function examines the filenames of 'message' for maildir
+        flags, and adds or removes tags on 'message' as follows when
+        these flags are present:
+
+        Flag    Action if present
+        ----    -----------------
+        'D'     Adds the "draft" tag to the message
+        'F'     Adds the "flagged" tag to the message
+        'P'     Adds the "passed" tag to the message
+        'R'     Adds the "replied" tag to the message
+        'S'     Removes the "unread" tag from the message
+
+        For each flag that is not present, the opposite action
+        (add/remove) is performed for the corresponding tags.
+
+        Flags are identified as trailing components of the filename
+        after a sequence of ":2,".
+
+        If there are multiple filenames associated with this message,
+        the flag is considered present if it appears in one or more
+        filenames. (That is, the flags from the multiple filenames are
+        combined with the logical OR operator.)
+        """
+        ret = capi.lib.notmuch_message_maildir_flags_to_tags(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def to_maildir_flags(self):
+        """Update the message's maildir flags based on the notmuch tags.
+
+        If the message's filename is in a maildir directory, that is a
+        directory named ``new`` or ``cur``, and has a valid maildir
+        filename then the flags will be added as such:
+
+        'D' if the message has the "draft" tag
+        'F' if the message has the "flagged" tag
+        'P' if the message has the "passed" tag
+        'R' if the message has the "replied" tag
+        'S' if the message does not have the "unread" tag
+
+        Any existing flags unmentioned in the list above will be
+        preserved in the renaming.
+
+        Also, if this filename is in a directory named "new", rename it to
+        be within the neighboring directory named "cur".
+
+        In case there are multiple files associated with the message
+        all filenames will get the same logic applied.
+        """
+        ret = capi.lib.notmuch_message_tags_to_maildir_flags(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+
+class TagsIter(base.NotmuchObject, collections.abc.Iterator):
+    """Iterator over tags.
+
+    This is only an iterator, not a container so calling
+    :meth:`__iter__` does not return a new, replenished iterator but
+    only itself.
+
+    :param parent: The parent object to keep alive.
+    :param tags_p: The CFFI pointer to the C-level tags iterator.
+    :param encoding: Which codec to use.  The default *None* does not
+       decode at all and will return the unmodified bytes.
+       Otherwise this is passed on to :func:`str.decode`.
+    :param errors: If using a codec, this is the error handler.
+       See :func:`str.decode` to which this is passed on.
+
+    :raises ObjectDestroyedError: if used after destroyed.
+    """
+    _tags_p = base.MemoryPointer()
+
+    def __init__(self, parent, tags_p, *, encoding=None, errors='strict'):
+        self._parent = parent
+        self._tags_p = tags_p
+        self._encoding = encoding
+        self._errors = errors
+
+    def __del__(self):
+        self._destroy()
+
+    @property
+    def alive(self):
+        if not self._parent.alive:
+            return False
+        try:
+            self._tags_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def _destroy(self):
+        if self.alive:
+            try:
+                capi.lib.notmuch_tags_destroy(self._tags_p)
+            except errors.ObjectDestroyedError:
+                pass
+        self._tags_p = None
+
+    def __iter__(self):
+        """Return the iterator itself.
+
+        Note that as this is an iterator and not a container this will
+        not return a new iterator.  Thus any elements already consumed
+        will not be yielded by the :meth:`__next__` method anymore.
+        """
+        return self
+
+    def __next__(self):
+        if not capi.lib.notmuch_tags_valid(self._tags_p):
+            self._destroy()
+            raise StopIteration()
+        tag_p = capi.lib.notmuch_tags_get(self._tags_p)
+        tag = capi.ffi.string(tag_p)
+        if self._encoding:
+            tag = tag.decode(encoding=self._encoding, errors=self._errors)
+        capi.lib.notmuch_tags_move_to_next(self._tags_p)
+        return tag
+
+    def __repr__(self):
+        try:
+            self._tags_p
+        except errors.ObjectDestroyedError:
+            return '<TagsIter (exhausted)>'
+        else:
+            return '<TagsIter>'
diff --git a/bindings/python-cffi/notmuch2/_thread.py b/bindings/python-cffi/notmuch2/_thread.py
new file mode 100644 (file)
index 0000000..e883f30
--- /dev/null
@@ -0,0 +1,194 @@
+import collections.abc
+import weakref
+
+from notmuch2 import _base as base
+from notmuch2 import _capi as capi
+from notmuch2 import _errors as errors
+from notmuch2 import _message as message
+from notmuch2 import _tags as tags
+
+
+__all__ = ['Thread']
+
+
+class Thread(base.NotmuchObject, collections.abc.Iterable):
+    _thread_p = base.MemoryPointer()
+
+    def __init__(self, parent, thread_p, *, db):
+        self._parent = parent
+        self._thread_p = thread_p
+        self._db = db
+
+    @property
+    def alive(self):
+        if not self._parent.alive:
+            return False
+        try:
+            self._thread_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def __del__(self):
+        self._destroy()
+
+    def _destroy(self):
+        if self.alive:
+            capi.lib.notmuch_thread_destroy(self._thread_p)
+        self._thread_p = None
+
+    @property
+    def threadid(self):
+        """The thread ID as a :class:`BinString`.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_thread_get_thread_id(self._thread_p)
+        return base.BinString.from_cffi(ret)
+
+    def __len__(self):
+        """Return the number of messages in the thread.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        return capi.lib.notmuch_thread_get_total_messages(self._thread_p)
+
+    def toplevel(self):
+        """Return an iterator of the toplevel messages.
+
+        :returns: An iterator yielding :class:`Message` instances.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        msgs_p = capi.lib.notmuch_thread_get_toplevel_messages(self._thread_p)
+        return message.MessageIter(self, msgs_p,
+                                   db=self._db,
+                                   msg_cls=message.OwnedMessage)
+
+    def __iter__(self):
+        """Return an iterator over all the messages in the thread.
+
+        :returns: An iterator yielding :class:`Message` instances.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        msgs_p = capi.lib.notmuch_thread_get_messages(self._thread_p)
+        return message.MessageIter(self, msgs_p,
+                                   db=self._db,
+                                   msg_cls=message.OwnedMessage)
+
+    @property
+    def matched(self):
+        """The number of messages in this thread which matched the query.
+
+        Of the messages in the thread this gives the count of messages
+        which did directly match the search query which this thread
+        originates from.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        return capi.lib.notmuch_thread_get_matched_messages(self._thread_p)
+
+    @property
+    def authors(self):
+        """A comma-separated string of all authors in the thread.
+
+        Authors of messages which matched the query the thread was
+        retrieved from will be at the head of the string, ordered by
+        date of their messages.  Following this will be the authors of
+        the other messages in the thread, also ordered by date of
+        their messages.  Both groups of authors are separated by the
+        ``|`` character.
+
+        :returns: The stringified list of authors.
+        :rtype: BinString
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_thread_get_authors(self._thread_p)
+        return base.BinString.from_cffi(ret)
+
+    @property
+    def subject(self):
+        """The subject of the thread, taken from the first message.
+
+        The thread's subject is taken to be the subject of the first
+        message according to query sort order.
+
+        :returns: The thread's subject.
+        :rtype: BinString
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_thread_get_subject(self._thread_p)
+        return base.BinString.from_cffi(ret)
+
+    @property
+    def first(self):
+        """Return the date of the oldest message in the thread.
+
+        The time the first message was sent as an integer number of
+        seconds since the *epoch*, 1 Jan 1970.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        return capi.lib.notmuch_thread_get_oldest_date(self._thread_p)
+
+    @property
+    def last(self):
+        """Return the date of the newest message in the thread.
+
+        The time the last message was sent as an integer number of
+        seconds since the *epoch*, 1 Jan 1970.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        return capi.lib.notmuch_thread_get_newest_date(self._thread_p)
+
+    @property
+    def tags(self):
+        """Return an immutable set with all tags used in this thread.
+
+        This returns an immutable set-like object implementing the
+        collections.abc.Set Abstract Base Class.  Due to the
+        underlying libnotmuch implementation some operations have
+        different performance characteristics then plain set objects.
+        Mainly any lookup operation is O(n) rather then O(1).
+
+        Normal usage treats tags as UTF-8 encoded unicode strings so
+        they are exposed to Python as normal unicode string objects.
+        If you need to handle tags stored in libnotmuch which are not
+        valid unicode do check the :class:`ImmutableTagSet` docs for
+        how to handle this.
+
+        :rtype: ImmutableTagSet
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        try:
+            ref = self._cached_tagset
+        except AttributeError:
+            tagset = None
+        else:
+            tagset = ref()
+        if tagset is None:
+            tagset = tags.ImmutableTagSet(
+                self, '_thread_p', capi.lib.notmuch_thread_get_tags)
+            self._cached_tagset = weakref.ref(tagset)
+        return tagset
+
+
+class ThreadIter(base.NotmuchIter):
+
+    def __init__(self, parent, threads_p, *, db):
+        self._db = db
+        super().__init__(parent, threads_p,
+                         fn_destroy=capi.lib.notmuch_threads_destroy,
+                         fn_valid=capi.lib.notmuch_threads_valid,
+                         fn_get=capi.lib.notmuch_threads_get,
+                         fn_next=capi.lib.notmuch_threads_move_to_next)
+
+    def __next__(self):
+        thread_p = super().__next__()
+        return Thread(self, thread_p, db=self._db)
diff --git a/bindings/python-cffi/setup.py b/bindings/python-cffi/setup.py
new file mode 100644 (file)
index 0000000..55fb2d2
--- /dev/null
@@ -0,0 +1,25 @@
+import setuptools
+from _notmuch_config import *
+
+with open(NOTMUCH_VERSION_FILE) as fp:
+    VERSION = fp.read().strip()
+
+setuptools.setup(
+    name='notmuch2',
+    version=VERSION,
+    description='Pythonic bindings for the notmuch mail database using CFFI',
+    author='Floris Bruynooghe',
+    author_email='flub@devork.be',
+    setup_requires=['cffi>=1.0.0'],
+    install_requires=['cffi>=1.0.0'],
+    packages=setuptools.find_packages(exclude=['tests']),
+    cffi_modules=['notmuch2/_build.py:ffibuilder'],
+    classifiers=[
+        'Development Status :: 3 - Alpha',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: GNU General Public License (GPL)',
+        'Programming Language :: Python :: 3',
+        'Topic :: Communications :: Email',
+        'Topic :: Software Development :: Libraries',
+    ],
+)
diff --git a/bindings/python-cffi/tests/conftest.py b/bindings/python-cffi/tests/conftest.py
new file mode 100644 (file)
index 0000000..fe90c78
--- /dev/null
@@ -0,0 +1,149 @@
+import email.message
+import mailbox
+import pathlib
+import shutil
+import socket
+import subprocess
+import textwrap
+import time
+import os
+
+import pytest
+
+
+def pytest_report_header():
+    which = shutil.which('notmuch')
+    vers = subprocess.run(['notmuch', '--version'], stdout=subprocess.PIPE)
+    return ['{} ({})'.format(vers.stdout.decode(errors='replace').strip(),which)]
+
+
+@pytest.fixture(scope='function')
+def tmppath(tmpdir):
+    """The tmpdir fixture wrapped in pathlib.Path."""
+    return pathlib.Path(str(tmpdir))
+
+
+@pytest.fixture
+def notmuch(maildir):
+    """Return a function which runs notmuch commands on our test maildir.
+
+    This uses the notmuch-config file created by the ``maildir``
+    fixture.
+    """
+    def run(*args):
+        """Run a notmuch command.
+
+        This function runs with a timeout error as many notmuch
+        commands may block if multiple processes are trying to open
+        the database in write-mode.  It is all too easy to
+        accidentally do this in the unittests.
+        """
+        cfg_fname = maildir.path / 'notmuch-config'
+        cmd = ['notmuch'] + list(args)
+        env = os.environ.copy()
+        env['NOTMUCH_CONFIG'] = str(cfg_fname)
+        proc = subprocess.run(cmd,
+                              timeout=120,
+                              env=env)
+        proc.check_returncode()
+    return run
+
+
+@pytest.fixture
+def maildir(tmppath):
+    """A basic test interface to a valid maildir directory.
+
+    This creates a valid maildir and provides a simple mechanism to
+    deliver test emails to it.  It also writes a notmuch-config file
+    in the top of the maildir.
+    """
+    cur = tmppath / 'cur'
+    cur.mkdir()
+    new = tmppath / 'new'
+    new.mkdir()
+    tmp = tmppath / 'tmp'
+    tmp.mkdir()
+    cfg_fname = tmppath/'notmuch-config'
+    with cfg_fname.open('w') as fp:
+        fp.write(textwrap.dedent("""\
+            [database]
+            path={tmppath!s}
+            [user]
+            name=Some Hacker
+            primary_email=dst@example.com
+            [new]
+            tags=unread;inbox;
+            ignore=
+            [search]
+            exclude_tags=deleted;spam;
+            [maildir]
+            synchronize_flags=true
+            """.format(tmppath=tmppath)))
+    return MailDir(tmppath)
+
+
+class MailDir:
+    """An interface around a correct maildir."""
+
+    def __init__(self, path):
+        self._path = pathlib.Path(path)
+        self.mailbox = mailbox.Maildir(str(path))
+        self._idcount = 0
+
+    @property
+    def path(self):
+        """The pathname of the maildir."""
+        return self._path
+
+    def _next_msgid(self):
+        """Return a new unique message ID."""
+        msgid = '{}@{}'.format(self._idcount, socket.getfqdn())
+        self._idcount += 1
+        return msgid
+
+    def deliver(self,
+                subject='Test mail',
+                body='This is a test mail',
+                to='dst@example.com',
+                frm='src@example.com',
+                headers=None,
+                new=False,      # Move to new dir or cur dir?
+                keywords=None,  # List of keywords or labels
+                seen=False,     # Seen flag (cur dir only)
+                replied=False,  # Replied flag (cur dir only)
+                flagged=False):  # Flagged flag (cur dir only)
+        """Deliver a new mail message in the mbox.
+
+        This does only adds the message to maildir, does not insert it
+        into the notmuch database.
+
+        :returns: A tuple of (msgid, pathname).
+        """
+        msgid = self._next_msgid()
+        when = time.time()
+        msg = email.message.EmailMessage()
+        msg.add_header('Received', 'by MailDir; {}'.format(time.ctime(when)))
+        msg.add_header('Message-ID', '<{}>'.format(msgid))
+        msg.add_header('Date', time.ctime(when))
+        msg.add_header('From', frm)
+        msg.add_header('To', to)
+        msg.add_header('Subject', subject)
+        if headers:
+            for h, v in headers:
+                msg.add_header(h, v)
+        msg.set_content(body)
+        mdmsg = mailbox.MaildirMessage(msg)
+        if not new:
+            mdmsg.set_subdir('cur')
+        if flagged:
+            mdmsg.add_flag('F')
+        if replied:
+            mdmsg.add_flag('R')
+        if seen:
+            mdmsg.add_flag('S')
+        boxid = self.mailbox.add(mdmsg)
+        basename = boxid
+        if mdmsg.get_info():
+            basename += mailbox.Maildir.colon + mdmsg.get_info()
+        msgpath = self.path / mdmsg.get_subdir() / basename
+        return (msgid, msgpath)
diff --git a/bindings/python-cffi/tests/test_base.py b/bindings/python-cffi/tests/test_base.py
new file mode 100644 (file)
index 0000000..d3280a6
--- /dev/null
@@ -0,0 +1,116 @@
+import pytest
+
+from notmuch2 import _base as base
+from notmuch2 import _errors as errors
+
+
+class TestNotmuchObject:
+
+    def test_no_impl_methods(self):
+        class Object(base.NotmuchObject):
+            pass
+        with pytest.raises(TypeError):
+            Object()
+
+    def test_impl_methods(self):
+
+        class Object(base.NotmuchObject):
+
+            def __init__(self):
+                pass
+
+            @property
+            def alive(self):
+                pass
+
+            def _destroy(self, parent=False):
+                pass
+
+        Object()
+
+    def test_del(self):
+        destroyed = False
+
+        class Object(base.NotmuchObject):
+
+            def __init__(self):
+                pass
+
+            @property
+            def alive(self):
+                pass
+
+            def _destroy(self, parent=False):
+                nonlocal destroyed
+                destroyed = True
+
+        o = Object()
+        o.__del__()
+        assert destroyed
+
+
+class TestMemoryPointer:
+
+    @pytest.fixture
+    def obj(self):
+        class Cls:
+            ptr = base.MemoryPointer()
+        return Cls()
+
+    def test_unset(self, obj):
+        with pytest.raises(errors.ObjectDestroyedError):
+            obj.ptr
+
+    def test_set(self, obj):
+        obj.ptr = 'some'
+        assert obj.ptr == 'some'
+
+    def test_cleared(self, obj):
+        obj.ptr = 'some'
+        obj.ptr
+        obj.ptr = None
+        with pytest.raises(errors.ObjectDestroyedError):
+            obj.ptr
+
+    def test_two_instances(self, obj):
+        obj2 = obj.__class__()
+        obj.ptr = 'foo'
+        obj2.ptr = 'bar'
+        assert obj.ptr != obj2.ptr
+
+
+class TestBinString:
+
+    def test_type(self):
+        s = base.BinString(b'foo')
+        assert isinstance(s, str)
+
+    def test_init_bytes(self):
+        s = base.BinString(b'foo')
+        assert s == 'foo'
+
+    def test_init_str(self):
+        s = base.BinString('foo')
+        assert s == 'foo'
+
+    def test_bytes(self):
+        s = base.BinString(b'foo')
+        assert bytes(s) == b'foo'
+
+    def test_invalid_utf8(self):
+        s = base.BinString(b'\x80foo')
+        assert s == 'foo'
+        assert bytes(s) == b'\x80foo'
+
+    def test_errors(self):
+        s = base.BinString(b'\x80foo', errors='replace')
+        assert s == '�foo'
+        assert bytes(s) == b'\x80foo'
+
+    def test_encoding(self):
+        # pound sign: '£' == '\u00a3' latin-1: b'\xa3', utf-8: b'\xc2\xa3'
+        with pytest.raises(UnicodeDecodeError):
+            base.BinString(b'\xa3', errors='strict')
+        s = base.BinString(b'\xa3', encoding='latin-1', errors='strict')
+        assert s == '£'
+        assert bytes(s) == b'\xa3'
diff --git a/bindings/python-cffi/tests/test_config.py b/bindings/python-cffi/tests/test_config.py
new file mode 100644 (file)
index 0000000..2a7f42f
--- /dev/null
@@ -0,0 +1,60 @@
+import collections.abc
+
+import pytest
+
+import notmuch2._database as dbmod
+
+import notmuch2._config as config
+
+
+class TestIter:
+
+    @pytest.fixture
+    def db(self, maildir):
+        with dbmod.Database.create(maildir.path) as db:
+            yield db
+
+    def test_type(self, db):
+        assert isinstance(db.config, collections.abc.MutableMapping)
+        assert isinstance(db.config, config.ConfigMapping)
+
+    def test_alive(self, db):
+        assert db.config.alive
+
+    def test_set_get(self, maildir):
+        # Ensure get-set works from different db objects
+        with dbmod.Database.create(maildir.path, config=dbmod.Database.CONFIG.EMPTY) as db0:
+            db0.config['spam'] = 'ham'
+        with dbmod.Database(maildir.path, config=dbmod.Database.CONFIG.EMPTY) as db1:
+            assert db1.config['spam'] == 'ham'
+
+    def test_get_keyerror(self, db):
+        with pytest.raises(KeyError):
+            val = db.config['not-a-key']
+            print(repr(val))
+
+    def test_iter(self, db):
+        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):
+        defaults = len(db.config)
+        db.config['spam'] = 'ham'
+        assert len(db.config) == defaults + 1
+        db.config['eggs'] = 'bacon'
+        assert len(db.config) == defaults + 2
+
+    def test_del(self, db):
+        db.config['spam'] = 'ham'
+        assert db.config.get('spam') == 'ham'
+        del db.config['spam']
+        assert db.config.get('spam') is None
diff --git a/bindings/python-cffi/tests/test_database.py b/bindings/python-cffi/tests/test_database.py
new file mode 100644 (file)
index 0000000..f1d12ea
--- /dev/null
@@ -0,0 +1,342 @@
+import collections
+import configparser
+import os
+import pathlib
+
+import pytest
+
+import notmuch2
+import notmuch2._errors as errors
+import notmuch2._database as dbmod
+import notmuch2._message as message
+
+
+@pytest.fixture
+def db(maildir):
+    with dbmod.Database.create(maildir.path, config=notmuch2.Database.CONFIG.EMPTY) as db:
+        yield db
+
+
+class TestDefaultDb:
+    """Tests for reading the default database.
+
+    The error cases are fairly undefined, some relevant Python error
+    will come out if you give it a bad filename or if the file does
+    not parse correctly.  So we're not testing this too deeply.
+    """
+
+    def test_config_pathname_default(self, monkeypatch):
+        monkeypatch.delenv('NOTMUCH_CONFIG', raising=False)
+        user = pathlib.Path('~/.notmuch-config').expanduser()
+        assert dbmod._config_pathname() == user
+
+    def test_config_pathname_env(self, monkeypatch):
+        monkeypatch.setenv('NOTMUCH_CONFIG', '/some/random/path')
+        assert dbmod._config_pathname() == pathlib.Path('/some/random/path')
+
+    def test_default_path_nocfg(self, monkeypatch, tmppath):
+        monkeypatch.setenv('NOTMUCH_CONFIG', str(tmppath/'foo'))
+        with pytest.raises(FileNotFoundError):
+            dbmod.Database.default_path()
+
+    def test_default_path_cfg_is_dir(self, monkeypatch, tmppath):
+        monkeypatch.setenv('NOTMUCH_CONFIG', str(tmppath))
+        with pytest.raises(IsADirectoryError):
+            dbmod.Database.default_path()
+
+    def test_default_path_parseerr(self, monkeypatch, tmppath):
+        cfg = tmppath / 'notmuch-config'
+        with cfg.open('w') as fp:
+            fp.write('invalid')
+        monkeypatch.setenv('NOTMUCH_CONFIG', str(cfg))
+        with pytest.raises(configparser.Error):
+            dbmod.Database.default_path()
+
+    def test_default_path_parse(self, monkeypatch, tmppath):
+        cfg = tmppath / 'notmuch-config'
+        with cfg.open('w') as fp:
+            fp.write('[database]\n')
+            fp.write('path={!s}'.format(tmppath))
+        monkeypatch.setenv('NOTMUCH_CONFIG', str(cfg))
+        assert dbmod.Database.default_path() == tmppath
+
+    def test_default_path_param(self, monkeypatch, tmppath):
+        cfg_dummy = tmppath / 'dummy'
+        monkeypatch.setenv('NOTMUCH_CONFIG', str(cfg_dummy))
+        cfg_real = tmppath / 'notmuch_config'
+        with cfg_real.open('w') as fp:
+            fp.write('[database]\n')
+            fp.write('path={!s}'.format(cfg_real/'mail'))
+        assert dbmod.Database.default_path(cfg_real) == cfg_real/'mail'
+
+
+class TestCreate:
+
+    def test_create(self, tmppath, db):
+        assert tmppath.joinpath('.notmuch/xapian/').exists()
+
+    def test_create_already_open(self, tmppath, db):
+        with pytest.raises(errors.NotmuchError):
+            db.create(tmppath)
+
+    def test_create_existing(self, tmppath, db):
+        with pytest.raises(errors.DatabaseExistsError):
+            dbmod.Database.create(path=tmppath)
+
+    def test_close(self, db):
+        db.close()
+
+    def test_del_noclose(self, db):
+        del db
+
+    def test_close_del(self, db):
+        db.close()
+        del db
+
+    def test_closed_attr(self, db):
+        assert not db.closed
+        db.close()
+        assert db.closed
+
+    def test_ctx(self, db):
+        with db as ctx:
+            assert ctx is db
+            assert not db.closed
+        assert db.closed
+
+    def test_path(self, db, tmppath):
+        assert db.path == tmppath
+
+    def test_version(self, db):
+        assert db.version > 0
+
+    def test_needs_upgrade(self, db):
+        assert db.needs_upgrade in (True, False)
+
+
+class TestAtomic:
+
+    def test_exit_early(self, db):
+        with pytest.raises(errors.UnbalancedAtomicError):
+            with db.atomic() as ctx:
+                ctx.force_end()
+
+    def test_exit_late(self, db):
+        with db.atomic() as ctx:
+            pass
+        with pytest.raises(errors.UnbalancedAtomicError):
+            ctx.force_end()
+
+    def test_abort(self, db):
+        with db.atomic() as txn:
+            txn.abort()
+        assert db.closed
+
+
+class TestRevision:
+
+    def test_single_rev(self, db):
+        r = db.revision()
+        assert isinstance(r, dbmod.DbRevision)
+        assert isinstance(r.rev, int)
+        assert isinstance(r.uuid, bytes)
+        assert r is r
+        assert r == r
+        assert r <= r
+        assert r >= r
+        assert not r < r
+        assert not r > r
+
+    def test_diff_db(self, tmppath):
+        dbpath0 = tmppath.joinpath('db0')
+        dbpath0.mkdir()
+        dbpath1 = tmppath.joinpath('db1')
+        dbpath1.mkdir()
+        db0 = dbmod.Database.create(path=dbpath0)
+        db1 = dbmod.Database.create(path=dbpath1)
+        r_db0 = db0.revision()
+        r_db1 = db1.revision()
+        assert r_db0 != r_db1
+        assert r_db0.uuid != r_db1.uuid
+
+    def test_cmp(self, db, maildir):
+        rev0 = db.revision()
+        _, pathname = maildir.deliver()
+        db.add(pathname, sync_flags=False)
+        rev1 = db.revision()
+        assert rev0 < rev1
+        assert rev0 <= rev1
+        assert not rev0 > rev1
+        assert not rev0 >= rev1
+        assert not rev0 == rev1
+        assert rev0 != rev1
+
+    # XXX add tests for revisions comparisons
+
+class TestMessages:
+
+    def test_add_message(self, db, maildir):
+        msgid, pathname = maildir.deliver()
+        msg, dup = db.add(pathname, sync_flags=False)
+        assert isinstance(msg, message.Message)
+        assert msg.path == pathname
+        assert msg.messageid == msgid
+
+    def test_add_message_str(self, db, maildir):
+        msgid, pathname = maildir.deliver()
+        msg, dup = db.add(str(pathname), sync_flags=False)
+
+    def test_add_message_bytes(self, db, maildir):
+        msgid, pathname = maildir.deliver()
+        msg, dup = db.add(os.fsencode(bytes(pathname)), sync_flags=False)
+
+    def test_remove_message(self, db, maildir):
+        msgid, pathname = maildir.deliver()
+        msg, dup = db.add(pathname, sync_flags=False)
+        assert db.find(msgid)
+        dup = db.remove(pathname)
+        with pytest.raises(LookupError):
+            db.find(msgid)
+
+    def test_remove_message_str(self, db, maildir):
+        msgid, pathname = maildir.deliver()
+        msg, dup = db.add(pathname, sync_flags=False)
+        assert db.find(msgid)
+        dup = db.remove(str(pathname))
+        with pytest.raises(LookupError):
+            db.find(msgid)
+
+    def test_remove_message_bytes(self, db, maildir):
+        msgid, pathname = maildir.deliver()
+        msg, dup = db.add(pathname, sync_flags=False)
+        assert db.find(msgid)
+        dup = db.remove(os.fsencode(bytes(pathname)))
+        with pytest.raises(LookupError):
+            db.find(msgid)
+
+    def test_find_message(self, db, maildir):
+        msgid, pathname = maildir.deliver()
+        msg0, dup = db.add(pathname, sync_flags=False)
+        msg1 = db.find(msgid)
+        assert isinstance(msg1, message.Message)
+        assert msg1.messageid == msgid == msg0.messageid
+        assert msg1.path == pathname == msg0.path
+
+    def test_find_message_notfound(self, db):
+        with pytest.raises(LookupError):
+            db.find('foo')
+
+    def test_get_message(self, db, maildir):
+        msgid, pathname = maildir.deliver()
+        msg0, _ = db.add(pathname, sync_flags=False)
+        msg1 = db.get(pathname)
+        assert isinstance(msg1, message.Message)
+        assert msg1.messageid == msgid == msg0.messageid
+        assert msg1.path == pathname == msg0.path
+
+    def test_get_message_str(self, db, maildir):
+        msgid, pathname = maildir.deliver()
+        db.add(pathname, sync_flags=False)
+        msg = db.get(str(pathname))
+        assert msg.messageid == msgid
+
+    def test_get_message_bytes(self, db, maildir):
+        msgid, pathname = maildir.deliver()
+        db.add(pathname, sync_flags=False)
+        msg = db.get(os.fsencode(bytes(pathname)))
+        assert msg.messageid == msgid
+
+
+class TestTags:
+    # We just want to test this behaves like a set at a hight level.
+    # The set semantics are tested in detail in the test_tags module.
+
+    def test_type(self, db):
+        assert isinstance(db.tags, collections.abc.Set)
+
+    def test_none(self, db):
+        itags = iter(db.tags)
+        with pytest.raises(StopIteration):
+            next(itags)
+        assert len(db.tags) == 0
+        assert not db.tags
+
+    def test_some(self, db, maildir):
+        _, pathname = maildir.deliver()
+        msg, _ = db.add(pathname, sync_flags=False)
+        msg.tags.add('hello')
+        itags = iter(db.tags)
+        assert next(itags) == 'hello'
+        with pytest.raises(StopIteration):
+            next(itags)
+        assert 'hello' in msg.tags
+
+    def test_cache(self, db):
+        assert db.tags is db.tags
+
+    def test_iters(self, db):
+        i1 = iter(db.tags)
+        i2 = iter(db.tags)
+        assert i1 is not i2
+
+
+class TestQuery:
+
+    @pytest.fixture
+    def db(self, maildir, notmuch):
+        """Return a read-only notmuch2.Database.
+
+        The database will have 3 messages, 2 threads.
+        """
+        msgid, _ = maildir.deliver(body='foo')
+        maildir.deliver(body='bar')
+        maildir.deliver(body='baz',
+                        headers=[('In-Reply-To', '<{}>'.format(msgid))])
+        notmuch('new')
+        with dbmod.Database(maildir.path, 'rw', config=notmuch2.Database.CONFIG.EMPTY) as db:
+            yield db
+
+    def test_count_messages(self, db):
+        assert db.count_messages('*') == 3
+
+    def test_messages_type(self, db):
+        msgs = db.messages('*')
+        assert isinstance(msgs, collections.abc.Iterator)
+
+    def test_message_no_results(self, db):
+        msgs = db.messages('not_a_matching_query')
+        with pytest.raises(StopIteration):
+            next(msgs)
+
+    def test_message_match(self, db):
+        msgs = db.messages('*')
+        msg = next(msgs)
+        assert isinstance(msg, notmuch2.Message)
+
+    def test_count_threads(self, db):
+        assert db.count_threads('*') == 2
+
+    def test_threads_type(self, db):
+        threads = db.threads('*')
+        assert isinstance(threads, collections.abc.Iterator)
+
+    def test_threads_no_match(self, db):
+        threads = db.threads('not_a_matching_query')
+        with pytest.raises(StopIteration):
+            next(threads)
+
+    def test_threads_match(self, db):
+        threads = db.threads('*')
+        thread = next(threads)
+        assert isinstance(thread, notmuch2.Thread)
+
+    def test_use_threaded_message_twice(self, db):
+        thread = next(db.threads('*'))
+        for msg in thread.toplevel():
+            assert isinstance(msg, notmuch2.Message)
+            assert msg.alive
+            del msg
+        for msg in thread:
+            assert isinstance(msg, notmuch2.Message)
+            assert msg.alive
+            del msg
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'
diff --git a/bindings/python-cffi/tests/test_message.py b/bindings/python-cffi/tests/test_message.py
new file mode 100644 (file)
index 0000000..56701d0
--- /dev/null
@@ -0,0 +1,229 @@
+import collections.abc
+import time
+import pathlib
+
+import pytest
+
+import notmuch2
+
+
+class TestMessage:
+    MaildirMsg = collections.namedtuple('MaildirMsg', ['msgid', 'path'])
+
+    @pytest.fixture
+    def maildir_msg(self, maildir):
+        msgid, path = maildir.deliver()
+        return self.MaildirMsg(msgid, path)
+
+    @pytest.fixture
+    def db(self, maildir):
+        with notmuch2.Database.create(maildir.path) as db:
+            yield db
+
+    @pytest.fixture
+    def msg(self, db, maildir_msg):
+        msg, dup = db.add(maildir_msg.path, sync_flags=False)
+        yield msg
+
+    def test_type(self, msg):
+        assert isinstance(msg, notmuch2.NotmuchObject)
+        assert isinstance(msg, notmuch2.Message)
+
+    def test_alive(self, msg):
+        assert msg.alive
+
+    def test_hash(self, msg):
+        assert hash(msg)
+
+    def test_eq(self, db, msg):
+        copy = db.get(msg.path)
+        assert msg == copy
+
+    def test_messageid_type(self, msg):
+        assert isinstance(msg.messageid, str)
+        assert isinstance(msg.messageid, notmuch2.BinString)
+        assert isinstance(bytes(msg.messageid), bytes)
+
+    def test_messageid(self, msg, maildir_msg):
+        assert msg.messageid == maildir_msg.msgid
+
+    def test_messageid_find(self, db, msg):
+        copy = db.find(msg.messageid)
+        assert msg.messageid == copy.messageid
+
+    def test_threadid_type(self, msg):
+        assert isinstance(msg.threadid, str)
+        assert isinstance(msg.threadid, notmuch2.BinString)
+        assert isinstance(bytes(msg.threadid), bytes)
+
+    def test_path_type(self, msg):
+        assert isinstance(msg.path, pathlib.Path)
+
+    def test_path(self, msg, maildir_msg):
+        assert msg.path == maildir_msg.path
+
+    def test_pathb_type(self, msg):
+        assert isinstance(msg.pathb, bytes)
+
+    def test_pathb(self, msg, maildir_msg):
+        assert msg.path == maildir_msg.path
+
+    def test_filenames_type(self, msg):
+        ifn = msg.filenames()
+        assert isinstance(ifn, collections.abc.Iterator)
+
+    def test_filenames(self, msg):
+        ifn = msg.filenames()
+        fn = next(ifn)
+        assert fn == msg.path
+        assert isinstance(fn, pathlib.Path)
+        with pytest.raises(StopIteration):
+            next(ifn)
+        assert list(msg.filenames()) == [msg.path]
+
+    def test_filenamesb_type(self, msg):
+        ifn = msg.filenamesb()
+        assert isinstance(ifn, collections.abc.Iterator)
+
+    def test_filenamesb(self, msg):
+        ifn = msg.filenamesb()
+        fn = next(ifn)
+        assert fn == msg.pathb
+        assert isinstance(fn, bytes)
+        with pytest.raises(StopIteration):
+            next(ifn)
+        assert list(msg.filenamesb()) == [msg.pathb]
+
+    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.
+        now = int(time.time())
+        assert abs(now - msg.date) < 3600*24
+
+    def test_header(self, msg):
+        assert msg.header('from') == 'src@example.com'
+
+    def test_header_not_present(self, msg):
+        with pytest.raises(LookupError):
+            msg.header('foo')
+
+    def test_freeze(self, msg):
+        with msg.frozen():
+            msg.tags.add('foo')
+            msg.tags.add('bar')
+            msg.tags.discard('foo')
+        assert 'foo' not in msg.tags
+        assert 'bar' in msg.tags
+
+    def test_freeze_err(self, msg):
+        msg.tags.add('foo')
+        try:
+            with msg.frozen():
+                msg.tags.clear()
+                raise Exception('oops')
+        except Exception:
+            assert 'foo' in msg.tags
+        else:
+            pytest.fail('Context manager did not raise')
+
+    def test_replies_type(self, msg):
+        assert isinstance(msg.replies(), collections.abc.Iterator)
+
+    def test_replies(self, msg):
+        with pytest.raises(StopIteration):
+            next(msg.replies())
+
+
+class TestProperties:
+
+    @pytest.fixture
+    def props(self, maildir):
+        msgid, path = maildir.deliver()
+        with notmuch2.Database.create(maildir.path) as db:
+            msg, dup = db.add(path, sync_flags=False)
+            yield msg.properties
+
+    def test_type(self, props):
+        assert isinstance(props, collections.abc.MutableMapping)
+
+    def test_add_single(self, props):
+        props['foo'] = 'bar'
+        assert props['foo'] == 'bar'
+        props.add('bar', 'baz')
+        assert props['bar'] == 'baz'
+
+    def test_add_dup(self, props):
+        props.add('foo', 'bar')
+        props.add('foo', 'baz')
+        assert props['foo'] == 'bar'
+        assert (set(props.getall('foo', exact=True))
+                == {('foo', 'bar'), ('foo', 'baz')})
+
+    def test_len(self, props):
+        props.add('foo', 'a')
+        props.add('foo', 'b')
+        props.add('bar', 'a')
+        assert len(props) == 3
+        assert len(props.keys()) == 2
+        assert len(props.values()) == 2
+        assert len(props.items()) == 3
+
+    def test_del(self, props):
+        props.add('foo', 'a')
+        props.add('foo', 'b')
+        del props['foo']
+        with pytest.raises(KeyError):
+            props['foo']
+
+    def test_remove(self, props):
+        props.add('foo', 'a')
+        props.add('foo', 'b')
+        props.remove('foo', 'a')
+        assert props['foo'] == 'b'
+
+    def test_view_abcs(self, props):
+        assert isinstance(props.keys(), collections.abc.KeysView)
+        assert isinstance(props.values(), collections.abc.ValuesView)
+        assert isinstance(props.items(), collections.abc.ItemsView)
+
+    def test_pop(self, props):
+        props.add('foo', 'a')
+        props.add('foo', 'b')
+        val = props.pop('foo')
+        assert val == 'a'
+
+    def test_pop_default(self, props):
+        with pytest.raises(KeyError):
+            props.pop('foo')
+        assert props.pop('foo', 'default') == 'default'
+
+    def test_popitem(self, props):
+        props.add('foo', 'a')
+        assert props.popitem() == ('foo', 'a')
+        with pytest.raises(KeyError):
+            props.popitem()
+
+    def test_clear(self, props):
+        props.add('foo', 'a')
+        props.clear()
+        assert len(props) == 0
+
+    def test_getall(self, props):
+        props.add('foo', 'a')
+        assert set(props.getall('foo')) == {('foo', 'a')}
+
+    def test_getall_prefix(self, props):
+        props.add('foo', 'a')
+        props.add('foobar', 'b')
+        assert set(props.getall('foo')) == {('foo', 'a'), ('foobar', 'b')}
+
+    def test_getall_exact(self, props):
+        props.add('foo', 'a')
+        props.add('foobar', 'b')
+        assert set(props.getall('foo', exact=True)) == {('foo', 'a')}
diff --git a/bindings/python-cffi/tests/test_tags.py b/bindings/python-cffi/tests/test_tags.py
new file mode 100644 (file)
index 0000000..f2c6209
--- /dev/null
@@ -0,0 +1,242 @@
+"""Tests for the behaviour of immutable and mutable tagsets.
+
+This module tests the Pythonic behaviour of the sets.
+"""
+
+import collections
+import subprocess
+import textwrap
+
+import pytest
+
+from notmuch2 import _database as database
+from notmuch2 import _tags as tags
+
+
+class TestImmutable:
+
+    @pytest.fixture
+    def tagset(self, maildir, notmuch):
+        """An non-empty immutable tagset.
+
+        This will have the default new mail tags: inbox, unread.
+        """
+        maildir.deliver()
+        notmuch('new')
+        with database.Database(maildir.path, config=database.Database.CONFIG.EMPTY) as db:
+            yield db.tags
+
+    def test_type(self, tagset):
+        assert isinstance(tagset, tags.ImmutableTagSet)
+        assert isinstance(tagset, collections.abc.Set)
+
+    def test_hash(self, tagset, maildir, notmuch):
+        h0 = hash(tagset)
+        notmuch('tag', '+foo', '*')
+        with database.Database(maildir.path, config=database.Database.CONFIG.EMPTY) as db:
+            h1 = hash(db.tags)
+        assert h0 != h1
+
+    def test_eq(self, tagset):
+        assert tagset == tagset
+
+    def test_neq(self, tagset, maildir, notmuch):
+        notmuch('tag', '+foo', '*')
+        with database.Database(maildir.path, config=database.Database.CONFIG.EMPTY) as db:
+            assert tagset != db.tags
+
+    def test_contains(self, tagset):
+        print(tuple(tagset))
+        assert 'unread' in tagset
+        assert 'foo' not in tagset
+
+    def test_isdisjoint(self, tagset):
+        assert tagset.isdisjoint(set(['spam', 'ham']))
+        assert not tagset.isdisjoint(set(['inbox']))
+
+    def test_issubset(self, tagset):
+        assert {'inbox'} <= tagset
+        assert {'inbox'}.issubset(tagset)
+        assert tagset <= {'inbox', 'unread', 'spam'}
+        assert tagset.issubset({'inbox', 'unread', 'spam'})
+
+    def test_issuperset(self, tagset):
+        assert {'inbox', 'unread', 'spam'} >= tagset
+        assert {'inbox', 'unread', 'spam'}.issuperset(tagset)
+        assert tagset >= {'inbox'}
+        assert tagset.issuperset({'inbox'})
+
+    def test_iter(self, tagset):
+        expected = sorted(['unread', 'inbox'])
+        found = []
+        for tag in tagset:
+            assert isinstance(tag, str)
+            found.append(tag)
+        assert expected == sorted(found)
+
+    def test_special_iter(self, tagset):
+        expected = sorted([b'unread', b'inbox'])
+        found = []
+        for tag in tagset.iter():
+            assert isinstance(tag, bytes)
+            found.append(tag)
+        assert expected == sorted(found)
+
+    def test_special_iter_codec(self, tagset):
+        for tag in tagset.iter(encoding='ascii', errors='surrogateescape'):
+            assert isinstance(tag, str)
+
+    def test_len(self, tagset):
+        assert len(tagset) == 2
+
+    def test_and(self, tagset):
+        common = tagset & {'unread'}
+        assert isinstance(common, set)
+        assert isinstance(common, collections.abc.Set)
+        assert common == {'unread'}
+        common = tagset.intersection({'unread'})
+        assert isinstance(common, set)
+        assert isinstance(common, collections.abc.Set)
+        assert common == {'unread'}
+
+    def test_or(self, tagset):
+        res = tagset | {'foo'}
+        assert isinstance(res, set)
+        assert isinstance(res, collections.abc.Set)
+        assert res == {'unread', 'inbox', 'foo'}
+        res = tagset.union({'foo'})
+        assert isinstance(res, set)
+        assert isinstance(res, collections.abc.Set)
+        assert res == {'unread', 'inbox', 'foo'}
+
+    def test_sub(self, tagset):
+        res = tagset - {'unread'}
+        assert isinstance(res, set)
+        assert isinstance(res, collections.abc.Set)
+        assert res == {'inbox'}
+        res = tagset.difference({'unread'})
+        assert isinstance(res, set)
+        assert isinstance(res, collections.abc.Set)
+        assert res == {'inbox'}
+
+    def test_rsub(self, tagset):
+        res = {'foo', 'unread'} - tagset
+        assert isinstance(res, set)
+        assert isinstance(res, collections.abc.Set)
+        assert res == {'foo'}
+
+    def test_xor(self, tagset):
+        res = tagset ^ {'unread', 'foo'}
+        assert isinstance(res, set)
+        assert isinstance(res, collections.abc.Set)
+        assert res == {'inbox', 'foo'}
+        res = tagset.symmetric_difference({'unread', 'foo'})
+        assert isinstance(res, set)
+        assert isinstance(res, collections.abc.Set)
+        assert res == {'inbox', 'foo'}
+
+    def test_rxor(self, tagset):
+        res = {'unread', 'foo'} ^ tagset
+        assert isinstance(res, set)
+        assert isinstance(res, collections.abc.Set)
+        assert res == {'inbox', 'foo'}
+
+    def test_copy(self, tagset):
+        res = tagset.copy()
+        assert isinstance(res, set)
+        assert isinstance(res, collections.abc.Set)
+        assert res == {'inbox', 'unread'}
+
+
+class TestMutableTagset:
+
+    @pytest.fixture
+    def tagset(self, maildir, notmuch):
+        """An non-empty mutable tagset.
+
+        This will have the default new mail tags: inbox, unread.
+        """
+        _, pathname = maildir.deliver()
+        notmuch('new')
+        with database.Database(maildir.path,
+                               mode=database.Mode.READ_WRITE,
+                               config=database.Database.CONFIG.EMPTY) as db:
+            msg = db.get(pathname)
+            yield msg.tags
+
+    def test_type(self, tagset):
+        assert isinstance(tagset, collections.abc.MutableSet)
+        assert isinstance(tagset, tags.MutableTagSet)
+
+    def test_hash(self, tagset):
+        assert not isinstance(tagset, collections.abc.Hashable)
+        with pytest.raises(TypeError):
+            hash(tagset)
+
+    def test_add(self, tagset):
+        assert 'foo' not in tagset
+        tagset.add('foo')
+        assert 'foo' in tagset
+
+    def test_discard(self, tagset):
+        assert 'inbox' in tagset
+        tagset.discard('inbox')
+        assert 'inbox' not in tagset
+
+    def test_discard_not_present(self, tagset):
+        assert 'foo' not in tagset
+        tagset.discard('foo')
+
+    def test_clear(self, tagset):
+        assert len(tagset) > 0
+        tagset.clear()
+        assert len(tagset) == 0
+
+    def test_from_maildir_flags(self, maildir, notmuch):
+        _, pathname = maildir.deliver(flagged=True)
+        notmuch('new')
+        with database.Database(maildir.path,
+                               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()
+            assert 'flagged' in msg.tags
+
+    def test_to_maildir_flags(self, maildir, notmuch):
+        _, pathname = maildir.deliver(flagged=True)
+        notmuch('new')
+        with database.Database(maildir.path,
+                               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
+            msg.tags.discard('flagged')
+            msg.tags.to_maildir_flags()
+            flags = msg.path.name.split(',')[-1]
+            assert 'F' not in flags
+
+    def test_isdisjoint(self, tagset):
+        assert tagset.isdisjoint(set(['spam', 'ham']))
+        assert not tagset.isdisjoint(set(['inbox']))
+
+    def test_issubset(self, tagset):
+        assert {'inbox'} <= tagset
+        assert {'inbox'}.issubset(tagset)
+        assert not {'spam'} <= tagset
+        assert not {'spam'}.issubset(tagset)
+        assert tagset <= {'inbox', 'unread', 'spam'}
+        assert tagset.issubset({'inbox', 'unread', 'spam'})
+        assert not {'inbox', 'unread', 'spam'} <= tagset
+        assert not {'inbox', 'unread', 'spam'}.issubset(tagset)
+
+    def test_issuperset(self, tagset):
+        assert {'inbox', 'unread', 'spam'} >= tagset
+        assert {'inbox', 'unread', 'spam'}.issuperset(tagset)
+        assert tagset >= {'inbox'}
+        assert tagset.issuperset({'inbox'})
+
+    def test_union(self, tagset):
+        assert {'spam'}.union(tagset) == {'inbox', 'unread', 'spam'}
+        assert tagset.union({'spam'}) == {'inbox', 'unread', 'spam'}
diff --git a/bindings/python-cffi/tests/test_thread.py b/bindings/python-cffi/tests/test_thread.py
new file mode 100644 (file)
index 0000000..619d2aa
--- /dev/null
@@ -0,0 +1,109 @@
+import collections.abc
+import time
+
+import pytest
+
+import notmuch2
+
+
+@pytest.fixture
+def thread(maildir, notmuch):
+    """Return a single thread with one matched message."""
+    msgid, _ = maildir.deliver(body='foo')
+    maildir.deliver(body='bar',
+                    headers=[('In-Reply-To', '<{}>'.format(msgid))])
+    notmuch('new')
+    with notmuch2.Database(maildir.path, config=notmuch2.Database.CONFIG.EMPTY) as db:
+        yield next(db.threads('foo'))
+
+
+def test_type(thread):
+    assert isinstance(thread, notmuch2.Thread)
+    assert isinstance(thread, collections.abc.Iterable)
+
+
+def test_threadid(thread):
+    assert isinstance(thread.threadid, notmuch2.BinString)
+    assert thread.threadid
+
+
+def test_len(thread):
+    assert len(thread) == 2
+
+
+def test_toplevel_type(thread):
+    assert isinstance(thread.toplevel(), collections.abc.Iterator)
+
+
+def test_toplevel(thread):
+    msgs = thread.toplevel()
+    assert isinstance(next(msgs), notmuch2.Message)
+    with pytest.raises(StopIteration):
+        next(msgs)
+
+
+def test_toplevel_reply(thread):
+    msg = next(thread.toplevel())
+    assert isinstance(next(msg.replies()), notmuch2.Message)
+
+
+def test_iter(thread):
+    msgs = list(iter(thread))
+    assert len(msgs) == len(thread)
+    for msg in msgs:
+        assert isinstance(msg, notmuch2.Message)
+
+
+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)
+
+
+def test_authors(thread):
+    assert thread.authors == 'src@example.com'
+
+
+def test_subject(thread):
+    assert thread.subject == 'Test mail'
+
+
+def test_first(thread):
+    # XXX Someone seems to treat things as local time instead of
+    #     UTC or the other way around.
+    now = int(time.time())
+    assert abs(now - thread.first) < 3600*24
+
+
+def test_last(thread):
+    # XXX Someone seems to treat things as local time instead of
+    #     UTC or the other way around.
+    now = int(time.time())
+    assert abs(now - thread.last) < 3600*24
+
+
+def test_first_last(thread):
+    # Sadly we only have second resolution so these will always be the
+    # same time in our tests.
+    assert thread.first <= thread.last
+
+
+def test_tags_type(thread):
+    assert isinstance(thread.tags, notmuch2.ImmutableTagSet)
+
+
+def test_tags_cache(thread):
+    assert thread.tags is thread.tags
+
+
+def test_tags(thread):
+    assert 'inbox' in thread.tags
diff --git a/bindings/python-cffi/tox.ini b/bindings/python-cffi/tox.ini
new file mode 100644 (file)
index 0000000..7cf93be
--- /dev/null
@@ -0,0 +1,19 @@
+[pytest]
+minversion = 3.0
+addopts = -ra --cov=notmuch2 --cov=tests
+
+[tox]
+envlist = py35,py36,py37,py38,pypy35,pypy36
+
+[testenv]
+deps =
+     cffi
+     pytest
+     pytest-cov
+commands = pytest --cov={envsitepackagesdir}/notmuch2 {posargs}
+
+[testenv:pypy35]
+basepython = pypy3.5
+
+[testenv:pypy36]
+basepython = pypy3.6
diff --git a/bindings/python/.gitignore b/bindings/python/.gitignore
new file mode 100644 (file)
index 0000000..601acdd
--- /dev/null
@@ -0,0 +1,4 @@
+*.py[co]
+/docs/build
+/docs/html
+/build/
diff --git a/bindings/python/MANIFEST.in b/bindings/python/MANIFEST.in
new file mode 100644 (file)
index 0000000..c83be4b
--- /dev/null
@@ -0,0 +1,2 @@
+include notmuch
+#recursive-include docs/html *
\ No newline at end of file
diff --git a/bindings/python/README b/bindings/python/README
new file mode 100644 (file)
index 0000000..5bf076d
--- /dev/null
@@ -0,0 +1,17 @@
+notmuch -- The python interface to notmuch
+==========================================
+
+This module makes the functionality of the notmuch library
+(`https://notmuchmail.org`_) available to python. Successful import of
+this module depends on a libnotmuch.so|dll being available on the
+user's system.
+
+If you have downloaded the full source tarball, you can create the
+documentation with sphinx installed, go to the docs directory and
+"make html". A static version of the documentation is available at:
+
+  https://notmuch.readthedocs.io/projects/notmuch-python/
+
+To build the python bindings, do
+
+  python setup.py install --prefix=path/to/your/preferred/location
diff --git a/bindings/python/docs/COPYING b/bindings/python/docs/COPYING
new file mode 100644 (file)
index 0000000..e600086
--- /dev/null
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/bindings/python/docs/Makefile b/bindings/python/docs/Makefile
new file mode 100644 (file)
index 0000000..bd6de38
--- /dev/null
@@ -0,0 +1,88 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+       @echo "Please use \`make <target>' where <target> is one of"
+       @echo "  html      to make standalone HTML files"
+       @echo "  dirhtml   to make HTML files named index.html in directories"
+       @echo "  pickle    to make pickle files"
+       @echo "  json      to make JSON files"
+       @echo "  htmlhelp  to make HTML files and a HTML help project"
+       @echo "  qthelp    to make HTML files and a qthelp project"
+       @echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+       @echo "  changes   to make an overview of all changed/added/deprecated items"
+       @echo "  linkcheck to check all external links for integrity"
+       @echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+       -rm -rf build/*
+
+html:
+       $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) html
+       @echo
+       @echo "Build finished. The HTML pages are in html."
+
+dirhtml:
+       $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) build/dirhtml
+       @echo
+       @echo "Build finished. The HTML pages are in build/dirhtml."
+
+pickle:
+       $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle
+       @echo
+       @echo "Build finished; now you can process the pickle files."
+
+json:
+       $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json
+       @echo
+       @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+       $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp
+       @echo
+       @echo "Build finished; now you can run HTML Help Workshop with the" \
+             ".hhp project file in build/htmlhelp."
+
+qthelp:
+       $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) build/qthelp
+       @echo
+       @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+             ".qhcp project file in build/qthelp, like this:"
+       @echo "# qcollectiongenerator build/qthelp/pyDNS.qhcp"
+       @echo "To view the help file:"
+       @echo "# assistant -collectionFile build/qthelp/pyDNS.qhc"
+
+latex:
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex
+       @echo
+       @echo "Build finished; the LaTeX files are in build/latex."
+       @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+             "run these through (pdf)latex."
+
+changes:
+       $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes
+       @echo
+       @echo "The overview file is in build/changes."
+
+linkcheck:
+       $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck
+       @echo
+       @echo "Link check complete; look for any errors in the above output " \
+             "or in build/linkcheck/output.txt."
+
+doctest:
+       $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) build/doctest
+       @echo "Testing of doctests in the sources finished, look at the " \
+             "results in build/doctest/output.txt."
diff --git a/bindings/python/docs/source/conf.py b/bindings/python/docs/source/conf.py
new file mode 100644 (file)
index 0000000..8b43c5c
--- /dev/null
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+#
+# pyDNS documentation build configuration file, created by
+# sphinx-quickstart on Tue Feb  2 10:00:47 2010.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+from unittest.mock import Mock
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0,os.path.abspath('../..'))
+
+MOCK_MODULES = [
+    'ctypes',
+]
+for mod_name in MOCK_MODULES:
+    sys.modules[mod_name] = Mock()
+
+
+from notmuch import __VERSION__,__AUTHOR__
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo']
+autoclass_content = "both"
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'notmuch'
+copyright = u'2010-2012, ' + __AUTHOR__
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = __VERSION__
+# The full version, including alpha/beta/rc tags.
+release = __VERSION__
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = []
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = False
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+html_use_modindex = False
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'notmuchdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'notmuch.tex', u'notmuch Documentation',
+   u'notmuch contributors', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'https://docs.python.org/': None}
diff --git a/bindings/python/docs/source/database.rst b/bindings/python/docs/source/database.rst
new file mode 100644 (file)
index 0000000..660de91
--- /dev/null
@@ -0,0 +1,54 @@
+:class:`Database` -- The underlying notmuch database
+====================================================
+
+.. currentmodule:: notmuch
+
+.. autoclass:: Database([path=None[, create=False[, mode=MODE.READ_ONLY]]])
+
+   .. automethod:: create
+
+   .. automethod:: open(path, status=MODE.READ_ONLY)
+
+   .. automethod:: close
+
+   .. automethod:: get_path
+
+   .. automethod:: get_version
+
+   .. automethod:: needs_upgrade
+
+   .. automethod:: upgrade
+
+   .. automethod:: begin_atomic
+
+   .. automethod:: end_atomic
+
+   .. automethod:: get_directory
+
+   .. automethod:: index_file
+
+   .. automethod:: remove_message
+
+   .. automethod:: find_message
+
+   .. automethod:: find_message_by_filename
+
+   .. automethod:: get_all_tags
+
+   .. automethod:: create_query
+
+   .. automethod:: get_config
+
+   .. automethod:: get_configs
+
+   .. automethod:: set_config
+
+   .. attribute:: Database.MODE
+
+     Defines constants that are used as the mode in which to open a database.
+
+     MODE.READ_ONLY
+       Open the database in read-only mode
+
+     MODE.READ_WRITE
+       Open the database in read-write mode
diff --git a/bindings/python/docs/source/filesystem.rst b/bindings/python/docs/source/filesystem.rst
new file mode 100644 (file)
index 0000000..13fe119
--- /dev/null
@@ -0,0 +1,32 @@
+Files and directories
+=====================
+
+.. currentmodule:: notmuch
+
+:class:`Filenames` -- An iterator over filenames
+------------------------------------------------
+
+.. autoclass:: Filenames
+
+   .. method:: Filenames.__len__
+   .. warning::
+      :meth:`__len__` was removed in version 0.22 as it exhausted the
+      iterator and broke list(Filenames()). Use `len(list(names))`
+      instead.
+
+:class:`Directory` -- A directory entry in the database
+-------------------------------------------------------
+
+.. autoclass:: Directory
+
+   .. automethod:: Directory.get_child_files
+
+   .. automethod:: Directory.get_child_directories
+
+   .. automethod:: Directory.get_mtime
+
+   .. automethod:: Directory.set_mtime
+
+   .. autoattribute:: Directory.mtime
+
+   .. autoattribute:: Directory.path
diff --git a/bindings/python/docs/source/index.rst b/bindings/python/docs/source/index.rst
new file mode 100644 (file)
index 0000000..bef7e60
--- /dev/null
@@ -0,0 +1,36 @@
+Welcome to :mod:`notmuch`'s documentation
+=========================================
+
+.. currentmodule:: notmuch
+
+The :mod:`notmuch` module provides an interface to the `notmuch
+<https://notmuchmail.org>`_ functionality, directly interfacing to a
+shared notmuch library.  Within :mod:`notmuch`, the classes
+:class:`Database`, :class:`Query` provide most of the core
+functionality, returning :class:`Threads`, :class:`Messages` and
+:class:`Tags`.
+
+.. moduleauthor:: Sebastian Spaeth <Sebastian@SSpaeth.de>
+
+:License: This module is covered under the GNU GPL v3 (or later).
+
+.. toctree::
+   :maxdepth: 1
+
+   quickstart
+   notes
+   status_and_errors
+   database
+   query
+   messages
+   message
+   tags
+   threads
+   thread
+   filesystem
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/bindings/python/docs/source/message.rst b/bindings/python/docs/source/message.rst
new file mode 100644 (file)
index 0000000..b003392
--- /dev/null
@@ -0,0 +1,54 @@
+:class:`Message` -- A single message
+====================================
+
+.. currentmodule:: notmuch
+
+.. autoclass:: Message
+
+   .. automethod:: get_message_id
+
+   .. automethod:: get_thread_id
+
+   .. automethod:: get_replies
+
+   .. automethod:: get_filename
+
+   .. automethod:: get_filenames
+
+   .. attribute:: FLAG
+
+        FLAG.MATCH
+          This flag is automatically set by a
+         Query.search_threads on those messages that match the
+         query. This allows us to distinguish matches from the rest
+         of the messages in that thread.
+
+   .. automethod:: get_flag
+
+   .. automethod:: set_flag
+
+   .. automethod:: get_date
+
+   .. automethod:: get_header
+
+   .. automethod:: get_tags
+
+   .. automethod:: get_property
+
+   .. automethod:: get_properties
+
+   .. automethod:: maildir_flags_to_tags
+
+   .. automethod:: tags_to_maildir_flags
+
+   .. automethod:: remove_tag
+
+   .. automethod:: add_tag
+
+   .. automethod:: remove_all_tags
+
+   .. automethod:: freeze
+
+   .. automethod:: thaw
+
+   .. automethod:: __str__
diff --git a/bindings/python/docs/source/messages.rst b/bindings/python/docs/source/messages.rst
new file mode 100644 (file)
index 0000000..3ccf505
--- /dev/null
@@ -0,0 +1,15 @@
+:class:`Messages` -- A bunch of messages
+========================================
+
+.. currentmodule:: notmuch
+
+.. autoclass:: Messages
+
+   .. automethod:: collect_tags
+
+   .. method:: __len__()
+
+   .. warning::
+
+      :meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke
+      list(Messages()). Use the :meth:`Query.count_messages` function or use `len(list(msgs))`.
diff --git a/bindings/python/docs/source/notes.rst b/bindings/python/docs/source/notes.rst
new file mode 100644 (file)
index 0000000..a792748
--- /dev/null
@@ -0,0 +1,6 @@
+Interfacing with notmuch
+========================
+
+.. todo:: move the note about talloc out of the code
+
+.. automodule:: notmuch
diff --git a/bindings/python/docs/source/query.rst b/bindings/python/docs/source/query.rst
new file mode 100644 (file)
index 0000000..785e984
--- /dev/null
@@ -0,0 +1,43 @@
+:class:`Query` -- A search query
+================================
+
+.. currentmodule:: notmuch
+
+.. autoclass:: Query
+
+   .. automethod:: create
+
+   .. attribute:: Query.SORT
+
+     Defines constants that are used as the mode in which to open a database.
+
+     SORT.OLDEST_FIRST
+       Sort by message date, oldest first.
+
+     SORT.NEWEST_FIRST
+       Sort by message date, newest first.
+
+     SORT.MESSAGE_ID
+       Sort by email message ID.
+
+     SORT.UNSORTED
+       Do not apply a special sort order (returns results in document id
+       order).
+
+   .. automethod:: set_sort
+
+   .. attribute::  sort
+
+      Instance attribute :attr:`sort` contains the sort order (see
+      :attr:`Query.SORT`) if explicitly specified via
+      :meth:`set_sort`. By default it is set to `None`.
+
+   .. automethod:: exclude_tag
+
+   .. automethod:: search_threads
+
+   .. automethod:: search_messages
+
+   .. automethod:: count_messages
+
+   .. automethod:: count_threads
diff --git a/bindings/python/docs/source/quickstart.rst b/bindings/python/docs/source/quickstart.rst
new file mode 100644 (file)
index 0000000..609f42e
--- /dev/null
@@ -0,0 +1,19 @@
+Quickstart and examples
+=======================
+
+.. todo:: write a nice introduction
+.. todo:: improve the examples
+
+Notmuch can be imported as::
+
+    import notmuch
+
+or::
+
+    from notmuch import Query, Database
+
+    db = Database('path', create=True)
+    msgs = Query(db, 'from:myself').search_messages()
+
+    for msg in msgs:
+        print(msg)
diff --git a/bindings/python/docs/source/status_and_errors.rst b/bindings/python/docs/source/status_and_errors.rst
new file mode 100644 (file)
index 0000000..68913f1
--- /dev/null
@@ -0,0 +1,57 @@
+.. currentmodule:: notmuch
+
+Status and Errors
+=================
+
+Some methods return a status, indicating if an operation was successful and what the error was. Most of these status codes are expressed as a specific value, the :class:`notmuch.STATUS`.
+
+.. note::
+
+    Prior to version 0.12 the exception classes and the enumeration
+    :class:`notmuch.STATUS` were defined in `notmuch.globals`. They
+    have since then been moved into `notmuch.errors`.
+
+:class:`STATUS` -- Notmuch operation return value
+--------------------------------------------------
+
+.. autoclass:: notmuch.STATUS
+   :inherited-members:
+
+.. automethod:: notmuch.STATUS.status2str
+
+:exc:`NotmuchError` -- A Notmuch execution error
+------------------------------------------------
+Whenever an error occurs, we throw a special Exception :exc:`NotmuchError`, or a more fine grained Exception which is derived from it. This means it is always safe to check for NotmuchErrors if you want to catch all errors. If you are interested in more fine grained exceptions, you can use those below.
+
+.. autoexception:: NotmuchError
+
+The following exceptions are all directly derived from NotmuchError. Each of them corresponds to a specific :class:`notmuch.STATUS` value. You can either check the :attr:`status` attribute of a NotmuchError to see if a specific error has occurred, or you can directly check for the following Exception types:
+
+.. autoexception:: OutOfMemoryError(message=None)
+   :members:
+.. autoexception:: ReadOnlyDatabaseError(message=None)
+   :members:
+.. autoexception:: XapianError(message=None)
+   :members:
+.. autoexception:: FileError(message=None)
+   :members:
+.. autoexception:: FileNotEmailError(message=None)
+   :members:
+.. autoexception:: DuplicateMessageIdError(message=None)
+   :members:
+.. autoexception:: NullPointerError(message=None)
+   :members:
+.. autoexception:: TagTooLongError(message=None)
+   :members:
+.. autoexception:: UnbalancedFreezeThawError(message=None)
+   :members:
+.. autoexception:: UnbalancedAtomicError(message=None)
+   :members:
+.. autoexception:: UnsupportedOperationError(message=None)
+   :members:
+.. autoexception:: UpgradeRequiredError(message=None)
+   :members:
+.. autoexception:: PathError(message=None)
+   :members:
+.. autoexception:: NotInitializedError(message=None)
+   :members:
diff --git a/bindings/python/docs/source/tags.rst b/bindings/python/docs/source/tags.rst
new file mode 100644 (file)
index 0000000..31527d4
--- /dev/null
@@ -0,0 +1,17 @@
+:class:`Tags` -- Notmuch tags
+-----------------------------
+
+.. currentmodule:: notmuch
+
+.. autoclass:: Tags
+   :members:
+
+   .. method:: __len__
+
+       .. warning::
+
+            :meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke
+            list(Tags()). Use :meth:`len(list(msgs))` instead if you need to know the number of
+            tags.
+
+   .. automethod:: __str__
diff --git a/bindings/python/docs/source/thread.rst b/bindings/python/docs/source/thread.rst
new file mode 100644 (file)
index 0000000..4067872
--- /dev/null
@@ -0,0 +1,26 @@
+:class:`Thread` -- A single thread
+==================================
+
+.. currentmodule:: notmuch
+
+.. autoclass:: Thread
+
+  .. automethod:: get_thread_id
+
+  .. automethod:: get_total_messages
+
+  .. automethod:: get_toplevel_messages
+
+  .. automethod:: get_matched_messages
+
+  .. automethod:: get_authors
+
+  .. automethod:: get_subject
+
+  .. automethod:: get_oldest_date
+
+  .. automethod:: get_newest_date
+
+  .. automethod:: get_tags
+
+  .. automethod:: __str__
diff --git a/bindings/python/docs/source/threads.rst b/bindings/python/docs/source/threads.rst
new file mode 100644 (file)
index 0000000..46ce5be
--- /dev/null
@@ -0,0 +1,14 @@
+:class:`Threads` -- Threads iterator
+====================================
+
+.. currentmodule:: notmuch
+
+.. autoclass:: Threads
+
+   .. method:: __len__
+   .. warning::
+      :meth:`__len__` was removed in version 0.22 as it exhausted the
+      iterator and broke list(Threads()). Use `len(list(msgs))`
+      instead.
+
+   .. automethod:: __str__
diff --git a/bindings/python/notmuch/__init__.py b/bindings/python/notmuch/__init__.py
new file mode 100644 (file)
index 0000000..cf627ff
--- /dev/null
@@ -0,0 +1,84 @@
+"""The :mod:`notmuch` module provides most of the functionality that a user is
+likely to need.
+
+.. note:: The underlying notmuch library is build on a hierarchical
+    memory allocator called talloc. All objects derive from a
+    top-level :class:`Database` object.
+
+    This means that as soon as an object is deleted, all underlying
+    derived objects such as Queries, Messages, Message, and Tags will
+    be freed by the underlying library as well. Accessing these
+    objects will then lead to segfaults and other unexpected behavior.
+
+    We implement reference counting, so that parent objects can be
+    automatically freed when they are not needed anymore. For
+    example::
+
+            db = Database('path',create=True)
+            msgs = Query(db,'from:myself').search_messages()
+
+    This returns :class:`Messages` which internally contains a
+    reference to its parent :class:`Query` object. Otherwise the
+    Query() would be immediately freed, taking our *msgs* down with
+    it.
+
+    In this case, the above Query() object will be automatically freed
+    whenever we delete all derived objects, ie in our case:
+    `del(msgs)` would also delete the parent Query. It would not
+    delete the parent Database() though, as that is still referenced
+    from the variable *db* in which it is stored.
+
+    Pretty much the same is valid for all other objects in the
+    hierarchy, such as :class:`Query`, :class:`Messages`,
+    :class:`Message`, and :class:`Tags`.
+"""
+
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010-2011 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+from .database import Database
+from .directory import Directory
+from .filenames import Filenames
+from .message import Message
+from .messages import Messages
+from .query import Query
+from .tag import Tags
+from .thread import Thread
+from .threads import Threads
+from .globals import nmlib
+from .errors import (
+    STATUS,
+    NotmuchError,
+    OutOfMemoryError,
+    ReadOnlyDatabaseError,
+    XapianError,
+    FileError,
+    FileNotEmailError,
+    DuplicateMessageIdError,
+    NullPointerError,
+    TagTooLongError,
+    UnbalancedFreezeThawError,
+    UnbalancedAtomicError,
+    NotInitializedError,
+    UnsupportedOperationError,
+    UpgradeRequiredError,
+    PathError,
+)
+from .version import __VERSION__
+__LICENSE__ = "GPL v3+"
+__AUTHOR__ = 'Sebastian Spaeth <Sebastian@SSpaeth.de>'
diff --git a/bindings/python/notmuch/compat.py b/bindings/python/notmuch/compat.py
new file mode 100644 (file)
index 0000000..4a94e05
--- /dev/null
@@ -0,0 +1,74 @@
+'''
+This file is part of notmuch.
+
+This module handles differences between python2.x and python3.x and
+allows the notmuch bindings to support both version families with one
+source tree.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+Copyright 2012 Justus Winter <4winter@informatik.uni-hamburg.de>
+'''
+
+import sys
+
+if sys.version_info[0] == 2:
+    from ConfigParser import SafeConfigParser
+
+    class Python3StringMixIn(object):
+        def __str__(self):
+            return unicode(self).encode('utf-8')
+
+    def encode_utf8(value):
+        '''
+        Ensure a nicely utf-8 encoded string to pass to wrapped
+        libnotmuch functions.
+
+        C++ code expects strings to be well formatted and unicode
+        strings to have no null bytes.
+        '''
+        if not isinstance(value, basestring):
+            raise TypeError('Expected str or unicode, got %s' % type(value))
+
+        if isinstance(value, unicode):
+            return value.encode('utf-8', 'replace')
+
+        return value
+else:
+    from configparser import ConfigParser as SafeConfigParser
+
+    if not hasattr(SafeConfigParser, 'readfp'):   # py >= 3.12
+        SafeConfigParser.readfp = SafeConfigParser.read_file
+
+    class Python3StringMixIn(object):
+        def __str__(self):
+            return self.__unicode__()
+
+    def encode_utf8(value):
+        '''
+        Ensure a nicely utf-8 encoded string to pass to wrapped
+        libnotmuch functions.
+
+        C++ code expects strings to be well formatted and unicode
+        strings to have no null bytes.
+        '''
+        if not isinstance(value, str):
+            raise TypeError('Expected str, got %s' % type(value))
+
+        return value.encode('utf-8', 'replace')
+
+# We import the SafeConfigParser class on behalf of other code to cope
+# with the differences between Python 2 and 3.
+SafeConfigParser # avoid warning about unused import
diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py
new file mode 100644 (file)
index 0000000..8fb507f
--- /dev/null
@@ -0,0 +1,789 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+
+import os
+import codecs
+import warnings
+from ctypes import c_char_p, c_void_p, c_uint, byref, POINTER
+from .compat import SafeConfigParser
+from .globals import (
+    nmlib,
+    Enum,
+    _str,
+    NotmuchConfigListP,
+    NotmuchDatabaseP,
+    NotmuchDirectoryP,
+    NotmuchIndexoptsP,
+    NotmuchMessageP,
+    NotmuchTagsP,
+)
+from .errors import (
+    STATUS,
+    FileError,
+    NotmuchError,
+    NullPointerError,
+    NotInitializedError,
+)
+from .message import Message
+from .tag import Tags
+from .query import Query
+from .directory import Directory
+
+class Database(object):
+    """The :class:`Database` is the highest-level object that notmuch
+    provides. It references a notmuch database, and can be opened in
+    read-only or read-write mode. A :class:`Query` can be derived from
+    or be applied to a specific database to find messages. Also adding
+    and removing messages to the database happens via this
+    object. Modifications to the database are not atmic by default (see
+    :meth:`begin_atomic`) and once a database has been modified, all
+    other database objects pointing to the same data-base will throw an
+    :exc:`XapianError` as the underlying database has been
+    modified. Close and reopen the database to continue working with it.
+
+    :class:`Database` objects implement the context manager protocol
+    so you can use the :keyword:`with` statement to ensure that the
+    database is properly closed. See :meth:`close` for more
+    information.
+
+    .. note::
+
+        Any function in this class can and will throw an
+        :exc:`NotInitializedError` if the database was not initialized
+        properly.
+    """
+    _std_db_path = None
+    """Class attribute to cache user's default database"""
+
+    MODE = Enum(['READ_ONLY', 'READ_WRITE'])
+    """Constants: Mode in which to open the database"""
+
+    DECRYPTION_POLICY = Enum(['FALSE', 'TRUE', 'AUTO', 'NOSTASH'])
+    """Constants: policies for decrypting messages during indexing"""
+
+    """notmuch_database_get_directory"""
+    _get_directory = nmlib.notmuch_database_get_directory
+    _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)]
+    _get_directory.restype = c_uint
+
+    """notmuch_database_get_path"""
+    _get_path = nmlib.notmuch_database_get_path
+    _get_path.argtypes = [NotmuchDatabaseP]
+    _get_path.restype = c_char_p
+
+    """notmuch_database_get_version"""
+    _get_version = nmlib.notmuch_database_get_version
+    _get_version.argtypes = [NotmuchDatabaseP]
+    _get_version.restype = c_uint
+
+    """notmuch_database_get_revision"""
+    _get_revision = nmlib.notmuch_database_get_revision
+    _get_revision.argtypes = [NotmuchDatabaseP, POINTER(c_char_p)]
+    _get_revision.restype = c_uint
+
+    """notmuch_database_open"""
+    _open = nmlib.notmuch_database_open
+    _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)]
+    _open.restype = c_uint
+
+    """notmuch_database_upgrade"""
+    _upgrade = nmlib.notmuch_database_upgrade
+    _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
+    _upgrade.restype = c_uint
+
+    """ notmuch_database_find_message"""
+    _find_message = nmlib.notmuch_database_find_message
+    _find_message.argtypes = [NotmuchDatabaseP, c_char_p,
+                              POINTER(NotmuchMessageP)]
+    _find_message.restype = c_uint
+
+    """notmuch_database_find_message_by_filename"""
+    _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
+    _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
+                                          POINTER(NotmuchMessageP)]
+    _find_message_by_filename.restype = c_uint
+
+    """notmuch_database_get_all_tags"""
+    _get_all_tags = nmlib.notmuch_database_get_all_tags
+    _get_all_tags.argtypes = [NotmuchDatabaseP]
+    _get_all_tags.restype = NotmuchTagsP
+
+    """notmuch_database_create"""
+    _create = nmlib.notmuch_database_create
+    _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)]
+    _create.restype = c_uint
+
+    def __init__(self, path = None, create = False,
+                 mode = MODE.READ_ONLY):
+        """If *path* is `None`, we will try to read a users notmuch
+        configuration and use his configured database. The location of the
+        configuration file can be specified through the environment variable
+        *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
+
+        If *create* is `True`, the database will always be created in
+        :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
+
+        :param path:   Directory to open/create the database in (see
+                       above for behavior if `None`)
+        :type path:    `str` or `None`
+        :param create: Pass `False` to open an existing, `True` to create a new
+                       database.
+        :type create:  bool
+        :param mode:   Mode to open a database in. Is always
+                       :attr:`MODE`.READ_WRITE when creating a new one.
+        :type mode:    :attr:`MODE`
+        :raises: :exc:`NotmuchError` or derived exception in case of
+            failure.
+        """
+        self._db = None
+        self.mode = mode
+        if path is None:
+            # no path specified. use a user's default database
+            if Database._std_db_path is None:
+                #the following line throws a NotmuchError if it fails
+                Database._std_db_path = self._get_user_default_db()
+            path = Database._std_db_path
+
+        if create == False:
+            self.open(path, mode)
+        else:
+            self.create(path)
+
+    _destroy = nmlib.notmuch_database_destroy
+    _destroy.argtypes = [NotmuchDatabaseP]
+    _destroy.restype = c_uint
+
+    def __del__(self):
+        if self._db:
+            status = self._destroy(self._db)
+            if status != STATUS.SUCCESS:
+                raise NotmuchError(status)
+
+    def _assert_db_is_initialized(self):
+        """Raises :exc:`NotInitializedError` if self._db is `None`"""
+        if not self._db:
+            raise NotInitializedError()
+
+    def create(self, path):
+        """Creates a new notmuch database
+
+        This function is used by __init__() and usually does not need
+        to be called directly. It wraps the underlying
+        *notmuch_database_create* function and creates a new notmuch
+        database at *path*. It will always return a database in :attr:`MODE`
+        .READ_WRITE mode as creating an empty database for
+        reading only does not make a great deal of sense.
+
+        :param path: A directory in which we should create the database.
+        :type path: str
+        :raises: :exc:`NotmuchError` in case of any failure
+                    (possibly after printing an error message on stderr).
+        """
+        if self._db:
+            raise NotmuchError(message="Cannot create db, this Database() "
+                                       "already has an open one.")
+
+        db = NotmuchDatabaseP()
+        status = Database._create(_str(path), byref(db))
+
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+        self._db = db
+        return status
+
+    def open(self, path, mode=0):
+        """Opens an existing database
+
+        This function is used by __init__() and usually does not need
+        to be called directly. It wraps the underlying
+        *notmuch_database_open* function.
+
+        :param status: Open the database in read-only or read-write mode
+        :type status:  :attr:`MODE`
+        :raises: Raises :exc:`NotmuchError` in case of any failure
+                    (possibly after printing an error message on stderr).
+        """
+        db = NotmuchDatabaseP()
+        status = Database._open(_str(path), mode, byref(db))
+
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+        self._db = db
+        return status
+
+    _close = nmlib.notmuch_database_close
+    _close.argtypes = [NotmuchDatabaseP]
+    _close.restype = c_uint
+
+    def close(self):
+        '''
+        Closes the notmuch database.
+
+        .. warning::
+
+            This function closes the notmuch database. From that point
+            on every method invoked on any object ever derived from
+            the closed database may cease to function and raise a
+            NotmuchError.
+        '''
+        if self._db:
+            status = self._close(self._db)
+            if status != STATUS.SUCCESS:
+                raise NotmuchError(status)
+
+    def __enter__(self):
+        '''
+        Implements the context manager protocol.
+        '''
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        '''
+        Implements the context manager protocol.
+        '''
+        self.close()
+
+    def get_path(self):
+        """Returns the file path of an open database"""
+        self._assert_db_is_initialized()
+        return Database._get_path(self._db).decode('utf-8')
+
+    def get_version(self):
+        """Returns the database format version
+
+        :returns: The database version as positive integer
+        """
+        self._assert_db_is_initialized()
+        return Database._get_version(self._db)
+
+    def get_revision (self):
+        """Returns the committed database revision and UUID
+
+        :returns: (revision, uuid) The database revision as a positive integer
+        and the UUID of the database.
+        """
+        self._assert_db_is_initialized()
+        uuid = c_char_p ()
+        revision = Database._get_revision(self._db, byref (uuid))
+        return (revision, uuid.value.decode ('utf-8'))
+
+    _needs_upgrade = nmlib.notmuch_database_needs_upgrade
+    _needs_upgrade.argtypes = [NotmuchDatabaseP]
+    _needs_upgrade.restype = bool
+
+    def needs_upgrade(self):
+        """Does this database need to be upgraded before writing to it?
+
+        If this function returns `True` then no functions that modify the
+        database (:meth:`index_file`,
+        :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
+        etc.) will work unless :meth:`upgrade` is called successfully first.
+
+        :returns: `True` or `False`
+        """
+        self._assert_db_is_initialized()
+        return self._needs_upgrade(self._db)
+
+    def upgrade(self):
+        """Upgrades the current database
+
+        After opening a database in read-write mode, the client should
+        check if an upgrade is needed (notmuch_database_needs_upgrade) and
+        if so, upgrade with this function before making any modifications.
+
+        NOT IMPLEMENTED: The optional progress_notify callback can be
+        used by the caller to provide progress indication to the
+        user. If non-NULL it will be called periodically with
+        'progress' as a floating-point value in the range of [0.0..1.0]
+        indicating the progress made so far in the upgrade process.
+
+        :TODO: catch exceptions, document return values and etc...
+        """
+        self._assert_db_is_initialized()
+        status = Database._upgrade(self._db, None, None)
+        # TODO: catch exceptions, document return values and etc
+        return status
+
+    _begin_atomic = nmlib.notmuch_database_begin_atomic
+    _begin_atomic.argtypes = [NotmuchDatabaseP]
+    _begin_atomic.restype = c_uint
+
+    def begin_atomic(self):
+        """Begin an atomic database operation
+
+        Any modifications performed between a successful
+        :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to
+        the database atomically.  Note that, unlike a typical database
+        transaction, this only ensures atomicity, not durability;
+        neither begin nor end necessarily flush modifications to disk.
+
+        :returns: :attr:`STATUS`.SUCCESS or raises
+        :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
+                    Xapian exception occurred; atomic section not entered.
+
+        *Added in notmuch 0.9*"""
+        self._assert_db_is_initialized()
+        status = self._begin_atomic(self._db)
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+        return status
+
+    _end_atomic = nmlib.notmuch_database_end_atomic
+    _end_atomic.argtypes = [NotmuchDatabaseP]
+    _end_atomic.restype = c_uint
+
+    def end_atomic(self):
+        """Indicate the end of an atomic database operation
+
+        See :meth:`begin_atomic` for details.
+
+        :returns: :attr:`STATUS`.SUCCESS or raises
+
+        :raises:
+            :exc:`NotmuchError`:
+                :attr:`STATUS`.XAPIAN_EXCEPTION
+                    A Xapian exception occurred; atomic section not
+                    ended.
+                :attr:`STATUS`.UNBALANCED_ATOMIC:
+                    end_atomic has been called more times than begin_atomic.
+
+        *Added in notmuch 0.9*"""
+        self._assert_db_is_initialized()
+        status = self._end_atomic(self._db)
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+        return status
+
+    def get_directory(self, path):
+        """Returns a :class:`Directory` of path,
+
+        :param path: An unicode string containing the path relative to the path
+              of database (see :meth:`get_path`), or else should be an absolute
+              path with initial components that match the path of 'database'.
+        :returns: :class:`Directory` or raises an exception.
+        :raises: :exc:`FileError` if path is not relative database or absolute
+                 with initial components same as database.
+        """
+        self._assert_db_is_initialized()
+
+        # sanity checking if path is valid, and make path absolute
+        if path and path[0] == os.sep:
+            # we got an absolute path
+            if not path.startswith(self.get_path()):
+                # but its initial components are not equal to the db path
+                raise FileError('Database().get_directory() called '
+                                'with a wrong absolute path')
+            abs_dirpath = path
+        else:
+            #we got a relative path, make it absolute
+            abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
+
+        dir_p = NotmuchDirectoryP()
+        status = Database._get_directory(self._db, _str(path), byref(dir_p))
+
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+        if not dir_p:
+            return None
+
+        # return the Directory, init it with the absolute path
+        return Directory(abs_dirpath, dir_p, self)
+
+    _get_default_indexopts = nmlib.notmuch_database_get_default_indexopts
+    _get_default_indexopts.argtypes = [NotmuchDatabaseP]
+    _get_default_indexopts.restype = NotmuchIndexoptsP
+
+    _indexopts_set_decrypt_policy = nmlib.notmuch_indexopts_set_decrypt_policy
+    _indexopts_set_decrypt_policy.argtypes = [NotmuchIndexoptsP, c_uint]
+    _indexopts_set_decrypt_policy.restype = None
+
+    _indexopts_destroy = nmlib.notmuch_indexopts_destroy
+    _indexopts_destroy.argtypes = [NotmuchIndexoptsP]
+    _indexopts_destroy.restype = None
+
+    _index_file = nmlib.notmuch_database_index_file
+    _index_file.argtypes = [NotmuchDatabaseP, c_char_p,
+                             c_void_p,
+                             POINTER(NotmuchMessageP)]
+    _index_file.restype = c_uint
+
+    def index_file(self, filename, sync_maildir_flags=False, decrypt_policy=None):
+        """Adds a new message to the database
+
+        :param filename: should be a path relative to the path of the
+            open database (see :meth:`get_path`), or else should be an
+            absolute filename with initial components that match the
+            path of the database.
+
+            The file should be a single mail message (not a
+            multi-message mbox) that is expected to remain at its
+            current location, since the notmuch database will reference
+            the filename, and will not copy the entire contents of the
+            file.
+
+        :param sync_maildir_flags: If the message contains Maildir
+            flags, we will -depending on the notmuch configuration- sync
+            those tags to initial notmuch tags, if set to `True`. It is
+            `False` by default to remain consistent with the libnotmuch
+            API. You might want to look into the underlying method
+            :meth:`Message.maildir_flags_to_tags`.
+
+        :param decrypt_policy: If the message contains any encrypted
+            parts, and decrypt_policy is set to
+            :attr:`DECRYPTION_POLICY`.TRUE, notmuch will try to
+            decrypt the message and index the cleartext, stashing any
+            discovered session keys.  If it is set to
+            :attr:`DECRYPTION_POLICY`.FALSE, it will never try to
+            decrypt during indexing.  If it is set to
+            :attr:`DECRYPTION_POLICY`.AUTO, then it will try to use
+            any stashed session keys it knows about, but will not try
+            to access the user's secret keys.
+            :attr:`DECRYPTION_POLICY`.NOSTASH behaves the same as
+            :attr:`DECRYPTION_POLICY`.TRUE except that no session keys
+            are stashed in the database.  If decrypt_policy is set to
+            None (the default), then the database itself will decide
+            whether to decrypt, based on the `index.decrypt`
+            configuration setting (see notmuch-config(1)).
+
+        :returns: On success, we return
+
+           1) a :class:`Message` object that can be used for things
+              such as adding tags to the just-added message.
+           2) one of the following :attr:`STATUS` values:
+
+              :attr:`STATUS`.SUCCESS
+                  Message successfully added to database.
+              :attr:`STATUS`.DUPLICATE_MESSAGE_ID
+                  Message has the same message ID as another message already
+                  in the database. The new filename was successfully added
+                  to the list of the filenames for the existing message.
+
+        :rtype:   2-tuple(:class:`Message`, :attr:`STATUS`)
+
+        :raises: Raises a :exc:`NotmuchError` with the following meaning.
+              If such an exception occurs, nothing was added to the database.
+
+              :attr:`STATUS`.FILE_ERROR
+                      An error occurred trying to open the file, (such as
+                      permission denied, or file not found, etc.).
+              :attr:`STATUS`.FILE_NOT_EMAIL
+                      The contents of filename don't look like an email
+                      message.
+              :attr:`STATUS`.READ_ONLY_DATABASE
+                      Database was opened in read-only mode so no message can
+                      be added.
+        """
+        self._assert_db_is_initialized()
+        msg_p = NotmuchMessageP()
+        indexopts = c_void_p(None)
+        if decrypt_policy is not None:
+            indexopts = self._get_default_indexopts(self._db)
+            self._indexopts_set_decrypt_policy(indexopts, decrypt_policy)
+
+        status = self._index_file(self._db, _str(filename), indexopts, byref(msg_p))
+
+        if indexopts:
+            self._indexopts_destroy(indexopts)
+
+        if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
+            raise NotmuchError(status)
+
+        #construct Message() and return
+        msg = Message(msg_p, self)
+        #automatic sync initial tags from Maildir flags
+        if sync_maildir_flags:
+            msg.maildir_flags_to_tags()
+        return (msg, status)
+
+    def add_message(self, filename, sync_maildir_flags=False):
+        """Deprecated alias for :meth:`index_file`
+        """
+        warnings.warn(
+                "This function is deprecated and will be removed in the future, use index_file.", DeprecationWarning)
+
+        return self.index_file(filename, sync_maildir_flags=sync_maildir_flags)
+
+    _remove_message = nmlib.notmuch_database_remove_message
+    _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
+    _remove_message.restype = c_uint
+
+    def remove_message(self, filename):
+        """Removes a message (filename) from the given notmuch database
+
+        Note that only this particular filename association is removed from
+        the database. If the same message (as determined by the message ID)
+        is still available via other filenames, then the message will
+        persist in the database for those filenames. When the last filename
+        is removed for a particular message, the database content for that
+        message will be entirely removed.
+
+        :returns: A :attr:`STATUS` value with the following meaning:
+
+             :attr:`STATUS`.SUCCESS
+               The last filename was removed and the message was removed
+               from the database.
+             :attr:`STATUS`.DUPLICATE_MESSAGE_ID
+               This filename was removed but the message persists in the
+               database with at least one other filename.
+
+        :raises: Raises a :exc:`NotmuchError` with the following meaning.
+             If such an exception occurs, nothing was removed from the
+             database.
+
+             :attr:`STATUS`.READ_ONLY_DATABASE
+               Database was opened in read-only mode so no message can be
+               removed.
+        """
+        self._assert_db_is_initialized()
+        status = self._remove_message(self._db, _str(filename))
+        if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
+            raise NotmuchError(status)
+        return status
+
+    def find_message(self, msgid):
+        """Returns a :class:`Message` as identified by its message ID
+
+        Wraps the underlying *notmuch_database_find_message* function.
+
+        :param msgid: The message ID
+        :type msgid: unicode or str
+        :returns: :class:`Message` or `None` if no message is found.
+        :raises:
+            :exc:`OutOfMemoryError`
+                  If an Out-of-memory occurred while constructing the message.
+            :exc:`XapianError`
+                  In case of a Xapian Exception. These exceptions
+                  include "Database modified" situations, e.g. when the
+                  notmuch database has been modified by another program
+                  in the meantime. In this case, you should close and
+                  reopen the database and retry.
+            :exc:`NotInitializedError` if
+                    the database was not initialized.
+        """
+        self._assert_db_is_initialized()
+        msg_p = NotmuchMessageP()
+        status = Database._find_message(self._db, _str(msgid), byref(msg_p))
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+        return msg_p and Message(msg_p, self) or None
+
+    def find_message_by_filename(self, filename):
+        """Find a message with the given filename
+
+        :returns: If the database contains a message with the given
+            filename, then a class:`Message:` is returned.  This
+            function returns None if no message is found with the given
+            filename.
+
+        :raises: :exc:`OutOfMemoryError` if an Out-of-memory occurred while
+                 constructing the message.
+        :raises: :exc:`XapianError` in case of a Xapian Exception.
+                 These exceptions include "Database modified"
+                 situations, e.g. when the notmuch database has been
+                 modified by another program in the meantime. In this
+                 case, you should close and reopen the database and
+                 retry.
+        :raises: :exc:`NotInitializedError` if the database was not
+                 initialized.
+
+        *Added in notmuch 0.9*"""
+        self._assert_db_is_initialized()
+
+        msg_p = NotmuchMessageP()
+        status = Database._find_message_by_filename(self._db, _str(filename),
+                                                    byref(msg_p))
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+        return msg_p and Message(msg_p, self) or None
+
+    def get_all_tags(self):
+        """Returns :class:`Tags` with a list of all tags found in the database
+
+        :returns: :class:`Tags`
+        :exception: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
+                    on error
+        """
+        self._assert_db_is_initialized()
+        tags_p = Database._get_all_tags(self._db)
+        if not tags_p:
+            raise NullPointerError()
+        return Tags(tags_p, self)
+
+    def create_query(self, querystring):
+        """Returns a :class:`Query` derived from this database
+
+        This is a shorthand method for doing::
+
+          # short version
+          # Automatically frees the Database() when 'q' is deleted
+
+          q  = Database(dbpath).create_query('from:"Biene Maja"')
+
+          # long version, which is functionally equivalent but will keep the
+          # Database in the 'db' variable around after we delete 'q':
+
+          db = Database(dbpath)
+          q  = Query(db,'from:"Biene Maja"')
+
+        This function is a python extension and not in the underlying C API.
+        """
+        return Query(self, querystring)
+
+    """notmuch_database_status_string"""
+    _status_string = nmlib.notmuch_database_status_string
+    _status_string.argtypes = [NotmuchDatabaseP]
+    _status_string.restype = c_char_p
+
+    def status_string(self):
+        """Returns the status string of the database
+
+        This is sometimes used for additional error reporting
+        """
+        self._assert_db_is_initialized()
+        s = Database._status_string(self._db)
+        if s:
+            return s.decode('utf-8', 'ignore')
+        return s
+
+    def __repr__(self):
+        return "'Notmuch DB " + self.get_path() + "'"
+
+    def _get_user_default_db(self):
+        """ Reads a user's notmuch config and returns his db location
+
+        Throws a NotmuchError if it cannot find it"""
+        config = SafeConfigParser()
+        conf_f = os.getenv('NOTMUCH_CONFIG',
+                           os.path.expanduser('~/.notmuch-config'))
+        config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
+        if not config.has_option('database', 'path'):
+            raise NotmuchError(message="No DB path specified"
+                                       " and no user default found")
+        db_path = config.get('database', 'path')
+        if not os.path.isabs(db_path):
+            db_path = os.path.expanduser(os.path.join("~", db_path))
+        return db_path
+
+    """notmuch_database_get_config"""
+    _get_config = nmlib.notmuch_database_get_config
+    _get_config.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(c_char_p)]
+    _get_config.restype = c_uint
+
+    def get_config(self, key):
+        """Return the value of the given config key.
+
+        Note that only config values that are stored in the database are
+        searched and returned.  The config file is not read.
+
+        :param key: the config key under which a value should be looked up, it
+                    should probably be in the form "section.key"
+        :type key:  str
+        :returns:   the config value or the empty string if no value is present
+                    for that key
+        :rtype:     str
+        :raises:    :exc:`NotmuchError` in case of failure.
+
+        """
+        self._assert_db_is_initialized()
+        return_string = c_char_p()
+        status = self._get_config(self._db, _str(key), byref(return_string))
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+        return return_string.value.decode('utf-8')
+
+    """notmuch_database_get_config_list"""
+    _get_config_list = nmlib.notmuch_database_get_config_list
+    _get_config_list.argtypes = [NotmuchDatabaseP, c_char_p,
+                                 POINTER(NotmuchConfigListP)]
+    _get_config_list.restype = c_uint
+
+    _config_list_valid = nmlib.notmuch_config_list_valid
+    _config_list_valid.argtypes = [NotmuchConfigListP]
+    _config_list_valid.restype = bool
+
+    _config_list_key = nmlib.notmuch_config_list_key
+    _config_list_key.argtypes = [NotmuchConfigListP]
+    _config_list_key.restype = c_char_p
+
+    _config_list_value = nmlib.notmuch_config_list_value
+    _config_list_value.argtypes = [NotmuchConfigListP]
+    _config_list_value.restype = c_char_p
+
+    _config_list_move_to_next = nmlib.notmuch_config_list_move_to_next
+    _config_list_move_to_next.argtypes = [NotmuchConfigListP]
+    _config_list_move_to_next.restype = None
+
+    _config_list_destroy = nmlib.notmuch_config_list_destroy
+    _config_list_destroy.argtypes = [NotmuchConfigListP]
+    _config_list_destroy.restype = None
+
+    def get_configs(self, prefix=''):
+        """Return a generator of key, value pairs where the start of key
+        matches the given prefix
+
+        Note that only config values that are stored in the database are
+        searched and returned.  The config file is not read.  If no `prefix` is
+        given all config values are returned.
+
+        This could be used to get all named queries into a dict for example::
+
+            queries = {k[6:]: v for k, v in db.get_configs('query.')}
+
+        :param prefix: a string by which the keys should be selected
+        :type prefix:  str
+        :yields:       all key-value pairs where `prefix` matches the beginning
+                       of the key
+        :ytype:        pairs of str
+        :raises:      :exc:`NotmuchError` in case of failure.
+
+        """
+        self._assert_db_is_initialized()
+        config_list_p = NotmuchConfigListP()
+        status = self._get_config_list(self._db, _str(prefix),
+                                       byref(config_list_p))
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+        while self._config_list_valid(config_list_p):
+            key = self._config_list_key(config_list_p).decode('utf-8')
+            value = self._config_list_value(config_list_p).decode('utf-8')
+            yield key, value
+            self._config_list_move_to_next(config_list_p)
+
+    """notmuch_database_set_config"""
+    _set_config = nmlib.notmuch_database_set_config
+    _set_config.argtypes = [NotmuchDatabaseP, c_char_p, c_char_p]
+    _set_config.restype = c_uint
+
+    def set_config(self, key, value):
+        """Set a config value in the notmuch database.
+
+        If an empty string is provided as `value` the `key` is unset!
+
+        :param key:   the key to set
+        :type key:    str
+        :param value: the value to store under `key`
+        :type value:  str
+        :returns:     None
+        :raises:      :exc:`NotmuchError` in case of failure.
+
+        """
+        self._assert_db_is_initialized()
+        status = self._set_config(self._db, _str(key), _str(value))
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
diff --git a/bindings/python/notmuch/directory.py b/bindings/python/notmuch/directory.py
new file mode 100644 (file)
index 0000000..b30c9e3
--- /dev/null
@@ -0,0 +1,185 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+
+from ctypes import c_uint, c_long
+from .globals import (
+    nmlib,
+    NotmuchDirectoryP,
+    NotmuchFilenamesP
+)
+from .errors import (
+    STATUS,
+    NotmuchError,
+    NotInitializedError,
+)
+from .filenames import Filenames
+
+class Directory(object):
+    """Represents a directory entry in the notmuch directory
+
+    Modifying attributes of this object will modify the
+    database, not the real directory attributes.
+
+    The Directory object is usually derived from another object
+    e.g. via :meth:`Database.get_directory`, and will automatically be
+    become invalid whenever that parent is deleted. You should
+    therefore initialized this object handing it a reference to the
+    parent, preventing the parent from automatically being garbage
+    collected.
+    """
+
+    """notmuch_directory_get_mtime"""
+    _get_mtime = nmlib.notmuch_directory_get_mtime
+    _get_mtime.argtypes = [NotmuchDirectoryP]
+    _get_mtime.restype = c_long
+
+    """notmuch_directory_set_mtime"""
+    _set_mtime = nmlib.notmuch_directory_set_mtime
+    _set_mtime.argtypes = [NotmuchDirectoryP, c_long]
+    _set_mtime.restype = c_uint
+
+    """notmuch_directory_get_child_files"""
+    _get_child_files = nmlib.notmuch_directory_get_child_files
+    _get_child_files.argtypes = [NotmuchDirectoryP]
+    _get_child_files.restype = NotmuchFilenamesP
+
+    """notmuch_directory_get_child_directories"""
+    _get_child_directories = nmlib.notmuch_directory_get_child_directories
+    _get_child_directories.argtypes = [NotmuchDirectoryP]
+    _get_child_directories.restype = NotmuchFilenamesP
+
+    def _assert_dir_is_initialized(self):
+        """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
+        if dir_p is None"""
+        if not self._dir_p:
+            raise NotInitializedError()
+
+    def __init__(self, path, dir_p, parent):
+        """
+        :param path:   The absolute path of the directory object.
+        :param dir_p:  The pointer to an internal notmuch_directory_t object.
+        :param parent: The object this Directory is derived from
+                       (usually a :class:`Database`). We do not directly use
+                       this, but store a reference to it as long as
+                       this Directory object lives. This keeps the
+                       parent object alive.
+        """
+        self._path = path
+        self._dir_p = dir_p
+        self._parent = parent
+
+    def set_mtime(self, mtime):
+        """Sets the mtime value of this directory in the database
+
+        The intention is for the caller to use the mtime to allow efficient
+        identification of new messages to be added to the database. The
+        recommended usage is as follows:
+
+        * Read the mtime of a directory from the filesystem
+
+        * Call :meth:`Database.index_file` for all mail files in
+          the directory
+
+        * Call notmuch_directory_set_mtime with the mtime read from the
+          filesystem.  Then, when wanting to check for updates to the
+          directory in the future, the client can call :meth:`get_mtime`
+          and know that it only needs to add files if the mtime of the
+          directory and files are newer than the stored timestamp.
+
+          .. note::
+
+                :meth:`get_mtime` function does not allow the caller to
+                distinguish a timestamp of 0 from a non-existent timestamp. So
+                don't store a timestamp of 0 unless you are comfortable with
+                that.
+
+        :param mtime: A (time_t) timestamp
+        :raises: :exc:`XapianError` a Xapian exception occurred, mtime
+                 not stored
+        :raises: :exc:`ReadOnlyDatabaseError` the database was opened
+                 in read-only mode so directory mtime cannot be modified
+        :raises: :exc:`NotInitializedError` the directory object has not
+                 been initialized
+        """
+        self._assert_dir_is_initialized()
+        status = Directory._set_mtime(self._dir_p, mtime)
+
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+
+    def get_mtime(self):
+        """Gets the mtime value of this directory in the database
+
+        Retrieves a previously stored mtime for this directory.
+
+        :param mtime: A (time_t) timestamp
+        :raises: :exc:`NotmuchError`:
+
+                        :attr:`STATUS`.NOT_INITIALIZED
+                          The directory has not been initialized
+        """
+        self._assert_dir_is_initialized()
+        return Directory._get_mtime(self._dir_p)
+
+    # Make mtime attribute a property of Directory()
+    mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
+                     and setting of the Directory *mtime* (read-write)
+
+                     See :meth:`get_mtime` and :meth:`set_mtime` for usage and
+                     possible exceptions.""")
+
+    def get_child_files(self):
+        """Gets a Filenames iterator listing all the filenames of
+        messages in the database within the given directory.
+
+        The returned filenames will be the basename-entries only (not
+        complete paths.
+        """
+        self._assert_dir_is_initialized()
+        files_p = Directory._get_child_files(self._dir_p)
+        return Filenames(files_p, self)
+
+    def get_child_directories(self):
+        """Gets a :class:`Filenames` iterator listing all the filenames of
+        sub-directories in the database within the given directory
+
+        The returned filenames will be the basename-entries only (not
+        complete paths.
+        """
+        self._assert_dir_is_initialized()
+        files_p = Directory._get_child_directories(self._dir_p)
+        return Filenames(files_p, self)
+
+    @property
+    def path(self):
+        """Returns the absolute path of this Directory (read-only)"""
+        return self._path
+
+    def __repr__(self):
+        """Object representation"""
+        return "<notmuch Directory object '%s'>" % self._path
+
+    _destroy = nmlib.notmuch_directory_destroy
+    _destroy.argtypes = [NotmuchDirectoryP]
+    _destroy.restype = None
+
+    def __del__(self):
+        """Close and free the Directory"""
+        if self._dir_p:
+            self._destroy(self._dir_p)
diff --git a/bindings/python/notmuch/errors.py b/bindings/python/notmuch/errors.py
new file mode 100644 (file)
index 0000000..b7684ef
--- /dev/null
@@ -0,0 +1,204 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+
+from ctypes import c_char_p, c_int
+
+from .globals import (
+    nmlib,
+    Enum,
+    Python3StringMixIn,
+)
+
+class Status(Enum):
+    """Enum with a string representation of a notmuch_status_t value."""
+    _status2str = nmlib.notmuch_status_to_string
+    _status2str.restype = c_char_p
+    _status2str.argtypes = [c_int]
+
+    def __init__(self, statuslist):
+        """It is initialized with a list of strings that are available as
+        Status().string1 - Status().stringn attributes.
+        """
+        super(Status, self).__init__(statuslist)
+
+    @classmethod
+    def status2str(self, status):
+        """Get a (unicode) string representation of a notmuch_status_t value."""
+        # define strings for custom error messages
+        if status == STATUS.NOT_INITIALIZED:
+            return "Operation on uninitialized object impossible."
+        return unicode(Status._status2str(status))
+
+STATUS = Status(['SUCCESS',
+  'OUT_OF_MEMORY',
+  'READ_ONLY_DATABASE',
+  'XAPIAN_EXCEPTION',
+  'FILE_ERROR',
+  'FILE_NOT_EMAIL',
+  'DUPLICATE_MESSAGE_ID',
+  'NULL_POINTER',
+  'TAG_TOO_LONG',
+  'UNBALANCED_FREEZE_THAW',
+  'UNBALANCED_ATOMIC',
+  'UNSUPPORTED_OPERATION',
+  'UPGRADE_REQUIRED',
+  'PATH_ERROR',
+  'NOT_INITIALIZED'])
+"""STATUS is a class, whose attributes provide constants that serve as return
+indicators for notmuch functions. Currently the following ones are defined. For
+possible return values and specific meaning for each method, see the method
+description.
+
+  * SUCCESS
+  * OUT_OF_MEMORY
+  * READ_ONLY_DATABASE
+  * XAPIAN_EXCEPTION
+  * FILE_ERROR
+  * FILE_NOT_EMAIL
+  * DUPLICATE_MESSAGE_ID
+  * NULL_POINTER
+  * TAG_TOO_LONG
+  * UNBALANCED_FREEZE_THAW
+  * UNBALANCED_ATOMIC
+  * UNSUPPORTED_OPERATION
+  * UPGRADE_REQUIRED
+  * PATH_ERROR
+  * NOT_INITIALIZED
+
+Invoke the class method `notmuch.STATUS.status2str` with a status value as
+argument to receive a human readable string"""
+STATUS.__name__ = 'STATUS'
+
+
+class NotmuchError(Exception, Python3StringMixIn):
+    """Is initiated with a (notmuch.STATUS[, message=None]). It will not
+    return an instance of the class NotmuchError, but a derived instance
+    of a more specific Error Message, e.g. OutOfMemoryError. Each status
+    but SUCCESS has a corresponding subclassed Exception."""
+
+    @classmethod
+    def get_exc_subclass(cls, status):
+        """Returns a fine grained Exception() type,
+        detailing the error status"""
+        subclasses = {
+            STATUS.OUT_OF_MEMORY: OutOfMemoryError,
+            STATUS.READ_ONLY_DATABASE: ReadOnlyDatabaseError,
+            STATUS.XAPIAN_EXCEPTION: XapianError,
+            STATUS.FILE_ERROR: FileError,
+            STATUS.FILE_NOT_EMAIL: FileNotEmailError,
+            STATUS.DUPLICATE_MESSAGE_ID: DuplicateMessageIdError,
+            STATUS.NULL_POINTER: NullPointerError,
+            STATUS.TAG_TOO_LONG: TagTooLongError,
+            STATUS.UNBALANCED_FREEZE_THAW: UnbalancedFreezeThawError,
+            STATUS.UNBALANCED_ATOMIC: UnbalancedAtomicError,
+            STATUS.UNSUPPORTED_OPERATION: UnsupportedOperationError,
+            STATUS.UPGRADE_REQUIRED: UpgradeRequiredError,
+            STATUS.PATH_ERROR: PathError,
+            STATUS.NOT_INITIALIZED: NotInitializedError,
+        }
+        assert 0 < status <= len(subclasses)
+        return subclasses[status]
+
+    def __new__(cls, *args, **kwargs):
+        """Return a correct subclass of NotmuchError if needed
+
+        We return a NotmuchError instance if status is None (or 0) and a
+        subclass that inherits from NotmuchError depending on the
+        'status' parameter otherwise."""
+        # get 'status'. Passed in as arg or kwarg?
+        status = args[0] if len(args) else kwargs.get('status', None)
+        # no 'status' or cls is subclass already, return 'cls' instance
+        if not status or cls != NotmuchError:
+            return super(NotmuchError, cls).__new__(cls)
+        subclass = cls.get_exc_subclass(status)  # which class to use?
+        return subclass.__new__(subclass, *args, **kwargs)
+
+    def __init__(self, status=None, message=None):
+        self.status = status
+        self.message = message
+
+    def __unicode__(self):
+        if self.message is not None:
+            return self.message
+        elif self.status is not None:
+            return STATUS.status2str(self.status)
+        else:
+            return 'Unknown error'
+
+
+# List of Subclassed exceptions that correspond to STATUS values and are
+# subclasses of NotmuchError.
+class OutOfMemoryError(NotmuchError):
+    status = STATUS.OUT_OF_MEMORY
+
+
+class ReadOnlyDatabaseError(NotmuchError):
+    status = STATUS.READ_ONLY_DATABASE
+
+
+class XapianError(NotmuchError):
+    status = STATUS.XAPIAN_EXCEPTION
+
+
+class FileError(NotmuchError):
+    status = STATUS.FILE_ERROR
+
+
+class FileNotEmailError(NotmuchError):
+    status = STATUS.FILE_NOT_EMAIL
+
+
+class DuplicateMessageIdError(NotmuchError):
+    status = STATUS.DUPLICATE_MESSAGE_ID
+
+
+class NullPointerError(NotmuchError):
+    status = STATUS.NULL_POINTER
+
+
+class TagTooLongError(NotmuchError):
+    status = STATUS.TAG_TOO_LONG
+
+
+class UnbalancedFreezeThawError(NotmuchError):
+    status = STATUS.UNBALANCED_FREEZE_THAW
+
+
+class UnbalancedAtomicError(NotmuchError):
+    status = STATUS.UNBALANCED_ATOMIC
+
+
+class UnsupportedOperationError(NotmuchError):
+    status = STATUS.UNSUPPORTED_OPERATION
+
+
+class UpgradeRequiredError(NotmuchError):
+    status = STATUS.UPGRADE_REQUIRED
+
+
+class PathError(NotmuchError):
+    status = STATUS.PATH_ERROR
+
+
+class NotInitializedError(NotmuchError):
+    """Derived from NotmuchError, this occurs if the underlying data
+    structure (e.g. database is not initialized (yet) or an iterator has
+    been exhausted. You can test for NotmuchError with .status =
+    STATUS.NOT_INITIALIZED"""
+    status = STATUS.NOT_INITIALIZED
diff --git a/bindings/python/notmuch/filenames.py b/bindings/python/notmuch/filenames.py
new file mode 100644 (file)
index 0000000..3bbc22b
--- /dev/null
@@ -0,0 +1,131 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+from ctypes import c_char_p
+from .globals import (
+    nmlib,
+    NotmuchFilenamesP,
+    Python3StringMixIn,
+)
+from .errors import (
+    NullPointerError,
+    NotInitializedError,
+)
+
+
+class Filenames(Python3StringMixIn):
+    """Represents a list of filenames as returned by notmuch
+
+    Objects of this class implement the iterator protocol.
+
+    .. note::
+
+        The underlying library only provides a one-time iterator (it
+        cannot reset the iterator to the start). Thus iterating over
+        the function will "exhaust" the list of tags, and a subsequent
+        iteration attempt will raise a
+        :exc:`NotInitializedError`. Also note, that any function that
+        uses iteration (nearly all) will also exhaust the tags. So
+        both::
+
+           for name in filenames: print name
+
+        as well as::
+
+           list_of_names = list(names)
+
+        and even a simple::
+
+           #str() iterates over all tags to construct a space separated list
+           print(str(filenames))
+
+        will "exhaust" the Filenames. However, you can use
+        :meth:`Message.get_filenames` repeatedly to get fresh
+        Filenames objects to perform various actions on filenames.
+    """
+
+    #notmuch_filenames_get
+    _get = nmlib.notmuch_filenames_get
+    _get.argtypes = [NotmuchFilenamesP]
+    _get.restype = c_char_p
+
+    def __init__(self, files_p, parent):
+        """
+        :param files_p: A pointer to an underlying *notmuch_tags_t*
+             structure. These are not publicly exposed, so a user
+             will almost never instantiate a :class:`Tags` object
+             herself. They are usually handed back as a result,
+             e.g. in :meth:`Database.get_all_tags`.  *tags_p* must be
+             valid, we will raise an :exc:`NullPointerError`
+             if it is `None`.
+        :type files_p: :class:`ctypes.c_void_p`
+        :param parent: The parent object (ie :class:`Message` these
+             filenames are derived from, and saves a
+             reference to it, so we can automatically delete the db object
+             once all derived objects are dead.
+        """
+        if not files_p:
+            raise NullPointerError()
+
+        self._files_p = files_p
+        #save reference to parent object so we keep it alive
+        self._parent = parent
+
+    def __iter__(self):
+        """ Make Filenames an iterator """
+        return self
+
+    _valid = nmlib.notmuch_filenames_valid
+    _valid.argtypes = [NotmuchFilenamesP]
+    _valid.restype = bool
+
+    _move_to_next = nmlib.notmuch_filenames_move_to_next
+    _move_to_next.argtypes = [NotmuchFilenamesP]
+    _move_to_next.restype = None
+
+    def __next__(self):
+        if not self._files_p:
+            raise NotInitializedError()
+
+        if not self._valid(self._files_p):
+            self._files_p = None
+            raise StopIteration
+
+        file_ = Filenames._get(self._files_p)
+        self._move_to_next(self._files_p)
+        return file_.decode('utf-8', 'ignore')
+    next = __next__ # python2.x iterator protocol compatibility
+
+    def __unicode__(self):
+        """Represent Filenames() as newline-separated list of full paths
+
+        .. note::
+
+            This method exhausts the iterator object, so you will not be able to
+            iterate over them again.
+        """
+        return "\n".join(self)
+
+    _destroy = nmlib.notmuch_filenames_destroy
+    _destroy.argtypes = [NotmuchFilenamesP]
+    _destroy.restype = None
+
+    def __del__(self):
+        """Close and free the notmuch filenames"""
+        if self._files_p:
+            self._destroy(self._files_p)
diff --git a/bindings/python/notmuch/globals.py b/bindings/python/notmuch/globals.py
new file mode 100644 (file)
index 0000000..11e328b
--- /dev/null
@@ -0,0 +1,105 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+
+from ctypes import CDLL, Structure, POINTER
+from notmuch.version import SOVERSION
+
+#-----------------------------------------------------------------------------
+#package-global instance of the notmuch library
+try:
+    from os import uname
+    if uname()[0] == 'Darwin':
+        nmlib = CDLL("libnotmuch.{0:s}.dylib".format(SOVERSION))
+    else:
+        nmlib = CDLL("libnotmuch.so.{0:s}".format(SOVERSION))
+except:
+    raise ImportError("Could not find shared 'notmuch' library.")
+
+from .compat import Python3StringMixIn, encode_utf8 as _str
+
+# We import these on behalf of other modules.  Silence warning about
+# these symbols not being used.
+Python3StringMixIn
+_str
+
+class Enum(object):
+    """Provides ENUMS as "code=Enum(['a','b','c'])" where code.a=0 etc..."""
+    def __init__(self, names):
+        for number, name in enumerate(names):
+            setattr(self, name, number)
+
+
+class NotmuchDatabaseS(Structure):
+    pass
+NotmuchDatabaseP = POINTER(NotmuchDatabaseS)
+
+
+class NotmuchQueryS(Structure):
+    pass
+NotmuchQueryP = POINTER(NotmuchQueryS)
+
+
+class NotmuchThreadsS(Structure):
+    pass
+NotmuchThreadsP = POINTER(NotmuchThreadsS)
+
+
+class NotmuchThreadS(Structure):
+    pass
+NotmuchThreadP = POINTER(NotmuchThreadS)
+
+
+class NotmuchMessagesS(Structure):
+    pass
+NotmuchMessagesP = POINTER(NotmuchMessagesS)
+
+
+class NotmuchMessageS(Structure):
+    pass
+NotmuchMessageP = POINTER(NotmuchMessageS)
+
+
+class NotmuchMessagePropertiesS(Structure):
+    pass
+NotmuchMessagePropertiesP = POINTER(NotmuchMessagePropertiesS)
+
+
+class NotmuchTagsS(Structure):
+    pass
+NotmuchTagsP = POINTER(NotmuchTagsS)
+
+
+class NotmuchDirectoryS(Structure):
+    pass
+NotmuchDirectoryP = POINTER(NotmuchDirectoryS)
+
+
+class NotmuchFilenamesS(Structure):
+    pass
+NotmuchFilenamesP = POINTER(NotmuchFilenamesS)
+
+
+class NotmuchConfigListS(Structure):
+    pass
+NotmuchConfigListP = POINTER(NotmuchConfigListS)
+
+
+class NotmuchIndexoptsS(Structure):
+    pass
+NotmuchIndexoptsP = POINTER(NotmuchIndexoptsS)
diff --git a/bindings/python/notmuch/message.py b/bindings/python/notmuch/message.py
new file mode 100644 (file)
index 0000000..e71dbe3
--- /dev/null
@@ -0,0 +1,721 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+               Jesse Rosenthal <jrosenthal@jhu.edu>
+"""
+
+
+from ctypes import c_char_p, c_long, c_uint, c_int, POINTER, byref
+from datetime import date
+from .globals import (
+    nmlib,
+    Enum,
+    _str,
+    Python3StringMixIn,
+    NotmuchTagsP,
+    NotmuchMessageP,
+    NotmuchMessagesP,
+    NotmuchMessagePropertiesP,
+    NotmuchFilenamesP,
+)
+from .errors import (
+    STATUS,
+    NotmuchError,
+    NullPointerError,
+    NotInitializedError,
+)
+from .tag import Tags
+from .filenames import Filenames
+
+import email
+import sys
+
+
+class Message(Python3StringMixIn):
+    r"""Represents a single Email message
+
+    Technically, this wraps the underlying *notmuch_message_t*
+    structure. A user will usually not create these objects themselves
+    but get them as search results.
+
+    As it implements :meth:`__cmp__`, it is possible to compare two
+    :class:`Message`\s using `if msg1 == msg2: ...`.
+    """
+
+    """notmuch_message_get_filename (notmuch_message_t *message)"""
+    _get_filename = nmlib.notmuch_message_get_filename
+    _get_filename.argtypes = [NotmuchMessageP]
+    _get_filename.restype = c_char_p
+
+    """return all filenames for a message"""
+    _get_filenames = nmlib.notmuch_message_get_filenames
+    _get_filenames.argtypes = [NotmuchMessageP]
+    _get_filenames.restype = NotmuchFilenamesP
+
+    """notmuch_message_get_flag"""
+    _get_flag = nmlib.notmuch_message_get_flag
+    _get_flag.argtypes = [NotmuchMessageP, c_uint]
+    _get_flag.restype = bool
+
+    """notmuch_message_set_flag"""
+    _set_flag = nmlib.notmuch_message_set_flag
+    _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
+    _set_flag.restype = None
+
+    """notmuch_message_get_message_id (notmuch_message_t *message)"""
+    _get_message_id = nmlib.notmuch_message_get_message_id
+    _get_message_id.argtypes = [NotmuchMessageP]
+    _get_message_id.restype = c_char_p
+
+    """notmuch_message_get_thread_id"""
+    _get_thread_id = nmlib.notmuch_message_get_thread_id
+    _get_thread_id.argtypes = [NotmuchMessageP]
+    _get_thread_id.restype = c_char_p
+
+    """notmuch_message_get_replies"""
+    _get_replies = nmlib.notmuch_message_get_replies
+    _get_replies.argtypes = [NotmuchMessageP]
+    _get_replies.restype = NotmuchMessagesP
+
+    """notmuch_message_get_tags (notmuch_message_t *message)"""
+    _get_tags = nmlib.notmuch_message_get_tags
+    _get_tags.argtypes = [NotmuchMessageP]
+    _get_tags.restype = NotmuchTagsP
+
+    _get_date = nmlib.notmuch_message_get_date
+    _get_date.argtypes = [NotmuchMessageP]
+    _get_date.restype = c_long
+
+    _get_header = nmlib.notmuch_message_get_header
+    _get_header.argtypes = [NotmuchMessageP, c_char_p]
+    _get_header.restype = c_char_p
+
+    """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
+    _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
+    _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
+    _tags_to_maildir_flags.restype = c_int
+
+    """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
+    _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
+    _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
+    _maildir_flags_to_tags.restype = c_int
+
+    """notmuch_message_get_property"""
+    _get_property = nmlib.notmuch_message_get_property
+    _get_property.argtypes = [NotmuchMessageP, c_char_p, POINTER(c_char_p)]
+    _get_property.restype = c_int
+
+    """notmuch_message_get_properties"""
+    _get_properties = nmlib.notmuch_message_get_properties
+    _get_properties.argtypes = [NotmuchMessageP, c_char_p, c_int]
+    _get_properties.restype = NotmuchMessagePropertiesP
+
+    """notmuch_message_properties_valid"""
+    _properties_valid = nmlib.notmuch_message_properties_valid
+    _properties_valid.argtypes = [NotmuchMessagePropertiesP]
+    _properties_valid.restype = bool
+
+    """notmuch_message_properties_value"""
+    _properties_value = nmlib.notmuch_message_properties_value
+    _properties_value.argtypes = [NotmuchMessagePropertiesP]
+    _properties_value.restype = c_char_p
+
+    """notmuch_message_properties_key"""
+    _properties_key = nmlib.notmuch_message_properties_key
+    _properties_key.argtypes = [NotmuchMessagePropertiesP]
+    _properties_key.restype = c_char_p
+
+    """notmuch_message_properties_move_to_next"""
+    _properties_move_to_next = nmlib.notmuch_message_properties_move_to_next
+    _properties_move_to_next.argtypes = [NotmuchMessagePropertiesP]
+    _properties_move_to_next.restype = None
+
+    #Constants: Flags that can be set/get with set_flag
+    FLAG = Enum(['MATCH'])
+
+    def __init__(self, msg_p, parent=None):
+        """
+        :param msg_p: A pointer to an internal notmuch_message_t
+            Structure.  If it is `None`, we will raise an
+            :exc:`NullPointerError`.
+
+        :param parent: A 'parent' object is passed which this message is
+              derived from. We save a reference to it, so we can
+              automatically delete the parent object once all derived
+              objects are dead.
+        """
+        if not msg_p:
+            raise NullPointerError()
+        self._msg = msg_p
+        #keep reference to parent, so we keep it alive
+        self._parent = parent
+
+    def get_message_id(self):
+        """Returns the message ID
+
+        :returns: String with a message ID
+        :raises: :exc:`NotInitializedError` if the message
+                    is not initialized.
+        """
+        if not self._msg:
+            raise NotInitializedError()
+        return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
+
+    def get_thread_id(self):
+        """Returns the thread ID
+
+        The returned string belongs to 'message' will only be valid for as
+        long as the message is valid.
+
+        This function will not return `None` since Notmuch ensures that every
+        message belongs to a single thread.
+
+        :returns: String with a thread ID
+        :raises: :exc:`NotInitializedError` if the message
+                    is not initialized.
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
+
+    def get_replies(self):
+        """Gets all direct replies to this message as :class:`Messages`
+        iterator
+
+        .. note::
+
+            This call only makes sense if 'message' was ultimately obtained from
+            a :class:`Thread` object, (such as by coming directly from the
+            result of calling :meth:`Thread.get_toplevel_messages` or by any
+            number of subsequent calls to :meth:`get_replies`). If this message
+            was obtained through some non-thread means, (such as by a call to
+            :meth:`Query.search_messages`), then this function will return
+            an empty Messages iterator.
+
+        :returns: :class:`Messages`.
+        :raises: :exc:`NotInitializedError` if the message
+                    is not initialized.
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        msgs_p = Message._get_replies(self._msg)
+
+        from .messages import Messages, EmptyMessagesResult
+
+        if not msgs_p:
+            return EmptyMessagesResult(self)
+
+        return Messages(msgs_p, self)
+
+    def get_date(self):
+        """Returns time_t of the message date
+
+        For the original textual representation of the Date header from the
+        message call notmuch_message_get_header() with a header value of
+        "date".
+
+        :returns: A time_t timestamp.
+        :rtype: c_unit64
+        :raises: :exc:`NotInitializedError` if the message
+                    is not initialized.
+        """
+        if not self._msg:
+            raise NotInitializedError()
+        return Message._get_date(self._msg)
+
+    def get_header(self, header):
+        """Get the value of the specified header.
+
+        The value will be read from the actual message file, not from
+        the notmuch database. The header name is case insensitive.
+
+        Returns an empty string ("") if the message does not contain a
+        header line matching 'header'.
+
+        :param header: The name of the header to be retrieved.
+                       It is not case-sensitive.
+        :type header: str
+        :returns: The header value as string
+        :raises: :exc:`NotInitializedError` if the message is not
+                 initialized
+        :raises: :exc:`NullPointerError` if any error occurred
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        #Returns NULL if any error occurs.
+        header = Message._get_header(self._msg, _str(header))
+        if header == None:
+            raise NullPointerError()
+        return header.decode('UTF-8', 'ignore')
+
+    def get_filename(self):
+        """Returns the file path of the message file
+
+        :returns: Absolute file path & name of the message file
+        :raises: :exc:`NotInitializedError` if the message
+              is not initialized.
+        """
+        if not self._msg:
+            raise NotInitializedError()
+        return Message._get_filename(self._msg).decode('utf-8', 'ignore')
+
+    def get_filenames(self):
+        """Get all filenames for the email corresponding to 'message'
+
+        Returns a Filenames() generator with all absolute filepaths for
+        messages recorded to have the same Message-ID. These files must
+        not necessarily have identical content."""
+        if not self._msg:
+            raise NotInitializedError()
+
+        files_p = Message._get_filenames(self._msg)
+
+        return Filenames(files_p, self)
+
+    def get_flag(self, flag):
+        """Checks whether a specific flag is set for this message
+
+        The method :meth:`Query.search_threads` sets
+        *Message.FLAG.MATCH* for those messages that match the
+        query. This method allows us to get the value of this flag.
+
+        :param flag: One of the :attr:`Message.FLAG` values (currently only
+                     *Message.FLAG.MATCH*
+        :returns: An unsigned int (0/1), indicating whether the flag is set.
+        :raises: :exc:`NotInitializedError` if the message
+              is not initialized.
+        """
+        if not self._msg:
+            raise NotInitializedError()
+        return Message._get_flag(self._msg, flag)
+
+    def set_flag(self, flag, value):
+        """Sets/Unsets a specific flag for this message
+
+        :param flag: One of the :attr:`Message.FLAG` values (currently only
+                     *Message.FLAG.MATCH*
+        :param value: A bool indicating whether to set or unset the flag.
+
+        :raises: :exc:`NotInitializedError` if the message
+              is not initialized.
+        """
+        if not self._msg:
+            raise NotInitializedError()
+        self._set_flag(self._msg, flag, value)
+
+    def get_tags(self):
+        """Returns the message tags
+
+        :returns: A :class:`Tags` iterator.
+        :raises: :exc:`NotInitializedError` if the message is not
+                 initialized
+        :raises: :exc:`NullPointerError` if any error occurred
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        tags_p = Message._get_tags(self._msg)
+        if not tags_p:
+            raise NullPointerError()
+        return Tags(tags_p, self)
+
+    _add_tag = nmlib.notmuch_message_add_tag
+    _add_tag.argtypes = [NotmuchMessageP, c_char_p]
+    _add_tag.restype = c_uint
+
+    def add_tag(self, tag, sync_maildir_flags=False):
+        """Adds a tag to the given message
+
+        Adds a tag to the current message. The maximal tag length is defined in
+        the notmuch library and is currently 200 bytes.
+
+        :param tag: String with a 'tag' to be added.
+
+        :param sync_maildir_flags: If notmuch configuration is set to do
+            this, add maildir flags corresponding to notmuch tags. See
+            underlying method :meth:`tags_to_maildir_flags`. Use False
+            if you want to add/remove many tags on a message without
+            having to physically rename the file every time. Do note,
+            that this will do nothing when a message is frozen, as tag
+            changes will not be committed to the database yet.
+
+        :returns: STATUS.SUCCESS if the tag was successfully added.
+                  Raises an exception otherwise.
+        :raises: :exc:`NullPointerError` if the `tag` argument is NULL
+        :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
+                 Message.NOTMUCH_TAG_MAX)
+        :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
+                 in read-only mode so message cannot be modified
+        :raises: :exc:`NotInitializedError` if message has not been
+                 initialized
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        status = self._add_tag(self._msg, _str(tag))
+
+        # bail out on failure
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+
+        if sync_maildir_flags:
+            self.tags_to_maildir_flags()
+        return STATUS.SUCCESS
+
+    _remove_tag = nmlib.notmuch_message_remove_tag
+    _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
+    _remove_tag.restype = c_uint
+
+    def remove_tag(self, tag, sync_maildir_flags=False):
+        """Removes a tag from the given message
+
+        If the message has no such tag, this is a non-operation and
+        will report success anyway.
+
+        :param tag: String with a 'tag' to be removed.
+        :param sync_maildir_flags: If notmuch configuration is set to do
+            this, add maildir flags corresponding to notmuch tags. See
+            underlying method :meth:`tags_to_maildir_flags`. Use False
+            if you want to add/remove many tags on a message without
+            having to physically rename the file every time. Do note,
+            that this will do nothing when a message is frozen, as tag
+            changes will not be committed to the database yet.
+
+        :returns: STATUS.SUCCESS if the tag was successfully removed or if
+                  the message had no such tag.
+                  Raises an exception otherwise.
+        :raises: :exc:`NullPointerError` if the `tag` argument is NULL
+        :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
+                 Message.NOTMUCH_TAG_MAX)
+        :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
+                 in read-only mode so message cannot be modified
+        :raises: :exc:`NotInitializedError` if message has not been
+                 initialized
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        status = self._remove_tag(self._msg, _str(tag))
+        # bail out on error
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+
+        if sync_maildir_flags:
+            self.tags_to_maildir_flags()
+        return STATUS.SUCCESS
+
+    _remove_all_tags = nmlib.notmuch_message_remove_all_tags
+    _remove_all_tags.argtypes = [NotmuchMessageP]
+    _remove_all_tags.restype = c_uint
+
+    def remove_all_tags(self, sync_maildir_flags=False):
+        """Removes all tags from the given message.
+
+        See :meth:`freeze` for an example showing how to safely
+        replace tag values.
+
+
+        :param sync_maildir_flags: If notmuch configuration is set to do
+            this, add maildir flags corresponding to notmuch tags. See
+            :meth:`tags_to_maildir_flags`. Use False if you want to
+            add/remove many tags on a message without having to
+            physically rename the file every time. Do note, that this
+            will do nothing when a message is frozen, as tag changes
+            will not be committed to the database yet.
+
+        :returns: STATUS.SUCCESS if the tags were successfully removed.
+                  Raises an exception otherwise.
+        :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
+                 in read-only mode so message cannot be modified
+        :raises: :exc:`NotInitializedError` if message has not been
+                 initialized
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        status = self._remove_all_tags(self._msg)
+
+        # bail out on error
+        if status != STATUS.SUCCESS:
+            raise NotmuchError(status)
+
+        if sync_maildir_flags:
+            self.tags_to_maildir_flags()
+        return STATUS.SUCCESS
+
+    _freeze = nmlib.notmuch_message_freeze
+    _freeze.argtypes = [NotmuchMessageP]
+    _freeze.restype = c_uint
+
+    def get_property(self, prop):
+        """ Retrieve the value for a single property key
+
+        :param prop: The name of the property to get.
+        :returns: String with the property value or None if there is no such
+                  key. In the case of multiple values for the given key, the
+                  first one is retrieved.
+        :raises: :exc:`NotInitializedError` if message has not been
+                 initialized
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        value = c_char_p()
+        status = Message._get_property(self._msg, _str(prop), byref(value))
+        if status != 0:
+            raise NotmuchError(status)
+
+        if value is None or value.value is None:
+            return None
+        return value.value.decode('utf-8')
+
+    def get_properties(self, prop="", exact=False):
+        """ Get the properties of the message, returning a generator of
+        name, value pairs.
+
+        The generator will yield once per value. There might be more than one
+        value on each name, so the generator might yield the same name several
+        times.
+
+        :param prop: The name of the property to get. Otherwise it will return
+                     the full list of properties of the message.
+        :param exact: if True, require exact match with key. Otherwise
+                      treat as prefix.
+        :yields:  Each property values as a pair of `name, value`
+        :ytype:   pairs of str
+        :raises: :exc:`NotInitializedError` if message has not been
+                 initialized
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        properties = Message._get_properties(self._msg, _str(prop), exact)
+        while Message._properties_valid(properties):
+            key = Message._properties_key(properties)
+            value = Message._properties_value(properties)
+            yield key.decode("utf-8"), value.decode("utf-8")
+            Message._properties_move_to_next(properties)
+
+    def freeze(self):
+        """Freezes the current state of 'message' within the database
+
+        This means that changes to the message state, (via :meth:`add_tag`,
+        :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
+        committed to the database until the message is :meth:`thaw` ed.
+
+        Multiple calls to freeze/thaw are valid and these calls will
+        "stack". That is there must be as many calls to thaw as to freeze
+        before a message is actually thawed.
+
+        The ability to do freeze/thaw allows for safe transactions to
+        change tag values. For example, explicitly setting a message to
+        have a given set of tags might look like this::
+
+          msg.freeze()
+          msg.remove_all_tags(False)
+          for tag in new_tags:
+              msg.add_tag(tag, False)
+          msg.thaw()
+          msg.tags_to_maildir_flags()
+
+        With freeze/thaw used like this, the message in the database is
+        guaranteed to have either the full set of original tag values, or
+        the full set of new tag values, but nothing in between.
+
+        Imagine the example above without freeze/thaw and the operation
+        somehow getting interrupted. This could result in the message being
+        left with no tags if the interruption happened after
+        :meth:`remove_all_tags` but before :meth:`add_tag`.
+
+        :returns: STATUS.SUCCESS if the message was successfully frozen.
+                  Raises an exception otherwise.
+        :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
+                 in read-only mode so message cannot be modified
+        :raises: :exc:`NotInitializedError` if message has not been
+                 initialized
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        status = self._freeze(self._msg)
+
+        if STATUS.SUCCESS == status:
+            # return on success
+            return status
+
+        raise NotmuchError(status)
+
+    _thaw = nmlib.notmuch_message_thaw
+    _thaw.argtypes = [NotmuchMessageP]
+    _thaw.restype = c_uint
+
+    def thaw(self):
+        """Thaws the current 'message'
+
+        Thaw the current 'message', synchronizing any changes that may have
+        occurred while 'message' was frozen into the notmuch database.
+
+        See :meth:`freeze` for an example of how to use this
+        function to safely provide tag changes.
+
+        Multiple calls to freeze/thaw are valid and these calls with
+        "stack". That is there must be as many calls to thaw as to freeze
+        before a message is actually thawed.
+
+        :returns: STATUS.SUCCESS if the message was successfully frozen.
+                  Raises an exception otherwise.
+        :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made
+                 to thaw an unfrozen message. That is, there have been
+                 an unbalanced number of calls to :meth:`freeze` and
+                 :meth:`thaw`.
+        :raises: :exc:`NotInitializedError` if message has not been
+                 initialized
+        """
+        if not self._msg:
+            raise NotInitializedError()
+
+        status = self._thaw(self._msg)
+
+        if STATUS.SUCCESS == status:
+            # return on success
+            return status
+
+        raise NotmuchError(status)
+
+    def is_match(self):
+        """(Not implemented)"""
+        return self.get_flag(Message.FLAG.MATCH)
+
+    def tags_to_maildir_flags(self):
+        """Synchronize notmuch tags to file Maildir flags
+
+              'D' if the message has the "draft" tag
+              'F' if the message has the "flagged" tag
+              'P' if the message has the "passed" tag
+              'R' if the message has the "replied" tag
+              'S' if the message does not have the "unread" tag
+
+        Any existing flags unmentioned in the list above will be
+        preserved in the renaming.
+
+        Also, if this filename is in a directory named "new", rename it
+        to be within the neighboring directory named "cur".
+
+        Do note that calling this method while a message is frozen might
+        not work yet, as the modified tags have not been committed yet
+        to the database.
+
+        :returns: a :class:`STATUS` value. In short, you want to see
+            notmuch.STATUS.SUCCESS here. See there for details."""
+        if not self._msg:
+            raise NotInitializedError()
+        return Message._tags_to_maildir_flags(self._msg)
+
+    def maildir_flags_to_tags(self):
+        """Synchronize file Maildir flags to notmuch tags
+
+            Flag    Action if present
+            ----    -----------------
+            'D'     Adds the "draft" tag to the message
+            'F'     Adds the "flagged" tag to the message
+            'P'     Adds the "passed" tag to the message
+            'R'     Adds the "replied" tag to the message
+            'S'     Removes the "unread" tag from the message
+
+        For each flag that is not present, the opposite action
+        (add/remove) is performed for the corresponding tags.  If there
+        are multiple filenames associated with this message, the flag is
+        considered present if it appears in one or more filenames. (That
+        is, the flags from the multiple filenames are combined with the
+        logical OR operator.)
+
+        As a convenience, you can set the sync_maildir_flags parameter in
+        :meth:`Database.index_file` to implicitly call this.
+
+        :returns: a :class:`STATUS`. In short, you want to see
+            notmuch.STATUS.SUCCESS here. See there for details."""
+        if not self._msg:
+            raise NotInitializedError()
+        return Message._maildir_flags_to_tags(self._msg)
+
+    def __repr__(self):
+        """Represent a Message() object by str()"""
+        return self.__str__()
+
+    def __unicode__(self):
+        format = "%s (%s) (%s)"
+        return format % (self.get_header('from'),
+                         self.get_tags(),
+                         date.fromtimestamp(self.get_date()),
+                        )
+
+    def get_message_parts(self):
+        """Output like notmuch show"""
+        fp = open(self.get_filename(), 'rb')
+        if sys.version_info[0] < 3:
+            email_msg = email.message_from_file(fp)
+        else:
+            email_msg = email.message_from_binary_file(fp)
+        fp.close()
+
+        out = []
+        for msg in email_msg.walk():
+            if not msg.is_multipart():
+                out.append(msg)
+        return out
+
+    def get_part(self, num):
+        """Returns the nth message body part"""
+        parts = self.get_message_parts()
+        if (num <= 0 or num > len(parts)):
+            return ""
+        else:
+            out_part = parts[(num - 1)]
+            return out_part.get_payload(decode=True)
+
+    def __hash__(self):
+        """Implement hash(), so we can use Message() sets"""
+        file = self.get_filename()
+        if not file:
+            return None
+        return hash(file)
+
+    def __cmp__(self, other):
+        """Implement cmp(), so we can compare Message()s
+
+        2 messages are considered equal if they point to the same
+        Message-Id and if they point to the same file names. If 2
+        Messages derive from different queries where some files have
+        been added or removed, the same messages would not be considered
+        equal (as they do not point to the same set of files
+        any more)."""
+        res = cmp(self.get_message_id(), other.get_message_id())
+        if res:
+            res = cmp(list(self.get_filenames()), list(other.get_filenames()))
+        return res
+
+    _destroy = nmlib.notmuch_message_destroy
+    _destroy.argtypes = [NotmuchMessageP]
+    _destroy.restype = None
+
+    def __del__(self):
+        """Close and free the notmuch Message"""
+        if self._msg:
+            self._destroy(self._msg)
diff --git a/bindings/python/notmuch/messages.py b/bindings/python/notmuch/messages.py
new file mode 100644 (file)
index 0000000..3801c66
--- /dev/null
@@ -0,0 +1,199 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+               Jesse Rosenthal <jrosenthal@jhu.edu>
+"""
+
+from .globals import (
+    nmlib,
+    NotmuchTagsP,
+    NotmuchMessageP,
+    NotmuchMessagesP,
+)
+from .errors import (
+    NullPointerError,
+    NotInitializedError,
+)
+from .tag import Tags
+from .message import Message
+
+class Messages(object):
+    r"""Represents a list of notmuch messages
+
+    This object provides an iterator over a list of notmuch messages
+    (Technically, it provides a wrapper for the underlying
+    *notmuch_messages_t* structure). Do note that the underlying library
+    only provides a one-time iterator (it cannot reset the iterator to
+    the start). Thus iterating over the function will "exhaust" the list
+    of messages, and a subsequent iteration attempt will raise a
+    :exc:`NotInitializedError`. If you need to
+    re-iterate over a list of messages you will need to retrieve a new
+    :class:`Messages` object or cache your :class:`Message`\s in a list
+    via::
+
+       msglist = list(msgs)
+
+    You can store and reuse the single :class:`Message` objects as often
+    as you want as long as you keep the parent :class:`Messages` object
+    around. (Due to hierarchical memory allocation, all derived
+    :class:`Message` objects will be invalid when we delete the parent
+    :class:`Messages` object, even if it was already exhausted.) So
+    this works::
+
+      db   = Database()
+      msgs = Query(db,'').search_messages() #get a Messages() object
+      msglist = list(msgs)
+
+      # msgs is "exhausted" now and msgs.next() will raise an exception.
+      # However it will be kept alive until all retrieved Message()
+      # objects are also deleted. If you do e.g. an explicit del(msgs)
+      # here, the following lines would fail.
+
+      # You can reiterate over *msglist* however as often as you want.
+      # It is simply a list with :class:`Message`s.
+
+      print (msglist[0].get_filename())
+      print (msglist[1].get_filename())
+      print (msglist[0].get_message_id())
+
+
+    As :class:`Message` implements both __hash__() and __cmp__(), it is
+    possible to make sets out of :class:`Messages` and use set
+    arithmetic (this happens in python and will of course be *much*
+    slower than redoing a proper query with the appropriate filters::
+
+        s1, s2 = set(msgs1), set(msgs2)
+        s.union(s2)
+        s1 -= s2
+        ...
+
+    Be careful when using set arithmetic between message sets derived
+    from different Databases (ie the same database reopened after
+    messages have changed). If messages have added or removed associated
+    files in the meantime, it is possible that the same message would be
+    considered as a different object (as it points to a different file).
+    """
+
+    #notmuch_messages_get
+    _get = nmlib.notmuch_messages_get
+    _get.argtypes = [NotmuchMessagesP]
+    _get.restype = NotmuchMessageP
+
+    _collect_tags = nmlib.notmuch_messages_collect_tags
+    _collect_tags.argtypes = [NotmuchMessagesP]
+    _collect_tags.restype = NotmuchTagsP
+
+    def __init__(self, msgs_p, parent=None):
+        """
+        :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
+             structure. These are not publicly exposed, so a user
+             will almost never instantiate a :class:`Messages` object
+             herself. They are usually handed back as a result,
+             e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
+             valid, we will raise an :exc:`NullPointerError` if it is
+             `None`.
+        :type msgs_p: :class:`ctypes.c_void_p`
+        :param parent: The parent object
+             (ie :class:`Query`) these tags are derived from. It saves
+             a reference to it, so we can automatically delete the db
+             object once all derived objects are dead.
+        :TODO: Make the iterator work more than once and cache the tags in
+               the Python object.(?)
+        """
+        if not msgs_p:
+            raise NullPointerError()
+
+        self._msgs = msgs_p
+        #store parent, so we keep them alive as long as self  is alive
+        self._parent = parent
+
+    def collect_tags(self):
+        """Return the unique :class:`Tags` in the contained messages
+
+        :returns: :class:`Tags`
+        :exceptions: :exc:`NotInitializedError` if not init'ed
+
+        .. note::
+
+            :meth:`collect_tags` will iterate over the messages and therefore
+            will not allow further iterations.
+        """
+        if not self._msgs:
+            raise NotInitializedError()
+
+        # collect all tags (returns NULL on error)
+        tags_p = Messages._collect_tags(self._msgs)
+        #reset _msgs as we iterated over it and can do so only once
+        self._msgs = None
+
+        if not tags_p:
+            raise NullPointerError()
+        return Tags(tags_p, self)
+
+    def __iter__(self):
+        """ Make Messages an iterator """
+        return self
+
+    _valid = nmlib.notmuch_messages_valid
+    _valid.argtypes = [NotmuchMessagesP]
+    _valid.restype = bool
+
+    _move_to_next = nmlib.notmuch_messages_move_to_next
+    _move_to_next.argtypes = [NotmuchMessagesP]
+    _move_to_next.restype = None
+
+    def __next__(self):
+        if not self._msgs:
+            raise NotInitializedError()
+
+        if not self._valid(self._msgs):
+            self._msgs = None
+            raise StopIteration
+
+        msg = Message(Messages._get(self._msgs), self)
+        self._move_to_next(self._msgs)
+        return msg
+    next = __next__ # python2.x iterator protocol compatibility
+
+    def __nonzero__(self):
+        '''
+        Implement truth value testing. If __nonzero__ is not
+        implemented, the python runtime would fall back to `len(..) >
+        0` thus exhausting the iterator.
+
+        :returns: True if the wrapped iterator has at least one more object
+                  left.
+        '''
+        return self._msgs and self._valid(self._msgs)
+
+    _destroy = nmlib.notmuch_messages_destroy
+    _destroy.argtypes = [NotmuchMessagesP]
+    _destroy.restype = None
+
+    def __del__(self):
+        """Close and free the notmuch Messages"""
+        if self._msgs:
+            self._destroy(self._msgs)
+
+class EmptyMessagesResult(Messages):
+    def __init__(self, parent):
+        self._msgs = None
+        self._parent = parent
+
+    def __next__(self):
+        raise StopIteration()
+    next = __next__
diff --git a/bindings/python/notmuch/query.py b/bindings/python/notmuch/query.py
new file mode 100644 (file)
index 0000000..ffb86df
--- /dev/null
@@ -0,0 +1,237 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+
+from ctypes import c_char_p, c_uint, POINTER, byref
+from .globals import (
+    nmlib,
+    Enum,
+    _str,
+    NotmuchQueryP,
+    NotmuchThreadsP,
+    NotmuchDatabaseP,
+    NotmuchMessagesP,
+)
+from .errors import (
+    NotmuchError,
+    NullPointerError,
+    NotInitializedError,
+)
+from .threads import Threads
+from .messages import Messages
+
+
+class Query(object):
+    """Represents a search query on an opened :class:`Database`.
+
+    A query selects and filters a subset of messages from the notmuch
+    database we derive from.
+
+    :class:`Query` provides an instance attribute :attr:`sort`, which
+    contains the sort order (if specified via :meth:`set_sort`) or
+    `None`.
+
+    Any function in this class may throw an :exc:`NotInitializedError`
+    in case the underlying query object was not set up correctly.
+
+    .. note:: Do remember that as soon as we tear down this object,
+           all underlying derived objects such as threads,
+           messages, tags etc will be freed by the underlying library
+           as well. Accessing these objects will lead to segfaults and
+           other unexpected behavior. See above for more details.
+    """
+    # constants
+    SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED'])
+    """Constants: Sort order in which to return results"""
+
+    def __init__(self, db, querystr):
+        """
+        :param db: An open database which we derive the Query from.
+        :type db: :class:`Database`
+        :param querystr: The query string for the message.
+        :type querystr: utf-8 encoded str or unicode
+        """
+        self._db = None
+        self._query = None
+        self.sort = None
+        self.create(db, querystr)
+
+    def _assert_query_is_initialized(self):
+        """Raises :exc:`NotInitializedError` if self._query is `None`"""
+        if not self._query:
+            raise NotInitializedError()
+
+    """notmuch_query_create"""
+    _create = nmlib.notmuch_query_create
+    _create.argtypes = [NotmuchDatabaseP, c_char_p]
+    _create.restype = NotmuchQueryP
+
+    def create(self, db, querystr):
+        """Creates a new query derived from a Database
+
+        This function is utilized by __init__() and usually does not need to
+        be called directly.
+
+        :param db: Database to create the query from.
+        :type db: :class:`Database`
+        :param querystr: The query string
+        :type querystr: utf-8 encoded str or unicode
+        :raises:
+            :exc:`NullPointerError` if the query creation failed
+                (e.g. too little memory).
+            :exc:`NotInitializedError` if the underlying db was not
+                initialized.
+        """
+        db._assert_db_is_initialized()
+        # create reference to parent db to keep it alive
+        self._db = db
+        # create query, return None if too little mem available
+        query_p = Query._create(db._db, _str(querystr))
+        if not query_p:
+            raise NullPointerError
+        self._query = query_p
+
+    _set_sort = nmlib.notmuch_query_set_sort
+    _set_sort.argtypes = [NotmuchQueryP, c_uint]
+    _set_sort.restype = None
+
+    def set_sort(self, sort):
+        """Set the sort order future results will be delivered in
+
+        :param sort: Sort order (see :attr:`Query.SORT`)
+        """
+        self._assert_query_is_initialized()
+        self.sort = sort
+        self._set_sort(self._query, sort)
+
+    _exclude_tag = nmlib.notmuch_query_add_tag_exclude
+    _exclude_tag.argtypes = [NotmuchQueryP, c_char_p]
+    _exclude_tag.restype = None
+
+    def exclude_tag(self, tagname):
+        """Add a tag that will be excluded from the query results by default.
+
+        This exclusion will be overridden if this tag appears explicitly in the
+        query.
+
+        :param tagname: Name of the tag to be excluded
+        """
+        self._assert_query_is_initialized()
+        self._exclude_tag(self._query, _str(tagname))
+
+    """notmuch_query_search_threads"""
+    _search_threads = nmlib.notmuch_query_search_threads
+    _search_threads.argtypes = [NotmuchQueryP, POINTER(NotmuchThreadsP)]
+    _search_threads.restype = c_uint
+
+    def search_threads(self):
+        r"""Execute a query for threads
+
+        Execute a query for threads, returning a :class:`Threads` iterator.
+        The returned threads are owned by the query and as such, will only be
+        valid until the Query is deleted.
+
+        The method sets :attr:`Message.FLAG`\.MATCH for those messages that
+        match the query. The method :meth:`Message.get_flag` allows us
+        to get the value of this flag.
+
+        :returns: :class:`Threads`
+        :raises: :exc:`NullPointerError` if search_threads failed
+        """
+        self._assert_query_is_initialized()
+        threads_p = NotmuchThreadsP() # == NULL
+        status = Query._search_threads(self._query, byref(threads_p))
+        if status != 0:
+            raise NotmuchError(status)
+
+        if not threads_p:
+            raise NullPointerError
+        return Threads(threads_p, self)
+
+    """notmuch_query_search_messages_st"""
+    _search_messages = nmlib.notmuch_query_search_messages
+    _search_messages.argtypes = [NotmuchQueryP, POINTER(NotmuchMessagesP)]
+    _search_messages.restype = c_uint
+
+    def search_messages(self):
+        """Filter messages according to the query and return
+        :class:`Messages` in the defined sort order
+
+        :returns: :class:`Messages`
+        :raises: :exc:`NullPointerError` if search_messages failed
+        """
+        self._assert_query_is_initialized()
+        msgs_p = NotmuchMessagesP() # == NULL
+        status = Query._search_messages(self._query, byref(msgs_p))
+        if status != 0:
+            raise NotmuchError(status)
+
+        if not msgs_p:
+            raise NullPointerError
+        return Messages(msgs_p, self)
+
+    _count_messages = nmlib.notmuch_query_count_messages
+    _count_messages.argtypes = [NotmuchQueryP, POINTER(c_uint)]
+    _count_messages.restype = c_uint
+
+    def count_messages(self):
+        '''
+        This function performs a search and returns Xapian's best
+        guess as to the number of matching messages.
+
+        :returns: the estimated number of messages matching this query
+        :rtype:   int
+        '''
+        self._assert_query_is_initialized()
+        count = c_uint(0)
+        status = Query._count_messages(self._query, byref(count))
+        if status != 0:
+            raise NotmuchError(status)
+        return count.value
+
+    _count_threads = nmlib.notmuch_query_count_threads
+    _count_threads.argtypes = [NotmuchQueryP, POINTER(c_uint)]
+    _count_threads.restype = c_uint
+
+    def count_threads(self):
+        '''
+        This function performs a search and returns the number of
+        unique thread IDs in the matching messages. This is the same
+        as number of threads matching a search.
+
+        Note that this is a significantly heavier operation than
+        meth:`Query.count_messages`.
+
+        :returns: the number of threads returned by this query
+        :rtype:   int
+        '''
+        self._assert_query_is_initialized()
+        count = c_uint(0)
+        status = Query._count_threads(self._query, byref(count))
+        if status != 0:
+            raise NotmuchError(status)
+        return count.value
+
+    _destroy = nmlib.notmuch_query_destroy
+    _destroy.argtypes = [NotmuchQueryP]
+    _destroy.restype = None
+
+    def __del__(self):
+        """Close and free the Query"""
+        if self._query:
+            self._destroy(self._query)
diff --git a/bindings/python/notmuch/tag.py b/bindings/python/notmuch/tag.py
new file mode 100644 (file)
index 0000000..fbb18ce
--- /dev/null
@@ -0,0 +1,141 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+from ctypes import c_char_p
+from .globals import (
+    nmlib,
+    Python3StringMixIn,
+    NotmuchTagsP,
+)
+from .errors import (
+    NullPointerError,
+    NotInitializedError,
+)
+
+
+class Tags(Python3StringMixIn):
+    """Represents a list of notmuch tags
+
+    This object provides an iterator over a list of notmuch tags (which
+    are unicode instances).
+
+    Do note that the underlying library only provides a one-time
+    iterator (it cannot reset the iterator to the start). Thus iterating
+    over the function will "exhaust" the list of tags, and a subsequent
+    iteration attempt will raise a :exc:`NotInitializedError`.
+    Also note, that any function that uses iteration (nearly all) will
+    also exhaust the tags. So both::
+
+      for tag in tags: print tag
+
+    as well as::
+
+       number_of_tags = len(tags)
+
+    and even a simple::
+
+       #str() iterates over all tags to construct a space separated list
+       print(str(tags))
+
+    will "exhaust" the Tags. If you need to re-iterate over a list of
+    tags you will need to retrieve a new :class:`Tags` object.
+    """
+
+    #notmuch_tags_get
+    _get = nmlib.notmuch_tags_get
+    _get.argtypes = [NotmuchTagsP]
+    _get.restype = c_char_p
+
+    def __init__(self, tags_p, parent=None):
+        """
+        :param tags_p: A pointer to an underlying *notmuch_tags_t*
+             structure. These are not publicly exposed, so a user
+             will almost never instantiate a :class:`Tags` object
+             herself. They are usually handed back as a result,
+             e.g. in :meth:`Database.get_all_tags`.  *tags_p* must be
+             valid, we will raise an :exc:`NullPointerError` if it is
+             `None`.
+        :type tags_p: :class:`ctypes.c_void_p`
+        :param parent: The parent object (ie :class:`Database` or
+             :class:`Message` these tags are derived from, and saves a
+             reference to it, so we can automatically delete the db object
+             once all derived objects are dead.
+        :TODO: Make the iterator optionally work more than once by
+               cache the tags in the Python object(?)
+        """
+        if not tags_p:
+            raise NullPointerError()
+
+        self._tags = tags_p
+        #save reference to parent object so we keep it alive
+        self._parent = parent
+
+    def __iter__(self):
+        """ Make Tags an iterator """
+        return self
+
+    _valid = nmlib.notmuch_tags_valid
+    _valid.argtypes = [NotmuchTagsP]
+    _valid.restype = bool
+
+    _move_to_next = nmlib.notmuch_tags_move_to_next
+    _move_to_next.argtypes = [NotmuchTagsP]
+    _move_to_next.restype = None
+
+    def __next__(self):
+        if not self._tags:
+            raise NotInitializedError()
+        if not self._valid(self._tags):
+            self._tags = None
+            raise StopIteration
+        tag = Tags._get(self._tags).decode('UTF-8')
+        self._move_to_next(self._tags)
+        return tag
+    next = __next__ # python2.x iterator protocol compatibility
+
+    def __nonzero__(self):
+        '''
+        Implement truth value testing. If __nonzero__ is not
+        implemented, the python runtime would fall back to `len(..) >
+        0` thus exhausting the iterator.
+
+        :returns: True if the wrapped iterator has at least one more object
+                  left.
+        '''
+        return self._tags and self._valid(self._tags)
+
+    def __unicode__(self):
+        """string representation of :class:`Tags`: a space separated list of tags
+
+        .. note::
+
+            As this iterates over the tags, we will not be able to iterate over
+            them again (as in retrieve them)! If the tags have been exhausted
+            already, this will raise a :exc:`NotInitializedError`on subsequent
+            attempts.
+        """
+        return " ".join(self)
+
+    _destroy = nmlib.notmuch_tags_destroy
+    _destroy.argtypes = [NotmuchTagsP]
+    _destroy.restype = None
+
+    def __del__(self):
+        """Close and free the notmuch tags"""
+        if self._tags:
+            self._destroy(self._tags)
diff --git a/bindings/python/notmuch/thread.py b/bindings/python/notmuch/thread.py
new file mode 100644 (file)
index 0000000..9aa0e08
--- /dev/null
@@ -0,0 +1,281 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+
+from ctypes import c_char_p, c_long, c_int
+from .globals import (
+    nmlib,
+    NotmuchThreadP,
+    NotmuchMessagesP,
+    NotmuchTagsP,
+)
+from .errors import (
+    NullPointerError,
+    NotInitializedError,
+)
+from .messages import Messages
+from .tag import Tags
+from datetime import date
+
+class Thread(object):
+    """Represents a single message thread."""
+
+    """notmuch_thread_get_thread_id"""
+    _get_thread_id = nmlib.notmuch_thread_get_thread_id
+    _get_thread_id.argtypes = [NotmuchThreadP]
+    _get_thread_id.restype = c_char_p
+
+    """notmuch_thread_get_authors"""
+    _get_authors = nmlib.notmuch_thread_get_authors
+    _get_authors.argtypes = [NotmuchThreadP]
+    _get_authors.restype = c_char_p
+
+    """notmuch_thread_get_subject"""
+    _get_subject = nmlib.notmuch_thread_get_subject
+    _get_subject.argtypes = [NotmuchThreadP]
+    _get_subject.restype = c_char_p
+
+    """notmuch_thread_get_toplevel_messages"""
+    _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages
+    _get_toplevel_messages.argtypes = [NotmuchThreadP]
+    _get_toplevel_messages.restype = NotmuchMessagesP
+
+    _get_newest_date = nmlib.notmuch_thread_get_newest_date
+    _get_newest_date.argtypes = [NotmuchThreadP]
+    _get_newest_date.restype = c_long
+
+    _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
+    _get_oldest_date.argtypes = [NotmuchThreadP]
+    _get_oldest_date.restype = c_long
+
+    """notmuch_thread_get_tags"""
+    _get_tags = nmlib.notmuch_thread_get_tags
+    _get_tags.argtypes = [NotmuchThreadP]
+    _get_tags.restype = NotmuchTagsP
+
+    def __init__(self, thread_p, parent=None):
+        """
+        :param thread_p: A pointer to an internal notmuch_thread_t
+            Structure.  These are not publicly exposed, so a user
+            will almost never instantiate a :class:`Thread` object
+            herself. They are usually handed back as a result,
+            e.g. when iterating through :class:`Threads`. *thread_p*
+            must be valid, we will raise an :exc:`NullPointerError`
+            if it is `None`.
+
+        :param parent: A 'parent' object is passed which this message is
+              derived from. We save a reference to it, so we can
+              automatically delete the parent object once all derived
+              objects are dead.
+        """
+        if not thread_p:
+            raise NullPointerError()
+        self._thread = thread_p
+        #keep reference to parent, so we keep it alive
+        self._parent = parent
+
+    def get_thread_id(self):
+        """Get the thread ID of 'thread'
+
+        The returned string belongs to 'thread' and will only be valid
+        for as long as the thread is valid.
+
+        :returns: String with a message ID
+        :raises: :exc:`NotInitializedError` if the thread
+                    is not initialized.
+        """
+        if not self._thread:
+            raise NotInitializedError()
+        return Thread._get_thread_id(self._thread).decode('utf-8', 'ignore')
+
+    _get_total_messages = nmlib.notmuch_thread_get_total_messages
+    _get_total_messages.argtypes = [NotmuchThreadP]
+    _get_total_messages.restype = c_int
+
+    def get_total_messages(self):
+        """Get the total number of messages in 'thread'
+
+        :returns: The number of all messages in the database
+                  belonging to this thread. Contrast with
+                  :meth:`get_matched_messages`.
+        :raises: :exc:`NotInitializedError` if the thread
+                    is not initialized.
+        """
+        if not self._thread:
+            raise NotInitializedError()
+        return self._get_total_messages(self._thread)
+
+    def get_toplevel_messages(self):
+        """Returns a :class:`Messages` iterator for the top-level messages in
+           'thread'
+
+           This iterator will not necessarily iterate over all of the messages
+           in the thread. It will only iterate over the messages in the thread
+           which are not replies to other messages in the thread.
+
+        :returns: :class:`Messages`
+        :raises: :exc:`NotInitializedError` if query is not initialized
+        :raises: :exc:`NullPointerError` if search_messages failed
+        """
+        if not self._thread:
+            raise NotInitializedError()
+
+        msgs_p = Thread._get_toplevel_messages(self._thread)
+
+        if not msgs_p:
+            raise NullPointerError()
+
+        return Messages(msgs_p, self)
+
+    """notmuch_thread_get_messages"""
+    _get_messages = nmlib.notmuch_thread_get_messages
+    _get_messages.argtypes = [NotmuchThreadP]
+    _get_messages.restype = NotmuchMessagesP
+
+    def get_messages(self):
+        """Returns a :class:`Messages` iterator for all messages in 'thread'
+
+        :returns: :class:`Messages`
+        :raises: :exc:`NotInitializedError` if query is not initialized
+        :raises: :exc:`NullPointerError` if get_messages failed
+        """
+        if not self._thread:
+            raise NotInitializedError()
+
+        msgs_p = Thread._get_messages(self._thread)
+
+        if not msgs_p:
+            raise NullPointerError()
+
+        return Messages(msgs_p, self)
+
+    _get_matched_messages = nmlib.notmuch_thread_get_matched_messages
+    _get_matched_messages.argtypes = [NotmuchThreadP]
+    _get_matched_messages.restype = c_int
+
+    def get_matched_messages(self):
+        """Returns the number of messages in 'thread' that matched the query
+
+        :returns: The number of all messages belonging to this thread that
+                  matched the :class:`Query`from which this thread was created.
+                  Contrast with :meth:`get_total_messages`.
+        :raises: :exc:`NotInitializedError` if the thread
+                    is not initialized.
+        """
+        if not self._thread:
+            raise NotInitializedError()
+        return self._get_matched_messages(self._thread)
+
+    def get_authors(self):
+        """Returns the authors of 'thread'
+
+        The returned string is a comma-separated list of the names of the
+        authors of mail messages in the query results that belong to this
+        thread.
+
+        The returned string belongs to 'thread' and will only be valid for
+        as long as this Thread() is not deleted.
+        """
+        if not self._thread:
+            raise NotInitializedError()
+        authors = Thread._get_authors(self._thread)
+        if not authors:
+            return None
+        return authors.decode('UTF-8', 'ignore')
+
+    def get_subject(self):
+        """Returns the Subject of 'thread'
+
+        The returned string belongs to 'thread' and will only be valid for
+        as long as this Thread() is not deleted.
+        """
+        if not self._thread:
+            raise NotInitializedError()
+        subject = Thread._get_subject(self._thread)
+        if not subject:
+            return None
+        return subject.decode('UTF-8', 'ignore')
+
+    def get_newest_date(self):
+        """Returns time_t of the newest message date
+
+        :returns: A time_t timestamp.
+        :rtype: c_unit64
+        :raises: :exc:`NotInitializedError` if the message
+                    is not initialized.
+        """
+        if not self._thread:
+            raise NotInitializedError()
+        return Thread._get_newest_date(self._thread)
+
+    def get_oldest_date(self):
+        """Returns time_t of the oldest message date
+
+        :returns: A time_t timestamp.
+        :rtype: c_unit64
+        :raises: :exc:`NotInitializedError` if the message
+                    is not initialized.
+        """
+        if not self._thread:
+            raise NotInitializedError()
+        return Thread._get_oldest_date(self._thread)
+
+    def get_tags(self):
+        """ Returns the message tags
+
+        In the Notmuch database, tags are stored on individual
+        messages, not on threads. So the tags returned here will be all
+        tags of the messages which matched the search and which belong to
+        this thread.
+
+        The :class:`Tags` object is owned by the thread and as such, will only
+        be valid for as long as this :class:`Thread` is valid (e.g. until the
+        query from which it derived is explicitly deleted).
+
+        :returns: A :class:`Tags` iterator.
+        :raises: :exc:`NotInitializedError` if query is not initialized
+        :raises: :exc:`NullPointerError` if search_messages failed
+        """
+        if not self._thread:
+            raise NotInitializedError()
+
+        tags_p = Thread._get_tags(self._thread)
+        if not tags_p:
+            raise NullPointerError()
+        return Tags(tags_p, self)
+
+    def __unicode__(self):
+        frm = "thread:%s %12s [%d/%d] %s; %s (%s)"
+
+        return frm % (self.get_thread_id(),
+                      date.fromtimestamp(self.get_newest_date()),
+                      self.get_matched_messages(),
+                      self.get_total_messages(),
+                      self.get_authors(),
+                      self.get_subject(),
+                      self.get_tags(),
+                     )
+
+    _destroy = nmlib.notmuch_thread_destroy
+    _destroy.argtypes = [NotmuchThreadP]
+    _destroy.restype = None
+
+    def __del__(self):
+        """Close and free the notmuch Thread"""
+        if self._thread:
+            self._destroy(self._thread)
diff --git a/bindings/python/notmuch/threads.py b/bindings/python/notmuch/threads.py
new file mode 100644 (file)
index 0000000..0c382d5
--- /dev/null
@@ -0,0 +1,152 @@
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+
+from .globals import (
+    nmlib,
+    Python3StringMixIn,
+    NotmuchThreadP,
+    NotmuchThreadsP,
+)
+from .errors import (
+    NullPointerError,
+    NotInitializedError,
+)
+from .thread import Thread
+
+class Threads(Python3StringMixIn):
+    """Represents a list of notmuch threads
+
+    This object provides an iterator over a list of notmuch threads
+    (Technically, it provides a wrapper for the underlying
+    *notmuch_threads_t* structure). Do note that the underlying
+    library only provides a one-time iterator (it cannot reset the
+    iterator to the start). Thus iterating over the function will
+    "exhaust" the list of threads, and a subsequent iteration attempt
+    will raise a :exc:`NotInitializedError`. Also
+    note, that any function that uses iteration will also
+    exhaust the messages. So both::
+
+      for thread in threads: print thread
+
+    as well as::
+
+       list_of_threads = list(threads)
+
+    will "exhaust" the threads. If you need to re-iterate over a list of
+    messages you will need to retrieve a new :class:`Threads` object.
+
+    Things are not as bad as it seems though, you can store and reuse
+    the single Thread objects as often as you want as long as you
+    keep the parent Threads object around. (Recall that due to
+    hierarchical memory allocation, all derived Threads objects will
+    be invalid when we delete the parent Threads() object, even if it
+    was already "exhausted".) So this works::
+
+      db   = Database()
+      threads = Query(db,'').search_threads() #get a Threads() object
+      threadlist = []
+      for thread in threads:
+         threadlist.append(thread)
+
+      # threads is "exhausted" now.
+      # However it will be kept around until all retrieved Thread() objects are
+      # also deleted. If you did e.g. an explicit del(threads) here, the
+      # following lines would fail.
+
+      # You can reiterate over *threadlist* however as often as you want.
+      # It is simply a list with Thread objects.
+
+      print (threadlist[0].get_thread_id())
+      print (threadlist[1].get_thread_id())
+      print (threadlist[0].get_total_messages())
+    """
+
+    #notmuch_threads_get
+    _get = nmlib.notmuch_threads_get
+    _get.argtypes = [NotmuchThreadsP]
+    _get.restype = NotmuchThreadP
+
+    def __init__(self, threads_p, parent=None):
+        """
+        :param threads_p:  A pointer to an underlying *notmuch_threads_t*
+             structure. These are not publicly exposed, so a user
+             will almost never instantiate a :class:`Threads` object
+             herself. They are usually handed back as a result,
+             e.g. in :meth:`Query.search_threads`.  *threads_p* must be
+             valid, we will raise an :exc:`NullPointerError` if it is
+             `None`.
+        :type threads_p: :class:`ctypes.c_void_p`
+        :param parent: The parent object
+             (ie :class:`Query`) these tags are derived from. It saves
+             a reference to it, so we can automatically delete the db
+             object once all derived objects are dead.
+        :TODO: Make the iterator work more than once and cache the tags in
+               the Python object.(?)
+        """
+        if not threads_p:
+            raise NullPointerError()
+
+        self._threads = threads_p
+        #store parent, so we keep them alive as long as self  is alive
+        self._parent = parent
+
+    def __iter__(self):
+        """ Make Threads an iterator """
+        return self
+
+    _valid = nmlib.notmuch_threads_valid
+    _valid.argtypes = [NotmuchThreadsP]
+    _valid.restype = bool
+
+    _move_to_next = nmlib.notmuch_threads_move_to_next
+    _move_to_next.argtypes = [NotmuchThreadsP]
+    _move_to_next.restype = None
+
+    def __next__(self):
+        if not self._threads:
+            raise NotInitializedError()
+
+        if not self._valid(self._threads):
+            self._threads = None
+            raise StopIteration
+
+        thread = Thread(Threads._get(self._threads), self)
+        self._move_to_next(self._threads)
+        return thread
+    next = __next__ # python2.x iterator protocol compatibility
+
+    def __nonzero__(self):
+        '''
+        Implement truth value testing. If __nonzero__ is not
+        implemented, the python runtime would fall back to `len(..) >
+        0` thus exhausting the iterator.
+
+        :returns: True if the wrapped iterator has at least one more object
+                  left.
+        '''
+        return self._threads and self._valid(self._threads)
+
+    _destroy = nmlib.notmuch_threads_destroy
+    _destroy.argtypes = [NotmuchThreadsP]
+    _destroy.restype = None
+
+    def __del__(self):
+        """Close and free the notmuch Threads"""
+        if self._threads:
+            self._destroy(self._threads)
diff --git a/bindings/python/notmuch/version.py b/bindings/python/notmuch/version.py
new file mode 100644 (file)
index 0000000..f4fd171
--- /dev/null
@@ -0,0 +1,3 @@
+# this file should be kept in sync with ../../../version
+__VERSION__ = '0.38.2'
+SOVERSION = '5'
diff --git a/bindings/python/setup.py b/bindings/python/setup.py
new file mode 100644 (file)
index 0000000..6308b9f
--- /dev/null
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+
+"""
+This file is part of notmuch.
+
+Notmuch is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation, either version 3 of the License, or (at your
+option) any later version.
+
+Notmuch is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
+"""
+
+import os
+from distutils.core import setup
+
+# get the notmuch version number without importing the notmuch module
+version_file = os.path.join(os.path.dirname(__file__),
+                            'notmuch', 'version.py')
+exec(compile(open(version_file).read(), version_file, 'exec'))
+assert '__VERSION__' in globals(), \
+    'Failed to read the notmuch binding version number'
+
+setup(name='notmuch',
+      version=__VERSION__,
+      description='Python binding of the notmuch mail search and indexing library.',
+      author='Sebastian Spaeth',
+      author_email='Sebastian@SSpaeth.de',
+      url='https://notmuchmail.org/',
+      download_url='https://notmuchmail.org/releases/notmuch-%s.tar.gz' % __VERSION__,
+      packages=['notmuch'],
+      keywords=['library', 'email'],
+      long_description='''Overview
+========
+
+The notmuch module provides an interface to the `notmuch
+<https://notmuchmail.org>`_ functionality, directly interfacing with a
+shared notmuch library. Notmuch provides a maildatabase that allows
+for extremely quick searching and filtering of your email according to
+various criteria.
+
+The documentation for the latest notmuch release can be `viewed
+online <https://notmuch.readthedocs.io/>`_.
+
+Requirements
+------------
+
+You need to have notmuch installed (or rather libnotmuch.so.1). Also,
+notmuch makes use of the ctypes library, and has only been tested with
+python >= 2.5. It will not work on earlier python versions.
+''',
+      classifiers=['Development Status :: 3 - Alpha',
+                   'Intended Audience :: Developers',
+                   'License :: OSI Approved :: GNU General Public License (GPL)',
+                   'Programming Language :: Python :: 2',
+                   'Programming Language :: Python :: 3',
+                   'Topic :: Communications :: Email',
+                   'Topic :: Software Development :: Libraries'
+                   ],
+      platforms='',
+      license='https://www.gnu.org/licenses/gpl-3.0.txt',
+     )
diff --git a/bindings/ruby/.gitignore b/bindings/ruby/.gitignore
new file mode 100644 (file)
index 0000000..c57ae63
--- /dev/null
@@ -0,0 +1,7 @@
+# .gitignore for bindings/ruby
+
+# Generated files
+/Makefile
+/mkmf.log
+/notmuch.so
+*.o
diff --git a/bindings/ruby/README b/bindings/ruby/README
new file mode 100644 (file)
index 0000000..a2946b6
--- /dev/null
@@ -0,0 +1,7 @@
+To build the the notmuch ruby extension, run the following commands
+from the *top level* notmuch source directory:
+
+% ./configure
+% make ruby-bindings
+
+The generic documentation about building notmuch also applies.
diff --git a/bindings/ruby/database.c b/bindings/ruby/database.c
new file mode 100644 (file)
index 0000000..ed224ef
--- /dev/null
@@ -0,0 +1,493 @@
+/* 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"
+
+VALUE
+notmuch_rb_database_alloc (VALUE klass)
+{
+    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;
+}
+
+/*
+ * call-seq: Notmuch::Database.new(path [, {:create => false, :mode => Notmuch::MODE_READ_ONLY}]) => DB
+ *
+ * Create or open a notmuch database using the given path.
+ *
+ * If :create is +true+, create the database instead of opening.
+ *
+ * The argument :mode specifies the open mode of the database.
+ */
+VALUE
+notmuch_rb_database_initialize (int argc, VALUE *argv, VALUE self)
+{
+    const char *path;
+    int create, mode;
+    VALUE pathv, hashv;
+    VALUE modev;
+    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, "02", &pathv, &hashv);
+
+    if (!NIL_P (pathv)) {
+       SafeStringValue (pathv);
+       path = RSTRING_PTR (pathv);
+    }
+
+    if (!NIL_P (hashv)) {
+       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");
+               }
+           }
+       }
+       if (rcreate != Qundef)
+           create = RTEST (rcreate);
+    }
+
+    rb_check_typeddata (self, &notmuch_rb_database_type);
+    if (create)
+       ret = notmuch_database_create (path, &database);
+    else
+       ret = notmuch_database_open_with_config (path, mode, NULL, NULL, &database, NULL);
+    notmuch_rb_status_raise (ret);
+
+    DATA_PTR (self) = notmuch_rb_object_create (database, "notmuch_rb_database");
+
+    return self;
+}
+
+/*
+ * call-seq: Notmuch::Database.open(path [, ahash]) {|db| ...}
+ *
+ * Identical to new, except that when it is called with a block, it yields with
+ * the new instance and closes it, and returns the result which is returned from
+ * the block.
+ */
+VALUE
+notmuch_rb_database_open (int argc, VALUE *argv, VALUE klass)
+{
+    VALUE obj;
+
+    obj = rb_class_new_instance (argc, argv, klass);
+    if (!rb_block_given_p ())
+       return obj;
+
+    return rb_ensure (rb_yield, obj, notmuch_rb_database_close, obj);
+}
+
+/*
+ * call-seq: DB.close => nil
+ *
+ * Close the notmuch database.
+ */
+VALUE
+notmuch_rb_database_close (VALUE self)
+{
+    notmuch_database_t *db;
+    notmuch_status_t ret;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    ret = notmuch_database_close (db);
+    notmuch_rb_status_raise (ret);
+
+    return Qnil;
+}
+
+/*
+ * call-seq: DB.path => String
+ *
+ * Return the path of the database
+ */
+VALUE
+notmuch_rb_database_path (VALUE self)
+{
+    notmuch_database_t *db;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    return rb_str_new2 (notmuch_database_get_path (db));
+}
+
+/*
+ * call-seq: DB.version => Fixnum
+ *
+ * Return the version of the database
+ */
+VALUE
+notmuch_rb_database_version (VALUE self)
+{
+    notmuch_database_t *db;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    return INT2FIX (notmuch_database_get_version (db));
+}
+
+/*
+ * call-seq: DB.needs_upgrade? => true or false
+ *
+ * Return the +true+ if the database needs upgrading, +false+ otherwise
+ */
+VALUE
+notmuch_rb_database_needs_upgrade (VALUE self)
+{
+    notmuch_database_t *db;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    return notmuch_database_needs_upgrade (db) ? Qtrue : Qfalse;
+}
+
+static void
+notmuch_rb_upgrade_notify (void *closure, double progress)
+{
+    VALUE *block = (VALUE *) closure;
+    rb_funcall (*block, ID_call, 1, rb_float_new (progress));
+}
+
+/*
+ * call-seq: DB.upgrade! [{|progress| block }] => nil
+ *
+ * Upgrade the database.
+ *
+ * If a block is given the block is called with a progress indicator as a
+ * floating point value in the range of [0.0..1.0].
+ */
+VALUE
+notmuch_rb_database_upgrade (VALUE self)
+{
+    notmuch_status_t ret;
+    void (*pnotify) (void *closure, double progress);
+    notmuch_database_t *db;
+    VALUE block;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    if (rb_block_given_p ()) {
+       pnotify = notmuch_rb_upgrade_notify;
+       block = rb_block_proc ();
+    }
+    else
+       pnotify = NULL;
+
+    ret = notmuch_database_upgrade (db, pnotify, pnotify ? &block : NULL);
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
+
+/*
+ * call-seq: DB.begin_atomic => nil
+ *
+ * Begin an atomic database operation.
+ */
+VALUE
+notmuch_rb_database_begin_atomic (VALUE self)
+{
+    notmuch_status_t ret;
+    notmuch_database_t *db;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    ret = notmuch_database_begin_atomic (db);
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
+
+/*
+ * call-seq: DB.end_atomic => nil
+ *
+ * Indicate the end of an atomic database operation.
+ */
+VALUE
+notmuch_rb_database_end_atomic (VALUE self)
+{
+    notmuch_status_t ret;
+    notmuch_database_t *db;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    ret = notmuch_database_end_atomic (db);
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
+
+/*
+ * call-seq: DB.get_directory(path) => DIR
+ *
+ * Retrieve a directory object from the database for 'path'
+ */
+VALUE
+notmuch_rb_database_get_directory (VALUE self, VALUE pathv)
+{
+    const char *path;
+    notmuch_status_t ret;
+    notmuch_directory_t *dir;
+    notmuch_database_t *db;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    SafeStringValue (pathv);
+    path = RSTRING_PTR (pathv);
+
+    ret = notmuch_database_get_directory (db, path, &dir);
+    notmuch_rb_status_raise (ret);
+    if (dir)
+       return Data_Wrap_Notmuch_Object (notmuch_rb_cDirectory, &notmuch_rb_directory_type, dir);
+    return Qnil;
+}
+
+/*
+ * call-seq: DB.add_message(path) => MESSAGE, isdup
+ *
+ * Add a message to the database and return it.
+ *
+ * +isdup+ is a boolean that specifies whether the added message was a
+ * duplicate.
+ */
+VALUE
+notmuch_rb_database_add_message (VALUE self, VALUE pathv)
+{
+    const char *path;
+    notmuch_status_t ret;
+    notmuch_message_t *message;
+    notmuch_database_t *db;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    SafeStringValue (pathv);
+    path = RSTRING_PTR (pathv);
+
+    ret = notmuch_database_index_file (db, path, NULL, &message);
+    notmuch_rb_status_raise (ret);
+    return rb_assoc_new (Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message),
+        (ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) ? Qtrue : Qfalse);
+}
+
+/*
+ * call-seq: DB.remove_message (path) => isdup
+ *
+ * Remove a message from the database.
+ *
+ * +isdup+ is a boolean that specifies whether the removed message was a
+ * duplicate.
+ */
+VALUE
+notmuch_rb_database_remove_message (VALUE self, VALUE pathv)
+{
+    const char *path;
+    notmuch_status_t ret;
+    notmuch_database_t *db;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    SafeStringValue (pathv);
+    path = RSTRING_PTR (pathv);
+
+    ret = notmuch_database_remove_message (db, path);
+    notmuch_rb_status_raise (ret);
+    return (ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) ? Qtrue : Qfalse;
+}
+
+/*
+ * call-seq: DB.find_message(id) => MESSAGE or nil
+ *
+ * Find a message by message id.
+ */
+VALUE
+notmuch_rb_database_find_message (VALUE self, VALUE idv)
+{
+    const char *id;
+    notmuch_status_t ret;
+    notmuch_database_t *db;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    SafeStringValue (idv);
+    id = RSTRING_PTR (idv);
+
+    ret = notmuch_database_find_message (db, id, &message);
+    notmuch_rb_status_raise (ret);
+
+    if (message)
+       return Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message);
+    return Qnil;
+}
+
+/*
+ * call-seq: DB.find_message_by_filename(path) => MESSAGE or nil
+ *
+ * Find a message by filename.
+ */
+VALUE
+notmuch_rb_database_find_message_by_filename (VALUE self, VALUE pathv)
+{
+    const char *path;
+    notmuch_status_t ret;
+    notmuch_database_t *db;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    SafeStringValue (pathv);
+    path = RSTRING_PTR (pathv);
+
+    ret = notmuch_database_find_message_by_filename (db, path, &message);
+    notmuch_rb_status_raise (ret);
+
+    if (message)
+       return Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message);
+    return Qnil;
+}
+
+/*
+ * call-seq: DB.get_all_tags() => TAGS
+ *
+ * Returns a list of all tags found in the database.
+ */
+VALUE
+notmuch_rb_database_get_all_tags (VALUE self)
+{
+    notmuch_database_t *db;
+    notmuch_tags_t *tags;
+
+    Data_Get_Notmuch_Database (self, db);
+
+    tags = notmuch_database_get_all_tags (db);
+    if (!tags) {
+       const char *msg = notmuch_database_status_string (db);
+       if (!msg)
+           msg = "Unknown notmuch error";
+
+       rb_raise (notmuch_rb_eBaseError, "%s", msg);
+    }
+    return notmuch_rb_tags_get (tags);
+}
+
+/*
+ * call-seq:
+ *   DB.query(query) => QUERY
+ *   DB.query(query, sort:, excluded_tags:, omit_excluded:) => 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 (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);
+    qstr = RSTRING_PTR (qstrv);
+
+    query = notmuch_query_create (db, qstr);
+    if (!query)
+        rb_raise (notmuch_rb_eMemoryError, "Out of memory");
+
+    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);
+}
diff --git a/bindings/ruby/defs.h b/bindings/ruby/defs.h
new file mode 100644 (file)
index 0000000..a2cb38c
--- /dev/null
@@ -0,0 +1,369 @@
+/* The Ruby interface to the notmuch mail library
+ *
+ * Copyright © 2010, 2011, 2012 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>
+ */
+
+#ifndef DEFS_H
+#define DEFS_H
+
+#include <notmuch.h>
+#include <ruby.h>
+#include <talloc.h>
+
+extern VALUE notmuch_rb_cDatabase;
+extern VALUE notmuch_rb_cDirectory;
+extern VALUE notmuch_rb_cFileNames;
+extern VALUE notmuch_rb_cQuery;
+extern VALUE notmuch_rb_cThreads;
+extern VALUE notmuch_rb_cThread;
+extern VALUE notmuch_rb_cMessages;
+extern VALUE notmuch_rb_cMessage;
+
+extern VALUE notmuch_rb_eBaseError;
+extern VALUE notmuch_rb_eDatabaseError;
+extern VALUE notmuch_rb_eMemoryError;
+extern VALUE notmuch_rb_eReadOnlyError;
+extern VALUE notmuch_rb_eXapianError;
+extern VALUE notmuch_rb_eFileError;
+extern VALUE notmuch_rb_eFileNotEmailError;
+extern VALUE notmuch_rb_eNullPointerError;
+extern VALUE notmuch_rb_eTagTooLongError;
+extern VALUE notmuch_rb_eUnbalancedFreezeThawError;
+extern VALUE notmuch_rb_eUnbalancedAtomicError;
+
+extern ID ID_call;
+
+/* RSTRING_PTR() is new in ruby-1.9 */
+#if !defined(RSTRING_PTR)
+# define RSTRING_PTR(v) (RSTRING((v))->ptr)
+#endif /* !defined (RSTRING_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_Object(obj, type, ptr)                        \
+    do {                                                       \
+       notmuch_rb_object_t *rb_wrapper;                        \
+       Data_Get_Notmuch_Rb_Object ((obj), (type), rb_wrapper); \
+       (ptr) = rb_wrapper->nm_object;                          \
+    } 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_Database(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_database_type, (ptr))
+
+#define Data_Get_Notmuch_Directory(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_directory_type, (ptr))
+
+#define Data_Get_Notmuch_Query(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_query_type, (ptr))
+
+#define Data_Get_Notmuch_Threads(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_threads_type, (ptr))
+
+#define Data_Get_Notmuch_Messages(obj, ptr) \
+    Data_Get_Notmuch_Object ((obj), &notmuch_rb_messages_type, (ptr))
+
+#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
+notmuch_rb_status_raise (notmuch_status_t status);
+
+/* database.c */
+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);
+
+VALUE
+notmuch_rb_database_open (int argc, VALUE *argv, VALUE klass);
+
+VALUE
+notmuch_rb_database_close (VALUE self);
+
+VALUE
+notmuch_rb_database_path (VALUE self);
+
+VALUE
+notmuch_rb_database_version (VALUE self);
+
+VALUE
+notmuch_rb_database_needs_upgrade (VALUE self);
+
+VALUE
+notmuch_rb_database_upgrade (VALUE self);
+
+VALUE
+notmuch_rb_database_begin_atomic (VALUE self);
+
+VALUE
+notmuch_rb_database_end_atomic (VALUE self);
+
+VALUE
+notmuch_rb_database_get_directory (VALUE self, VALUE pathv);
+
+VALUE
+notmuch_rb_database_add_message (VALUE self, VALUE pathv);
+
+VALUE
+notmuch_rb_database_remove_message (VALUE self, VALUE pathv);
+
+VALUE
+notmuch_rb_database_find_message (VALUE self, VALUE idv);
+
+VALUE
+notmuch_rb_database_find_message_by_filename (VALUE self, VALUE pathv);
+
+VALUE
+notmuch_rb_database_get_all_tags (VALUE self);
+
+VALUE
+notmuch_rb_database_query_create (int argc, VALUE *argv, VALUE self);
+
+/* directory.c */
+VALUE
+notmuch_rb_directory_destroy (VALUE self);
+
+VALUE
+notmuch_rb_directory_get_mtime (VALUE self);
+
+VALUE
+notmuch_rb_directory_set_mtime (VALUE self, VALUE mtimev);
+
+VALUE
+notmuch_rb_directory_get_child_files (VALUE self);
+
+VALUE
+notmuch_rb_directory_get_child_directories (VALUE self);
+
+/* filenames.c */
+VALUE
+notmuch_rb_filenames_get (notmuch_filenames_t *fnames);
+
+/* query.c */
+VALUE
+notmuch_rb_query_destroy (VALUE self);
+
+VALUE
+notmuch_rb_query_get_sort (VALUE self);
+
+VALUE
+notmuch_rb_query_set_sort (VALUE self, VALUE sortv);
+
+VALUE
+notmuch_rb_query_get_string (VALUE self);
+
+VALUE
+notmuch_rb_query_add_tag_exclude (VALUE self, VALUE tagv);
+
+VALUE
+notmuch_rb_query_set_omit_excluded (VALUE self, VALUE omitv);
+
+VALUE
+notmuch_rb_query_search_threads (VALUE self);
+
+VALUE
+notmuch_rb_query_search_messages (VALUE self);
+
+VALUE
+notmuch_rb_query_count_messages (VALUE self);
+
+VALUE
+notmuch_rb_query_count_threads (VALUE self);
+
+/* threads.c */
+VALUE
+notmuch_rb_threads_destroy (VALUE self);
+
+VALUE
+notmuch_rb_threads_each (VALUE self);
+
+/* messages.c */
+VALUE
+notmuch_rb_messages_destroy (VALUE self);
+
+VALUE
+notmuch_rb_messages_each (VALUE self);
+
+VALUE
+notmuch_rb_messages_collect_tags (VALUE self);
+
+/* thread.c */
+VALUE
+notmuch_rb_thread_destroy (VALUE self);
+
+VALUE
+notmuch_rb_thread_get_thread_id (VALUE self);
+
+VALUE
+notmuch_rb_thread_get_total_messages (VALUE self);
+
+VALUE
+notmuch_rb_thread_get_toplevel_messages (VALUE self);
+
+VALUE
+notmuch_rb_thread_get_messages (VALUE self);
+
+VALUE
+notmuch_rb_thread_get_matched_messages (VALUE self);
+
+VALUE
+notmuch_rb_thread_get_authors (VALUE self);
+
+VALUE
+notmuch_rb_thread_get_subject (VALUE self);
+
+VALUE
+notmuch_rb_thread_get_oldest_date (VALUE self);
+
+VALUE
+notmuch_rb_thread_get_newest_date (VALUE self);
+
+VALUE
+notmuch_rb_thread_get_tags (VALUE self);
+
+/* message.c */
+VALUE
+notmuch_rb_message_destroy (VALUE self);
+
+VALUE
+notmuch_rb_message_get_message_id (VALUE self);
+
+VALUE
+notmuch_rb_message_get_thread_id (VALUE self);
+
+VALUE
+notmuch_rb_message_get_replies (VALUE self);
+
+VALUE
+notmuch_rb_message_get_filename (VALUE self);
+
+VALUE
+notmuch_rb_message_get_filenames (VALUE self);
+
+VALUE
+notmuch_rb_message_get_flag (VALUE self, VALUE flagv);
+
+VALUE
+notmuch_rb_message_set_flag (VALUE self, VALUE flagv, VALUE valuev);
+
+VALUE
+notmuch_rb_message_get_date (VALUE self);
+
+VALUE
+notmuch_rb_message_get_header (VALUE self, VALUE headerv);
+
+VALUE
+notmuch_rb_message_get_tags (VALUE self);
+
+VALUE
+notmuch_rb_message_add_tag (VALUE self, VALUE tagv);
+
+VALUE
+notmuch_rb_message_remove_tag (VALUE self, VALUE tagv);
+
+VALUE
+notmuch_rb_message_remove_all_tags (VALUE self);
+
+VALUE
+notmuch_rb_message_maildir_flags_to_tags (VALUE self);
+
+VALUE
+notmuch_rb_message_tags_to_maildir_flags (VALUE self);
+
+VALUE
+notmuch_rb_message_freeze (VALUE self);
+
+VALUE
+notmuch_rb_message_thaw (VALUE self);
+
+/* tags.c */
+VALUE
+notmuch_rb_tags_get (notmuch_tags_t *tags);
+
+/* init.c */
+void
+Init_notmuch (void);
+
+#endif
diff --git a/bindings/ruby/directory.c b/bindings/ruby/directory.c
new file mode 100644 (file)
index 0000000..f267d82
--- /dev/null
@@ -0,0 +1,110 @@
+/* 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: DIR.destroy! => nil
+ *
+ * Destroys the directory, freeing all resources allocated for it.
+ */
+VALUE
+notmuch_rb_directory_destroy (VALUE self)
+{
+    notmuch_rb_object_destroy (self, &notmuch_rb_directory_type);
+
+    return Qnil;
+}
+
+/*
+ * call-seq: DIR.mtime => fixnum
+ *
+ * Returns the mtime of the directory or +0+ if no mtime has been previously
+ * stored.
+ */
+VALUE
+notmuch_rb_directory_get_mtime (VALUE self)
+{
+    notmuch_directory_t *dir;
+
+    Data_Get_Notmuch_Directory (self, dir);
+
+    return UINT2NUM (notmuch_directory_get_mtime (dir));
+}
+
+/*
+ * call-seq: DIR.mtime=(fixnum) => nil
+ *
+ * Store an mtime within the database for the directory object.
+ */
+VALUE
+notmuch_rb_directory_set_mtime (VALUE self, VALUE mtimev)
+{
+    notmuch_status_t ret;
+    notmuch_directory_t *dir;
+
+    Data_Get_Notmuch_Directory (self, dir);
+
+    if (!FIXNUM_P (mtimev))
+       rb_raise (rb_eTypeError, "First argument not a fixnum");
+
+    ret = notmuch_directory_set_mtime (dir, FIX2UINT (mtimev));
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
+
+/*
+ * call-seq: DIR.child_files => FILENAMES
+ *
+ * Return a Notmuch::FileNames object, which is an +Enumerable+ listing all the
+ * filenames of messages in the database within the given directory.
+ */
+VALUE
+notmuch_rb_directory_get_child_files (VALUE self)
+{
+    notmuch_directory_t *dir;
+    notmuch_filenames_t *fnames;
+
+    Data_Get_Notmuch_Directory (self, dir);
+
+    fnames = notmuch_directory_get_child_files (dir);
+
+    return notmuch_rb_filenames_get (fnames);
+}
+
+/*
+ * call-seq: DIR.child_directories => FILENAMES
+ *
+ * Return a Notmuch::FileNames object, which is an +Enumerable+ listing all the
+ * directories in the database within the given directory.
+ */
+VALUE
+notmuch_rb_directory_get_child_directories (VALUE self)
+{
+    notmuch_directory_t *dir;
+    notmuch_filenames_t *fnames;
+
+    Data_Get_Notmuch_Directory (self, dir);
+
+    fnames = notmuch_directory_get_child_directories (dir);
+
+    return notmuch_rb_filenames_get (fnames);
+}
diff --git a/bindings/ruby/extconf.rb b/bindings/ruby/extconf.rb
new file mode 100644 (file)
index 0000000..d914537
--- /dev/null
@@ -0,0 +1,26 @@
+#!/usr/bin/env ruby
+# coding: utf-8
+# Copyright 2010, 2011, 2012 Ali Polatel <alip@exherbo.org>
+# Distributed under the terms of the GNU General Public License v3
+
+require 'mkmf'
+
+dir = File.join(ENV['NOTMUCH_SRCDIR'], 'lib')
+
+# includes
+$INCFLAGS = "-I#{dir} #{$INCFLAGS}"
+
+if ENV['EXTRA_LDFLAGS']
+  $LDFLAGS += " " + ENV['EXTRA_LDFLAGS']
+end
+
+if not ENV['LIBNOTMUCH']
+  exit 1
+end
+
+$LOCAL_LIBS += ENV['LIBNOTMUCH']
+$LIBS += " -ltalloc"
+
+# Create Makefile
+dir_config('notmuch')
+create_makefile('notmuch')
diff --git a/bindings/ruby/filenames.c b/bindings/ruby/filenames.c
new file mode 100644 (file)
index 0000000..60c3fb8
--- /dev/null
@@ -0,0 +1,11 @@
+#include "defs.h"
+
+VALUE
+notmuch_rb_filenames_get (notmuch_filenames_t *fnames)
+{
+    VALUE rb_array = rb_ary_new ();
+
+    for (; notmuch_filenames_valid (fnames); notmuch_filenames_move_to_next (fnames))
+       rb_ary_push (rb_array, rb_str_new2 (notmuch_filenames_get (fnames)));
+    return rb_array;
+}
diff --git a/bindings/ruby/init.c b/bindings/ruby/init.c
new file mode 100644 (file)
index 0000000..2d1994a
--- /dev/null
@@ -0,0 +1,377 @@
+/* The Ruby interface to the notmuch mail library
+ *
+ * Copyright © 2010, 2011, 2012 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"
+
+VALUE notmuch_rb_cDatabase;
+VALUE notmuch_rb_cDirectory;
+VALUE notmuch_rb_cQuery;
+VALUE notmuch_rb_cThreads;
+VALUE notmuch_rb_cThread;
+VALUE notmuch_rb_cMessages;
+VALUE notmuch_rb_cMessage;
+
+VALUE notmuch_rb_eBaseError;
+VALUE notmuch_rb_eDatabaseError;
+VALUE notmuch_rb_eMemoryError;
+VALUE notmuch_rb_eReadOnlyError;
+VALUE notmuch_rb_eXapianError;
+VALUE notmuch_rb_eFileError;
+VALUE notmuch_rb_eFileNotEmailError;
+VALUE notmuch_rb_eNullPointerError;
+VALUE notmuch_rb_eTagTooLongError;
+VALUE notmuch_rb_eUnbalancedFreezeThawError;
+VALUE notmuch_rb_eUnbalancedAtomicError;
+
+ID ID_call;
+
+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
+ *
+ * == Summary
+ *
+ * Ruby extension to the <tt>notmuch</tt> mail library.
+ *
+ * == Classes
+ *
+ * Following are the classes that are most likely to be of interest to
+ * the user:
+ *
+ * - Notmuch::Database
+ * - Notmuch::Query
+ * - Notmuch::Threads
+ * - Notmuch::Messages
+ * - Notmuch::Thread
+ * - Notmuch::Message
+ */
+
+void
+Init_notmuch (void)
+{
+    VALUE mod;
+
+    ID_call = rb_intern ("call");
+
+    mod = rb_define_module ("Notmuch");
+
+    /*
+     * Document-const: Notmuch::MODE_READ_ONLY
+     *
+     * Open the database in read only mode
+     */
+    rb_define_const (mod, "MODE_READ_ONLY", INT2FIX (NOTMUCH_DATABASE_MODE_READ_ONLY));
+    /*
+     * Document-const: Notmuch::MODE_READ_WRITE
+     *
+     * Open the database in read write mode
+     */
+    rb_define_const (mod, "MODE_READ_WRITE", INT2FIX (NOTMUCH_DATABASE_MODE_READ_WRITE));
+    /*
+     * Document-const: Notmuch::SORT_OLDEST_FIRST
+     *
+     * Sort query results by oldest first
+     */
+    rb_define_const (mod, "SORT_OLDEST_FIRST", INT2FIX (NOTMUCH_SORT_OLDEST_FIRST));
+    /*
+     * Document-const: Notmuch::SORT_NEWEST_FIRST
+     *
+     * Sort query results by newest first
+     */
+    rb_define_const (mod, "SORT_NEWEST_FIRST", INT2FIX (NOTMUCH_SORT_NEWEST_FIRST));
+    /*
+     * Document-const: Notmuch::SORT_MESSAGE_ID
+     *
+     * Sort query results by message id
+     */
+    rb_define_const (mod, "SORT_MESSAGE_ID", INT2FIX (NOTMUCH_SORT_MESSAGE_ID));
+    /*
+     * Document-const: Notmuch::SORT_UNSORTED
+     *
+     * Do not sort query results
+     */
+    rb_define_const (mod, "SORT_UNSORTED", INT2FIX (NOTMUCH_SORT_UNSORTED));
+    /*
+     * Document-const: Notmuch::MESSAGE_FLAG_MATCH
+     *
+     * Message flag "match"
+     */
+    rb_define_const (mod, "MESSAGE_FLAG_MATCH", INT2FIX (NOTMUCH_MESSAGE_FLAG_MATCH));
+    /*
+     * Document-const: Notmuch::MESSAGE_FLAG_EXCLUDED
+     *
+     * Message flag "excluded"
+     */
+    rb_define_const (mod, "MESSAGE_FLAG_EXCLUDED", INT2FIX (NOTMUCH_MESSAGE_FLAG_EXCLUDED));
+    /*
+     * Document-const: Notmuch::TAG_MAX
+     *
+     * 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
+     *
+     * Base class for all notmuch exceptions
+     */
+    notmuch_rb_eBaseError = rb_define_class_under (mod, "BaseError", rb_eStandardError);
+    /*
+     * Document-class: Notmuch::DatabaseError
+     *
+     * Raised when the database can't be created or opened.
+     */
+    notmuch_rb_eDatabaseError = rb_define_class_under (mod, "DatabaseError", notmuch_rb_eBaseError);
+    /*
+     * Document-class: Notmuch::MemoryError
+     *
+     * Raised when notmuch is out of memory
+     */
+    notmuch_rb_eMemoryError = rb_define_class_under (mod, "MemoryError", notmuch_rb_eBaseError);
+    /*
+     * Document-class: Notmuch::ReadOnlyError
+     *
+     * Raised when an attempt was made to write to a database opened in read-only
+     * mode.
+     */
+    notmuch_rb_eReadOnlyError = rb_define_class_under (mod, "ReadOnlyError", notmuch_rb_eBaseError);
+    /*
+     * Document-class: Notmuch::XapianError
+     *
+     * Raised when a Xapian exception occurs
+     */
+    notmuch_rb_eXapianError = rb_define_class_under (mod, "XapianError", notmuch_rb_eBaseError);
+    /*
+     * Document-class: Notmuch::FileError
+     *
+     * Raised when an error occurs trying to read or write to a file.
+     */
+    notmuch_rb_eFileError = rb_define_class_under (mod, "FileError", notmuch_rb_eBaseError);
+    /*
+     * Document-class: Notmuch::FileNotEmailError
+     *
+     * Raised when a file is presented that doesn't appear to be an email message.
+     */
+    notmuch_rb_eFileNotEmailError = rb_define_class_under (mod, "FileNotEmailError", notmuch_rb_eBaseError);
+    /*
+     * Document-class: Notmuch::NullPointerError
+     *
+     * Raised when the user erroneously passes a +NULL+ pointer to a notmuch
+     * function.
+     */
+    notmuch_rb_eNullPointerError = rb_define_class_under (mod, "NullPointerError", notmuch_rb_eBaseError);
+    /*
+     * Document-class: Notmuch::TagTooLongError
+     *
+     * Raised when a tag value is too long (exceeds Notmuch::TAG_MAX)
+     */
+    notmuch_rb_eTagTooLongError = rb_define_class_under (mod, "TagTooLongError", notmuch_rb_eBaseError);
+    /*
+     * Document-class: Notmuch::UnbalancedFreezeThawError
+     *
+     * Raised when the notmuch_message_thaw function has been called more times
+     * than notmuch_message_freeze.
+     */
+    notmuch_rb_eUnbalancedFreezeThawError = rb_define_class_under (mod, "UnbalancedFreezeThawError",
+                                                                  notmuch_rb_eBaseError);
+    /*
+     * Document-class: Notmuch::UnbalancedAtomicError
+     *
+     * Raised when notmuch_database_end_atomic has been called more times than
+     * notmuch_database_begin_atomic
+     */
+    notmuch_rb_eUnbalancedAtomicError = rb_define_class_under (mod, "UnbalancedAtomicError",
+                                                              notmuch_rb_eBaseError);
+    /*
+     * Document-class: Notmuch::Database
+     *
+     * Notmuch database interaction
+     */
+    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 */
+    rb_define_method (notmuch_rb_cDatabase, "needs_upgrade?", notmuch_rb_database_needs_upgrade, 0); /* in database.c */
+    rb_define_method (notmuch_rb_cDatabase, "upgrade!", notmuch_rb_database_upgrade, 0); /* in database.c */
+    rb_define_method (notmuch_rb_cDatabase, "begin_atomic", notmuch_rb_database_begin_atomic, 0); /* in database.c */
+    rb_define_method (notmuch_rb_cDatabase, "end_atomic", notmuch_rb_database_end_atomic, 0); /* in database.c */
+    rb_define_method (notmuch_rb_cDatabase, "get_directory", notmuch_rb_database_get_directory, 1); /* in database.c */
+    rb_define_method (notmuch_rb_cDatabase, "add_message", notmuch_rb_database_add_message, 1); /* in database.c */
+    rb_define_method (notmuch_rb_cDatabase, "remove_message", notmuch_rb_database_remove_message, 1); /* in database.c */
+    rb_define_method (notmuch_rb_cDatabase, "find_message",
+                     notmuch_rb_database_find_message, 1); /* in database.c */
+    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 */
+
+    /*
+     * Document-class: Notmuch::Directory
+     *
+     * Notmuch directory
+     */
+    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 */
+    rb_define_method (notmuch_rb_cDirectory, "mtime=", notmuch_rb_directory_set_mtime, 1); /* in directory.c */
+    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::Query
+     *
+     * Notmuch query
+     */
+    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 */
+    rb_define_method (notmuch_rb_cQuery, "sort=", notmuch_rb_query_set_sort, 1); /* in query.c */
+    rb_define_method (notmuch_rb_cQuery, "to_s", notmuch_rb_query_get_string, 0); /* in query.c */
+    rb_define_method (notmuch_rb_cQuery, "add_tag_exclude", notmuch_rb_query_add_tag_exclude, 1); /* in query.c */
+    rb_define_method (notmuch_rb_cQuery, "omit_excluded=", notmuch_rb_query_set_omit_excluded, 1); /* in query.c */
+    rb_define_method (notmuch_rb_cQuery, "search_threads", notmuch_rb_query_search_threads, 0); /* in query.c */
+    rb_define_method (notmuch_rb_cQuery, "search_messages", notmuch_rb_query_search_messages, 0); /* in query.c */
+    rb_define_method (notmuch_rb_cQuery, "count_messages", notmuch_rb_query_count_messages, 0); /* in query.c */
+    rb_define_method (notmuch_rb_cQuery, "count_threads", notmuch_rb_query_count_threads, 0); /* in query.c */
+
+    /*
+     * Document-class: Notmuch::Threads
+     *
+     * Notmuch threads
+     */
+    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 */
+    rb_include_module (notmuch_rb_cThreads, rb_mEnumerable);
+
+    /*
+     * Document-class: Notmuch::Messages
+     *
+     * Notmuch messages
+     */
+    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 */
+    rb_define_method (notmuch_rb_cMessages, "tags", notmuch_rb_messages_collect_tags, 0); /* in messages.c */
+    rb_include_module (notmuch_rb_cMessages, rb_mEnumerable);
+
+    /*
+     * Document-class: Notmuch::Thread
+     *
+     * Notmuch thread
+     */
+    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 */
+    rb_define_method (notmuch_rb_cThread, "total_messages", notmuch_rb_thread_get_total_messages, 0); /* in thread.c */
+    rb_define_method (notmuch_rb_cThread, "toplevel_messages", notmuch_rb_thread_get_toplevel_messages, 0); /* in thread.c */
+    rb_define_method (notmuch_rb_cThread, "messages", notmuch_rb_thread_get_messages, 0); /* in thread.c */
+    rb_define_method (notmuch_rb_cThread, "matched_messages", notmuch_rb_thread_get_matched_messages, 0); /* in thread.c */
+    rb_define_method (notmuch_rb_cThread, "authors", notmuch_rb_thread_get_authors, 0); /* in thread.c */
+    rb_define_method (notmuch_rb_cThread, "subject", notmuch_rb_thread_get_subject, 0); /* in thread.c */
+    rb_define_method (notmuch_rb_cThread, "oldest_date", notmuch_rb_thread_get_oldest_date, 0); /* in thread.c */
+    rb_define_method (notmuch_rb_cThread, "newest_date", notmuch_rb_thread_get_newest_date, 0); /* in thread.c */
+    rb_define_method (notmuch_rb_cThread, "tags", notmuch_rb_thread_get_tags, 0); /* in thread.c */
+
+    /*
+     * Document-class: Notmuch::Message
+     *
+     * Notmuch message
+     */
+    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 */
+    rb_define_method (notmuch_rb_cMessage, "thread_id", notmuch_rb_message_get_thread_id, 0); /* in message.c */
+    rb_define_method (notmuch_rb_cMessage, "replies", notmuch_rb_message_get_replies, 0); /* in message.c */
+    rb_define_method (notmuch_rb_cMessage, "filename", notmuch_rb_message_get_filename, 0); /* in message.c */
+    rb_define_method (notmuch_rb_cMessage, "filenames", notmuch_rb_message_get_filenames, 0); /* in message.c */
+    rb_define_method (notmuch_rb_cMessage, "get_flag", notmuch_rb_message_get_flag, 1); /* in message.c */
+    rb_define_method (notmuch_rb_cMessage, "set_flag", notmuch_rb_message_set_flag, 2); /* in message.c */
+    rb_define_method (notmuch_rb_cMessage, "date", notmuch_rb_message_get_date, 0); /* in message.c */
+    rb_define_method (notmuch_rb_cMessage, "header", notmuch_rb_message_get_header, 1); /* in message.c */
+    rb_define_alias (notmuch_rb_cMessage, "[]", "header");
+    rb_define_method (notmuch_rb_cMessage, "tags", notmuch_rb_message_get_tags, 0); /* in message.c */
+    rb_define_method (notmuch_rb_cMessage, "add_tag", notmuch_rb_message_add_tag, 1); /* in message.c */
+    rb_define_alias (notmuch_rb_cMessage, "<<", "add_tag");
+    rb_define_method (notmuch_rb_cMessage, "remove_tag", notmuch_rb_message_remove_tag, 1); /* in message.c */
+    rb_define_method (notmuch_rb_cMessage, "remove_all_tags", notmuch_rb_message_remove_all_tags, 0); /* in message.c */
+    rb_define_method (notmuch_rb_cMessage, "maildir_flags_to_tags", notmuch_rb_message_maildir_flags_to_tags, 0); /* in message.c */
+    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 */
+}
diff --git a/bindings/ruby/message.c b/bindings/ruby/message.c
new file mode 100644 (file)
index 0000000..13c182f
--- /dev/null
@@ -0,0 +1,366 @@
+/* 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: MESSAGE.destroy! => nil
+ *
+ * Destroys the message, freeing all resources allocated for it.
+ */
+VALUE
+notmuch_rb_message_destroy (VALUE self)
+{
+    notmuch_rb_object_destroy (self, &notmuch_rb_message_type);
+
+    return Qnil;
+}
+
+/*
+ * call-seq: MESSAGE.message_id => String
+ *
+ * Get the message ID of 'message'.
+ */
+VALUE
+notmuch_rb_message_get_message_id (VALUE self)
+{
+    const char *msgid;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    msgid = notmuch_message_get_message_id (message);
+
+    return rb_str_new2 (msgid);
+}
+
+/*
+ * call-seq: MESSAGE.thread_id => String
+ *
+ * Get the thread ID of 'message'.
+ */
+VALUE
+notmuch_rb_message_get_thread_id (VALUE self)
+{
+    const char *tid;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    tid = notmuch_message_get_thread_id (message);
+
+    return rb_str_new2 (tid);
+}
+
+/*
+ * call-seq: MESSAGE.replies => MESSAGES
+ *
+ * Get a Notmuch::Messages enumerable for all of the replies to 'message'.
+ */
+VALUE
+notmuch_rb_message_get_replies (VALUE self)
+{
+    notmuch_messages_t *messages;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    messages = notmuch_message_get_replies (message);
+
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
+}
+
+/*
+ * call-seq: MESSAGE.filename => String
+ *
+ * Get a filename for the email corresponding to 'message'
+ */
+VALUE
+notmuch_rb_message_get_filename (VALUE self)
+{
+    const char *fname;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    fname = notmuch_message_get_filename (message);
+
+    return rb_str_new2 (fname);
+}
+
+/*
+ * call-seq: MESSAGE.filenames => FILENAMES
+ *
+ * Get all filenames for the email corresponding to MESSAGE.
+ */
+VALUE
+notmuch_rb_message_get_filenames (VALUE self)
+{
+    notmuch_filenames_t *fnames;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    fnames = notmuch_message_get_filenames (message);
+
+    return notmuch_rb_filenames_get (fnames);
+}
+
+/*
+ * call-seq: MESSAGE.get_flag (flag) => true or false
+ *
+ * Get a value of a flag for the email corresponding to 'message'
+ */
+VALUE
+notmuch_rb_message_get_flag (VALUE self, VALUE flagv)
+{
+    notmuch_message_t *message;
+    notmuch_bool_t is_set;
+    notmuch_status_t status;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    if (!FIXNUM_P (flagv))
+       rb_raise (rb_eTypeError, "Flag not a Fixnum");
+
+    status = notmuch_message_get_flag_st (message, FIX2INT (flagv), &is_set);
+    notmuch_rb_status_raise (status);
+
+    return is_set ? Qtrue : Qfalse;
+}
+
+/*
+ * call-seq: MESSAGE.set_flag (flag, value) => nil
+ *
+ * Set a value of a flag for the email corresponding to 'message'
+ */
+VALUE
+notmuch_rb_message_set_flag (VALUE self, VALUE flagv, VALUE valuev)
+{
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    if (!FIXNUM_P (flagv))
+       rb_raise (rb_eTypeError, "Flag not a Fixnum");
+
+    notmuch_message_set_flag (message, FIX2INT (flagv), RTEST (valuev));
+
+    return Qnil;
+}
+
+/*
+ * call-seq: MESSAGE.date => Fixnum
+ *
+ * Get the date of 'message'
+ */
+VALUE
+notmuch_rb_message_get_date (VALUE self)
+{
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    return UINT2NUM (notmuch_message_get_date (message));
+}
+
+/*
+ * call-seq: MESSAGE.header (name) => String
+ *
+ * Get the value of the specified header from 'message'
+ */
+VALUE
+notmuch_rb_message_get_header (VALUE self, VALUE headerv)
+{
+    const char *header, *value;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    SafeStringValue (headerv);
+    header = RSTRING_PTR (headerv);
+
+    value = notmuch_message_get_header (message, header);
+    if (!value)
+       rb_raise (notmuch_rb_eMemoryError, "Out of memory");
+
+    return rb_str_new2 (value);
+}
+
+/*
+ * call-seq: MESSAGE.tags => TAGS
+ *
+ * Get a Notmuch::Tags enumerable for all of the tags of 'message'.
+ */
+VALUE
+notmuch_rb_message_get_tags (VALUE self)
+{
+    notmuch_message_t *message;
+    notmuch_tags_t *tags;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    tags = notmuch_message_get_tags (message);
+    if (!tags)
+       rb_raise (notmuch_rb_eMemoryError, "Out of memory");
+
+    return notmuch_rb_tags_get (tags);
+}
+
+/*
+ * call-seq: MESSAGE.add_tag (tag) => true
+ *
+ * Add a tag to the 'message'
+ */
+VALUE
+notmuch_rb_message_add_tag (VALUE self, VALUE tagv)
+{
+    const char *tag;
+    notmuch_status_t ret;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    SafeStringValue (tagv);
+    tag = RSTRING_PTR (tagv);
+
+    ret = notmuch_message_add_tag (message, tag);
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
+
+/*
+ * call-seq: MESSAGE.remove_tag (tag) => true
+ *
+ * Remove a tag from the 'message'
+ */
+VALUE
+notmuch_rb_message_remove_tag (VALUE self, VALUE tagv)
+{
+    const char *tag;
+    notmuch_status_t ret;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    SafeStringValue (tagv);
+    tag = RSTRING_PTR (tagv);
+
+    ret = notmuch_message_remove_tag (message, tag);
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
+
+/*
+ * call-seq: MESSAGE.remove_all_tags => true
+ *
+ * Remove all tags of the 'message'
+ */
+VALUE
+notmuch_rb_message_remove_all_tags (VALUE self)
+{
+    notmuch_status_t ret;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    ret = notmuch_message_remove_all_tags (message);
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
+
+/*
+ * call-seq: MESSAGE.maildir_flags_to_tags => true
+ *
+ * Add/remove tags according to maildir flags in the message filename (s)
+ */
+VALUE
+notmuch_rb_message_maildir_flags_to_tags (VALUE self)
+{
+    notmuch_status_t ret;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    ret = notmuch_message_maildir_flags_to_tags (message);
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
+
+/*
+ * call-seq: MESSAGE.tags_to_maildir_flags => true
+ *
+ * Rename message filename (s) to encode tags as maildir flags
+ */
+VALUE
+notmuch_rb_message_tags_to_maildir_flags (VALUE self)
+{
+    notmuch_status_t ret;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    ret = notmuch_message_tags_to_maildir_flags (message);
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
+
+/*
+ * call-seq: MESSAGE.freeze => true
+ *
+ * Freeze the 'message'
+ */
+VALUE
+notmuch_rb_message_freeze (VALUE self)
+{
+    notmuch_status_t ret;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    ret = notmuch_message_freeze (message);
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
+
+/*
+ * call-seq: MESSAGE.thaw => true
+ *
+ * Thaw a 'message'
+ */
+VALUE
+notmuch_rb_message_thaw (VALUE self)
+{
+    notmuch_status_t ret;
+    notmuch_message_t *message;
+
+    Data_Get_Notmuch_Message (self, message);
+
+    ret = notmuch_message_thaw (message);
+    notmuch_rb_status_raise (ret);
+
+    return Qtrue;
+}
diff --git a/bindings/ruby/messages.c b/bindings/ruby/messages.c
new file mode 100644 (file)
index 0000000..6369d05
--- /dev/null
@@ -0,0 +1,75 @@
+/* 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: MESSAGES.destroy! => nil
+ *
+ * Destroys the messages, freeing all resources allocated for it.
+ */
+VALUE
+notmuch_rb_messages_destroy (VALUE self)
+{
+    notmuch_rb_object_destroy (self, &notmuch_rb_messages_type);
+
+    return Qnil;
+}
+
+/* call-seq: MESSAGES.each {|item| block } => MESSAGES
+ *
+ * Calls +block+ once for each message in +self+, passing that element as a
+ * parameter.
+ */
+VALUE
+notmuch_rb_messages_each (VALUE self)
+{
+    notmuch_message_t *message;
+    notmuch_messages_t *messages;
+
+    Data_Get_Notmuch_Messages (self, messages);
+
+    for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) {
+       message = notmuch_messages_get (messages);
+       rb_yield (Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message));
+    }
+
+    return self;
+}
+
+/*
+ * call-seq: MESSAGES.tags => TAGS
+ *
+ * Collect tags from the messages
+ */
+VALUE
+notmuch_rb_messages_collect_tags (VALUE self)
+{
+    notmuch_tags_t *tags;
+    notmuch_messages_t *messages;
+
+    Data_Get_Notmuch_Messages (self, messages);
+
+    tags = notmuch_messages_collect_tags (messages);
+    if (!tags)
+       rb_raise (notmuch_rb_eMemoryError, "Out of memory");
+
+    return notmuch_rb_tags_get (tags);
+}
diff --git a/bindings/ruby/query.c b/bindings/ruby/query.c
new file mode 100644 (file)
index 0000000..077def0
--- /dev/null
@@ -0,0 +1,206 @@
+/* The Ruby interface to the notmuch mail library
+ *
+ * Copyright © 2010, 2011, 2012 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: QUERY.destroy! => nil
+ *
+ * Destroys the query, freeing all resources allocated for it.
+ */
+VALUE
+notmuch_rb_query_destroy (VALUE self)
+{
+    notmuch_rb_object_destroy (self, &notmuch_rb_query_type);
+
+    return Qnil;
+}
+
+/*
+ * call-seq: QUERY.sort => fixnum
+ *
+ * Get sort type of the +QUERY+
+ */
+VALUE
+notmuch_rb_query_get_sort (VALUE self)
+{
+    notmuch_query_t *query;
+
+    Data_Get_Notmuch_Query (self, query);
+
+    return INT2FIX (notmuch_query_get_sort (query));
+}
+
+/*
+ * call-seq: QUERY.sort=(fixnum) => nil
+ *
+ * Set sort type of the +QUERY+
+ */
+VALUE
+notmuch_rb_query_set_sort (VALUE self, VALUE sortv)
+{
+    notmuch_query_t *query;
+
+    Data_Get_Notmuch_Query (self, query);
+
+    if (!FIXNUM_P (sortv))
+       rb_raise (rb_eTypeError, "Not a Fixnum");
+
+    notmuch_query_set_sort (query, FIX2UINT (sortv));
+
+    return Qnil;
+}
+
+/*
+ * call-seq: QUERY.to_s => string
+ *
+ * Get query string of the +QUERY+
+ */
+VALUE
+notmuch_rb_query_get_string (VALUE self)
+{
+    notmuch_query_t *query;
+
+    Data_Get_Notmuch_Query (self, query);
+
+    return rb_str_new2 (notmuch_query_get_query_string (query));
+}
+
+/*
+ * call-seq: QUERY.add_tag_exclude(tag) => nil
+ *
+ * Add a tag that will be excluded from the query results by default.
+ */
+VALUE
+notmuch_rb_query_add_tag_exclude (VALUE self, VALUE tagv)
+{
+    notmuch_query_t *query;
+    const char *tag;
+
+    Data_Get_Notmuch_Query (self, query);
+    tag = RSTRING_PTR(tagv);
+
+    notmuch_query_add_tag_exclude(query, tag);
+    return Qnil;
+}
+
+/*
+ * call-seq: QUERY.omit_excluded=(fixnum) => nil
+ *
+ * Specify whether to omit excluded results or simply flag them.
+ * 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);
+
+    value = FIXNUM_P (omitv) ? FIX2UINT (omitv) : RTEST(omitv);
+    notmuch_query_set_omit_excluded (query, value);
+
+    return Qnil;
+}
+
+/*
+ * call-seq: QUERY.search_threads => THREADS
+ *
+ * Search for threads
+ */
+VALUE
+notmuch_rb_query_search_threads (VALUE self)
+{
+    notmuch_query_t *query;
+    notmuch_threads_t *threads;
+    notmuch_status_t status;
+
+    Data_Get_Notmuch_Query (self, query);
+
+    status = notmuch_query_search_threads (query, &threads);
+    if (status)
+       notmuch_rb_status_raise (status);
+
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cThreads, &notmuch_rb_threads_type, threads);
+}
+
+/*
+ * call-seq: QUERY.search_messages => MESSAGES
+ *
+ * Search for messages
+ */
+VALUE
+notmuch_rb_query_search_messages (VALUE self)
+{
+    notmuch_query_t *query;
+    notmuch_messages_t *messages;
+    notmuch_status_t status;
+
+    Data_Get_Notmuch_Query (self, query);
+
+    status = notmuch_query_search_messages (query, &messages);
+    if (status)
+       notmuch_rb_status_raise (status);
+
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
+}
+
+/*
+ * call-seq: QUERY.count_messages => Fixnum
+ *
+ * Return an estimate of the number of messages matching a search
+ */
+VALUE
+notmuch_rb_query_count_messages (VALUE self)
+{
+    notmuch_query_t *query;
+    notmuch_status_t status;
+    unsigned int count;
+
+    Data_Get_Notmuch_Query (self, query);
+
+    status = notmuch_query_count_messages (query, &count);
+    if (status)
+       notmuch_rb_status_raise (status);
+
+    return UINT2NUM(count);
+}
+
+/*
+ * call-seq: QUERY.count_threads => Fixnum
+ *
+ * Return an estimate of the number of threads matching a search
+ */
+VALUE
+notmuch_rb_query_count_threads (VALUE self)
+{
+    notmuch_query_t *query;
+    notmuch_status_t status;
+    unsigned int count;
+
+    Data_Get_Notmuch_Query (self, query);
+
+    status = notmuch_query_count_threads (query, &count);
+    if (status)
+       notmuch_rb_status_raise (status);
+
+    return UINT2NUM(count);
+}
diff --git a/bindings/ruby/rdoc.sh b/bindings/ruby/rdoc.sh
new file mode 100755 (executable)
index 0000000..1e867ff
--- /dev/null
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+if test -z "$RDOC"; then
+    RDOC=rdoc
+    if which rdoc19 >/dev/null 2>&1; then
+        RDOC=rdoc19
+    fi
+fi
+
+set -e
+set -x
+
+$RDOC --main 'Notmuch' --title 'Notmuch Ruby API' --op ruby *.c
+
+if test "$1" = "--upload"; then
+    rsync -avze ssh --delete --partial --progress ruby bach.exherbo.org:public_html/notmuch/
+fi
diff --git a/bindings/ruby/status.c b/bindings/ruby/status.c
new file mode 100644 (file)
index 0000000..a0f8863
--- /dev/null
@@ -0,0 +1,51 @@
+/* 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"
+
+void
+notmuch_rb_status_raise (notmuch_status_t status)
+{
+    switch (status) {
+    case NOTMUCH_STATUS_SUCCESS:
+    case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+       break;
+    case NOTMUCH_STATUS_OUT_OF_MEMORY:
+       rb_raise (notmuch_rb_eMemoryError, "out of memory");
+    case NOTMUCH_STATUS_READ_ONLY_DATABASE:
+       rb_raise (notmuch_rb_eReadOnlyError, "read-only database");
+    case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
+       rb_raise (notmuch_rb_eXapianError, "xapian exception");
+    case NOTMUCH_STATUS_FILE_ERROR:
+       rb_raise (notmuch_rb_eFileError, "failed to read/write file");
+    case NOTMUCH_STATUS_FILE_NOT_EMAIL:
+       rb_raise (notmuch_rb_eFileNotEmailError, "file not email");
+    case NOTMUCH_STATUS_NULL_POINTER:
+       rb_raise (notmuch_rb_eNullPointerError, "null pointer");
+    case NOTMUCH_STATUS_TAG_TOO_LONG:
+       rb_raise (notmuch_rb_eTagTooLongError, "tag too long");
+    case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
+       rb_raise (notmuch_rb_eUnbalancedFreezeThawError, "unbalanced freeze/thaw");
+    case NOTMUCH_STATUS_UNBALANCED_ATOMIC:
+       rb_raise (notmuch_rb_eUnbalancedAtomicError, "unbalanced atomic");
+    default:
+       rb_raise (notmuch_rb_eBaseError, "unknown notmuch error");
+    }
+}
diff --git a/bindings/ruby/tags.c b/bindings/ruby/tags.c
new file mode 100644 (file)
index 0000000..b64874d
--- /dev/null
@@ -0,0 +1,13 @@
+#include "defs.h"
+
+VALUE
+notmuch_rb_tags_get (notmuch_tags_t *tags)
+{
+    VALUE rb_array = rb_ary_new ();
+
+    for (; notmuch_tags_valid (tags); notmuch_tags_move_to_next (tags)) {
+       const char *tag = notmuch_tags_get (tags);
+       rb_ary_push (rb_array, rb_str_new2 (tag));
+    }
+    return rb_array;
+}
diff --git a/bindings/ruby/thread.c b/bindings/ruby/thread.c
new file mode 100644 (file)
index 0000000..b20ed89
--- /dev/null
@@ -0,0 +1,208 @@
+/* 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: THREAD.destroy! => nil
+ *
+ * Destroys the thread, freeing all resources allocated for it.
+ */
+VALUE
+notmuch_rb_thread_destroy (VALUE self)
+{
+    notmuch_rb_object_destroy (self, &notmuch_rb_thread_type);
+
+    return Qnil;
+}
+
+/*
+ * call-seq: THREAD.thread_id => String
+ *
+ * Returns the thread id
+ */
+VALUE
+notmuch_rb_thread_get_thread_id (VALUE self)
+{
+    const char *tid;
+    notmuch_thread_t *thread;
+
+    Data_Get_Notmuch_Thread (self, thread);
+
+    tid = notmuch_thread_get_thread_id (thread);
+
+    return rb_str_new2 (tid);
+}
+
+/*
+ * call-seq: THREAD.total_messages => fixnum
+ *
+ * Returns the number of total messages
+ */
+VALUE
+notmuch_rb_thread_get_total_messages (VALUE self)
+{
+    notmuch_thread_t *thread;
+
+    Data_Get_Notmuch_Thread (self, thread);
+
+    return INT2FIX (notmuch_thread_get_total_messages (thread));
+}
+
+/*
+ * call-seq: THREAD.toplevel_messages => MESSAGES
+ *
+ * Get a Notmuch::Messages iterator for the top level messages in thread.
+ */
+VALUE
+notmuch_rb_thread_get_toplevel_messages (VALUE self)
+{
+    notmuch_messages_t *messages;
+    notmuch_thread_t *thread;
+
+    Data_Get_Notmuch_Thread (self, thread);
+
+    messages = notmuch_thread_get_toplevel_messages (thread);
+    if (!messages)
+       rb_raise (notmuch_rb_eMemoryError, "Out of memory");
+
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
+}
+
+/*
+ * call-seq: THREAD.messages => MESSAGES
+ *
+ * Get a Notmuch::Messages iterator for the all messages in thread.
+ */
+VALUE
+notmuch_rb_thread_get_messages (VALUE self)
+{
+    notmuch_messages_t *messages;
+    notmuch_thread_t *thread;
+
+    Data_Get_Notmuch_Thread (self, thread);
+
+    messages = notmuch_thread_get_messages (thread);
+    if (!messages)
+       rb_raise (notmuch_rb_eMemoryError, "Out of memory");
+
+    return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
+}
+
+/*
+ * call-seq: THREAD.matched_messages => fixnum
+ *
+ * Get the number of messages in thread that matched the search
+ */
+VALUE
+notmuch_rb_thread_get_matched_messages (VALUE self)
+{
+    notmuch_thread_t *thread;
+
+    Data_Get_Notmuch_Thread (self, thread);
+
+    return INT2FIX (notmuch_thread_get_matched_messages (thread));
+}
+
+/*
+ * call-seq: THREAD.authors => String
+ *
+ * Get a comma-separated list of the names of the authors.
+ */
+VALUE
+notmuch_rb_thread_get_authors (VALUE self)
+{
+    const char *authors;
+    notmuch_thread_t *thread;
+
+    Data_Get_Notmuch_Thread (self, thread);
+
+    authors = notmuch_thread_get_authors (thread);
+
+    return rb_str_new2 (authors);
+}
+
+/*
+ * call-seq: THREAD.subject => String
+ *
+ * Returns the subject of the thread
+ */
+VALUE
+notmuch_rb_thread_get_subject (VALUE self)
+{
+    const char *subject;
+    notmuch_thread_t *thread;
+
+    Data_Get_Notmuch_Thread (self, thread);
+
+    subject = notmuch_thread_get_subject (thread);
+
+    return rb_str_new2 (subject);
+}
+
+/*
+ * call-seq: THREAD.oldest_date => Fixnum
+ *
+ * Get the date of the oldest message in thread.
+ */
+VALUE
+notmuch_rb_thread_get_oldest_date (VALUE self)
+{
+    notmuch_thread_t *thread;
+
+    Data_Get_Notmuch_Thread (self, thread);
+
+    return UINT2NUM (notmuch_thread_get_oldest_date (thread));
+}
+
+/*
+ * call-seq: THREAD.newest_date => fixnum
+ *
+ * Get the date of the newest message in thread.
+ */
+VALUE
+notmuch_rb_thread_get_newest_date (VALUE self)
+{
+    notmuch_thread_t *thread;
+
+    Data_Get_Notmuch_Thread (self, thread);
+
+    return UINT2NUM (notmuch_thread_get_newest_date (thread));
+}
+
+/*
+ * call-seq: THREAD.tags => TAGS
+ *
+ * Get a Notmuch::Tags iterator for the tags of the thread
+ */
+VALUE
+notmuch_rb_thread_get_tags (VALUE self)
+{
+    notmuch_thread_t *thread;
+    notmuch_tags_t *tags;
+
+    Data_Get_Notmuch_Thread (self, thread);
+
+    tags = notmuch_thread_get_tags (thread);
+    if (!tags)
+       rb_raise (notmuch_rb_eMemoryError, "Out of memory");
+
+    return notmuch_rb_tags_get (tags);
+}
diff --git a/bindings/ruby/threads.c b/bindings/ruby/threads.c
new file mode 100644 (file)
index 0000000..5028026
--- /dev/null
@@ -0,0 +1,55 @@
+/* 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: THREADS.destroy! => nil
+ *
+ * Destroys the threads, freeing all resources allocated for it.
+ */
+VALUE
+notmuch_rb_threads_destroy (VALUE self)
+{
+    notmuch_rb_object_destroy (self, &notmuch_rb_threads_type);
+
+    return Qnil;
+}
+
+/* call-seq: THREADS.each {|item| block } => THREADS
+ *
+ * Calls +block+ once for each thread in +self+, passing that element as a
+ * parameter.
+ */
+VALUE
+notmuch_rb_threads_each (VALUE self)
+{
+    notmuch_thread_t *thread;
+    notmuch_threads_t *threads;
+
+    Data_Get_Notmuch_Threads (self, threads);
+
+    for (; notmuch_threads_valid (threads); notmuch_threads_move_to_next (threads)) {
+       thread = notmuch_threads_get (threads);
+       rb_yield (Data_Wrap_Notmuch_Object (notmuch_rb_cThread, &notmuch_rb_thread_type, thread));
+    }
+
+    return self;
+}
diff --git a/command-line-arguments.c b/command-line-arguments.c
new file mode 100644 (file)
index 0000000..5dea828
--- /dev/null
@@ -0,0 +1,320 @@
+#include <assert.h>
+#include <string.h>
+#include <stdio.h>
+#include "error_util.h"
+#include "command-line-arguments.h"
+
+typedef enum {
+    OPT_FAILED,         /* false */
+    OPT_OK,             /* good */
+    OPT_GIVEBACK,       /* pop one of the arguments you thought you were getting off the stack */
+} opt_handled;
+
+/*
+ * Search the array of keywords for a given argument, assigning the
+ * output variable to the corresponding value.  Return false if nothing
+ * matches.
+ */
+
+static opt_handled
+_process_keyword_arg (const notmuch_opt_desc_t *arg_desc, char next,
+                     const char *arg_str, bool negate)
+{
+    const notmuch_keyword_t *keywords;
+
+    if (next == '\0') {
+       /* No keyword given */
+       arg_str = "";
+    }
+
+    for (keywords = arg_desc->keywords; keywords->name; keywords++) {
+       if (strcmp (arg_str, keywords->name) != 0)
+           continue;
+
+       if (arg_desc->opt_flags && negate)
+           *arg_desc->opt_flags &= ~keywords->value;
+       else if (arg_desc->opt_flags)
+           *arg_desc->opt_flags |= keywords->value;
+       else
+           *arg_desc->opt_keyword = keywords->value;
+
+       return OPT_OK;
+    }
+
+    if (arg_desc->opt_keyword && arg_desc->keyword_no_arg_value && next != ':' && next != '=') {
+       for (keywords = arg_desc->keywords; keywords->name; keywords++) {
+           if (strcmp (arg_desc->keyword_no_arg_value, keywords->name) != 0)
+               continue;
+
+           *arg_desc->opt_keyword = keywords->value;
+           fprintf (stderr,
+                    "Warning: No known keyword option given for \"%s\", choosing value \"%s\"."
+                    "  Please specify the argument explicitly!\n", arg_desc->name,
+                    arg_desc->keyword_no_arg_value);
+
+           return OPT_GIVEBACK;
+       }
+       fprintf (stderr,
+                "No matching keyword for option \"%s\" and default value \"%s\" is invalid.\n",
+                arg_str,
+                arg_desc->name);
+       return OPT_FAILED;
+    }
+
+    if (next != '\0')
+       fprintf (stderr, "Unknown keyword argument \"%s\" for option \"%s\".\n", arg_str,
+                arg_desc->name);
+    else
+       fprintf (stderr, "Option \"%s\" needs a keyword argument.\n", arg_desc->name);
+    return OPT_FAILED;
+}
+
+static opt_handled
+_process_boolean_arg (const notmuch_opt_desc_t *arg_desc, char next,
+                     const char *arg_str, bool negate)
+{
+    bool value;
+
+    if (next == '\0' || strcmp (arg_str, "true") == 0) {
+       value = true;
+    } else if (strcmp (arg_str, "false") == 0) {
+       value = false;
+    } else {
+       fprintf (stderr, "Unknown argument \"%s\" for (boolean) option \"%s\".\n", arg_str,
+                arg_desc->name);
+       return OPT_FAILED;
+    }
+
+    *arg_desc->opt_bool = negate ? (! value) : value;
+
+    return OPT_OK;
+}
+
+static opt_handled
+_process_int_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str)
+{
+
+    char *endptr;
+
+    if (next == '\0' || arg_str[0] == '\0') {
+       fprintf (stderr, "Option \"%s\" needs an integer argument.\n", arg_desc->name);
+       return OPT_FAILED;
+    }
+
+    *arg_desc->opt_int = strtol (arg_str, &endptr, 10);
+    if (*endptr == '\0')
+       return OPT_OK;
+
+    fprintf (stderr, "Unable to parse argument \"%s\" for option \"%s\" as an integer.\n",
+            arg_str, arg_desc->name);
+    return OPT_FAILED;
+}
+
+static opt_handled
+_process_string_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str)
+{
+
+    if (next == '\0') {
+       fprintf (stderr, "Option \"%s\" needs a string argument.\n", arg_desc->name);
+       return OPT_FAILED;
+    }
+    if (arg_str[0] == '\0' && ! arg_desc->allow_empty) {
+       fprintf (stderr, "String argument for option \"%s\" must be non-empty.\n", arg_desc->name);
+       return OPT_FAILED;
+    }
+    *arg_desc->opt_string = arg_str;
+    return OPT_OK;
+}
+
+/* Return number of non-NULL opt_* fields in opt_desc. */
+static int
+_opt_set_count (const notmuch_opt_desc_t *opt_desc)
+{
+    return
+       (bool) opt_desc->opt_inherit +
+       (bool) opt_desc->opt_bool +
+       (bool) opt_desc->opt_int +
+       (bool) opt_desc->opt_keyword +
+       (bool) opt_desc->opt_flags +
+       (bool) opt_desc->opt_string +
+       (bool) opt_desc->opt_position;
+}
+
+/* Return true if opt_desc is valid. */
+static bool
+_opt_valid (const notmuch_opt_desc_t *opt_desc)
+{
+    int n = _opt_set_count (opt_desc);
+
+    if (n > 1)
+       INTERNAL_ERROR ("more than one non-NULL opt_* field for argument \"%s\"",
+                       opt_desc->name);
+
+    return n > 0;
+}
+
+/*
+ * Search for the {pos_arg_index}th position argument, return false if
+ * that does not exist.
+ */
+
+bool
+parse_position_arg (const char *arg_str, int pos_arg_index,
+                   const notmuch_opt_desc_t *arg_desc)
+{
+
+    int pos_arg_counter = 0;
+
+    while (_opt_valid (arg_desc)) {
+       if (arg_desc->opt_position) {
+           if (pos_arg_counter == pos_arg_index) {
+               *arg_desc->opt_position = arg_str;
+               if (arg_desc->present)
+                   *arg_desc->present = true;
+               return true;
+           }
+           pos_arg_counter++;
+       }
+       arg_desc++;
+    }
+    return false;
+}
+
+#define NEGATIVE_PREFIX "no-"
+
+/*
+ * Search for a non-positional (i.e. starting with --) argument matching arg,
+ * parse a possible value, and assign to *output_var
+ */
+
+int
+parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index)
+{
+    assert (argv);
+
+    const char *_arg = argv[opt_index];
+
+    assert (_arg);
+    assert (options);
+
+    const char *arg = _arg + 2; /* _arg starts with -- */
+    const char *negative_arg = NULL;
+
+    /* See if this is a --no-argument */
+    if (strlen (arg) > strlen (NEGATIVE_PREFIX) &&
+       strncmp (arg, NEGATIVE_PREFIX, strlen (NEGATIVE_PREFIX)) == 0) {
+       negative_arg = arg + strlen (NEGATIVE_PREFIX);
+    }
+
+    const notmuch_opt_desc_t *try;
+
+    const char *next_arg = NULL;
+
+    if (opt_index < argc - 1  && strncmp (argv[opt_index + 1], "--", 2) != 0)
+       next_arg = argv[opt_index + 1];
+
+    for (try = options; _opt_valid (try); try++) {
+       if (try->opt_inherit) {
+           int new_index = parse_option (argc, argv, try->opt_inherit, opt_index);
+           if (new_index >= 0)
+               return new_index;
+       }
+
+       if (! try->name)
+           continue;
+
+       char next;
+       const char *value;
+       bool negate = false;
+
+       if (strncmp (arg, try->name, strlen (try->name)) == 0) {
+           next = arg[strlen (try->name)];
+           value = arg + strlen (try->name) + 1;
+       } else if (negative_arg && (try->opt_bool || try->opt_flags) &&
+                  strncmp (negative_arg, try->name, strlen (try->name)) == 0) {
+           next = negative_arg[strlen (try->name)];
+           value = negative_arg + strlen (try->name) + 1;
+           /* The argument part of --no-argument matches, negate the result. */
+           negate = true;
+       } else {
+           continue;
+       }
+
+       /*
+        * If we have not reached the end of the argument (i.e. the
+        * next character is not a space or delimiter) then the
+        * argument could still match a longer option name later in
+        * the option table.
+        */
+       if (next != '=' && next != ':' && next != '\0')
+           continue;
+
+       bool lookahead = (next == '\0' && next_arg != NULL && ! try->opt_bool);
+
+       if (lookahead) {
+           next = ' ';
+           value = next_arg;
+           opt_index++;
+       }
+
+       opt_handled opt_status = OPT_FAILED;
+       if (try->opt_keyword || try->opt_flags)
+           opt_status = _process_keyword_arg (try, next, value, negate);
+       else if (try->opt_bool)
+           opt_status = _process_boolean_arg (try, next, value, negate);
+       else if (try->opt_int)
+           opt_status = _process_int_arg (try, next, value);
+       else if (try->opt_string)
+           opt_status = _process_string_arg (try, next, value);
+       else
+           INTERNAL_ERROR ("unknown or unhandled option \"%s\"", try->name);
+
+       if (opt_status == OPT_FAILED)
+           return -1;
+
+       if (lookahead && opt_status == OPT_GIVEBACK)
+           opt_index--;
+
+       if (try->present)
+           *try->present = true;
+
+       return opt_index + 1;
+    }
+    return -1;
+}
+
+/* See command-line-arguments.h for description */
+int
+parse_arguments (int argc, char **argv,
+                const notmuch_opt_desc_t *options, int opt_index)
+{
+
+    int pos_arg_index = 0;
+    bool more_args = true;
+
+    while (more_args && opt_index < argc) {
+       if (strncmp (argv[opt_index], "--", 2) != 0) {
+
+           more_args = parse_position_arg (argv[opt_index], pos_arg_index, options);
+
+           if (more_args) {
+               pos_arg_index++;
+               opt_index++;
+           }
+
+       } else {
+           int prev_opt_index = opt_index;
+
+           if (strlen (argv[opt_index]) == 2)
+               return opt_index + 1;
+
+           opt_index = parse_option (argc, argv, options, opt_index);
+           if (opt_index < 0) {
+               fprintf (stderr, "Unrecognized option: %s\n", argv[prev_opt_index]);
+               more_args = false;
+           }
+       }
+    }
+
+    return opt_index;
+}
diff --git a/command-line-arguments.h b/command-line-arguments.h
new file mode 100644 (file)
index 0000000..606e5cd
--- /dev/null
@@ -0,0 +1,82 @@
+#ifndef NOTMUCH_OPTS_H
+#define NOTMUCH_OPTS_H
+
+#include <stdbool.h>
+
+#include "notmuch.h"
+
+/*
+ * Describe one of the possibilities for a keyword option
+ * 'value' will be copied to the output variable
+ */
+
+typedef struct notmuch_keyword {
+    const char *name;
+    int value;
+} notmuch_keyword_t;
+
+/* Describe one option. */
+typedef struct notmuch_opt_desc {
+    /* One and only one of opt_* must be set. */
+    const struct notmuch_opt_desc *opt_inherit;
+    bool *opt_bool;
+    int *opt_int;
+    int *opt_keyword;
+    int *opt_flags;
+    const char **opt_string;
+    const char **opt_position;
+
+    /* for opt_keyword only: if no matching arguments were found, and
+     * keyword_no_arg_value is set, then use keyword_no_arg_value instead. */
+    const char *keyword_no_arg_value;
+
+    /* Must be set except for opt_inherit and opt_position. */
+    const char *name;
+
+    /* Optional, if non-NULL, set to true if the option is present. */
+    bool *present;
+
+    /* Optional, allow empty strings for opt_string. */
+    bool allow_empty;
+
+    /* Must be set for opt_keyword and opt_flags. */
+    const struct notmuch_keyword *keywords;
+} notmuch_opt_desc_t;
+
+
+/*
+ * This is the main entry point for command line argument parsing.
+ *
+ * Parse command line arguments according to structure options,
+ * starting at position opt_index.
+ *
+ * All output of parsed values is via pointers in options.
+ *
+ * Parsing stops at -- (consumed) or at the (k+1)st argument
+ * not starting with -- (a "positional argument") if options contains
+ * k positional argument descriptors.
+ *
+ * Returns the index of first non-parsed argument, or -1 in case of error.
+ *
+ */
+int
+parse_arguments (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index);
+
+/*
+ * If the argument parsing loop provided by parse_arguments is not
+ * flexible enough, then the user might be interested in the following
+ * routines, but note that the API to parse_option might have to
+ * change. See command-line-arguments.c for descriptions of these
+ * functions.
+ */
+
+int
+parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index);
+
+bool
+parse_position_arg (const char *arg,
+                   int position_arg_index,
+                   const notmuch_opt_desc_t *options);
+
+
+#endif
diff --git a/compat/.gitignore b/compat/.gitignore
new file mode 100644 (file)
index 0000000..7ede45e
--- /dev/null
@@ -0,0 +1 @@
+/zlib.pc
diff --git a/compat/Makefile b/compat/Makefile
new file mode 100644 (file)
index 0000000..fa25832
--- /dev/null
@@ -0,0 +1,5 @@
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/compat/Makefile.local b/compat/Makefile.local
new file mode 100644 (file)
index 0000000..c58ca74
--- /dev/null
@@ -0,0 +1,24 @@
+# -*- makefile-gmake -*-
+
+dir := compat
+extra_cflags += -I$(srcdir)/$(dir)
+
+notmuch_compat_srcs :=
+
+ifneq ($(HAVE_GETLINE),1)
+notmuch_compat_srcs += $(dir)/getline.c $(dir)/getdelim.c
+endif
+
+ifneq ($(HAVE_STRCASESTR),1)
+notmuch_compat_srcs += $(dir)/strcasestr.c
+endif
+
+ifneq ($(HAVE_STRSEP),1)
+notmuch_compat_srcs += $(dir)/strsep.c
+endif
+
+ifneq ($(HAVE_TIMEGM),1)
+notmuch_compat_srcs += $(dir)/timegm.c
+endif
+
+SRCS := $(SRCS) $(notmuch_compat_srcs)
diff --git a/compat/README b/compat/README
new file mode 100644 (file)
index 0000000..12aacf4
--- /dev/null
@@ -0,0 +1,21 @@
+notmuch/compat
+
+This directory consists of three things:
+
+1. Small programs used by the notmuch configure script to test for the
+   availability of certain system features, (library functions, etc.).
+
+   For example: have_getline.c
+
+2. Compatibility implementations of those system features for systems
+   that don't provide their own versions.
+
+   For example: getline.c
+
+   The compilation of these files is made conditional on the output of
+   the test programs from [1].
+
+3. Macro definitions abstracting compiler differences (e.g. function
+   attributes).
+
+   For example: function-attributes.h
diff --git a/compat/check_asctime.c b/compat/check_asctime.c
new file mode 100644 (file)
index 0000000..62ad69d
--- /dev/null
@@ -0,0 +1,12 @@
+#include <time.h>
+#include <stdio.h>
+
+int
+main ()
+{
+    struct tm tm;
+
+    (void) asctime_r (&tm, NULL);
+
+    return (0);
+}
diff --git a/compat/check_getpwuid.c b/compat/check_getpwuid.c
new file mode 100644 (file)
index 0000000..babeb74
--- /dev/null
@@ -0,0 +1,12 @@
+#include <stdio.h>
+#include <pwd.h>
+
+int
+main ()
+{
+    struct passwd passwd, *ignored;
+
+    (void) getpwuid_r (0, &passwd, NULL, 0, &ignored);
+
+    return (0);
+}
diff --git a/compat/compat.h b/compat/compat.h
new file mode 100644 (file)
index 0000000..59e9161
--- /dev/null
@@ -0,0 +1,77 @@
+/* notmuch - Not much of an email library, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+/* This header file defines functions that will only be conditionally
+ * compiled for compatibility on systems that don't provide their own
+ * implementations of the functions.
+ */
+
+#ifndef NOTMUCH_COMPAT_H
+#define NOTMUCH_COMPAT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if ! STD_GETPWUID
+#define _POSIX_PTHREAD_SEMANTICS 1
+#endif
+#if ! STD_ASCTIME
+#define _POSIX_PTHREAD_SEMANTICS 1
+#endif
+
+#if ! HAVE_GETLINE
+#include <stdio.h>
+#include <unistd.h>
+
+ssize_t
+getline (char **lineptr, size_t *n, FILE *stream);
+
+ssize_t
+getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp);
+
+#endif  /* !HAVE_GETLINE */
+
+#if ! HAVE_STRCASESTR
+char *strcasestr (const char *haystack, const char *needle);
+#endif  /* !HAVE_STRCASESTR */
+
+#if ! HAVE_STRSEP
+char *strsep (char **stringp, const char *delim);
+#endif  /* !HAVE_STRSEP */
+
+#if ! HAVE_TIMEGM
+#include <time.h>
+time_t timegm (struct tm *tm);
+#endif  /* !HAVE_TIMEGM */
+
+/* Silence gcc warnings about unused results.  These warnings exist
+ * for a reason; any use of this needs to be justified. */
+#ifdef __GNUC__
+#define IGNORE_RESULT(x) ({ __typeof__(x) __z = (x); (void) (__z = __z); })
+#else /* !__GNUC__ */
+#define IGNORE_RESULT(x) x
+#endif  /* __GNUC__ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  /* NOTMUCH_COMPAT_H */
diff --git a/compat/function-attributes.h b/compat/function-attributes.h
new file mode 100644 (file)
index 0000000..8f08bec
--- /dev/null
@@ -0,0 +1,47 @@
+/* function-attributes.h - Provides compiler abstractions for
+ *                         function attributes
+ *
+ * Copyright (c) 2012 Justus Winter <4winter@informatik.uni-hamburg.de>
+ *
+ * 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/ .
+ */
+
+#ifndef FUNCTION_ATTRIBUTES_H
+#define FUNCTION_ATTRIBUTES_H
+
+/* clang provides this macro to test for support for function
+ * attributes. If it isn't defined, this provides a compatibility
+ * macro for other compilers.
+ */
+#ifndef __has_attribute
+#define __has_attribute(x) 0
+#endif
+
+/* Provide a NORETURN_ATTRIBUTE macro similar to PRINTF_ATTRIBUTE from
+ * talloc.
+ *
+ * This attribute is understood by gcc since version 2.5. clang
+ * provides support for testing for function attributes.
+ */
+#ifndef NORETURN_ATTRIBUTE
+#if (__GNUC__ >= 3 ||                           \
+     (__GNUC__ == 2 && __GNUC_MINOR__ >= 5) ||  \
+    __has_attribute (noreturn))
+#define NORETURN_ATTRIBUTE __attribute__ ((noreturn))
+#else
+#define NORETURN_ATTRIBUTE
+#endif
+#endif
+
+#endif
diff --git a/compat/gen_zlib_pc.c b/compat/gen_zlib_pc.c
new file mode 100644 (file)
index 0000000..7c0ee72
--- /dev/null
@@ -0,0 +1,19 @@
+#include <stdio.h>
+#include <zlib.h>
+
+static const char *template =
+    "prefix=/usr\n"
+    "exec_prefix=${prefix}\n"
+    "libdir=${exec_prefix}/lib\n"
+    "\n"
+    "Name: zlib\n"
+    "Description: zlib compression library\n"
+    "Version: %s\n"
+    "Libs: -lz\n";
+
+int
+main (void)
+{
+    printf (template, ZLIB_VERSION);
+    return 0;
+}
diff --git a/compat/getdelim.c b/compat/getdelim.c
new file mode 100644 (file)
index 0000000..e5c1f07
--- /dev/null
@@ -0,0 +1,125 @@
+/* getdelim.c --- Implementation of replacement getdelim function.
+ * Copyright (C) 1994, 1996, 1997, 1998, 2001, 2003, 2005, 2006, 2007,
+ * 2008, 2009 Free Software Foundation, Inc.
+ *
+ * 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, 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.  */
+
+/* Ported from glibc by Simon Josefsson. */
+
+#include "compat.h"
+
+#include <stdio.h>
+
+#include <limits.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#ifndef SSIZE_MAX
+# define SSIZE_MAX ((ssize_t) (SIZE_MAX / 2))
+#endif
+
+#if USE_UNLOCKED_IO
+# include "unlocked-io.h"
+# define getc_maybe_unlocked(fp)        getc (fp)
+#elif ! HAVE_FLOCKFILE || ! HAVE_FUNLOCKFILE || ! HAVE_DECL_GETC_UNLOCKED
+# undef flockfile
+# undef funlockfile
+# define flockfile(x) ((void) 0)
+# define funlockfile(x) ((void) 0)
+# define getc_maybe_unlocked(fp)        getc (fp)
+#else
+# define getc_maybe_unlocked(fp)        getc_unlocked (fp)
+#endif
+
+/* Read up to (and including) a DELIMITER from FP into *LINEPTR (and
+ * NUL-terminate it).  *LINEPTR is a pointer returned from malloc (or
+ * NULL), pointing to *N characters of space.  It is realloc'ed as
+ * necessary.  Returns the number of characters read (not including
+ * the null terminator), or -1 on error or EOF.  */
+
+ssize_t
+getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp)
+{
+    ssize_t result = -1;
+    size_t cur_len = 0;
+
+    if (lineptr == NULL || n == NULL || fp == NULL) {
+       errno = EINVAL;
+       return -1;
+    }
+
+    flockfile (fp);
+
+    if (*lineptr == NULL || *n == 0) {
+       char *new_lineptr;
+       *n = 120;
+       new_lineptr = (char *) realloc (*lineptr, *n);
+       if (new_lineptr == NULL) {
+           result = -1;
+           goto unlock_return;
+       }
+       *lineptr = new_lineptr;
+    }
+
+    for (;;) {
+       int i;
+
+       i = getc_maybe_unlocked (fp);
+       if (i == EOF) {
+           result = -1;
+           break;
+       }
+
+       /* Make enough space for len+1 (for final NUL) bytes.  */
+       if (cur_len + 1 >= *n) {
+           size_t needed_max =
+               SSIZE_MAX < SIZE_MAX ? (size_t) SSIZE_MAX + 1 : SIZE_MAX;
+           size_t needed = 2 * *n + 1; /* Be generous. */
+           char *new_lineptr;
+
+           if (needed_max < needed)
+               needed = needed_max;
+           if (cur_len + 1 >= needed) {
+               result = -1;
+               errno = EOVERFLOW;
+               goto unlock_return;
+           }
+
+           new_lineptr = (char *) realloc (*lineptr, needed);
+           if (new_lineptr == NULL) {
+               result = -1;
+               goto unlock_return;
+           }
+
+           *lineptr = new_lineptr;
+           *n = needed;
+       }
+
+       (*lineptr)[cur_len] = i;
+       cur_len++;
+
+       if (i == delimiter)
+           break;
+    }
+    (*lineptr)[cur_len] = '\0';
+    result = cur_len ? (ssize_t) cur_len : result;
+
+  unlock_return:
+    funlockfile (fp); /* doesn't set errno */
+
+    return result;
+}
diff --git a/compat/getline.c b/compat/getline.c
new file mode 100644 (file)
index 0000000..2fcaba1
--- /dev/null
@@ -0,0 +1,29 @@
+/* getline.c --- Implementation of replacement getline function.
+ * Copyright (C) 2005, 2006, 2007 Free Software Foundation, Inc.
+ *
+ * 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, 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.  */
+
+/* Written by Simon Josefsson. */
+
+#include "compat.h"
+
+#include <stdio.h>
+
+ssize_t
+getline (char **lineptr, size_t *n, FILE *stream)
+{
+    return getdelim (lineptr, n, '\n', stream);
+}
diff --git a/compat/have_canonicalize_file_name.c b/compat/have_canonicalize_file_name.c
new file mode 100644 (file)
index 0000000..e560979
--- /dev/null
@@ -0,0 +1,11 @@
+#define _GNU_SOURCE
+#include <stdlib.h>
+
+int
+main ()
+{
+    char *found;
+    char *string;
+
+    found = canonicalize_file_name (string);
+}
diff --git a/compat/have_d_type.c b/compat/have_d_type.c
new file mode 100644 (file)
index 0000000..5338ee4
--- /dev/null
@@ -0,0 +1,11 @@
+#include <dirent.h>
+
+int
+main ()
+{
+    struct dirent ent;
+
+    (void) ent.d_type;
+
+    return 0;
+}
diff --git a/compat/have_getline.c b/compat/have_getline.c
new file mode 100644 (file)
index 0000000..6952a3b
--- /dev/null
@@ -0,0 +1,14 @@
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <sys/types.h>
+
+int
+main ()
+{
+    ssize_t count = 0;
+    size_t n = 0;
+    char **lineptr = NULL;
+    FILE *stream = NULL;
+
+    count = getline (lineptr, &n, stream);
+}
diff --git a/compat/have_strcasestr.c b/compat/have_strcasestr.c
new file mode 100644 (file)
index 0000000..8e00457
--- /dev/null
@@ -0,0 +1,12 @@
+#define _GNU_SOURCE
+#include <strings.h> /* strcasecmp() in POSIX */
+#include <string.h> /* strcasecmp() in *BSD */
+
+int
+main ()
+{
+    char *found;
+    const char *haystack, *needle;
+
+    found = strcasestr (haystack, needle);
+}
diff --git a/compat/have_strsep.c b/compat/have_strsep.c
new file mode 100644 (file)
index 0000000..dd4aae7
--- /dev/null
@@ -0,0 +1,12 @@
+#define _GNU_SOURCE
+#include <string.h>
+
+int
+main ()
+{
+    char *found;
+    char **stringp;
+    const char *delim;
+
+    found = strsep (stringp, delim);
+}
diff --git a/compat/have_timegm.c b/compat/have_timegm.c
new file mode 100644 (file)
index 0000000..8f7b380
--- /dev/null
@@ -0,0 +1,7 @@
+#include <time.h>
+
+int
+main ()
+{
+    return (int) timegm ((struct tm *) 0);
+}
diff --git a/compat/strcasestr.c b/compat/strcasestr.c
new file mode 100644 (file)
index 0000000..d4480bf
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * slow simplistic reimplementation of strcasestr for systems that
+ * don't include it in their library
+ *
+ * based on a GPL implementation in OpenTTD found under GPL v2
+ *
+ * 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, version 2.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.  */
+
+/* Imported into notmuch by Dirk Hohndel - original author unknown. */
+
+#include <string.h>
+
+#include "compat.h"
+
+char *
+strcasestr (const char *haystack, const char *needle)
+{
+    size_t hay_len = strlen (haystack);
+    size_t needle_len = strlen (needle);
+
+    while (hay_len >= needle_len) {
+       if (strncasecmp (haystack, needle, needle_len) == 0)
+           return (char *) haystack;
+
+       haystack++;
+       hay_len--;
+    }
+
+    return NULL;
+}
diff --git a/compat/strsep.c b/compat/strsep.c
new file mode 100644 (file)
index 0000000..4b6926d
--- /dev/null
@@ -0,0 +1,61 @@
+/* Copyright (C) 1992, 93, 96, 97, 98, 99, 2004 Free Software Foundation, Inc.
+ * This file is part of the GNU C Library.
+ *
+ * The GNU C Library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * The GNU C Library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with the GNU C Library; if not, write to the Free
+ * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+ * 02111-1307 USA.  */
+
+#include <string.h>
+
+/* Taken from glibc 2.6.1 */
+
+char *
+strsep (char **stringp, const char *delim)
+{
+    char *begin, *end;
+
+    begin = *stringp;
+    if (begin == NULL)
+       return NULL;
+
+    /* A frequent case is when the delimiter string contains only one
+     * character.  Here we don't need to call the expensive `strpbrk'
+     * function and instead work using `strchr'.  */
+    if (delim[0] == '\0' || delim[1] == '\0') {
+       char ch = delim[0];
+
+       if (ch == '\0')
+           end = NULL;
+       else {
+           if (*begin == ch)
+               end = begin;
+           else if (*begin == '\0')
+               end = NULL;
+           else
+               end = strchr (begin + 1, ch);
+       }
+    } else
+       /* Find the end of the token.  */
+       end = strpbrk (begin, delim);
+
+    if (end) {
+       /* Terminate the token and set *STRINGP past NUL character.  */
+       *end++ = '\0';
+       *stringp = end;
+    } else
+       /* No more delimiters; this is the last token.  */
+       *stringp = NULL;
+
+    return begin;
+}
diff --git a/compat/timegm.c b/compat/timegm.c
new file mode 100644 (file)
index 0000000..005a423
--- /dev/null
@@ -0,0 +1,56 @@
+/* timegm.c --- Implementation of replacement timegm function.
+ *
+ * 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, 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.  */
+
+/* Copyright 2013 Blake Jones. */
+
+#include <time.h>
+#include "compat.h"
+
+static int
+leapyear (int year)
+{
+    return ((year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0));
+}
+
+/*
+ * This is a simple implementation of timegm() which does what is needed
+ * by create_output() -- just turns the "struct tm" into a GMT time_t.
+ * It does not normalize any of the fields of the "struct tm", nor does
+ * it set tm_wday or tm_yday.
+ */
+time_t
+timegm (struct tm *tm)
+{
+    int monthlen[2][12] = {
+       { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
+       { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
+    };
+    int year, month, days;
+
+    days = 365 * (tm->tm_year - 70);
+    for (year = 70; year < tm->tm_year; year++) {
+       if (leapyear (1900 + year)) {
+           days++;
+       }
+    }
+    for (month = 0; month < tm->tm_mon; month++) {
+       days += monthlen[leapyear (1900 + year)][month];
+    }
+    days += tm->tm_mday - 1;
+
+    return ((((days * 24) + tm->tm_hour) * 60 + tm->tm_min) * 60 + tm->tm_sec);
+}
diff --git a/completion/Makefile b/completion/Makefile
new file mode 100644 (file)
index 0000000..de492a7
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/completion/Makefile.local b/completion/Makefile.local
new file mode 100644 (file)
index 0000000..54df463
--- /dev/null
@@ -0,0 +1,22 @@
+# -*- makefile-gmake -*-
+
+dir := completion
+
+# The dir variable will be re-assigned to later, so we can't use it
+# directly in any shell commands. Instead we save its value in other,
+# private variables that we can use in the commands.
+bash_script := $(srcdir)/$(dir)/notmuch-completion.bash
+zsh_scripts := $(srcdir)/$(dir)/zsh/_notmuch $(srcdir)/$(dir)/zsh/_email-notmuch
+
+install: install-$(dir)
+
+install-$(dir):
+       @echo $@
+ifeq ($(WITH_BASH),1)
+       mkdir -p "$(DESTDIR)$(bash_completion_dir)"
+       install -m0644 $(bash_script) "$(DESTDIR)$(bash_completion_dir)/notmuch"
+endif
+ifeq ($(WITH_ZSH),1)
+       mkdir -p "$(DESTDIR)$(zsh_completion_dir)"
+       install -m0644 $(zsh_scripts) "$(DESTDIR)$(zsh_completion_dir)"
+endif
diff --git a/completion/README b/completion/README
new file mode 100644 (file)
index 0000000..900e1c9
--- /dev/null
@@ -0,0 +1,16 @@
+notmuch completion
+
+This directory contains support for various shells to automatically
+complete partially entered notmuch command lines.
+
+notmuch-completion.bash
+
+  Command-line completion for the bash shell. This depends on
+  bash-completion package [1] version 2.0, which depends on bash
+  version 3.2 or later.
+
+  [1] https://github.com/scop/bash-completion
+
+zsh
+
+  Command-line completions for the zsh shell.
diff --git a/completion/notmuch-completion.bash b/completion/notmuch-completion.bash
new file mode 100644 (file)
index 0000000..3748846
--- /dev/null
@@ -0,0 +1,622 @@
+# bash completion for notmuch                              -*- shell-script -*-
+#
+# Copyright © 2013 Jani Nikula
+#
+# Based on the bash-completion package:
+# https://github.com/scop/bash-completion
+#
+# 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/ .
+#
+# Author: Jani Nikula <jani@nikula.org>
+#
+#
+# BUGS:
+#
+# Add space after an --option without parameter (e.g. reply --decrypt)
+# on completion.
+#
+
+_notmuch_shared_options="--help --uuid= --version"
+
+# $1: current input of the form prefix:partialinput, where prefix is
+# to or from.
+_notmuch_email()
+{
+    local output prefix cur
+
+    prefix="${1%%:*}"
+    cur="${1#*:}"
+
+    # Cut the input to be completed at punctuation because
+    # (apparently) Xapian does not support the trailing wildcard '*'
+    # operator for input with punctuation. We let compgen handle the
+    # extra filtering required.
+    cur="${cur%%[^a-zA-Z0-9]*}"
+
+    case "$prefix" in
+       # Note: It would be more accurate and less surprising to have
+       # output=recipients here for to: addresses, but as gathering
+       # the recipient addresses requires disk access for each
+       # matching message, this becomes prohibitively slow.
+       to|from) output=sender;;
+       *) return;;
+    esac
+
+    # Only emit plain, lower case, unique addresses.
+    notmuch address --output=$output $prefix:"${cur}*" | \
+       sed 's/[^<]*<\([^>]*\)>/\1/' | tr "[:upper:]" "[:lower:]" | sort -u
+}
+
+_notmuch_mimetype()
+{
+    # use mime types from mime-support package if available, and fall
+    # back to a handful of common ones otherwise
+    if [ -r "/etc/mime.types" ]; then
+       sed -n '/^[[:alpha:]]/{s/[[:space:]].*//;p;}' /etc/mime.types
+    else
+       cat <<EOF
+application/gzip
+application/msword
+application/pdf
+application/zip
+audio/mpeg
+audio/ogg
+image/gif
+image/jpeg
+image/png
+message/rfc822
+text/calendar
+text/html
+text/plain
+text/vcard
+text/x-diff
+text/x-vcalendar
+EOF
+    fi
+}
+
+_notmuch_search_terms()
+{
+    local cur prev words cword split
+    # handle search prefixes and tags with colons and equal signs
+    _init_completion -n := || return
+
+    case "${cur}" in
+       tag:*)
+           COMPREPLY=( $(compgen -P "tag:" -W "`notmuch search --output=tags \*`" -- ${cur##tag:}) )
+           ;;
+       to:*)
+           COMPREPLY=( $(compgen -P "to:" -W "`_notmuch_email ${cur}`" -- ${cur##to:}) )
+           ;;
+       from:*)
+           COMPREPLY=( $(compgen -P "from:" -W "`_notmuch_email ${cur}`" -- ${cur##from:}) )
+           ;;
+       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.mail_root`
+           compopt -o nospace
+           COMPREPLY=( $(compgen -d "$path/${cur##folder:}" | \
+               sed "s|^$path/||" | grep -v "\(^\|/\)\(cur\|new\|tmp\)$" ) )
+           ;;
+       mimetype:*)
+           compopt -o nospace
+           COMPREPLY=( $(compgen -P "mimetype:" -W "`_notmuch_mimetype ${cur}`" -- ${cur##mimetype:}) )
+           ;;
+       query:*)
+           compopt -o nospace
+           COMPREPLY=( $(compgen -P "query:" -W "`notmuch config list | sed -n '/^query\./s/^query\.\([^=]*\)=.*/\1/p'`" -- ${cur##query:}) )
+           ;;
+       *)
+           local search_terms="from: to: subject: attachment: mimetype: tag: id: thread: folder: path: date: lastmod: query: property:"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "${search_terms}" -- ${cur}) )
+           ;;
+    esac
+    # handle search prefixes and tags with colons
+    __ltrim_colon_completions "${cur}"
+}
+
+_notmuch_compact()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --backup)
+           _filedir -d
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           local options="--backup= --quiet ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           ;;
+    esac
+}
+
+_notmuch_config()
+{
+    local cur prev words cword split
+    _init_completion || return
+
+    case "${prev}" in
+       config)
+           COMPREPLY=( $(compgen -W "get set list" -- ${cur}) )
+           ;;
+       get|set)
+           COMPREPLY=( $(compgen -W "`notmuch config list | sed 's/=.*\$//'`" -- ${cur}) )
+           ;;
+       # these will also complete on config get, but we don't care
+       database.path)
+           _filedir -d
+           ;;
+       maildir.synchronize_flags)
+           COMPREPLY=( $(compgen -W "true false" -- ${cur}) )
+           ;;
+    esac
+}
+
+_notmuch_count()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --output)
+           COMPREPLY=( $( compgen -W "messages threads files" -- "${cur}" ) )
+           return
+           ;;
+       --exclude)
+           COMPREPLY=( $( compgen -W "true false" -- "${cur}" ) )
+           return
+           ;;
+       --input)
+           _filedir
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           local options="--output= --exclude= --batch --input= --lastmod ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           ;;
+       *)
+           _notmuch_search_terms
+           ;;
+    esac
+}
+
+_notmuch_dump()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --format)
+           COMPREPLY=( $( compgen -W "sup batch-tag" -- "${cur}" ) )
+           return
+           ;;
+       --output)
+           _filedir
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           local options="--gzip --format= --output= ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           ;;
+       *)
+           _notmuch_search_terms
+           ;;
+    esac
+}
+
+_notmuch_emacs_mua()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --to|--cc|--bcc)
+           COMPREPLY=( $(compgen -W "`_notmuch_email to:${cur}`" -- ${cur}) )
+           return
+           ;;
+       --body)
+           _filedir
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+        -*)
+           local options="--subject= --to= --cc= --bcc= --body= --no-window-system --client --auto-daemon --create-frame --print --help --hello"
+
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           ;;
+       *)
+           COMPREPLY=( $(compgen -W "`_notmuch_email to:${cur}`" -- ${cur}) )
+           return
+           ;;
+    esac
+}
+
+_notmuch_insert()
+{
+    local cur prev words cword split
+    # handle tags with colons and equal signs
+    _init_completion -s -n := || return
+
+    $split &&
+    case "${prev}" in
+       --folder)
+           local path=`notmuch config get database.mail_root`
+           compopt -o nospace
+           COMPREPLY=( $(compgen -d "$path/${cur}" | \
+               sed "s|^$path/||" | grep -v "\(^\|/\)\(cur\|new\|tmp\)$" ) )
+           return
+           ;;
+       --decrypt)
+           COMPREPLY=( $( compgen -W "true false auto nostash" -- "${cur}" ) )
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       --*)
+           local options="--create-folder --folder= --keep --no-hooks --decrypt= ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           return
+           ;;
+       +*)
+           COMPREPLY=( $(compgen -P "+" -W "`notmuch search --output=tags \*`" -- ${cur##+}) )
+           ;;
+       -*)
+           COMPREPLY=( $(compgen -P "-" -W "`notmuch search --output=tags \*`" -- ${cur##-}) )
+           ;;
+    esac
+    # handle tags with colons
+    __ltrim_colon_completions "${cur}"
+}
+
+_notmuch_new()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --decrypt)
+           COMPREPLY=( $( compgen -W "true false auto nostash" -- "${cur}" ) )
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           local options="--no-hooks --decrypt= --quiet ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "${options}" -- ${cur}) )
+           ;;
+    esac
+}
+
+_notmuch_reply()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --format)
+           COMPREPLY=( $( compgen -W "default json sexp headers-only" -- "${cur}" ) )
+           return
+           ;;
+       --reply-to)
+           COMPREPLY=( $( compgen -W "all sender" -- "${cur}" ) )
+           return
+           ;;
+       --decrypt)
+           COMPREPLY=( $( compgen -W "true auto false" -- "${cur}" ) )
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           local options="--format= --format-version= --reply-to= --decrypt= ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           ;;
+       *)
+           _notmuch_search_terms
+           ;;
+    esac
+}
+
+_notmuch_restore()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --format)
+           COMPREPLY=( $( compgen -W "sup batch-tag auto" -- "${cur}" ) )
+           return
+           ;;
+       --input)
+           _filedir
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           local options="--format= --accumulate --input= ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           ;;
+    esac
+}
+
+_notmuch_search()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --format)
+           COMPREPLY=( $( compgen -W "json sexp text text0" -- "${cur}" ) )
+           return
+           ;;
+       --output)
+           COMPREPLY=( $( compgen -W "summary threads messages files tags" -- "${cur}" ) )
+           return
+           ;;
+       --sort)
+           COMPREPLY=( $( compgen -W "newest-first oldest-first" -- "${cur}" ) )
+           return
+           ;;
+       --exclude)
+           COMPREPLY=( $( compgen -W "true false flag all" -- "${cur}" ) )
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           local options="--format= --output= --sort= --offset= --limit= --exclude= --duplicate= ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           ;;
+       *)
+           _notmuch_search_terms
+           ;;
+    esac
+}
+
+_notmuch_reindex()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --decrypt)
+           COMPREPLY=( $( compgen -W "true false auto nostash" -- "${cur}" ) )
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           local options="--decrypt= ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           ;;
+       *)
+           _notmuch_search_terms
+           ;;
+    esac
+}
+
+_notmuch_address()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --format)
+           COMPREPLY=( $( compgen -W "json sexp text text0" -- "${cur}" ) )
+           return
+           ;;
+       --output)
+           COMPREPLY=( $( compgen -W "sender recipients count address" -- "${cur}" ) )
+           return
+           ;;
+       --sort)
+           COMPREPLY=( $( compgen -W "newest-first oldest-first" -- "${cur}" ) )
+           return
+           ;;
+       --exclude)
+           COMPREPLY=( $( compgen -W "true false flag all" -- "${cur}" ) )
+           return
+           ;;
+       --deduplicate)
+           COMPREPLY=( $( compgen -W "no mailbox address" -- "${cur}" ) )
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           local options="--format= --output= --sort= --exclude= --deduplicate= ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           ;;
+       *)
+           _notmuch_search_terms
+           ;;
+    esac
+}
+
+_notmuch_show()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    $split &&
+    case "${prev}" in
+       --entire-thread)
+           COMPREPLY=( $( compgen -W "true false" -- "${cur}" ) )
+           return
+           ;;
+       --format)
+           COMPREPLY=( $( compgen -W "text json sexp mbox raw" -- "${cur}" ) )
+           return
+           ;;
+       --exclude|--body)
+           COMPREPLY=( $( compgen -W "true false" -- "${cur}" ) )
+           return
+           ;;
+        --decrypt)
+           COMPREPLY=( $( compgen -W "true auto false stash" -- "${cur}" ) )
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           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}) )
+           ;;
+       *)
+           _notmuch_search_terms
+           ;;
+    esac
+}
+
+_notmuch_tag()
+{
+    local cur prev words cword split
+    # handle tags with colons and equal signs
+    _init_completion -s -n := || return
+
+    $split &&
+    case "${prev}" in
+       --input)
+           _filedir
+           return
+           ;;
+    esac
+
+    ! $split &&
+    case "${cur}" in
+       --*)
+           local options="--batch --input= --remove-all ${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           return
+           ;;
+       +*)
+           COMPREPLY=( $(compgen -P "+" -W "`notmuch search --output=tags \*`" -- ${cur##+}) )
+           ;;
+       -*)
+           COMPREPLY=( $(compgen -P "-" -W "`notmuch search --output=tags \*`" -- ${cur##-}) )
+           ;;
+       *)
+           _notmuch_search_terms
+           return
+           ;;
+    esac
+    # handle tags with colons
+    __ltrim_colon_completions "${cur}"
+}
+
+_notmuch()
+{
+    local _notmuch_commands="compact config count dump help insert new reply restore reindex search address setup show tag emacs-mua"
+    local arg cur prev words cword split
+
+    # require bash-completion with _init_completion
+    type -t _init_completion >/dev/null 2>&1 || return
+
+    _init_completion || return
+
+    COMPREPLY=()
+
+    # subcommand
+    _get_first_arg
+
+    # complete --help option like the subcommand
+    if [ -z "${arg}" -a "${prev}" = "--help" ]; then
+       arg="help"
+    fi
+
+    if [ -z "${arg}" ]; then
+       # top level completion
+       case "${cur}" in
+           -*)
+               # XXX: handle ${_notmuch_shared_options} and --config=
+               local options="--help --version"
+               COMPREPLY=( $(compgen -W "${options}" -- ${cur}) )
+               ;;
+           *)
+               COMPREPLY=( $(compgen -W "${_notmuch_commands}" -- ${cur}) )
+               ;;
+       esac
+    elif [ "${arg}" = "help" ]; then
+       # handle help command specially due to _notmuch_commands usage
+       local help_topics="$_notmuch_commands hooks search-terms properties"
+       COMPREPLY=( $(compgen -W "${help_topics}" -- ${cur}) )
+    else
+       # complete using _notmuch_subcommand if one exist
+       local completion_func="_notmuch_${arg//-/_}"
+       declare -f $completion_func >/dev/null && $completion_func
+    fi
+} &&
+complete -F _notmuch notmuch
diff --git a/completion/zsh/_email-notmuch b/completion/zsh/_email-notmuch
new file mode 100644 (file)
index 0000000..1cd0d78
--- /dev/null
@@ -0,0 +1,9 @@
+#autoload
+
+local expl
+local -a notmuch_addr
+
+notmuch_addr=( ${(f)"$(notmuch address --deduplicate=address --output=address -- from:/$PREFIX/)"} )
+
+_description notmuch-addr expl 'email address (notmuch)'
+compadd "$expl[@]" -a notmuch_addr
diff --git a/completion/zsh/_notmuch b/completion/zsh/_notmuch
new file mode 100644 (file)
index 0000000..0bdd7f7
--- /dev/null
@@ -0,0 +1,295 @@
+#compdef notmuch -p notmuch-*
+
+# ZSH completion for `notmuch`
+# Copyright © 2018 Vincent Breitmoser <look@my.amazin.horse>
+
+_notmuch_command() {
+  local -a notmuch_commands
+  notmuch_commands=(
+    'help:display documentation for a subcommand'
+    'setup:interactively configure notmuch'
+
+    'address:output addresses from matching messages'
+    'compact:compact the notmuch database'
+    'config:access notmuch configuration file'
+    'count:count messages matching the given search terms'
+    'dump:creates a plain-text dump of the tags of each message'
+    'insert:add a message to the maildir and notmuch database'
+    'new:incorporate new mail into the notmuch database'
+    'reindex:re-index a set of messages'
+    'reply:constructs a reply template for a set of messages'
+    'restore:restores the tags from the given file (see notmuch dump)'
+    'search:search for messages matching the given search terms'
+    'show:show messages matching the given search terms'
+    'tag:add/remove tags for all messages matching the search terms'
+  )
+
+  if ((CURRENT == 1)); then
+    _describe -t commands 'notmuch command' notmuch_commands
+  else
+      local curcontext="$curcontext"
+      cmd=$words[1]
+      if (( $+functions[_notmuch_$cmd] )); then
+        _notmuch_$cmd
+      else
+        _message -e "unknown command $cmd"
+      fi
+  fi
+}
+
+_notmuch_term_tag _notmuch_term_is () {
+  local ret=1 expl
+  local -a notmuch_tags
+
+  notmuch_tags=( ${(f)"$(notmuch search --output=tags '*')"} )
+
+  _description notmuch-tag expl 'tag'
+  compadd "$expl[@]" -a notmuch_tags && ret=0
+  return $ret
+}
+
+_notmuch_term_to _notmuch_term_from() {
+  _email_addresses -c
+}
+
+_notmuch_term_mimetype() {
+  local ret=1 expl
+  local -a commontypes
+  commontypes=(
+    'text/plain'
+    'text/html'
+    'application/pdf'
+  )
+  _description typical-mimetypes expl 'common types'
+  compadd "$expl[@]" -a commontypes && ret=0
+
+  _mime_types && ret=0
+  return $ret
+}
+
+_notmuch_term_path() {
+  local ret=1 expl
+  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
+  return $ret
+}
+
+_notmuch_term_folder() {
+  local ret=1 expl
+  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)' )
+  _files "$expl[@]" -W $maildir -F ignoredfolders -/ && ret=0
+  return $ret
+}
+
+_notmuch_term_query() {
+  local ret=1
+  local line query_name
+  local -a query_names query_content
+  for line in ${(f)"$(notmuch config list | grep '^query.')"}; do
+    query_name=${${line%%=*}#query.}
+    query_names+=( $query_name )
+    query_content+=( "$query_name = ${line#*=}" )
+  done
+
+  _description notmuch-named-query expl 'named query'
+  compadd "$expl[@]" -d query_content -a query_names && ret=0
+  return $ret
+}
+
+_notmuch_search_term() {
+  local ret=1 expl match
+  setopt localoptions extendedglob
+
+  typeset -a notmuch_search_terms
+  notmuch_search_terms=(
+    'from' 'to' 'subject' 'attachment' 'mimetype' 'tag' 'id' 'thread' 'path' 'folder' 'date' 'lastmod' 'query' 'property'
+  )
+
+  if compset -P '(#b)([^:]#):'; then
+    if (( $+functions[_notmuch_term_$match[1]] )); then
+      _notmuch_term_$match[1] && ret=0
+      return $ret
+    elif (( $+notmuch_search_terms[(r)$match[1]] )); then
+      _message "search term '$match[1]'" && ret=0
+      return $ret
+    else
+      _message -e "unknown search term '$match[1]'"
+      return $ret
+    fi
+  fi
+
+  _description notmuch-term expl 'search term'
+  compadd "$expl[@]" -S ':' -a notmuch_search_terms && ret=0
+
+  if [[ $CURRENT -gt 1 && $words[CURRENT-1] != '--' ]]; then
+    _description notmuch-op expl 'boolean operator'
+    compadd "$expl[@]" -- and or not xor && ret=0
+  fi
+
+  return $ret
+}
+
+_notmuch_tagging_or_search() {
+  setopt localoptions extendedglob
+  local ret=1 expl
+  local -a notmuch_tags
+
+  # first arg that is a search term, or $#words+1
+  integer searchtermarg=$(( $words[(I)--] != 0 ? $words[(i)--] : $words[(i)^(-|+)*] ))
+
+  if (( CURRENT > 1 )); then
+    () {
+      local -a words=( $argv )
+      local CURRENT=$(( CURRENT - searchtermarg + 1 ))
+      _notmuch_search_term && ret=0
+    } $words[searchtermarg,$]
+  fi
+
+  # only complete +/- tags if we're before the first search term
+  if (( searchtermarg >= CURRENT )); then
+    if compset -P '+'; then
+      notmuch_tags=( ${(f)"$(notmuch search --output=tags '*')"} )
+      _description notmuch-tag expl 'add tag'
+      compadd "$expl[@]" -a notmuch_tags
+      return 0
+    elif compset -P '-'; then
+      notmuch_tags=( ${(f)"$(notmuch search --output=tags '*')"} )
+      _description notmuch-tag expl 'remove tag'
+      compadd "$expl[@]" -a notmuch_tags
+      return 0
+    else
+      _description notmuch-tag expl 'add or remove tags'
+      compadd "$expl[@]" -S '' -- '+' '-' && ret=0
+    fi
+  fi
+
+  return $ret
+}
+
+_notmuch_address() {
+  _arguments -S \
+    '--format=[set output format]:output format:(json sexp text text0)' \
+    '--format-version=[set output format version]:format version: ' \
+    '--sort=[sort results]:sorting:((newest-first\:"reverse chronological order" oldest-first\:"chronological order"))' \
+    '--output=[select output format]:output format:(sender recipients count address)' \
+    '--deduplicate=[deduplicate results]:deduplication mode:(no mailbox address)' \
+    '--exclude=[respect excluded tags setting]:exclude tags:(true false)' \
+    '*::search term:_notmuch_search_term'
+}
+
+_notmuch_compact() {
+  _arguments \
+    '--backup=[save a backup before compacting]:backup directory:_files -/' \
+    '--quiet[do not print progress or results]'
+}
+
+_notmuch_count() {
+  _arguments \
+     - normal \
+        '--lastmod[append lastmod and uuid to output]' \
+        '--exclude=[respect excluded tags setting]:exclude tags:(true false)' \
+        '--output=[select what to count]:output format:(messages threads files)' \
+        '*::search term:_notmuch_search_term' \
+    - batch \
+      '--batch[operate in batch mode]' \
+      '(--batch)--input=[read batch operations from file]:batch file:_files'
+}
+
+_notmuch_dump() {
+  _arguments -S \
+    '--gzip[compress output with gzip]' \
+    '--format=[specify output format]:output format:(batch-tag sup)' \
+    '*--include=[configure metadata to output (default all)]:metadata type:(config properties tags)' \
+    '--output=[write output to file]:output file:_files' \
+    '*::search term:_notmuch_search_term'
+}
+
+_notmuch_new() {
+  _arguments \
+    '--no-hooks[prevent hooks from being run]' \
+    '--quiet[do not print progress or results]' \
+    '--full-scan[don''t rely on directory modification times for scan]' \
+    '--decrypt=[decrypt messages]:decryption setting:((false\:"never decrypt" auto\:"decrypt if session key is known (default)" true\:"decrypt using secret keys" stash\:"decrypt, and store session keys"))'
+}
+
+_notmuch_reindex() {
+  _arguments \
+    '--decrypt=[decrypt messages]:decryption setting:((false\:"never decrypt" auto\:"decrypt if session key is known (default)" true\:"decrypt using secret keys" stash\:"decrypt, and store session keys"))' \
+    '*::search term:_notmuch_search_term'
+}
+
+_notmuch_search() {
+  _arguments -S \
+    '--max-threads=[display only the first x threads from the search results]:number of threads to show: ' \
+    '--first=[omit the first x threads from the search results]:number of threads to omit: ' \
+    '--sort=[sort results]:sorting:((newest-first\:"reverse chronological order" oldest-first\:"chronological order"))' \
+    '--output=[select what to output]:output:(summary threads messages files tags)' \
+    '*::search term:_notmuch_search_term'
+}
+
+_notmuch_show() {
+  _arguments -S \
+    '--entire-thread=[output entire threads]:show thread:(true false)' \
+    '--format=[set output format]:output format:(text json sexp mbox raw)' \
+    '--format-version=[set output format version]:format version: ' \
+    '--part=[output a single decoded mime part]:part number: ' \
+    '--verify[verify signed MIME parts]' \
+    '--decrypt=[decrypt messages]:decryption setting:((false\:"never decrypt" auto\:"decrypt if session key is known (default)" true\:"decrypt using secret keys" stash\:"decrypt, and store session keys"))' \
+    '--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'
+}
+
+_notmuch_reply() {
+  _arguments \
+    '--format=[set output format]:output format:(default json sexp headers-only)' \
+    '--format-version=[set output format version]:output format version: ' \
+    '--reply-to=[specify recipient types]:recipient types:(all sender)' \
+    '--decrypt=[decrypt messages]:decryption setting:((false\:"never decrypt" auto\:"decrypt if session key is known (default)" true\:"decrypt using secret keys"))' \
+    '*::search term:_notmuch_search_term'
+}
+
+_notmuch_restore() {
+  _arguments \
+    '--acumulate[add data to db instead of replacing]' \
+    '--format=[specify input format]:input format:(auto batch-tag sup)' \
+    '*--include=[configure metadata to import (default all)]:metadata type:(config properties tags)' \
+    '--input=[read from file]:notmuch dump file:_files'
+}
+
+_notmuch_tag() {
+  _arguments \
+    - normal \
+      '--remove-all[remove all tags from matching messages]:*:search term:_notmuch_search_term' \
+      '*::tag or search:_notmuch_tagging_or_search' \
+    - batch \
+      '--batch[operate in batch mode]' \
+      '(--batch)--input=[read batch operations from file]:batch file:_files'
+}
+
+_notmuch() {
+  if [[ $service == notmuch-* ]]; then
+    local compfunc=_${service//-/_}
+    (( $+functions[$compfunc] )) || return 1
+    $compfunc "$@"
+  else
+    _arguments \
+      '(* -)--help[show help]' \
+      '(* -)--version[show version]' \
+      '--config=-[specify config file]:config file:_files' \
+      '--uuid=-[check against database uuid or exit]:uuid: ' \
+      '*::notmuch commands:_notmuch_command'
+  fi
+}
+
+_notmuch "$@"
diff --git a/configure b/configure
new file mode 100755 (executable)
index 0000000..7afd08c
--- /dev/null
+++ b/configure
@@ -0,0 +1,1678 @@
+#! /bin/sh
+
+set -u
+
+# Test whether this shell is capable of parameter substring processing.
+( option='a/b'; : ${option#*/} ) 2>/dev/null || {
+    echo "
+The shell interpreting '$0' is lacking some required features.
+
+To work around this problem you may try to execute:
+
+    ksh $0 $*
+ or
+    bash $0 $*
+"
+    exit 1
+}
+
+# Store original IFS value so it can be changed (and restored) in many places.
+readonly DEFAULT_IFS="$IFS"
+
+# The top-level directory for the source. This ./configure and all Makefiles
+# are good with ${srcdir} usually being relative. Some components (e.g. tests)
+# are executed in subdirectories and for those it is simpler to use
+# ${NOTMUCH_SRCDIR} which holds absolute path to the source.
+srcdir=$(dirname "$0")
+NOTMUCH_SRCDIR=$(cd "$srcdir" && pwd)
+
+case $NOTMUCH_SRCDIR in ( *\'* | *['\"`$']* )
+       echo "Definitely unsafe characters in source path '$NOTMUCH_SRCDIR'".
+       exit 1
+esac
+
+case $PWD in ( *\'* | *['\"`$']* )
+       echo "Definitely unsafe characters in current directory '$PWD'".
+       exit 1
+esac
+
+# In case of whitespace, builds may work, tests definitely will not.
+case $NOTMUCH_SRCDIR in ( *["$IFS"]* )
+       echo "Whitespace in source path '$NOTMUCH_SRCDIR' not supported".
+       exit 1
+esac
+
+case $PWD in ( *["$IFS"]* )
+       echo "Whitespace in current directory '$PWD' not supported".
+       exit 1
+esac
+
+subdirs="util compat lib parse-time-string completion doc emacs"
+subdirs="${subdirs} performance-test test"
+subdirs="${subdirs} bindings"
+
+# For a non-srcdir configure invocation (such as ../configure), create
+# 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"
+       cp "$srcdir"/"$dir"/Makefile "$dir"
+    done
+
+    # Emacs only likes to generate compiled files next to the .el files
+    # by default so copy these as well (which is not ideal).
+    cp -a "$srcdir"/emacs/*.el emacs
+
+    # We were not able to create fully working Makefile using ruby mkmf.rb
+    # so ruby bindings source files are copied as well (ditto -- not ideal).
+    mkdir bindings/ruby
+    cp -a "$srcdir"/bindings/ruby/*.[ch] bindings/ruby
+    cp -a "$srcdir"/bindings/ruby/extconf.rb bindings/ruby
+
+    # Use the same hack to replicate python-cffi source for
+    # out-of-tree builds (again, not ideal).
+    mkdir bindings/python-cffi
+    cp -a "$srcdir"/bindings/python-cffi/tests \
+       "$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
+# environment variables)
+BASHCMD=${BASHCMD:-bash}
+PERL=${PERL:-perl}
+CC=${CC:-cc}
+CXX=${CXX:-c++}
+CFLAGS=${CFLAGS:--g -O2}
+CPPFLAGS=${CPPFLAGS:-}
+CXXFLAGS_for_sh=${CXXFLAGS:-${CFLAGS}}
+CXXFLAGS=${CXXFLAGS:-\$(CFLAGS)}
+LDFLAGS=${LDFLAGS:-}
+XAPIAN_CONFIG=${XAPIAN_CONFIG:-}
+PYTHON=${PYTHON:-}
+RUBY=${RUBY:-ruby}
+
+# We don't allow the EMACS or GZIP Makefile variables inherit values
+# from the environment as we do with CC and CXX above. The reason is
+# that these names as environment variables have existing uses other
+# than the program name that we want. (EMACS is set to 't' when a
+# shell is running within emacs and GZIP specifies arguments to pass
+# on the gzip command line).
+
+# Set the defaults for values the user can specify with command-line
+# options.
+PREFIX=/usr/local
+LIBDIR=
+WITH_DOCS=1
+WITH_API_DOCS=1
+WITH_PYTHON_DOCS=1
+WITH_EMACS=1
+WITH_DESKTOP=1
+WITH_BASH=1
+WITH_RPATH=1
+WITH_RUBY=1
+WITH_ZSH=1
+WITH_RETRY_LOCK=1
+
+usage ()
+{
+    cat <<EOF
+Usage: ./configure [options]...
+
+This script configures notmuch to build on your system.
+
+It verifies that dependencies are available, determines flags needed
+to compile and link against various required libraries, and identifies
+whether various system functions can be used or if locally-provided
+replacements will be built instead.
+
+Finally, it allows you to control various aspects of the build and
+installation process.
+
+First, some common variables can specified via environment variables:
+
+       CC              The C compiler to use
+       CFLAGS          Flags to pass to the C compiler
+       CPPFLAGS        Flags to pass to the C preprocessor
+       CXX             The C++ compiler to use
+       CXXFLAGS        Flags to pass to the C compiler
+       LDFLAGS         Flags to pass when linking
+
+Each of these values can further be controlled by specifying them
+later on the "make" command line.
+
+Other environment variables can be used to control configure itself,
+(and for which there is no equivalent build-time control):
+
+       XAPIAN_CONFIG   The program to use to determine flags for
+                       compiling and linking against the Xapian
+                       library. [$XAPIAN_CONFIG]
+       PYTHON          Name of python command to use in
+                       configure and the test suite.
+       RUBY            Name of ruby command to use in
+                       configure and the test suite.
+
+Additionally, various options can be specified on the configure
+command line.
+
+       --prefix=PREFIX Install files in PREFIX [$PREFIX]
+
+By default, "make install" will install the resulting program to
+$PREFIX/bin, documentation to $PREFIX/man, etc. You can
+specify an installation prefix other than $PREFIX using
+--prefix, for instance:
+
+       ./configure --prefix=\$HOME
+
+Fine tuning of some installation directories is available:
+
+       --libdir=DIR            Install libraries to DIR [PREFIX/lib]
+       --includedir=DIR        Install header files to DIR [PREFIX/include]
+       --mandir=DIR            Install man pages to DIR [PREFIX/share/man]
+       --infodir=DIR           Install man pages to DIR [PREFIX/share/man]
+       --sysconfdir=DIR        Read-only single-machine data [PREFIX/etc]
+       --emacslispdir=DIR      Emacs code [PREFIX/share/emacs/site-lisp]
+       --emacsetcdir=DIR       Emacs miscellaneous files [PREFIX/share/emacs/site-lisp]
+       --bashcompletiondir=DIR Bash completions files [PREFIX/share/bash-completion/completions]
+       --zshcompletiondir=DIR  Zsh completions files [PREFIX/share/zsh/site-functions]
+
+Some features can be disabled (--with-feature=no is equivalent to
+--without-feature) :
+
+       --without-bash-completion       Do not install bash completions files
+       --without-docs                  Do not install documentation
+       --without-api-docs              Do not install API man page
+       --without-emacs                 Do not install lisp file
+       --without-desktop               Do not install desktop file
+       --without-ruby                  Do not install ruby bindings
+       --without-zsh-completion        Do not install zsh completions files
+       --without-retry-lock            Do not use blocking xapian opens, even if available
+
+Additional options are accepted for compatibility with other
+configure-script calling conventions, but don't do anything yet:
+
+       --build=<cpu>-<vendor>-<os>     Currently ignored
+       --host=<cpu>-<vendor>-<os>      Currently ignored
+       --datadir=DIR                   Currently ignored
+       --localstatedir=DIR             Currently ignored
+       --libexecdir=DIR                Currently ignored
+       --disable-maintainer-mode       Currently ignored
+       --disable-dependency-tracking   Currently ignored
+
+EOF
+}
+
+# Parse command-line options
+for option; do
+    if [ "${option}" = '--help' ] ; then
+       usage
+       exit 0
+    elif [ "${option%%=*}" = '--prefix' ] ; then
+       PREFIX="${option#*=}"
+    elif [ "${option%%=*}" = '--libdir' ] ; then
+       LIBDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--includedir' ] ; then
+       INCLUDEDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--mandir' ] ; then
+       MANDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--infodir' ] ; then
+       INFODIR="${option#*=}"
+    elif [ "${option%%=*}" = '--sysconfdir' ] ; then
+       SYSCONFDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--emacslispdir' ] ; then
+       EMACSLISPDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--emacsetcdir' ] ; then
+       EMACSETCDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--bashcompletiondir' ] ; then
+       BASHCOMPLETIONDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--zshcompletiondir' ] ; then
+       ZSHCOMLETIONDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--with-docs' ]; then
+       if [ "${option#*=}" = 'no' ]; then
+           WITH_DOCS=0
+           WITH_API_DOCS=0
+       else
+           WITH_DOCS=1
+       fi
+    elif [ "${option}" = '--without-docs' ] ; then
+       WITH_DOCS=0
+       WITH_API_DOCS=0
+    elif [ "${option%%=*}" = '--with-api-docs' ]; then
+       if [ "${option#*=}" = 'no' ]; then
+           WITH_API_DOCS=0
+       else
+           WITH_API_DOCS=1
+       fi
+    elif [ "${option}" = '--without-api-docs' ] ; then
+       WITH_API_DOCS=0
+    elif [ "${option%%=*}" = '--with-emacs' ]; then
+       if [ "${option#*=}" = 'no' ]; then
+           WITH_EMACS=0
+       else
+           WITH_EMACS=1
+       fi
+    elif [ "${option}" = '--without-emacs' ] ; then
+       WITH_EMACS=0
+    elif [ "${option%%=*}" = '--with-desktop' ]; then
+       if [ "${option#*=}" = 'no' ]; then
+           WITH_DESKTOP=0
+       else
+           WITH_DESKTOP=1
+       fi
+    elif [ "${option}" = '--without-desktop' ] ; then
+       WITH_DESKTOP=0
+    elif [ "${option%%=*}" = '--with-bash-completion' ]; then
+       if [ "${option#*=}" = 'no' ]; then
+           WITH_BASH=0
+       else
+           WITH_BASH=1
+       fi
+    elif [ "${option}" = '--without-bash-completion' ] ; then
+       WITH_BASH=0
+    elif [ "${option%%=*}" = '--with-rpath' ]; then
+       if [ "${option#*=}" = 'no' ]; then
+           WITH_RPATH=0
+       else
+           WITH_RPATH=1
+       fi
+    elif [ "${option}" = '--without-rpath' ] ; then
+       WITH_RPATH=0
+    elif [ "${option%%=*}" = '--with-ruby' ]; then
+       if [ "${option#*=}" = 'no' ]; then
+           WITH_RUBY=0
+       else
+           WITH_RUBY=1
+       fi
+    elif [ "${option}" = '--without-ruby' ] ; then
+       WITH_RUBY=0
+    elif [ "${option%%=*}" = '--with-retry-lock' ]; then
+       if [ "${option#*=}" = 'no' ]; then
+           WITH_RETRY_LOCK=0
+       else
+           WITH_RETRY_LOCK=1
+       fi
+    elif [ "${option}" = '--without-retry-lock' ] ; then
+       WITH_RETRY_LOCK=0
+    elif [ "${option%%=*}" = '--with-zsh-completion' ]; then
+       if [ "${option#*=}" = 'no' ]; then
+           WITH_ZSH=0
+       else
+           WITH_ZSH=1
+       fi
+    elif [ "${option}" = '--without-zsh-completion' ] ; then
+       WITH_ZSH=0
+    elif [ "${option%%=*}" = '--build' ] ; then
+       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
+       true
+    else
+       echo "Unrecognized option: ${option}"
+       echo "See:"
+       echo "  $0 --help"
+       echo ""
+       exit 1
+    fi
+done
+
+# We set this value early, (rather than just while printing the
+# Makefile.config file later like most values), because we need to
+# actually investigate this value compared to the ldconfig_paths value
+# below.
+if [ -z "$LIBDIR" ] ; then
+    libdir_expanded="${PREFIX}/lib"
+else
+    # very non-general variable expansion
+    libdir_expanded=$(printf %s "$LIBDIR" | sed "s|\${prefix}|${PREFIX}|; s|\$prefix\>|${PREFIX}|; s|//*|/|g")
+fi
+
+cat <<EOF
+Welcome to Notmuch, a system for indexing, searching and tagging your email.
+
+We hope that the process of building and installing notmuch is quick
+and smooth so that you can soon be reading and processing your email
+more efficiently than ever.
+
+If anything goes wrong in the configure process, you can override any
+decisions it makes by manually editing the Makefile.config file that
+it creates. Also please do as much as you can to figure out what could
+be different on your machine compared to those of the notmuch
+developers. Then, please email those details to the Notmuch list
+(notmuch@notmuchmail.org) so that we can hopefully make future
+versions of notmuch easier for you to use.
+
+We'll now investigate your system to verify that all required
+dependencies are available:
+
+EOF
+
+errors=0
+printf "int main(void){return 0;}\n" > minimal.c
+
+printf "Sanity checking C compilation environment... "
+test_cmdline="${CC} ${CFLAGS} ${CPPFLAGS} minimal.c ${LDFLAGS} -o minimal"
+if  ${test_cmdline} > /dev/null 2>&1
+then
+    printf "OK.\n"
+else
+    printf "Fail.\n"
+    errors=$((errors + 1))
+    printf Executed:; printf ' %s' ${test_cmdline}; echo
+    ${test_cmdline}
+fi
+
+printf "Sanity checking C++ compilation environment... "
+test_cmdline="${CXX} ${CXXFLAGS_for_sh} ${CPPFLAGS} minimal.c ${LDFLAGS} -o minimal"
+if ${test_cmdline} > /dev/null 2>&1
+then
+    printf "OK.\n"
+else
+    printf "Fail.\n"
+    errors=$((errors + 1))
+    printf Executed:; printf ' %s' ${test_cmdline}; echo
+    ${test_cmdline}
+fi
+unset test_cmdline
+
+if [ $errors -gt 0 ]; then
+    cat <<EOF
+*** Error: Initial sanity checking of environment failed.  Please try
+running configure in a clean environment, and if the problem persists,
+report a bug.
+EOF
+    rm -f minimal minimal.c
+    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>
+#include "lib/notmuch.h"
+int main(void) {
+    printf("libnotmuch_version_major=%d\n",
+               LIBNOTMUCH_MAJOR_VERSION);
+    printf("libnotmuch_version_minor=%d\n",
+               LIBNOTMUCH_MINOR_VERSION);
+    printf("libnotmuch_version_release=%d\n",
+               LIBNOTMUCH_MICRO_VERSION);
+    return 0;
+}
+EOF
+if ${CC} ${CFLAGS} -I"$srcdir" _libversion.c -o _libversion > /dev/null 2>&1 \
+       && ./_libversion > _libversion.sh && . ./_libversion.sh
+then
+    printf "OK.\n"
+else
+    cat <<EOF
+
+*** Error: Reading lib/notmuch.h failed.
+Please try running configure again in a clean environment, and if the
+problem persists, report a bug.
+EOF
+    rm -f _libversion _libversion.c _libversion.sh
+    exit 1
+fi
+
+if pkg-config --version > /dev/null 2>&1; then
+    have_pkg_config=1
+else
+    have_pkg_config=0
+fi
+
+
+
+printf "Checking for Xapian development files (>= 1.4.0)... "
+have_xapian=0
+for xapian_config in ${XAPIAN_CONFIG} xapian-config; do
+    if ${xapian_config} --version > /dev/null 2>&1; then
+       xapian_version=$(${xapian_config} --version | sed -e 's/.* //')
+       case $xapian_version in
+               1.[4-9]* | 1.[1-9][0-9]* | [2-9]* | [1-9][0-9]*)
+                       printf "Yes (%s).\n" ${xapian_version}
+                       have_xapian=1
+                       xapian_cxxflags=$(${xapian_config} --cxxflags)
+                       xapian_ldflags=$(${xapian_config} --libs)
+                       ;;
+               *) printf "Xapian $xapian_version not supported... "
+       esac
+       break
+    fi
+done
+if [ ${have_xapian} = "0" ]; then
+    printf "No.\n"
+    errors=$((errors + 1))
+fi
+
+GMIME_MINVER=3.0.3
+
+printf "Checking for GMime development files (>= $GMIME_MINVER)... "
+if pkg-config --exists "gmime-3.0 >= $GMIME_MINVER"; then
+    printf "Yes.\n"
+    have_gmime=1
+    gmime_cflags=$(pkg-config --cflags gmime-3.0)
+    gmime_ldflags=$(pkg-config --libs gmime-3.0)
+
+    printf "Checking for GMime session key extraction support... "
+
+    cat > _check_session_keys.c <<EOF
+#include <gmime/gmime.h>
+#include <stdio.h>
+
+int main () {
+    GError *error = NULL;
+    GMimeParser *parser = NULL;
+    GMimeMultipartEncrypted *body = NULL;
+    GMimeDecryptResult *decrypt_result = NULL;
+    GMimeObject *output = NULL;
+
+    g_mime_init ();
+    parser = g_mime_parser_new ();
+    g_mime_parser_init_with_stream (parser, g_mime_stream_file_open("$srcdir/test/corpora/crypto/basic-encrypted.eml", "r", &error));
+    if (error) return !! fprintf (stderr, "failed to instantiate parser with test/corpora/crypto/basic-encrypted.eml\n");
+
+    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_EXPORT_SESSION_KEY, NULL, &decrypt_result, &error);
+    if (error || output == NULL) return !! fprintf (stderr, "decryption failed\n");
+
+    if (decrypt_result == NULL) return !! fprintf (stderr, "no GMimeDecryptResult found\n");
+    if (decrypt_result->session_key == NULL) return !! fprintf (stderr, "GMimeDecryptResult has no session key\n");
+
+    printf ("%s\n", decrypt_result->session_key);
+    return 0;
+}
+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))
+    elif ${CC} ${CFLAGS} ${gmime_cflags} _check_session_keys.c ${gmime_ldflags} -o _check_session_keys \
+          && 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"
+    else
+       cat <<EOF
+No.
+*** Error: Could not extract session keys from encrypted message.
+
+This is likely due to your GMime having been built against a old
+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 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"
+    fi
+
+    cat > _check_gmime_cert.c <<EOF
+#include <stdio.h>
+#include <gmime/gmime.h>
+
+int main () {
+    GError *error = NULL;
+    GMimeParser *parser = NULL;
+    GMimeApplicationPkcs7Mime *body = NULL;
+    GMimeSignatureList *sig_list = NULL;
+    GMimeSignature *sig = NULL;
+    GMimeCertificate *cert = NULL;
+    GMimeObject *output = NULL;
+    int len;
+
+    g_mime_init ();
+    parser = g_mime_parser_new ();
+    g_mime_parser_init_with_stream (parser, g_mime_stream_file_open("$srcdir/test/corpora/pkcs7/smime-onepart-signed.eml", "r", &error));
+    if (error) return !! fprintf (stderr, "failed to instantiate parser with test/corpora/pkcs7/smime-onepart-signed.eml\n");
+
+    body = GMIME_APPLICATION_PKCS7_MIME(g_mime_message_get_mime_part (g_mime_parser_construct_message (parser, NULL)));
+    if (body == NULL) return !!        fprintf (stderr, "did not find a application/pkcs7 message\n");
+
+    sig_list = g_mime_application_pkcs7_mime_verify (body, GMIME_VERIFY_NONE, &output, &error);
+    if (error || output == NULL) return !! fprintf (stderr, "verify failed\n");
+
+    if (sig_list == NULL) return !! fprintf (stderr, "no GMimeSignatureList found\n");
+    len = g_mime_signature_list_length (sig_list);
+    if (len != 1) return !! fprintf (stderr, "expected 1 signature, got %d\n", len);
+    sig = g_mime_signature_list_get_signature (sig_list, 0);
+    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");
+#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} -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
+*** 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
+       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))
+    fi
+    if [ -n "$TEMP_GPG" -a -d "$TEMP_GPG" ]; then
+       rm -rf "$TEMP_GPG"
+    fi
+
+    # see https://dev.gnupg.org/T3464
+    # there are problems verifying signatures when decrypting with session keys with GPGME 1.13.0 and 1.13.1
+    printf "Checking signature verification when decrypting using session keys... "
+
+    cat > _verify_sig_with_session_key.c <<EOF
+#include <stdio.h>
+#include <gmime/gmime.h>
+
+int main () {
+    GError *error = NULL;
+    GMimeParser *parser = NULL;
+    GMimeMultipartEncrypted *body = NULL;
+    GMimeDecryptResult *result = NULL;
+    GMimeSignatureList *sig_list = NULL;
+    GMimeSignature *sig = NULL;
+    GMimeObject *output = NULL;
+    GMimeSignatureStatus status;
+    int len;
+
+    g_mime_init ();
+    parser = g_mime_parser_new ();
+    g_mime_parser_init_with_stream (parser, g_mime_stream_file_open("$srcdir/test/corpora/crypto/encrypted-signed.eml", "r", &error));
+    if (error) return !! fprintf (stderr, "failed to instantiate parser with test/corpora/pkcs7/smime-onepart-signed.eml\n");
+
+    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:9E1CDF53BBF794EA34F894B5B68E1E56FB015EA69F81D2A5EAB7F96C7B65783E", &result, &error);
+    if (error || output == NULL) return !! fprintf (stderr, "decrypt failed\n");
+
+    sig_list = g_mime_decrypt_result_get_signatures (result);
+    if (sig_list == NULL) return !! fprintf (stderr, "sig_list is NULL\n");
+
+    if (sig_list == NULL) return !! fprintf (stderr, "no GMimeSignatureList found\n");
+    len = g_mime_signature_list_length (sig_list);
+    if (len != 1) return !! fprintf (stderr, "expected 1 signature, got %d\n", len);
+    sig = g_mime_signature_list_get_signature (sig_list, 0);
+    if (sig == NULL) return !! fprintf (stderr, "no GMimeSignature found at position 0\n");
+    status = g_mime_signature_get_status (sig);
+    if (status & GMIME_SIGNATURE_STATUS_KEY_MISSING) return !! fprintf (stderr, "signature status contains KEY_MISSING (see https://dev.gnupg.org/T3464)\n");
+
+    return 0;
+}
+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))
+    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/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
+*** 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
+    else
+       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"
+    fi
+else
+    have_gmime=0
+    printf "No.\n"
+    errors=$((errors + 1))
+fi
+
+# GMime already depends on Glib >= 2.12, but we use at least one Glib
+# function that only exists as of 2.22, (g_array_unref)
+printf "Checking for Glib development files (>= 2.22)... "
+have_glib=0
+if pkg-config --exists 'glib-2.0 >= 2.22'; then
+    printf "Yes.\n"
+    have_glib=1
+    # these are included in gmime cflags and ldflags
+    # glib_cflags=$(pkg-config --cflags glib-2.0)
+    # glib_ldflags=$(pkg-config --libs glib-2.0)
+else
+    printf "No.\n"
+    errors=$((errors + 1))
+fi
+
+if ! pkg-config --exists zlib; then
+  ${CC} -o compat/gen_zlib_pc "$srcdir"/compat/gen_zlib_pc.c >/dev/null 2>&1 &&
+  compat/gen_zlib_pc > compat/zlib.pc &&
+  PKG_CONFIG_PATH=${PKG_CONFIG_PATH:+$PKG_CONFIG_PATH:}compat &&
+  export PKG_CONFIG_PATH
+  rm -f compat/gen_zlib_pc
+fi
+
+printf "Checking for zlib (>= 1.2.5.2)... "
+have_zlib=0
+if pkg-config --atleast-version=1.2.5.2 zlib; then
+    printf "Yes.\n"
+    have_zlib=1
+    zlib_cflags=$(pkg-config --cflags zlib)
+    zlib_ldflags=$(pkg-config --libs zlib)
+else
+    printf "No.\n"
+    errors=$((errors + 1))
+fi
+
+printf "Checking for talloc development files... "
+if pkg-config --exists talloc; then
+    printf "Yes.\n"
+    have_talloc=1
+    talloc_cflags=$(pkg-config --cflags talloc)
+    talloc_ldflags=$(pkg-config --libs talloc)
+else
+    printf "No.\n"
+    have_talloc=0
+    talloc_cflags=
+    errors=$((errors + 1))
+fi
+
+printf "Checking for bash... "
+if command -v ${BASHCMD} > /dev/null; then
+    have_bash=1
+    bash_absolute=$(command -v ${BASHCMD})
+    printf "Yes (%s).\n" "$bash_absolute"
+else
+    have_bash=0
+    bash_absolute=
+    printf "No. (%s not found)\n" "${BASHCMD}"
+fi
+
+printf "Checking for perl... "
+if command -v ${PERL} > /dev/null; then
+    have_perl=1
+    perl_absolute=$(command -v ${PERL})
+    printf "Yes (%s).\n" "$perl_absolute"
+else
+    have_perl=0
+    perl_absolute=
+    printf "No. (%s not found)\n" "${PERL}"
+fi
+
+printf "Checking for python... "
+have_python=0
+
+for name in ${PYTHON} python3 python python2; do
+    if command -v $name > /dev/null; then
+       have_python=1
+       python=$name
+       printf "Yes (%s).\n" "$name"
+       break
+    fi
+done
+
+if [ $have_python -eq 0 ]; then
+    printf "No.\n"
+    errors=$((errors + 1))
+fi
+
+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
+    else
+       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_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
+    else
+       WITH_PYTHON_DOCS=0
+       printf "No (will not install CFFI-based python bindings).\n"
+    fi
+    rm -rf __pycache__  # cffi.FFI().verify() uses this space
+
+    printf "Checking for python3 pytest (>= 3.0)... "
+    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
+    else
+       printf "No (will not test CFFI-based python bindings).\n"
+    fi
+    rm -f $conf
+fi
+
+printf "Checking for valgrind development files... "
+if pkg-config --exists valgrind; then
+    printf "Yes.\n"
+    have_valgrind=1
+    valgrind_cflags=$(pkg-config --cflags valgrind)
+else
+    printf "No (but that's fine).\n"
+    have_valgrind=0
+    valgrind_cflags=
+fi
+
+printf "Checking for bash-completion (>= 1.90)... "
+if pkg-config --atleast-version=1.90 bash-completion; then
+    printf "Yes.\n"
+else
+    printf "No (will not install bash completion).\n"
+    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
+
+if [ -z "${EMACSETCDIR-}" ]; then
+    EMACSETCDIR="\$(prefix)/share/emacs/site-lisp"
+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"
+    else
+       printf "No (disabling emacs related parts of build)\n"
+       WITH_EMACS=0
+    fi
+fi
+
+have_doxygen=0
+if [ $WITH_API_DOCS = "1" ] ; then
+    printf "Checking if doxygen is available... "
+    if command -v doxygen > /dev/null; then
+       printf "Yes.\n"
+       have_doxygen=1
+    else
+       printf "No (so will not install api docs)\n"
+    fi
+fi
+
+have_ruby_dev=0
+if [ $WITH_RUBY = "1" ] ; then
+    printf "Checking for ruby development files... "
+    if ${RUBY} -e "require 'mkmf'"> /dev/null 2>&1; then
+       printf "Yes.\n"
+       have_ruby_dev=1
+    else
+       printf "No (skipping ruby bindings)\n"
+    fi
+fi
+
+have_sphinx=0
+have_makeinfo=0
+have_install_info=0
+if [ $WITH_DOCS = "1" ] ; then
+    printf "Checking if sphinx is available and supports nroff output... "
+    if command -v sphinx-build > /dev/null && ${python} -m sphinx.writers.manpage > /dev/null 2>&1 ; then
+       printf "Yes.\n"
+       have_sphinx=1
+    else
+       printf "No (so will not install man pages).\n"
+    fi
+    printf "Checking if makeinfo is available... "
+    if command -v makeinfo > /dev/null; then
+       printf "Yes.\n"
+       have_makeinfo=1
+    else
+       printf "No (so will not build info pages).\n"
+    fi
+    printf "Checking if install-info is available... "
+    if command -v install-info > /dev/null; then
+       printf "Yes.\n"
+       have_install_info=1
+    else
+       printf "No (so will not install info pages).\n"
+    fi
+fi
+
+if [ $WITH_DESKTOP = "1" ]; then
+    printf "Checking if desktop-file-install is available... "
+    if command -v desktop-file-install > /dev/null; then
+       printf "Yes.\n"
+    else
+       printf "No (so will not install .desktop file).\n"
+       WITH_DESKTOP=0
+    fi
+fi
+
+printf "Checking for cppcheck... "
+if command -v cppcheck > /dev/null; then
+    have_cppcheck=1
+    printf "Yes.\n"
+else
+    have_cppcheck=0
+    printf "No.\n"
+fi
+
+libdir_in_ldconfig=0
+
+printf "Checking which platform we are on... "
+uname=$(uname)
+if [ $uname = "Darwin" ] ; then
+    printf "Mac OS X.\n"
+    platform=MACOSX
+    linker_resolves_library_dependencies=0
+elif [ $uname = "SunOS" ] ; then
+    printf "Solaris.\n"
+    platform=SOLARIS
+    linker_resolves_library_dependencies=0
+elif [ $uname = "FreeBSD" ] ; then
+    printf "FreeBSD.\n"
+    platform=FREEBSD
+    linker_resolves_library_dependencies=0
+elif [ $uname = "OpenBSD" ] ; then
+    printf "OpenBSD.\n"
+    platform=OPENBSD
+    linker_resolves_library_dependencies=0
+elif [ $uname = "Linux" ] || [ $uname = "GNU" ] ; then
+    printf "%s\n" "$uname"
+    platform="$uname"
+    linker_resolves_library_dependencies=1
+
+    printf "Checking for %s in ldconfig... " "$libdir_expanded"
+    ldconfig_paths=$(/sbin/ldconfig -N -X -v 2>/dev/null | sed -n -e 's,^\(/.*\):\( (.*)\)\?$,\1,p')
+    # Separate ldconfig_paths only on newline (not on any potential
+    # embedded space characters in any filenames). Note, we use a
+    # literal newline in the source here rather than something like:
+    #
+    #  IFS=$(printf '\n')
+    #
+    # because the shell's command substitution deletes any trailing newlines.
+    IFS="
+"
+    for path in $ldconfig_paths; do
+       if [ "$path" -ef "$libdir_expanded" ]; then
+           libdir_in_ldconfig=1
+       fi
+    done
+    IFS=$DEFAULT_IFS
+    if [ "$libdir_in_ldconfig" = '0' ]; then
+       printf "No (will set RPATH)\n"
+    else
+       printf "Yes\n"
+    fi
+else
+    printf "Unknown.\n"
+    platform="$uname"
+    linker_resolves_library_dependencies=0
+    cat <<EOF
+
+*** Warning: Unknown platform. Notmuch might or might not build correctly.
+
+EOF
+fi
+
+if [ $errors -gt 0 ]; then
+    cat <<EOF
+
+*** Error: The dependencies of notmuch could not be satisfied. You will
+need to install the following packages before being able to compile
+notmuch:
+
+EOF
+    if [ $have_python -eq 0 ]; then
+       echo "  python interpreter"
+    fi
+    if [ $have_xapian -eq 0 ]; then
+       echo "  Xapian library (>= version 1.4.0, including development files such as headers)"
+       echo "  https://xapian.org/"
+    fi
+    if [ $have_zlib -eq 0 ]; then
+       echo "  zlib library (>= version 1.2.5.2, including development files such as headers)"
+       echo "  https://zlib.net/"
+       echo
+    fi
+    if [ $have_gmime -eq 0 ]; then
+       echo "  GMime library >= $GMIME_MINVER"
+       echo "  (including development files such as headers)"
+       echo "  https://github.com/jstedfast/gmime/"
+       echo
+    fi
+    if [ $have_glib -eq 0 ]; then
+       echo "  Glib library >= 2.22 (including development files such as headers)"
+       echo "  https://ftp.gnome.org/pub/gnome/sources/glib/"
+       echo
+    fi
+    if [ $have_talloc -eq 0 ]; then
+       echo "  The talloc library (including development files such as headers)"
+       echo "  https://talloc.samba.org/"
+       echo
+    fi
+    cat <<EOF
+With any luck, you're using a modern, package-based operating system
+that has all of these packages available in the distribution. In that
+case a simple command will install everything you need. For example:
+
+On Debian and similar systems:
+
+       sudo apt-get install libxapian-dev libgmime-3.0-dev libtalloc-dev zlib1g-dev
+
+Or on Fedora and similar systems:
+
+       sudo dnf install xapian-core-devel gmime30-devel libtalloc-devel zlib-devel
+
+On other systems, similar commands can be used, but the details of the
+package names may be different.
+
+EOF
+    if [ $have_pkg_config -eq 0 ]; then
+cat <<EOF
+Note: the pkg-config program is not available. This configure script
+uses pkg-config to find the compilation flags required to link against
+the various libraries needed by notmuch. It's possible you simply need
+to install pkg-config with a command such as:
+
+       sudo apt-get install pkg-config
+Or:
+       sudo dnf install pkgconfig
+
+But if pkg-config is not available for your system, then you will need
+to modify the configure script to manually set the cflags and ldflags
+variables to the correct values to link against each library in each
+case that pkg-config could not be used to determine those values.
+
+EOF
+    fi
+cat <<EOF
+When you have installed the necessary dependencies, you can run
+configure again to ensure the packages can be found, or simply run
+"make" to compile notmuch.
+
+EOF
+    exit 1
+fi
+
+printf "Checking for canonicalize_file_name... "
+if ${CC} -o compat/have_canonicalize_file_name "$srcdir"/compat/have_canonicalize_file_name.c > /dev/null 2>&1
+then
+    printf "Yes.\n"
+    have_canonicalize_file_name=1
+else
+    printf "No (will use our own instead).\n"
+    have_canonicalize_file_name=0
+fi
+rm -f compat/have_canonicalize_file_name
+
+
+printf "Checking for getline... "
+if ${CC} -o compat/have_getline "$srcdir"/compat/have_getline.c > /dev/null 2>&1
+then
+    printf "Yes.\n"
+    have_getline=1
+else
+    printf "No (will use our own instead).\n"
+    have_getline=0
+fi
+rm -f compat/have_getline
+
+printf "Checking for strcasestr... "
+if ${CC} -o compat/have_strcasestr "$srcdir"/compat/have_strcasestr.c > /dev/null 2>&1
+then
+    printf "Yes.\n"
+    have_strcasestr=1
+else
+    printf "No (will use our own instead).\n"
+    have_strcasestr=0
+fi
+rm -f compat/have_strcasestr
+
+printf "Checking for strsep... "
+if ${CC} -o compat/have_strsep "$srcdir"/compat/have_strsep.c > /dev/null 2>&1
+then
+    printf "Yes.\n"
+    have_strsep="1"
+else
+    printf "No (will use our own instead).\n"
+    have_strsep="0"
+fi
+rm -f compat/have_strsep
+
+printf "Checking for timegm... "
+if ${CC} -o compat/have_timegm "$srcdir"/compat/have_timegm.c > /dev/null 2>&1
+then
+    printf "Yes.\n"
+    have_timegm="1"
+else
+    printf "No (will use our own instead).\n"
+    have_timegm="0"
+fi
+rm -f compat/have_timegm
+
+cat <<EOF > _time_t.c
+#include <time.h>
+#include <assert.h>
+static_assert(sizeof(time_t) >= 8, "sizeof(time_t) < 8");
+EOF
+
+printf "Checking for 64 bit time_t... "
+if ${CC} -c _time_t.c -o /dev/null
+then
+    printf "Yes.\n"
+    have_64bit_time_t=1
+else
+    printf "No.\n"
+    have_64bit_time_t=0
+fi
+
+printf "Checking for dirent.d_type... "
+if ${CC} -o compat/have_d_type "$srcdir"/compat/have_d_type.c > /dev/null 2>&1
+then
+    printf "Yes.\n"
+    have_d_type="1"
+else
+    printf "No (will use stat instead).\n"
+    have_d_type="0"
+fi
+rm -f compat/have_d_type
+
+printf "Checking for standard version of getpwuid_r... "
+if ${CC} -o compat/check_getpwuid "$srcdir"/compat/check_getpwuid.c > /dev/null 2>&1
+then
+    printf "Yes.\n"
+    std_getpwuid=1
+else
+    printf "No (will define _POSIX_PTHREAD_SEMANTICS to get it).\n"
+    std_getpwuid=0
+fi
+rm -f compat/check_getpwuid
+
+printf "Checking for standard version of asctime_r... "
+if ${CC} -o compat/check_asctime "$srcdir"/compat/check_asctime.c > /dev/null 2>&1
+then
+    printf "Yes.\n"
+    std_asctime=1
+else
+    printf "No (will define _POSIX_PTHREAD_SEMANTICS to get it).\n"
+    std_asctime=0
+fi
+rm -f compat/check_asctime
+
+printf "Checking for rpath support... "
+if [ $WITH_RPATH = "1" ] && ${CC} -Wl,--enable-new-dtags -Wl,-rpath,/tmp/ -o minimal minimal.c >/dev/null 2>&1
+then
+    printf "Yes.\n"
+    rpath_ldflags="-Wl,--enable-new-dtags -Wl,-rpath,\$(libdir)"
+else
+    printf "No (nothing to worry about).\n"
+    rpath_ldflags=""
+fi
+
+printf "Checking for -Wl,--as-needed... "
+if ${CC} -Wl,--as-needed -o minimal minimal.c >/dev/null 2>&1
+then
+    printf "Yes.\n"
+    as_needed_ldflags="-Wl,--as-needed"
+else
+    printf "No (nothing to worry about).\n"
+    as_needed_ldflags=""
+fi
+
+printf "Checking for -Wl,--no-undefined... "
+if ${CC} -Wl,--no-undefined -o minimal minimal.c >/dev/null 2>&1
+then
+    printf "Yes.\n"
+    no_undefined_ldflags="-Wl,--no-undefined"
+else
+    printf "No (nothing to worry about).\n"
+    no_undefined_ldflags=""
+fi
+
+WARN_CXXFLAGS=""
+printf "Checking for available C++ compiler warning flags... "
+for flag in -Wall -Wextra -Wwrite-strings; do
+    if ${CC} $flag -o minimal minimal.c > /dev/null 2>&1
+    then
+       WARN_CXXFLAGS="${WARN_CXXFLAGS}${WARN_CXXFLAGS:+ }${flag}"
+    fi
+done
+printf "\n\t%s\n" "${WARN_CXXFLAGS}"
+
+WARN_CFLAGS="${WARN_CXXFLAGS}"
+printf "Checking for available C compiler warning flags... "
+for flag in -Wmissing-declarations; do
+    if ${CC} $flag -o minimal minimal.c > /dev/null 2>&1
+    then
+       WARN_CFLAGS="${WARN_CFLAGS}${WARN_CFLAGS:+ }${flag}"
+    fi
+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_gmime_cert.c _check_x509_validity _check_email \
+   _verify_sig_with_session_key.c _verify_sig_with_session_key
+
+# construct the Makefile.config
+cat > Makefile.config <<EOF
+# This Makefile.config was automatically generated by the ./configure
+# script of notmuch. If the configure script identified anything
+# incorrectly, then you can edit this file to try to correct things,
+# but be warned that if configure is run again it will destroy your
+# changes, (and this could happen by simply calling "make" if the
+# configure script is updated).
+
+# The top-level directory for the source, (the directory containing
+# the configure script). This may be different than the build
+# 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}
+
+configure_options = $@
+
+# We use vpath directives (rather than the VPATH variable) since the
+# VPATH variable matches targets as well as prerequisites, (which is
+# not useful since then a target left-over from a srcdir build would
+# cause a target to not be built in the non-srcdir build).
+#
+# Also, we don't use a single "vpath % \$(srcdir)" here because we
+# don't want the vpath to trigger for our emacs lisp compilation,
+# (unless we first find a way to convince emacs to build the .elc
+# target in a directory other than the directory of the .el
+# prerequisite). In the meantime, we're actually copying in the .el
+# files, (which is quite ugly).
+vpath %.c \$(srcdir)
+vpath %.cc \$(srcdir)
+vpath Makefile.% \$(srcdir)
+vpath %.py \$(srcdir)
+vpath %.rst \$(srcdir)
+
+# Library versions (used to make SONAME)
+# The major version of the library interface. This will control the soname.
+# As such, this number must be incremented for any incompatible change to
+# the library interface, (such as the deletion of an API or a major
+# semantic change that breaks formerly functioning code).
+#
+LIBNOTMUCH_VERSION_MAJOR = ${libnotmuch_version_major}
+
+# The minor version of the library interface. This should be incremented at
+# the time of release for any additions to the library interface,
+# (and when it is incremented, the release version of the library should
+#  be reset to 0).
+LIBNOTMUCH_VERSION_MINOR = ${libnotmuch_version_minor}
+
+# The release version the library interface. This should be incremented at
+# the time of release if there have been no changes to the interface, (but
+# simply compatible changes to the implementation).
+LIBNOTMUCH_VERSION_RELEASE = ${libnotmuch_version_release}
+
+# These are derived from the VERSION macros in lib/notmuch.h so
+# if you have to change them, something is wrong.
+
+# The C compiler to use
+CC = ${CC}
+
+# The C++ compiler to use
+CXX = ${CXX}
+
+# Command to execute emacs from Makefiles
+EMACS = emacs --quick
+
+# Default FLAGS for C compiler (can be overridden by user such as "make CFLAGS=-g")
+CFLAGS = ${CFLAGS}
+
+# Default FLAGS for C preprocessor (can be overridden by user such as "make CPPFLAGS=-I/usr/local/include")
+CPPFLAGS = ${CPPFLAGS}
+
+# Default FLAGS for C++ compiler (can be overridden by user such as "make CXXFLAGS=-g")
+CXXFLAGS = ${CXXFLAGS}
+
+# Default FLAGS for the linker (can be overridden by user such as "make LDFLAGS=-znow")
+LDFLAGS = ${LDFLAGS}
+
+# Flags to enable warnings when using the C++ compiler
+WARN_CXXFLAGS=${WARN_CXXFLAGS}
+
+# Flags to enable warnings when using the C compiler
+WARN_CFLAGS=${WARN_CFLAGS}
+
+# Name of python interpreter
+PYTHON = ${python}
+
+# Name of ruby interpreter
+RUBY = ${RUBY}
+
+# The prefix to which notmuch should be installed
+# Note: If you change this value here, be sure to ensure that the
+# LIBDIR_IN_LDCONFIG value below is still set correctly.
+prefix = ${PREFIX}
+
+# The directory to which libraries should be installed
+# Note: If you change this value here, be sure to ensure that the
+# LIBDIR_IN_LDCONFIG value below is still set correctly.
+libdir = ${LIBDIR:=\$(prefix)/lib}
+
+# Whether libdir is in a path configured into ldconfig
+LIBDIR_IN_LDCONFIG = ${libdir_in_ldconfig}
+
+# The directory to which header files should be installed
+includedir = ${INCLUDEDIR:=\$(prefix)/include}
+
+# The directory to which man pages should be installed
+mandir = ${MANDIR:=\$(prefix)/share/man}
+
+# The directory to which man pages should be installed
+infodir = ${INFODIR:=\$(prefix)/share/info}
+
+# The directory to which read-only (configuration) files should be installed
+sysconfdir = ${SYSCONFDIR:=\$(prefix)/etc}
+
+# The directory to which emacs lisp files should be installed
+emacslispdir=${EMACSLISPDIR}
+
+# The directory to which emacs miscellaneous (machine-independent) files should
+# be installed
+emacsetcdir=${EMACSETCDIR}
+
+# Whether bash exists, and if so where
+HAVE_BASH = ${have_bash}
+BASH_ABSOLUTE = ${bash_absolute}
+
+# Whether perl exists, and if so where
+HAVE_PERL = ${have_perl}
+PERL_ABSOLUTE = ${perl_absolute}
+
+# Whether there's a sphinx-build binary available for building documentation
+HAVE_SPHINX=${have_sphinx}
+
+# Whether there's a makeinfo binary available for building info format documentation
+HAVE_MAKEINFO=${have_makeinfo}
+
+# Whether there's an install-info binary available for installing info format documentation
+HAVE_INSTALL_INFO=${have_install_info}
+
+# Whether there's a doxygen binary available for building api documentation
+HAVE_DOXYGEN=${have_doxygen}
+
+# The directory to which desktop files should be installed
+desktop_dir = \$(prefix)/share/applications
+
+# The directory to which bash completions files should be installed
+bash_completion_dir = ${BASHCOMPLETIONDIR:=\$(prefix)/share/bash-completion/completions}
+
+# The directory to which zsh completions files should be installed
+zsh_completion_dir = ${ZSHCOMLETIONDIR:=\$(prefix)/share/zsh/site-functions}
+
+# Whether the canonicalize_file_name function is available (if not, then notmuch will
+# build its own version)
+HAVE_CANONICALIZE_FILE_NAME = ${have_canonicalize_file_name}
+
+# Whether the cppcheck static checker is available
+HAVE_CPPCHECK = ${have_cppcheck}
+
+# Whether the getline function is available (if not, then notmuch will
+# build its own version)
+HAVE_GETLINE = ${have_getline}
+
+# Are the ruby development files (and ruby) available? If not skip
+# building/testing ruby bindings.
+HAVE_RUBY_DEV = ${have_ruby_dev}
+
+# Is the python cffi package available?
+HAVE_PYTHON3_CFFI = ${have_python3_cffi}
+
+# Is the python pytest package available?
+HAVE_PYTHON3_PYTEST = ${have_python3_pytest}
+
+# Whether the strcasestr function is available (if not, then notmuch will
+# build its own version)
+HAVE_STRCASESTR = ${have_strcasestr}
+
+# Whether the strsep function is available (if not, then notmuch will
+# build its own version)
+HAVE_STRSEP = ${have_strsep}
+
+# Whether the timegm function is available (if not, then notmuch will
+# build its own version)
+HAVE_TIMEGM = ${have_timegm}
+
+# Whether struct dirent has d_type (if not, then notmuch will use stat)
+HAVE_D_TYPE = ${have_d_type}
+
+# Whether to have Xapian retry lock
+HAVE_XAPIAN_DB_RETRY_LOCK = ${WITH_RETRY_LOCK}
+
+# Whether the getpwuid_r function is standards-compliant
+# (if not, then notmuch will #define _POSIX_PTHREAD_SEMANTICS
+# to enable the standards-compliant version -- needed for Solaris)
+STD_GETPWUID = ${std_getpwuid}
+
+# Whether the asctime_r function is standards-compliant
+# (if not, then notmuch will #define _POSIX_PTHREAD_SEMANTICS
+# to enable the standards-compliant version -- needed for Solaris)
+STD_ASCTIME = ${std_asctime}
+
+# Supported platforms (so far) are: LINUX, MACOSX, SOLARIS, FREEBSD, OPENBSD
+PLATFORM = ${platform}
+
+# Whether the linker will automatically resolve the dependency of one
+# library on another (if not, then linking a binary requires linking
+# directly against both)
+LINKER_RESOLVES_LIBRARY_DEPENDENCIES = ${linker_resolves_library_dependencies}
+
+# Flags needed to compile and link against Xapian
+XAPIAN_CXXFLAGS = ${xapian_cxxflags}
+XAPIAN_LDFLAGS = ${xapian_ldflags}
+
+# Flags needed to compile and link against GMime
+GMIME_CFLAGS = ${gmime_cflags}
+GMIME_LDFLAGS = ${gmime_ldflags}
+
+# Flags needed to compile and link against zlib
+ZLIB_CFLAGS = ${zlib_cflags}
+ZLIB_LDFLAGS = ${zlib_ldflags}
+
+# Flags needed to compile and link against talloc
+TALLOC_CFLAGS = ${talloc_cflags}
+TALLOC_LDFLAGS = ${talloc_ldflags}
+
+# Flags needed to have linker set rpath attribute
+RPATH_LDFLAGS = ${rpath_ldflags}
+
+# Flags needed to have linker link only to necessary libraries
+AS_NEEDED_LDFLAGS = ${as_needed_ldflags}
+
+# Flags to have the linker flag undefined symbols in object files
+NO_UNDEFINED_LDFLAGS = ${no_undefined_ldflags}
+
+# Whether valgrind header files are available
+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}
+
+# Support for desktop file
+WITH_DESKTOP = ${WITH_DESKTOP}
+
+# Support for bash completion
+WITH_BASH = ${WITH_BASH}
+
+# Support for zsh completion
+WITH_ZSH = ${WITH_ZSH}
+
+# Combined flags for compiling and linking against all of the above
+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) \\
+       -DHAVE_STRCASESTR=\$(HAVE_STRCASESTR)                   \\
+       -DHAVE_STRSEP=\$(HAVE_STRSEP)                           \\
+       -DHAVE_TIMEGM=\$(HAVE_TIMEGM)                           \\
+       -DHAVE_D_TYPE=\$(HAVE_D_TYPE)                           \\
+       -DSTD_GETPWUID=\$(STD_GETPWUID)                         \\
+       -DSTD_ASCTIME=\$(STD_ASCTIME)                           \\
+       -DSILENCE_XAPIAN_DEPRECATION_WARNINGS                   \\
+       -DHAVE_XAPIAN_DB_RETRY_LOCK=\$(HAVE_XAPIAN_DB_RETRY_LOCK)
+
+CONFIGURE_CFLAGS = \$(COMMON_CONFIGURE_CFLAGS)
+
+CONFIGURE_CXXFLAGS = \$(COMMON_CONFIGURE_CFLAGS) \$(XAPIAN_CXXFLAGS)
+
+CONFIGURE_LDFLAGS = \$(GMIME_LDFLAGS) \$(TALLOC_LDFLAGS) \$(ZLIB_LDFLAGS) \$(XAPIAN_LDFLAGS) \$(SFSEXP_LDFLAGS)
+EOF
+
+# construct the sh.config
+cat > sh.config <<EOF
+# This sh.config was automatically generated by the ./configure
+# script of notmuch.
+
+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))
+
+# Whether bash exists, and if so where
+NOTMUCH_HAVE_BASH=${have_bash}
+NOTMUCH_BASH_ABSOLUTE=${bash_absolute}
+
+# Whether time_t is 64 bits (or more)
+NOTMUCH_HAVE_64BIT_TIME_T=${have_64bit_time_t}
+
+# Whether perl exists, and if so where
+NOTMUCH_HAVE_PERL=${have_perl}
+NOTMUCH_PERL_ABSOLUTE=${perl_absolute}
+
+# Name of python interpreter
+NOTMUCH_PYTHON=${python}
+
+# Name of ruby interpreter
+NOTMUCH_RUBY=${RUBY}
+
+# Are the ruby development files (and ruby) available? If not skip
+# building/testing ruby bindings.
+NOTMUCH_HAVE_RUBY_DEV=${have_ruby_dev}
+
+# Is the python cffi package available?
+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
+
+{
+    echo "# Generated by configure, run from doc/conf.py"
+    if [ $WITH_EMACS = "1" ]; then
+       echo "tags.add('WITH_EMACS')"
+    fi
+    if [ $WITH_PYTHON_DOCS = "1" ]; then
+       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
+
+All required packages were found. You may now run the following
+commands to compile and install notmuch:
+
+       make
+       sudo make install
+
+EOF
diff --git a/contrib/go/.gitignore b/contrib/go/.gitignore
new file mode 100644 (file)
index 0000000..223504b
--- /dev/null
@@ -0,0 +1,3 @@
+/src/github.com/
+/pkg/
+/bin/
diff --git a/contrib/go/LICENSE b/contrib/go/LICENSE
new file mode 100644 (file)
index 0000000..4362b49
--- /dev/null
@@ -0,0 +1,502 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+\f
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+\f
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+\f
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library 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
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/contrib/go/Makefile b/contrib/go/Makefile
new file mode 100644 (file)
index 0000000..1b9e750
--- /dev/null
@@ -0,0 +1,40 @@
+# Makefile for the go bindings of notmuch
+
+export GOPATH      ?= $(shell pwd)
+export CGO_CFLAGS  ?= -I../../../../lib
+export CGO_LDFLAGS ?= -L../../../../lib
+
+GO         ?= go
+GOFMT      ?= gofmt
+
+all: notmuch notmuch-addrlookup
+
+.PHONY: notmuch
+notmuch:
+       $(GO) install notmuch
+
+.PHONY: goconfig
+goconfig:
+       if [ ! -d github.com/msbranco/goconfig ]; then \
+           $(GO) get github.com/msbranco/goconfig; \
+       fi
+
+.PHONY: notmuch-addrlookup
+notmuch-addrlookup: notmuch goconfig
+       $(GO) install notmuch-addrlookup
+
+.PHONY: format
+format:
+       $(GOFMT) -w=true $(GOFMT_OPTS) src/notmuch
+       $(GOFMT) -w=true $(GOFMT_OPTS) src/notmuch-addrlookup
+
+.PHONY: check-format
+check-format:
+       $(GOFMT) -d=true $(GOFMT_OPTS) src/notmuch
+       $(GOFMT) -d=true $(GOFMT_OPTS) src/notmuch-addrlookup
+
+.PHONY: clean
+clean:
+       $(GO) clean notmuch
+       $(GO) clean notmuch-addrlookup
+       rm -rf pkg bin
diff --git a/contrib/go/README b/contrib/go/README
new file mode 100644 (file)
index 0000000..1825ae0
--- /dev/null
@@ -0,0 +1,16 @@
+go-notmuch
+==========
+
+simple go bindings to the libnotmuch library[1].
+
+they are heavily inspired from the vala bindings from Sebastian Spaeth: 
+  https://github.com/spaetz/vala-notmuch
+
+note: the whole library hasn't been wrapped (yet?)
+
+todo
+----
+
+ - improve notmuch-addrlookup regexp
+
+[1] https://notmuchmail.org/
diff --git a/contrib/go/src/notmuch-addrlookup/addrlookup.go b/contrib/go/src/notmuch-addrlookup/addrlookup.go
new file mode 100644 (file)
index 0000000..916e5bb
--- /dev/null
@@ -0,0 +1,261 @@
+package main
+
+// stdlib imports
+import "os"
+import "path"
+import "log"
+import "fmt"
+import "regexp"
+import "strings"
+import "sort"
+
+// 3rd-party imports
+import "notmuch"
+import "github.com/msbranco/goconfig"
+
+type mail_addr_freq struct {
+       addr  string
+       count [3]uint
+}
+
+type frequencies map[string]uint
+
+/* Used to sort the email addresses from most to least used */
+func sort_by_freq(m1, m2 *mail_addr_freq) int {
+       if m1.count[0] == m2.count[0] &&
+               m1.count[1] == m2.count[1] &&
+               m1.count[2] == m2.count[2] {
+               return 0
+       }
+
+       if m1.count[0] > m2.count[0] ||
+               m1.count[0] == m2.count[0] &&
+                       m1.count[1] > m2.count[1] ||
+               m1.count[0] == m2.count[0] &&
+                       m1.count[1] == m2.count[1] &&
+                       m1.count[2] > m2.count[2] {
+               return -1
+       }
+
+       return 1
+}
+
+type maddresses []*mail_addr_freq
+
+func (self *maddresses) Len() int {
+       return len(*self)
+}
+
+func (self *maddresses) Less(i, j int) bool {
+       m1 := (*self)[i]
+       m2 := (*self)[j]
+       v := sort_by_freq(m1, m2)
+       if v <= 0 {
+               return true
+       }
+       return false
+}
+
+func (self *maddresses) Swap(i, j int) {
+       (*self)[i], (*self)[j] = (*self)[j], (*self)[i]
+}
+
+// find most frequent real name for each mail address
+func frequent_fullname(freqs frequencies) string {
+       var maxfreq uint = 0
+       fullname := ""
+       freqs_sz := len(freqs)
+
+       for mail, freq := range freqs {
+               if (freq > maxfreq && mail != "") || freqs_sz == 1 {
+                       // only use the entry if it has a real name
+                       // or if this is the only entry
+                       maxfreq = freq
+                       fullname = mail
+               }
+       }
+       return fullname
+}
+
+func addresses_by_frequency(msgs *notmuch.Messages, name string, pass uint, addr_to_realname *map[string]*frequencies) *frequencies {
+
+       freqs := make(frequencies)
+
+       pattern := `\s*(("(\.|[^"])*"|[^,])*<?(?mail\b\w+([-+.]\w+)*\@\w+[-\.\w]*\.([-\.\w]+)*\w\b)>?)`
+       // pattern := "\\s*((\\\"(\\\\.|[^\\\\\"])*\\\"|[^,])*" +
+       //      "<?(?P<mail>\\b\\w+([-+.]\\w+)*\\@\\w+[-\\.\\w]*\\.([-\\.\\w]+)*\\w\\b)>?)"
+       pattern = `.*` + strings.ToLower(name) + `.*`
+       var re *regexp.Regexp = nil
+       var err error = nil
+       if re, err = regexp.Compile(pattern); err != nil {
+               log.Printf("error: %v\n", err)
+               return &freqs
+       }
+
+       headers := []string{"from"}
+       if pass == 1 {
+               headers = append(headers, "to", "cc", "bcc")
+       }
+
+       for ; msgs.Valid(); msgs.MoveToNext() {
+               msg := msgs.Get()
+               //println("==> msg [", msg.GetMessageId(), "]")
+               for _, header := range headers {
+                       froms := strings.ToLower(msg.GetHeader(header))
+                       //println("  froms: ["+froms+"]")
+                       for _, from := range strings.Split(froms, ",") {
+                               from = strings.Trim(from, " ")
+                               match := re.FindString(from)
+                               //println("  -> match: ["+match+"]")
+                               occ, ok := freqs[match]
+                               if !ok {
+                                       freqs[match] = 0
+                                       occ = 0
+                               }
+                               freqs[match] = occ + 1
+                       }
+               }
+       }
+       return &freqs
+}
+
+func search_address_passes(queries [3]*notmuch.Query, name string) []string {
+       var val []string
+       addr_freq := make(map[string]*mail_addr_freq)
+       addr_to_realname := make(map[string]*frequencies)
+
+       var pass uint = 0 // 0-based
+       for _, query := range queries {
+               if query == nil {
+                       //println("**warning: idx [",idx,"] contains a nil query")
+                       continue
+               }
+               msgs := query.SearchMessages()
+               ht := addresses_by_frequency(msgs, name, pass, &addr_to_realname)
+               for addr, count := range *ht {
+                       freq, ok := addr_freq[addr]
+                       if !ok {
+                               freq = &mail_addr_freq{addr: addr, count: [3]uint{0, 0, 0}}
+                       }
+                       freq.count[pass] = count
+                       addr_freq[addr] = freq
+               }
+               msgs.Destroy()
+               pass += 1
+       }
+
+       addrs := make(maddresses, len(addr_freq))
+       {
+               iaddr := 0
+               for _, freq := range addr_freq {
+                       addrs[iaddr] = freq
+                       iaddr += 1
+               }
+       }
+       sort.Sort(&addrs)
+
+       for _, addr := range addrs {
+               freqs, ok := addr_to_realname[addr.addr]
+               if ok {
+                       val = append(val, frequent_fullname(*freqs))
+               } else {
+                       val = append(val, addr.addr)
+               }
+       }
+       //println("val:",val)
+       return val
+}
+
+type address_matcher struct {
+       // the notmuch database
+       db *notmuch.Database
+       // full path of the notmuch database
+       user_db_path string
+       // user primary email
+       user_primary_email string
+       // user tag to mark from addresses as in the address book
+       user_addrbook_tag string
+}
+
+func new_address_matcher() *address_matcher {
+       // honor NOTMUCH_CONFIG
+       home := os.Getenv("NOTMUCH_CONFIG")
+       if home == "" {
+               home = os.Getenv("HOME")
+       }
+
+       cfg, err := goconfig.ReadConfigFile(path.Join(home, ".notmuch-config"))
+       if err != nil {
+               log.Fatalf("error loading config file:", err)
+       }
+
+       db_path, _ := cfg.GetString("database", "path")
+       primary_email, _ := cfg.GetString("user", "primary_email")
+       addrbook_tag, err := cfg.GetString("user", "addrbook_tag")
+       if err != nil {
+               addrbook_tag = "addressbook"
+       }
+
+       self := &address_matcher{db: nil,
+               user_db_path:       db_path,
+               user_primary_email: primary_email,
+               user_addrbook_tag:  addrbook_tag}
+       return self
+}
+
+func (self *address_matcher) run(name string) {
+       queries := [3]*notmuch.Query{}
+
+       // open the database
+       if db, status := notmuch.OpenDatabase(self.user_db_path,
+               notmuch.DATABASE_MODE_READ_ONLY); status == notmuch.STATUS_SUCCESS {
+               self.db = db
+       } else {
+               log.Fatalf("Failed to open the database: %v\n", status)
+       }
+
+       // pass 1: look at all from: addresses with the address book tag
+       query := "tag:" + self.user_addrbook_tag
+       if name != "" {
+               query = query + " and from:" + name + "*"
+       }
+       queries[0] = self.db.CreateQuery(query)
+
+       // pass 2: look at all to: addresses sent from our primary mail
+       query = ""
+       if name != "" {
+               query = "to:" + name + "*"
+       }
+       if self.user_primary_email != "" {
+               query = query + " from:" + self.user_primary_email
+       }
+       queries[1] = self.db.CreateQuery(query)
+
+       // if that leads only to a few hits, we check every from too
+       if queries[0].CountMessages()+queries[1].CountMessages() < 10 {
+               query = ""
+               if name != "" {
+                       query = "from:" + name + "*"
+               }
+               queries[2] = self.db.CreateQuery(query)
+       }
+
+       // actually retrieve and sort addresses
+       results := search_address_passes(queries, name)
+       for _, v := range results {
+               if v != "" && v != "\n" {
+                       fmt.Println(v)
+               }
+       }
+       return
+}
+
+func main() {
+       //fmt.Println("args:",os.Args)
+       app := new_address_matcher()
+       name := ""
+       if len(os.Args) > 1 {
+               name = os.Args[1]
+       }
+       app.run(name)
+}
diff --git a/contrib/go/src/notmuch/notmuch.go b/contrib/go/src/notmuch/notmuch.go
new file mode 100644 (file)
index 0000000..5496198
--- /dev/null
@@ -0,0 +1,1404 @@
+// Wrapper around the notmuch library
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include "notmuch.h"
+*/
+import "C"
+import "unsafe"
+
+// Status codes used for the return values of most functions
+type Status C.notmuch_status_t
+
+const (
+       STATUS_SUCCESS Status = iota
+       STATUS_OUT_OF_MEMORY
+       STATUS_READ_ONLY_DATABASE
+       STATUS_XAPIAN_EXCEPTION
+       STATUS_FILE_ERROR
+       STATUS_FILE_NOT_EMAIL
+       STATUS_DUPLICATE_MESSAGE_ID
+       STATUS_NULL_POINTER
+       STATUS_TAG_TOO_LONG
+       STATUS_UNBALANCED_FREEZE_THAW
+       STATUS_UNBALANCED_ATOMIC
+
+       STATUS_LAST_STATUS
+)
+
+func (self Status) String() string {
+       var p *C.char
+
+       // p is read-only
+       p = C.notmuch_status_to_string(C.notmuch_status_t(self))
+       if p != nil {
+               s := C.GoString(p)
+               return s
+       }
+       return ""
+}
+
+/* Various opaque data types. For each notmuch_<foo>_t see the various
+ * notmuch_<foo> functions below. */
+
+type Database struct {
+       db *C.notmuch_database_t
+}
+
+type Query struct {
+       query *C.notmuch_query_t
+}
+
+type Threads struct {
+       threads *C.notmuch_threads_t
+}
+
+type Thread struct {
+       thread *C.notmuch_thread_t
+}
+
+type Messages struct {
+       messages *C.notmuch_messages_t
+}
+
+type Message struct {
+       message *C.notmuch_message_t
+}
+
+type Tags struct {
+       tags *C.notmuch_tags_t
+}
+
+type Directory struct {
+       dir *C.notmuch_directory_t
+}
+
+type Filenames struct {
+       fnames *C.notmuch_filenames_t
+}
+
+type DatabaseMode C.notmuch_database_mode_t
+
+const (
+       DATABASE_MODE_READ_ONLY DatabaseMode = iota
+       DATABASE_MODE_READ_WRITE
+)
+
+// Create a new, empty notmuch database located at 'path'
+func NewDatabase(path string) (*Database, Status) {
+
+       var c_path *C.char = C.CString(path)
+       defer C.free(unsafe.Pointer(c_path))
+
+       if c_path == nil {
+               return nil, STATUS_OUT_OF_MEMORY
+       }
+
+       self := &Database{db: nil}
+       st := Status(C.notmuch_database_create(c_path, &self.db))
+       if st != STATUS_SUCCESS {
+               return nil, st
+       }
+       return self, st
+}
+
+/* Open an existing notmuch database located at 'path'.
+ *
+ * The database should have been created at some time in the past,
+ * (not necessarily by this process), by calling
+ * notmuch_database_create with 'path'. By default the database should be
+ * opened for reading only. In order to write to the database you need to
+ * pass the NOTMUCH_DATABASE_MODE_READ_WRITE mode.
+ *
+ * An existing notmuch database can be identified by the presence of a
+ * directory named ".notmuch" below 'path'.
+ *
+ * The caller should call notmuch_database_destroy when finished with
+ * this database.
+ *
+ * In case of any failure, this function returns NULL, (after printing
+ * an error message on stderr).
+ */
+func OpenDatabase(path string, mode DatabaseMode) (*Database, Status) {
+
+       var c_path *C.char = C.CString(path)
+       defer C.free(unsafe.Pointer(c_path))
+
+       if c_path == nil {
+               return nil, STATUS_OUT_OF_MEMORY
+       }
+
+       self := &Database{db: nil}
+       st := Status(C.notmuch_database_open(c_path, C.notmuch_database_mode_t(mode), &self.db))
+       if st != STATUS_SUCCESS {
+               return nil, st
+       }
+       return self, st
+}
+
+/* Close the given notmuch database, freeing all associated
+ * resources. See notmuch_database_open. */
+func (self *Database) Close() Status {
+       return Status(C.notmuch_database_destroy(self.db))
+}
+
+/* Return the database path of the given database.
+ */
+func (self *Database) GetPath() string {
+
+       /* The return value is a string owned by notmuch so should not be
+        * modified nor freed by the caller. */
+       var p *C.char = C.notmuch_database_get_path(self.db)
+       if p != nil {
+               s := C.GoString(p)
+               return s
+       }
+       return ""
+}
+
+/* Return the database format version of the given database. */
+func (self *Database) GetVersion() uint {
+       return uint(C.notmuch_database_get_version(self.db))
+}
+
+/* Does this database need to be upgraded before writing to it?
+ *
+ * If this function returns TRUE then no functions that modify the
+ * database (notmuch_database_index_file, notmuch_message_add_tag,
+ * notmuch_directory_set_mtime, etc.) will work unless the function
+ * notmuch_database_upgrade is called successfully first. */
+func (self *Database) NeedsUpgrade() bool {
+       do_upgrade := C.notmuch_database_needs_upgrade(self.db)
+       if do_upgrade == 0 {
+               return false
+       }
+       return true
+}
+
+// TODO: notmuch_database_upgrade
+
+/* Retrieve a directory object from the database for 'path'.
+ *
+ * Here, 'path' should be a path relative to the path of 'database'
+ * (see notmuch_database_get_path), or else should be an absolute path
+ * with initial components that match the path of 'database'.
+ *
+ * Can return NULL if a Xapian exception occurs.
+ */
+func (self *Database) GetDirectory(path string) (*Directory, Status) {
+       var c_path *C.char = C.CString(path)
+       defer C.free(unsafe.Pointer(c_path))
+
+       if c_path == nil {
+               return nil, STATUS_OUT_OF_MEMORY
+       }
+
+       var c_dir *C.notmuch_directory_t
+       st := Status(C.notmuch_database_get_directory(self.db, c_path, &c_dir))
+       if st != STATUS_SUCCESS || c_dir == nil {
+               return nil, st
+       }
+       return &Directory{dir: c_dir}, st
+}
+
+/* Add a new message to the given notmuch database.
+ *
+ * Here,'filename' should be a path relative to the path of
+ * 'database' (see notmuch_database_get_path), or else should be an
+ * absolute filename with initial components that match the path of
+ * 'database'.
+ *
+ * The file should be a single mail message (not a multi-message mbox)
+ * that is expected to remain at its current location, (since the
+ * notmuch database will reference the filename, and will not copy the
+ * entire contents of the file.
+ *
+ * If 'message' is not NULL, then, on successful return '*message'
+ * will be initialized to a message object that can be used for things
+ * such as adding tags to the just-added message. The user should call
+ * notmuch_message_destroy when done with the message. On any failure
+ * '*message' will be set to NULL.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully added to database.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred,
+ *     message not added.
+ *
+ * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: Message has the same message
+ *     ID as another message already in the database. The new
+ *     filename was successfully added to the message in the database
+ *     (if not already present).
+ *
+ * NOTMUCH_STATUS_FILE_ERROR: an error occurred trying to open the
+ *     file, (such as permission denied, or file not found,
+ *     etc.). Nothing added to the database.
+ *
+ * NOTMUCH_STATUS_FILE_NOT_EMAIL: the contents of filename don't look
+ *     like an email message. Nothing added to the database.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so no message can be added.
+ */
+func (self *Database) AddMessage(fname string) (*Message, Status) {
+       var c_fname *C.char = C.CString(fname)
+       defer C.free(unsafe.Pointer(c_fname))
+
+       if c_fname == nil {
+               return nil, STATUS_OUT_OF_MEMORY
+       }
+
+       var c_msg *C.notmuch_message_t = new(C.notmuch_message_t)
+       st := Status(C.notmuch_database_add_message(self.db, c_fname, &c_msg))
+
+       return &Message{message: c_msg}, st
+}
+
+/* Remove a message from the given notmuch database.
+ *
+ * Note that only this particular filename association is removed from
+ * the database. If the same message (as determined by the message ID)
+ * is still available via other filenames, then the message will
+ * persist in the database for those filenames. When the last filename
+ * is removed for a particular message, the database content for that
+ * message will be entirely removed.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: The last filename was removed and the
+ *     message was removed from the database.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred,
+ *     message not removed.
+ *
+ * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: This filename was removed but
+ *     the message persists in the database with at least one other
+ *     filename.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so no message can be removed.
+ */
+func (self *Database) RemoveMessage(fname string) Status {
+
+       var c_fname *C.char = C.CString(fname)
+       defer C.free(unsafe.Pointer(c_fname))
+
+       if c_fname == nil {
+               return STATUS_OUT_OF_MEMORY
+       }
+
+       st := C.notmuch_database_remove_message(self.db, c_fname)
+       return Status(st)
+}
+
+/* Find a message with the given message_id.
+ *
+ * If the database contains a message with the given message_id, then
+ * a new notmuch_message_t object is returned. The caller should call
+ * notmuch_message_destroy when done with the message.
+ *
+ * This function returns NULL in the following situations:
+ *
+ *     * No message is found with the given message_id
+ *     * An out-of-memory situation occurs
+ *     * A Xapian exception occurs
+ */
+func (self *Database) FindMessage(message_id string) (*Message, Status) {
+
+       var c_msg_id *C.char = C.CString(message_id)
+       defer C.free(unsafe.Pointer(c_msg_id))
+
+       if c_msg_id == nil {
+               return nil, STATUS_OUT_OF_MEMORY
+       }
+
+       msg := &Message{message: nil}
+       st := Status(C.notmuch_database_find_message(self.db, c_msg_id, &msg.message))
+       if st != STATUS_SUCCESS {
+               return nil, st
+       }
+       return msg, st
+}
+
+/* Return a list of all tags found in the database.
+ *
+ * This function creates a list of all tags found in the database. The
+ * resulting list contains all tags from all messages found in the database.
+ *
+ * On error this function returns NULL.
+ */
+func (self *Database) GetAllTags() *Tags {
+       tags := C.notmuch_database_get_all_tags(self.db)
+       if tags == nil {
+               return nil
+       }
+       return &Tags{tags: tags}
+}
+
+/* Create a new query for 'database'.
+ *
+ * Here, 'database' should be an open database, (see
+ * notmuch_database_open and notmuch_database_create).
+ *
+ * For the query string, we'll document the syntax here more
+ * completely in the future, but it's likely to be a specialized
+ * version of the general Xapian query syntax:
+ *
+ * https://xapian.org/docs/queryparser.html
+ *
+ * As a special case, passing either a length-zero string, (that is ""),
+ * or a string consisting of a single asterisk (that is "*"), will
+ * result in a query that returns all messages in the database.
+ *
+ * See notmuch_query_set_sort for controlling the order of results.
+ * See notmuch_query_search_messages and notmuch_query_search_threads
+ * to actually execute the query.
+ *
+ * User should call notmuch_query_destroy when finished with this
+ * query.
+ *
+ * Will return NULL if insufficient memory is available.
+ */
+func (self *Database) CreateQuery(query string) *Query {
+
+       var c_query *C.char = C.CString(query)
+       defer C.free(unsafe.Pointer(c_query))
+
+       if c_query == nil {
+               return nil
+       }
+
+       q := C.notmuch_query_create(self.db, c_query)
+       if q == nil {
+               return nil
+       }
+       return &Query{query: q}
+}
+
+/* Sort values for notmuch_query_set_sort */
+type Sort C.notmuch_sort_t
+
+const (
+       SORT_OLDEST_FIRST Sort = 0
+       SORT_NEWEST_FIRST
+       SORT_MESSAGE_ID
+       SORT_UNSORTED
+)
+
+/* Return the query_string of this query. See notmuch_query_create. */
+func (self *Query) String() string {
+       // FIXME: do we own 'q' or not ?
+       q := C.notmuch_query_get_query_string(self.query)
+       //defer C.free(unsafe.Pointer(q))
+
+       if q != nil {
+               s := C.GoString(q)
+               return s
+       }
+
+       return ""
+}
+
+/* Specify the sorting desired for this query. */
+func (self *Query) SetSort(sort Sort) {
+       C.notmuch_query_set_sort(self.query, C.notmuch_sort_t(sort))
+}
+
+/* Return the sort specified for this query. See notmuch_query_set_sort. */
+func (self *Query) GetSort() Sort {
+       return Sort(C.notmuch_query_get_sort(self.query))
+}
+
+/* Execute a query for threads, returning a notmuch_threads_t object
+ * which can be used to iterate over the results. The returned threads
+ * object is owned by the query and as such, will only be valid until
+ * notmuch_query_destroy.
+ *
+ * Typical usage might be:
+ *
+ *     notmuch_query_t *query;
+ *     notmuch_threads_t *threads;
+ *     notmuch_thread_t *thread;
+ *
+ *     query = notmuch_query_create (database, query_string);
+ *
+ *     for (threads = notmuch_query_search_threads (query);
+ *          notmuch_threads_valid (threads);
+ *          notmuch_threads_move_to_next (threads))
+ *     {
+ *         thread = notmuch_threads_get (threads);
+ *         ....
+ *         notmuch_thread_destroy (thread);
+ *     }
+ *
+ *     notmuch_query_destroy (query);
+ *
+ * Note: If you are finished with a thread before its containing
+ * query, you can call notmuch_thread_destroy to clean up some memory
+ * sooner (as in the above example). Otherwise, if your thread objects
+ * are long-lived, then you don't need to call notmuch_thread_destroy
+ * and all the memory will still be reclaimed when the query is
+ * destroyed.
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_threads_t object. (For consistency, we do provide a
+ * notmuch_threads_destroy function, but there's no good reason
+ * to call it if the query is about to be destroyed).
+ *
+ * If a Xapian exception occurs this function will return NULL.
+ */
+func (self *Query) SearchThreads() *Threads {
+       threads := C.notmuch_query_search_threads(self.query)
+       if threads == nil {
+               return nil
+       }
+       return &Threads{threads: threads}
+}
+
+/* Execute a query for messages, returning a notmuch_messages_t object
+ * which can be used to iterate over the results. The returned
+ * messages object is owned by the query and as such, will only be
+ * valid until notmuch_query_destroy.
+ *
+ * Typical usage might be:
+ *
+ *     notmuch_query_t *query;
+ *     notmuch_messages_t *messages;
+ *     notmuch_message_t *message;
+ *
+ *     query = notmuch_query_create (database, query_string);
+ *
+ *     for (messages = notmuch_query_search_messages (query);
+ *          notmuch_messages_valid (messages);
+ *          notmuch_messages_move_to_next (messages))
+ *     {
+ *         message = notmuch_messages_get (messages);
+ *         ....
+ *         notmuch_message_destroy (message);
+ *     }
+ *
+ *     notmuch_query_destroy (query);
+ *
+ * Note: If you are finished with a message before its containing
+ * query, you can call notmuch_message_destroy to clean up some memory
+ * sooner (as in the above example). Otherwise, if your message
+ * objects are long-lived, then you don't need to call
+ * notmuch_message_destroy and all the memory will still be reclaimed
+ * when the query is destroyed.
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_messages_t object. (For consistency, we do provide a
+ * notmuch_messages_destroy function, but there's no good
+ * reason to call it if the query is about to be destroyed).
+ *
+ * If a Xapian exception occurs this function will return NULL.
+ */
+func (self *Query) SearchMessages() *Messages {
+       msgs := C.notmuch_query_search_messages(self.query)
+       if msgs == nil {
+               return nil
+       }
+       return &Messages{messages: msgs}
+}
+
+/* Destroy a notmuch_query_t along with any associated resources.
+ *
+ * This will in turn destroy any notmuch_threads_t and
+ * notmuch_messages_t objects generated by this query, (and in
+ * turn any notmuch_thread_t and notmuch_message_t objects generated
+ * from those results, etc.), if such objects haven't already been
+ * destroyed.
+ */
+func (self *Query) Destroy() {
+       if self.query != nil {
+               C.notmuch_query_destroy(self.query)
+       }
+}
+
+/* Return an estimate of the number of messages matching a search
+ *
+ * This function performs a search and returns Xapian's best
+ * guess as to number of matching messages.
+ *
+ * If a Xapian exception occurs, this function may return 0 (after
+ * printing a message).
+ */
+func (self *Query) CountMessages() uint {
+       return uint(C.notmuch_query_count_messages(self.query))
+}
+
+/* Is the given 'threads' iterator pointing at a valid thread.
+ *
+ * When this function returns TRUE, notmuch_threads_get will return a
+ * valid object. Whereas when this function returns FALSE,
+ * notmuch_threads_get will return NULL.
+ *
+ * See the documentation of notmuch_query_search_threads for example
+ * code showing how to iterate over a notmuch_threads_t object.
+ */
+func (self *Threads) Valid() bool {
+       if self.threads == nil {
+               return false
+       }
+       valid := C.notmuch_threads_valid(self.threads)
+       if valid == 0 {
+               return false
+       }
+       return true
+}
+
+/* Get the current thread from 'threads' as a notmuch_thread_t.
+ *
+ * Note: The returned thread belongs to 'threads' and has a lifetime
+ * identical to it (and the query to which it belongs).
+ *
+ * See the documentation of notmuch_query_search_threads for example
+ * code showing how to iterate over a notmuch_threads_t object.
+ *
+ * If an out-of-memory situation occurs, this function will return
+ * NULL.
+ */
+func (self *Threads) Get() *Thread {
+       if self.threads == nil {
+               return nil
+       }
+       thread := C.notmuch_threads_get(self.threads)
+       if thread == nil {
+               return nil
+       }
+       return &Thread{thread}
+}
+
+/* Move the 'threads' iterator to the next thread.
+ *
+ * If 'threads' is already pointing at the last thread then the
+ * iterator will be moved to a point just beyond that last thread,
+ * (where notmuch_threads_valid will return FALSE and
+ * notmuch_threads_get will return NULL).
+ *
+ * See the documentation of notmuch_query_search_threads for example
+ * code showing how to iterate over a notmuch_threads_t object.
+ */
+func (self *Threads) MoveToNext() {
+       if self.threads == nil {
+               return
+       }
+       C.notmuch_threads_move_to_next(self.threads)
+}
+
+/* Destroy a notmuch_threads_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_threads_t object will be reclaimed when the
+ * containing query object is destroyed.
+ */
+func (self *Threads) Destroy() {
+       if self.threads != nil {
+               C.notmuch_threads_destroy(self.threads)
+       }
+}
+
+/**
+ * Get the thread ID of 'thread'.
+ *
+ * The returned string belongs to 'thread' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * thread is valid, (which is until notmuch_thread_destroy or until
+ * the query from which it derived is destroyed).
+ */
+func (self *Thread) GetThreadId() string {
+       if self.thread == nil {
+               return ""
+       }
+       id := C.notmuch_thread_get_thread_id(self.thread)
+       if id == nil {
+               return ""
+       }
+       return C.GoString(id)
+}
+
+/**
+ * Get the total number of messages in 'thread'.
+ *
+ * This count consists of all messages in the database belonging to
+ * this thread. Contrast with notmuch_thread_get_matched_messages() .
+ */
+func (self *Thread) GetTotalMessages() int {
+       if self.thread == nil {
+               return 0
+       }
+       return int(C.notmuch_thread_get_total_messages(self.thread))
+}
+
+/**
+ * Get a notmuch_messages_t iterator for the top-level messages in
+ * 'thread' in oldest-first order.
+ *
+ * This iterator will not necessarily iterate over all of the messages
+ * in the thread. It will only iterate over the messages in the thread
+ * which are not replies to other messages in the thread.
+ *
+ * The returned list will be destroyed when the thread is destroyed.
+ */
+func (self *Thread) GetToplevelMessages() (*Messages, Status) {
+       if self.thread == nil {
+               return nil, STATUS_NULL_POINTER
+       }
+
+       msgs := C.notmuch_thread_get_toplevel_messages(self.thread)
+       if msgs == nil {
+               return nil, STATUS_NULL_POINTER
+       }
+       return &Messages{msgs}, STATUS_SUCCESS
+}
+
+/**
+ * Get a notmuch_thread_t iterator for all messages in 'thread' in
+ * oldest-first order.
+ *
+ * The returned list will be destroyed when the thread is destroyed.
+ */
+func (self *Thread) GetMessages() (*Messages, Status) {
+       if self.thread == nil {
+               return nil, STATUS_NULL_POINTER
+       }
+
+       msgs := C.notmuch_thread_get_messages(self.thread)
+       if msgs == nil {
+               return nil, STATUS_NULL_POINTER
+       }
+       return &Messages{msgs}, STATUS_SUCCESS
+}
+
+/**
+ * Get the number of messages in 'thread' that matched the search.
+ *
+ * This count includes only the messages in this thread that were
+ * matched by the search from which the thread was created and were
+ * not excluded by any exclude tags passed in with the query (see
+ * notmuch_query_add_tag_exclude). Contrast with
+ * notmuch_thread_get_total_messages() .
+ */
+func (self *Thread) GetMatchedMessages() int {
+       if self.thread == nil {
+               return 0
+       }
+       return int(C.notmuch_thread_get_matched_messages(self.thread))
+}
+
+/**
+ * Get the authors of 'thread' as a UTF-8 string.
+ *
+ * The returned string is a comma-separated list of the names of the
+ * authors of mail messages in the query results that belong to this
+ * thread.
+ *
+ * The string contains authors of messages matching the query first, then
+ * non-matched authors (with the two groups separated by '|'). Within
+ * each group, authors are ordered by date.
+ *
+ * The returned string belongs to 'thread' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * thread is valid, (which is until notmuch_thread_destroy or until
+ * the query from which it derived is destroyed).
+ */
+func (self *Thread) GetAuthors() string {
+       if self.thread == nil {
+               return ""
+       }
+       str := C.notmuch_thread_get_authors(self.thread)
+       if str == nil {
+               return ""
+       }
+       return C.GoString(str)
+}
+
+/**
+ * Get the subject of 'thread' as a UTF-8 string.
+ *
+ * The subject is taken from the first message (according to the query
+ * order---see notmuch_query_set_sort) in the query results that
+ * belongs to this thread.
+ *
+ * The returned string belongs to 'thread' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * thread is valid, (which is until notmuch_thread_destroy or until
+ * the query from which it derived is destroyed).
+ */
+func (self *Thread) GetSubject() string {
+       if self.thread == nil {
+               return ""
+       }
+       str := C.notmuch_thread_get_subject(self.thread)
+       if str == nil {
+               return ""
+       }
+       return C.GoString(str)
+}
+
+/**
+ * Get the date of the oldest message in 'thread' as a time_t value.
+ */
+func (self *Thread) GetOldestDate() int64 {
+       if self.thread == nil {
+               return 0
+       }
+       date := C.notmuch_thread_get_oldest_date(self.thread)
+
+       return int64(date)
+}
+
+/**
+ * Get the date of the newest message in 'thread' as a time_t value.
+ */
+func (self *Thread) GetNewestDate() int64 {
+       if self.thread == nil {
+               return 0
+       }
+       date := C.notmuch_thread_get_newest_date(self.thread)
+
+       return int64(date)
+}
+
+/**
+ * Get the tags for 'thread', returning a notmuch_tags_t object which
+ * can be used to iterate over all tags.
+ *
+ * Note: In the Notmuch database, tags are stored on individual
+ * messages, not on threads. So the tags returned here will be all
+ * tags of the messages which matched the search and which belong to
+ * this thread.
+ *
+ * The tags object is owned by the thread and as such, will only be
+ * valid for as long as the thread is valid, (for example, until
+ * notmuch_thread_destroy or until the query from which it derived is
+ * destroyed).
+ *
+ * Typical usage might be:
+ *
+ *     notmuch_thread_t *thread;
+ *     notmuch_tags_t *tags;
+ *     const char *tag;
+ *
+ *     thread = notmuch_threads_get (threads);
+ *
+ *     for (tags = notmuch_thread_get_tags (thread);
+ *          notmuch_tags_valid (tags);
+ *          notmuch_tags_move_to_next (tags))
+ *     {
+ *         tag = notmuch_tags_get (tags);
+ *         ....
+ *     }
+ *
+ *     notmuch_thread_destroy (thread);
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_tags_t object. (For consistency, we do provide a
+ * notmuch_tags_destroy function, but there's no good reason to call
+ * it if the message is about to be destroyed).
+ */
+func (self *Thread) GetTags() *Tags {
+       if self.thread == nil {
+               return nil
+       }
+
+       tags := C.notmuch_thread_get_tags(self.thread)
+       if tags == nil {
+               return nil
+       }
+
+       return &Tags{tags}
+}
+
+/**
+ * Destroy a notmuch_thread_t object.
+ */
+func (self *Thread) Destroy() {
+       if self.thread != nil {
+               C.notmuch_thread_destroy(self.thread)
+       }
+}
+
+/* Is the given 'messages' iterator pointing at a valid message.
+ *
+ * When this function returns TRUE, notmuch_messages_get will return a
+ * valid object. Whereas when this function returns FALSE,
+ * notmuch_messages_get will return NULL.
+ *
+ * See the documentation of notmuch_query_search_messages for example
+ * code showing how to iterate over a notmuch_messages_t object.
+ */
+func (self *Messages) Valid() bool {
+       if self.messages == nil {
+               return false
+       }
+       valid := C.notmuch_messages_valid(self.messages)
+       if valid == 0 {
+               return false
+       }
+       return true
+}
+
+/* Get the current message from 'messages' as a notmuch_message_t.
+ *
+ * Note: The returned message belongs to 'messages' and has a lifetime
+ * identical to it (and the query to which it belongs).
+ *
+ * See the documentation of notmuch_query_search_messages for example
+ * code showing how to iterate over a notmuch_messages_t object.
+ *
+ * If an out-of-memory situation occurs, this function will return
+ * NULL.
+ */
+func (self *Messages) Get() *Message {
+       if self.messages == nil {
+               return nil
+       }
+       msg := C.notmuch_messages_get(self.messages)
+       if msg == nil {
+               return nil
+       }
+       return &Message{message: msg}
+}
+
+/* Move the 'messages' iterator to the next message.
+ *
+ * If 'messages' is already pointing at the last message then the
+ * iterator will be moved to a point just beyond that last message,
+ * (where notmuch_messages_valid will return FALSE and
+ * notmuch_messages_get will return NULL).
+ *
+ * See the documentation of notmuch_query_search_messages for example
+ * code showing how to iterate over a notmuch_messages_t object.
+ */
+func (self *Messages) MoveToNext() {
+       if self.messages == nil {
+               return
+       }
+       C.notmuch_messages_move_to_next(self.messages)
+}
+
+/* Destroy a notmuch_messages_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_messages_t object will be reclaimed when the containing
+ * query object is destroyed.
+ */
+func (self *Messages) Destroy() {
+       if self.messages != nil {
+               C.notmuch_messages_destroy(self.messages)
+       }
+}
+
+/* Return a list of tags from all messages.
+ *
+ * The resulting list is guaranteed not to contain duplicated tags.
+ *
+ * WARNING: You can no longer iterate over messages after calling this
+ * function, because the iterator will point at the end of the list.
+ * We do not have a function to reset the iterator yet and the only
+ * way how you can iterate over the list again is to recreate the
+ * message list.
+ *
+ * The function returns NULL on error.
+ */
+func (self *Messages) CollectTags() *Tags {
+       if self.messages == nil {
+               return nil
+       }
+       tags := C.notmuch_messages_collect_tags(self.messages)
+       if tags == nil {
+               return nil
+       }
+       return &Tags{tags: tags}
+}
+
+/* Get the message ID of 'message'.
+ *
+ * The returned string belongs to 'message' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * message is valid, (which is until the query from which it derived
+ * is destroyed).
+ *
+ * This function will not return NULL since Notmuch ensures that every
+ * message has a unique message ID, (Notmuch will generate an ID for a
+ * message if the original file does not contain one).
+ */
+func (self *Message) GetMessageId() string {
+
+       if self.message == nil {
+               return ""
+       }
+       id := C.notmuch_message_get_message_id(self.message)
+       // we don't own id
+       // defer C.free(unsafe.Pointer(id))
+       if id == nil {
+               return ""
+       }
+       return C.GoString(id)
+}
+
+/* Get the thread ID of 'message'.
+ *
+ * The returned string belongs to 'message' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * message is valid, (for example, until the user calls
+ * notmuch_message_destroy on 'message' or until a query from which it
+ * derived is destroyed).
+ *
+ * This function will not return NULL since Notmuch ensures that every
+ * message belongs to a single thread.
+ */
+func (self *Message) GetThreadId() string {
+
+       if self.message == nil {
+               return ""
+       }
+       id := C.notmuch_message_get_thread_id(self.message)
+       // we don't own id
+       // defer C.free(unsafe.Pointer(id))
+
+       if id == nil {
+               return ""
+       }
+
+       return C.GoString(id)
+}
+
+/* Get a notmuch_messages_t iterator for all of the replies to
+ * 'message'.
+ *
+ * Note: This call only makes sense if 'message' was ultimately
+ * obtained from a notmuch_thread_t object, (such as by coming
+ * directly from the result of calling notmuch_thread_get_
+ * toplevel_messages or by any number of subsequent
+ * calls to notmuch_message_get_replies).
+ *
+ * If 'message' was obtained through some non-thread means, (such as
+ * by a call to notmuch_query_search_messages), then this function
+ * will return NULL.
+ *
+ * If there are no replies to 'message', this function will return
+ * NULL. (Note that notmuch_messages_valid will accept that NULL
+ * value as legitimate, and simply return FALSE for it.)
+ */
+func (self *Message) GetReplies() *Messages {
+       if self.message == nil {
+               return nil
+       }
+       msgs := C.notmuch_message_get_replies(self.message)
+       if msgs == nil {
+               return nil
+       }
+       return &Messages{messages: msgs}
+}
+
+/* Get a filename for the email corresponding to 'message'.
+ *
+ * The returned filename is an absolute filename, (the initial
+ * component will match notmuch_database_get_path() ).
+ *
+ * The returned string belongs to the message so should not be
+ * modified or freed by the caller (nor should it be referenced after
+ * the message is destroyed).
+ *
+ * Note: If this message corresponds to multiple files in the mail
+ * store, (that is, multiple files contain identical message IDs),
+ * this function will arbitrarily return a single one of those
+ * filenames.
+ */
+func (self *Message) GetFileName() string {
+       if self.message == nil {
+               return ""
+       }
+       fname := C.notmuch_message_get_filename(self.message)
+       // we don't own fname
+       // defer C.free(unsafe.Pointer(fname))
+
+       if fname == nil {
+               return ""
+       }
+
+       return C.GoString(fname)
+}
+
+type Flag C.notmuch_message_flag_t
+
+const (
+       MESSAGE_FLAG_MATCH Flag = 0
+)
+
+/* Get a value of a flag for the email corresponding to 'message'. */
+func (self *Message) GetFlag(flag Flag) bool {
+       if self.message == nil {
+               return false
+       }
+       v := C.notmuch_message_get_flag(self.message, C.notmuch_message_flag_t(flag))
+       if v == 0 {
+               return false
+       }
+       return true
+}
+
+/* Set a value of a flag for the email corresponding to 'message'. */
+func (self *Message) SetFlag(flag Flag, value bool) {
+       if self.message == nil {
+               return
+       }
+       var v C.notmuch_bool_t = 0
+       if value {
+               v = 1
+       }
+       C.notmuch_message_set_flag(self.message, C.notmuch_message_flag_t(flag), v)
+}
+
+/* Get the timestamp (seconds since the epoch) of 'message'.
+ *
+ * Return status:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Timestamp successfully retrieved
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The 'message' argument is NULL
+ *
+ */
+func (self *Message) GetDate() (int64, Status) {
+       if self.message == nil {
+               return -1, STATUS_NULL_POINTER
+       }
+       timestamp := C.notmuch_message_get_date(self.message)
+       return int64(timestamp), STATUS_SUCCESS
+}
+
+/* Get the value of the specified header from 'message'.
+ *
+ * The value will be read from the actual message file, not from the
+ * notmuch database. The header name is case insensitive.
+ *
+ * The returned string belongs to the message so should not be
+ * modified or freed by the caller (nor should it be referenced after
+ * the message is destroyed).
+ *
+ * Returns an empty string ("") if the message does not contain a
+ * header line matching 'header'. Returns NULL if any error occurs.
+ */
+func (self *Message) GetHeader(header string) string {
+       if self.message == nil {
+               return ""
+       }
+
+       var c_header *C.char = C.CString(header)
+       defer C.free(unsafe.Pointer(c_header))
+
+       /* we don't own value */
+       value := C.notmuch_message_get_header(self.message, c_header)
+       if value == nil {
+               return ""
+       }
+
+       return C.GoString(value)
+}
+
+/* Get the tags for 'message', returning a notmuch_tags_t object which
+ * can be used to iterate over all tags.
+ *
+ * The tags object is owned by the message and as such, will only be
+ * valid for as long as the message is valid, (which is until the
+ * query from which it derived is destroyed).
+ *
+ * Typical usage might be:
+ *
+ *     notmuch_message_t *message;
+ *     notmuch_tags_t *tags;
+ *     const char *tag;
+ *
+ *     message = notmuch_database_find_message (database, message_id);
+ *
+ *     for (tags = notmuch_message_get_tags (message);
+ *          notmuch_tags_valid (tags);
+ *          notmuch_result_move_to_next (tags))
+ *     {
+ *         tag = notmuch_tags_get (tags);
+ *         ....
+ *     }
+ *
+ *     notmuch_message_destroy (message);
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_tags_t object. (For consistency, we do provide a
+ * notmuch_tags_destroy function, but there's no good reason to call
+ * it if the message is about to be destroyed).
+ */
+func (self *Message) GetTags() *Tags {
+       if self.message == nil {
+               return nil
+       }
+       tags := C.notmuch_message_get_tags(self.message)
+       if tags == nil {
+               return nil
+       }
+       return &Tags{tags: tags}
+}
+
+/* The longest possible tag value. */
+const TAG_MAX = 200
+
+/* Add a tag to the given message.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Tag successfully added to message
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The 'tag' argument is NULL
+ *
+ * NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long
+ *     (exceeds NOTMUCH_TAG_MAX)
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so message cannot be modified.
+ */
+func (self *Message) AddTag(tag string) Status {
+       if self.message == nil {
+               return STATUS_NULL_POINTER
+       }
+       c_tag := C.CString(tag)
+       defer C.free(unsafe.Pointer(c_tag))
+
+       return Status(C.notmuch_message_add_tag(self.message, c_tag))
+}
+
+/* Remove a tag from the given message.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Tag successfully removed from message
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The 'tag' argument is NULL
+ *
+ * NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long
+ *     (exceeds NOTMUCH_TAG_MAX)
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so message cannot be modified.
+ */
+func (self *Message) RemoveTag(tag string) Status {
+       if self.message == nil {
+               return STATUS_NULL_POINTER
+       }
+       c_tag := C.CString(tag)
+       defer C.free(unsafe.Pointer(c_tag))
+
+       return Status(C.notmuch_message_remove_tag(self.message, c_tag))
+}
+
+/* Remove all tags from the given message.
+ *
+ * See notmuch_message_freeze for an example showing how to safely
+ * replace tag values.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so message cannot be modified.
+ */
+func (self *Message) RemoveAllTags() Status {
+       if self.message == nil {
+               return STATUS_NULL_POINTER
+       }
+       return Status(C.notmuch_message_remove_all_tags(self.message))
+}
+
+/* Freeze the current state of 'message' within the database.
+ *
+ * This means that changes to the message state, (via
+ * notmuch_message_add_tag, notmuch_message_remove_tag, and
+ * notmuch_message_remove_all_tags), will not be committed to the
+ * database until the message is thawed with notmuch_message_thaw.
+ *
+ * Multiple calls to freeze/thaw are valid and these calls will
+ * "stack". That is there must be as many calls to thaw as to freeze
+ * before a message is actually thawed.
+ *
+ * The ability to do freeze/thaw allows for safe transactions to
+ * change tag values. For example, explicitly setting a message to
+ * have a given set of tags might look like this:
+ *
+ *    notmuch_message_freeze (message);
+ *
+ *    notmuch_message_remove_all_tags (message);
+ *
+ *    for (i = 0; i < NUM_TAGS; i++)
+ *        notmuch_message_add_tag (message, tags[i]);
+ *
+ *    notmuch_message_thaw (message);
+ *
+ * With freeze/thaw used like this, the message in the database is
+ * guaranteed to have either the full set of original tag values, or
+ * the full set of new tag values, but nothing in between.
+ *
+ * Imagine the example above without freeze/thaw and the operation
+ * somehow getting interrupted. This could result in the message being
+ * left with no tags if the interruption happened after
+ * notmuch_message_remove_all_tags but before notmuch_message_add_tag.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully frozen.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so message cannot be modified.
+ */
+func (self *Message) Freeze() Status {
+       if self.message == nil {
+               return STATUS_NULL_POINTER
+       }
+       return Status(C.notmuch_message_freeze(self.message))
+}
+
+/* Thaw the current 'message', synchronizing any changes that may have
+ * occurred while 'message' was frozen into the notmuch database.
+ *
+ * See notmuch_message_freeze for an example of how to use this
+ * function to safely provide tag changes.
+ *
+ * Multiple calls to freeze/thaw are valid and these calls with
+ * "stack". That is there must be as many calls to thaw as to freeze
+ * before a message is actually thawed.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully thawed, (or at least
+ *     its frozen count has successfully been reduced by 1).
+ *
+ * NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: An attempt was made to thaw
+ *     an unfrozen message. That is, there have been an unbalanced
+ *     number of calls to notmuch_message_freeze and
+ *     notmuch_message_thaw.
+ */
+func (self *Message) Thaw() Status {
+       if self.message == nil {
+               return STATUS_NULL_POINTER
+       }
+
+       return Status(C.notmuch_message_thaw(self.message))
+}
+
+/* Destroy a notmuch_message_t object.
+ *
+ * It can be useful to call this function in the case of a single
+ * query object with many messages in the result, (such as iterating
+ * over the entire database). Otherwise, it's fine to never call this
+ * function and there will still be no memory leaks. (The memory from
+ * the messages get reclaimed when the containing query is destroyed.)
+ */
+func (self *Message) Destroy() {
+       if self.message == nil {
+               return
+       }
+       C.notmuch_message_destroy(self.message)
+}
+
+/* Is the given 'tags' iterator pointing at a valid tag.
+ *
+ * When this function returns TRUE, notmuch_tags_get will return a
+ * valid string. Whereas when this function returns FALSE,
+ * notmuch_tags_get will return NULL.
+ *
+ * See the documentation of notmuch_message_get_tags for example code
+ * showing how to iterate over a notmuch_tags_t object.
+ */
+func (self *Tags) Valid() bool {
+       if self.tags == nil {
+               return false
+       }
+       v := C.notmuch_tags_valid(self.tags)
+       if v == 0 {
+               return false
+       }
+       return true
+}
+
+/* Get the current tag from 'tags' as a string.
+ *
+ * Note: The returned string belongs to 'tags' and has a lifetime
+ * identical to it (and the query to which it ultimately belongs).
+ *
+ * See the documentation of notmuch_message_get_tags for example code
+ * showing how to iterate over a notmuch_tags_t object.
+ */
+func (self *Tags) Get() string {
+       if self.tags == nil {
+               return ""
+       }
+       s := C.notmuch_tags_get(self.tags)
+       // we don't own 's'
+
+       return C.GoString(s)
+}
+func (self *Tags) String() string {
+       return self.Get()
+}
+
+/* Move the 'tags' iterator to the next tag.
+ *
+ * If 'tags' is already pointing at the last tag then the iterator
+ * will be moved to a point just beyond that last tag, (where
+ * notmuch_tags_valid will return FALSE and notmuch_tags_get will
+ * return NULL).
+ *
+ * See the documentation of notmuch_message_get_tags for example code
+ * showing how to iterate over a notmuch_tags_t object.
+ */
+func (self *Tags) MoveToNext() {
+       if self.tags == nil {
+               return
+       }
+       C.notmuch_tags_move_to_next(self.tags)
+}
+
+/* Destroy a notmuch_tags_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_tags_t object will be reclaimed when the containing
+ * message or query objects are destroyed.
+ */
+func (self *Tags) Destroy() {
+       if self.tags == nil {
+               return
+       }
+       C.notmuch_tags_destroy(self.tags)
+}
+
+// TODO: wrap notmuch_directory_<fct>
+
+/* Destroy a notmuch_directory_t object. */
+func (self *Directory) Destroy() {
+       if self.dir == nil {
+               return
+       }
+       C.notmuch_directory_destroy(self.dir)
+}
+
+// TODO: wrap notmuch_filenames_<fct>
+
+/* Destroy a notmuch_filenames_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_filenames_t object will be reclaimed when the
+ * containing directory object is destroyed.
+ *
+ * It is acceptable to pass NULL for 'filenames', in which case this
+ * function will do nothing.
+ */
+func (self *Filenames) Destroy() {
+       if self.fnames == nil {
+               return
+       }
+       C.notmuch_filenames_destroy(self.fnames)
+}
+
+/* EOF */
diff --git a/contrib/notmuch-mutt/.gitignore b/contrib/notmuch-mutt/.gitignore
new file mode 100644 (file)
index 0000000..116bb71
--- /dev/null
@@ -0,0 +1,2 @@
+/notmuch-mutt.1
+/README.html
diff --git a/contrib/notmuch-mutt/Makefile b/contrib/notmuch-mutt/Makefile
new file mode 100644 (file)
index 0000000..de933ea
--- /dev/null
@@ -0,0 +1,25 @@
+NAME = notmuch-mutt
+
+-include ../../Makefile.config
+PERL_ABSOLUTE ?= $(shell command -v perl 2>/dev/null)
+prefix ?= /usr/local
+sysconfdir ?= ${prefix}/etc
+mandir ?= ${prefix}/share/man
+
+all: $(NAME) $(NAME).1
+
+$(NAME).1: $(NAME)
+       pod2man $< > $@
+
+README.html: README
+       markdown $< > $@
+
+install: all
+       mkdir -p $(DESTDIR)$(prefix)/bin $(DESTDIR)$(mandir)/man1 $(DESTDIR)$(sysconfdir)/Muttrc.d
+       sed "1s|^#!.*|#! $(PERL_ABSOLUTE)|" < $(NAME) > $(DESTDIR)$(prefix)/bin/$(NAME)
+       chmod 755 $(DESTDIR)$(prefix)/bin/$(NAME)
+       install -m 644 $(NAME).1 $(DESTDIR)$(mandir)/man1/
+       install -m 644 $(NAME).rc $(DESTDIR)$(sysconfdir)/Muttrc.d/
+
+clean:
+       rm -f notmuch-mutt.1 README.html
diff --git a/contrib/notmuch-mutt/README b/contrib/notmuch-mutt/README
new file mode 100644 (file)
index 0000000..c752022
--- /dev/null
@@ -0,0 +1,59 @@
+notmuch-mutt: Notmuch (of a) helper for Mutt
+============================================
+
+notmuch-mutt provide integration among the [Mutt] [1] mail user agent and the
+[Notmuch] [2] mail indexer.
+
+notmuch-mutt offer two main integration features. The first one is the ability
+of stating a **search query interactively** and then jump to a fresh Maildir
+containing its search results only. The second one is the ability to
+**reconstruct threads on the fly** starting from the currently highlighted
+mail, which comes handy when a thread has been split across different maildirs,
+archived, or the like.
+
+notmuch-mutt enables to trigger mail searches via a Mutt macro (usually F8) and
+reconstruct threads via another (usually F9). Check the manpage for the 2-liner
+configuration snippet for your Mutt configuration files (~/.muttrc,
+/etc/Muttrc, or a /etc/Muttrc.d snippet).
+
+A [blog style introduction] [3] to notmuch-mutt is available and includes some
+more rationale for its existence.
+
+Arguably, some of the logics of notmuch-mutt could disappear by adding support
+for a --output=symlinks flag to notmuch.
+
+
+[1]: http://www.mutt.org/
+[2]: https://notmuchmail.org/
+[3]: https://upsilon.cc/~zack/blog/posts/2011/01/how_to_use_Notmuch_with_Mutt/
+
+
+Requirements
+------------
+
+To *run* notmuch-mutt you will need Perl with the following libraries:
+
+- Digest::SHA <https://metacpan.org/release/Digest-SHA>
+  (Debian package: libdigest-sha-perl)
+- Mail::Box <https://metacpan.org/pod/Mail::Box>
+  (Debian package: libmail-box-perl)
+- Mail::Header <https://metacpan.org/pod/Mail::Header>
+  (Debian package: libmailtools-perl)
+- Term::ReadLine::Gnu <https://metacpan.org/pod/Term::ReadLine::Gnu>
+  (Debian package: libterm-readline-gnu-perl)
+
+To *build* notmuch-mutt documentation you will need:
+
+- pod2man (coming with Perl) to generate the manpage
+- markdown to generate README.html out of this file
+
+
+License
+-------
+
+notmuch-mutt is copyright (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>.
+
+notmuch-mutt is released under the terms of the GNU General Public License
+(GPL), version 3 or above. A copy of the license is available online at
+<https://www.gnu.org/licenses/>.
+
diff --git a/contrib/notmuch-mutt/notmuch-mutt b/contrib/notmuch-mutt/notmuch-mutt
new file mode 100755 (executable)
index 0000000..b81252c
--- /dev/null
@@ -0,0 +1,338 @@
+#!/usr/bin/env perl
+#
+# notmuch-mutt - notmuch (of a) helper for Mutt
+#
+# 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.
+# A manpage can be obtained by running "pod2man notmuch-mutt > notmuch-mutt.1"
+
+use strict;
+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 Term::ReadLine;
+use Digest::SHA;
+
+
+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");
+}
+
+# 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,
+                                       create => 1);
+    $folder->close();
+}
+
+# search($maildir, $remove_dups, $query)
+# search mails according to $query with notmuch; store results in $maildir
+sub search($$$) {
+    my ($maildir, $remove_dups, $query) = @_;
+    my $dup_option = "";
+
+    my @args = qw/notmuch search --output=files/;
+    push @args, "--duplicate=1" if $remove_dups;
+    push @args, $query;
+
+    check_search_cache_maildir($maildir);
+    empty_search_cache_maildir($maildir);
+    open my $pipe, '-|', @args or die "Running @args failed: $!\n";
+    while (<$pipe>) {
+       chomp;
+       my $ln = "$maildir/cur/" . basename $_;
+       symlink $_, "$ln" or warn "Failed to symlink '$_', '$ln': $!\n";
+    }
+}
+
+sub prompt($$) {
+    my ($text, $default) = @_;
+    my $query = "";
+    my $term = Term::ReadLine->new( "notmuch-mutt" );
+    my $histfile = "$cache_dir/history";
+
+    $term->ornaments( 0 );
+    $term->unbind_key( ord( "\t" ) );
+    $term->MinLine( 3 );
+    $histfile = $ENV{MUTT_NOTMUCH_HISTFILE} if $ENV{MUTT_NOTMUCH_HISTFILE};
+    $term->ReadHistory($histfile) if (-r $histfile);
+    while (1) {
+       chomp($query = $term->readline($text, $default));
+       if ($query eq "?") {
+           system("man", "notmuch-search-terms");
+       } else {
+           $term->WriteHistory($histfile);
+           return $query;
+       }
+    }
+}
+
+sub get_message_id() {
+    my $mid = undef;
+    my @headers = ();
+
+    while (<STDIN>) {  # collect header lines in @headers
+       push(@headers, $_);
+       last if $_ =~ /^$/;
+    }
+    my $head = Mail::Header->new(\@headers);
+    $mid = $head->get("message-id") or undef;
+
+    if ($mid) {  # Message-ID header found
+       $mid =~ /^<(.*)>$/;  # extract message id
+       $mid = $1;
+    } else {  # Message-ID header not found, synthesize a message id
+             # based on SHA1, as notmuch would do.  See:
+             # https://git.notmuchmail.org/git/notmuch/blob/HEAD:/lib/sha1.c
+       my $sha = Digest::SHA->new(1);
+       $sha->add($_) foreach(@headers);
+       $sha->addfile(\*STDIN);
+       $mid = 'notmuch-sha1-' . $sha->hexdigest;
+    }
+
+    return $mid;
+}
+
+sub search_action($$$@) {
+    my ($interactive, $results_dir, $remove_dups, @params) = @_;
+
+    if (! $interactive) {
+       search($results_dir, $remove_dups, join(' ', @params));
+    } else {
+       my $query = prompt("search ('?' for man): ", join(' ', @params));
+       if ($query ne "") {
+           search($results_dir, $remove_dups, $query);
+       }
+    }
+}
+
+sub thread_action($$@) {
+    my ($results_dir, $remove_dups, @params) = @_;
+
+    my $mid = get_message_id();
+    if (! defined $mid) {
+       die "notmuch-mutt: cannot find Message-Id, abort.\n";
+    }
+
+    $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";
+
+    $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() {
+    my %podflags = ( "verbose" => 1,
+                   "exitval" => 2 );
+    pod2usage(%podflags);
+}
+
+sub main() {
+    mkpath($cache_dir) unless (-d $cache_dir);
+
+    my $results_dir = "$cache_dir/results";
+    my $interactive = 0;
+    my $help_needed = 0;
+    my $remove_dups = 0;
+
+    my $getopt = GetOptions(
+       "h|help" => \$help_needed,
+       "o|output-dir=s" => \$results_dir,
+       "p|prompt" => \$interactive,
+       "r|remove-dups" => \$remove_dups);
+    if (! $getopt || $#ARGV < 0) { die_usage() };
+    my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
+
+    foreach my $param (@params) {
+      $param =~ s/folder:=/folder:/g;
+    }
+
+    if ($help_needed) {
+       die_usage();
+    } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
+       print STDERR "Error: no search term provided\n\n";
+       die_usage();
+    } elsif ($action eq "search") {
+       search_action($interactive, $results_dir, $remove_dups, @params);
+    } elsif ($action eq "thread") {
+       thread_action($results_dir, $remove_dups, @params);
+    } elsif ($action eq "tag") {
+       tag_action(@params);
+    } else {
+       die_usage();
+    }
+}
+
+main();
+
+__END__
+
+=head1 NAME
+
+notmuch-mutt - notmuch (of a) helper for Mutt
+
+=head1 SYNOPSIS
+
+=over
+
+=item B<notmuch-mutt> [I<OPTION>]... search [I<SEARCH-TERM>]...
+
+=item B<notmuch-mutt> [I<OPTION>]... thread < I<MAIL>
+
+=item B<notmuch-mutt> [I<OPTION>]... tag [I<TAGS>]... < I<MAIL>
+
+=back
+
+=head1 DESCRIPTION
+
+notmuch-mutt is a frontend to the notmuch mail indexer capable of populating
+a maildir with search results.
+
+=head1 OPTIONS
+
+=over 4
+
+=item -o DIR
+
+=item --output-dir DIR
+
+Store search results as (symlink) messages under maildir DIR. Beware: DIR will
+be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>)
+
+=item -p
+
+=item --prompt
+
+Instead of using command line search terms, prompt the user for them (only for
+"search").
+
+=item -r
+
+=item --remove-dups
+
+Remove emails with duplicate message-ids from search results.  (Passes
+--duplicate=1 to notmuch search command.)  Note this can hide search
+results if an email accidentally or maliciously uses the same message-id
+as a different email.
+
+=item -h
+
+=item --help
+
+Show usage information and exit.
+
+=back
+
+=head1 INTEGRATION WITH MUTT
+
+notmuch-mutt can be used to integrate notmuch with the Mutt mail user agent
+(unsurprisingly, given the name). To that end, you should define macros like
+the following in your Mutt configuration (usually one of: F<~/.muttrc>,
+F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>):
+
+    macro index <F8> \
+    "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
+    <shell-escape>notmuch-mutt -r --prompt search<enter>\
+    <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
+    <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
+          "notmuch: search mail"
+
+    macro index <F9> \
+    "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
+    <pipe-message>notmuch-mutt -r thread<enter>\
+    <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
+    <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
+          "notmuch: reconstruct thread"
+
+    macro index <F6> \
+    "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
+    <pipe-message>notmuch-mutt tag -- -inbox<enter>\
+    <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
+          "notmuch: remove message from inbox"
+
+The first macro (activated by <F8>) prompts the user for notmuch search terms
+and then jump to a temporary maildir showing search results. The second macro
+(activated by <F9>) reconstructs the thread corresponding to the current mail
+and show it as search results. The third macro (activated by <F6>) removes the
+tag C<inbox> from the current message; by changing C<-inbox> this macro may be
+customised to add or remove tags appropriate to the users notmuch work-flow.
+
+To keep notmuch index current you should then periodically run C<notmuch
+new>. Depending on your local mail setup, you might want to do that via cron,
+as a hook triggered by mail retrieval, etc.
+
+=head1 SEE ALSO
+
+mutt(1), notmuch(1)
+
+=head1 AUTHOR
+
+Copyright: (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>
+
+License: GNU General Public License (GPL), version 3 or higher
+
+=cut
diff --git a/contrib/notmuch-mutt/notmuch-mutt.rc b/contrib/notmuch-mutt/notmuch-mutt.rc
new file mode 100644 (file)
index 0000000..6b299dc
--- /dev/null
@@ -0,0 +1,19 @@
+macro index <F8> \
+"<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
+<shell-escape>notmuch-mutt -r --prompt search<enter>\
+<change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
+<enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
+      "notmuch: search mail"
+
+macro index <F9> \
+"<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
+<pipe-message>notmuch-mutt -r thread<enter>\
+<change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
+<enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
+      "notmuch: reconstruct thread"
+
+macro index <F6> \
+"<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
+<pipe-message>notmuch-mutt tag -- -inbox<enter>\
+<enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
+      "notmuch: remove message from inbox"
diff --git a/debian/.gitignore b/debian/.gitignore
new file mode 100644 (file)
index 0000000..cd0decc
--- /dev/null
@@ -0,0 +1,14 @@
+/tmp/
+/libnotmuch-dev/
+/libnotmuch*/
+/notmuch-emacs/
+/elpa-notmuch/
+/notmuch/
+/notmuch-mutt/
+/notmuch-vim/
+/ruby-notmuch/
+/python*-notmuch/
+/*.debhelper
+/*.debhelper.log
+/*.substvars
+/files
diff --git a/debian/NEWS b/debian/NEWS
new file mode 100644 (file)
index 0000000..bf661fe
--- /dev/null
@@ -0,0 +1,74 @@
+notmuch (0.21~rc1-1) experimental; urgency=medium
+
+  This release of notmuch requires a non-reversible database upgrade
+  to support database revision tracking. This upgrade will happen on
+  the first run of 'notmuch-new' after updating. Notmuch will backup
+  your tags for your before doing the upgrade, but it never hurts to
+  make your own backup with notmuch dump.
+
+ -- David Bremner <bremner@debian.org>  Thu, 15 Oct 2015 08:13:04 -0300
+
+notmuch (0.19-1) experimental; urgency=medium
+
+  This release of notmuch again requires a non-reversable database
+  upgrade to support database features. This upgrade will happen on
+  the first run of 'notmuch-new' after updating. Notmuch will backup
+  your tags for your before doing the upgrade, but it never hurts to
+  make your own backup with notmuch dump.
+
+ -- David Bremner <bremner@debian.org>  Fri, 14 Nov 2014 20:34:55 +0100
+
+notmuch (0.18~rc0-1) experimental; urgency=low
+
+  This release of notmuch requires a non-reversable database upgrade
+  to support the new path: and updated folder: prefixes. Notmuch
+  will backup your tags for your before doing the upgrade, but it
+  never hurts to make your own backup with notmuch dump before
+  next running 'notmuch new'
+
+ -- David Bremner <bremner@debian.org>  Tue, 22 Apr 2014 09:32:11 +0900
+
+notmuch (0.17-1) unstable; urgency=low
+
+  Previously on big endian architectures like sparc and powerpc the
+  computation of SHA1 hashes was incorrect. This meant that messages
+  with overlong or missing message-ids were given different computed
+  message-ids than on more common little endian architectures like
+  i386 and amd64.  If you use notmuch on a big endian architecture,
+  you are strongly advised to make a backup of your tags using
+  `notmuch dump` before this upgrade.  You can locate the affected
+  files using something like:
+
+  notmuch dump | \
+    awk '/^notmuch-sha1-[0-9a-f]{40} / \
+      {system("notmuch search --exclude=false --output=files id:" $1)}'
+
+ -- David Bremner <bremner@debian.org>  Mon, 30 Dec 2013 20:31:16 -0400
+
+notmuch (0.16-1) unstable; urgency=low
+
+  The vim interface has been rewritten from scratch. In particular
+  it requires a version of vim with ruby support.
+
+ -- David Bremner <bremner@debian.org>  Sat, 16 Feb 2013 08:12:02 -0400
+
+notmuch (0.14-1) unstable; urgency=low
+
+  There is an incompatible change in option syntax for dump and restore
+  in this release. Please update your scripts.
+
+  From upstream NEWS:
+
+  The deprecated positional output file argument to notmuch dump has
+  been replaced with an --output option. The input file positional
+  argument for restore has been replaced with an --input option for
+  consistency with dump.
+
+ -- David Bremner <bremner@debian.org>  Sun, 05 Aug 2012 11:52:49 -0300
+
+notmuch (0.6~238) unstable; urgency=low
+
+  The emacs user interface to notmuch is now contained in a separate
+  package called notmuch-emacs.
+
+ -- David Bremner <bremner@debian.org>  Mon, 20 Jun 2011 23:57:55 -0300
diff --git a/debian/changelog b/debian/changelog
new file mode 100644 (file)
index 0000000..75765b6
--- /dev/null
@@ -0,0 +1,1770 @@
+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
+    - Fix include bug triggered by glib 2.67
+    - Fix race condition in T568-lib-thread
+
+ -- David Bremner <bremner@debian.org>  Thu, 18 Feb 2021 07:23:00 -0400
+
+notmuch (0.31.3-2) unstable; urgency=medium
+
+  * Don't install gdb on hppa (skip gdb based tests)
+
+ -- David Bremner <bremner@debian.org>  Sat, 26 Dec 2020 15:14:07 -0400
+
+notmuch (0.31.3-1) unstable; urgency=medium
+
+  * New upstream bugfix release
+  * Second fix for T360, fix regression on ppc64el
+  * Fix for exclude tags in notmuch2 python bindings
+  * Fix for memory error in notmuch_database_get_config_list
+
+ -- David Bremner <bremner@debian.org>  Fri, 25 Dec 2020 11:48:37 -0400
+
+notmuch (0.31.2-5) unstable; urgency=medium
+
+  * Use readelf instead of nm in T360, hopefully build in ppc64
+
+ -- David Bremner <bremner@debian.org>  Sun, 13 Dec 2020 08:24:23 -0400
+
+notmuch (0.31.2-4) unstable; urgency=medium
+
+  * Move prerequisite to file targets from phony ones. Thanks to
+    Lucas Nussbaum for the report. (Closes: #976934).
+
+ -- David Bremner <bremner@debian.org>  Thu, 10 Dec 2020 21:02:20 -0400
+
+notmuch (0.31.2-3) unstable; urgency=medium
+
+  * Switch to debhelper compat level 13
+
+ -- David Bremner <bremner@debian.org>  Mon, 09 Nov 2020 13:59:47 -0400
+
+notmuch (0.31.2-2) unstable; urgency=medium
+
+  * Run tests in verbose mode
+
+ -- David Bremner <bremner@debian.org>  Mon, 09 Nov 2020 08:45:38 -0400
+
+notmuch (0.31.2-1) unstable; urgency=medium
+
+  * Delete stray "version" file in upstream source
+
+ -- David Bremner <bremner@debian.org>  Sun, 08 Nov 2020 11:32:45 -0400
+
+notmuch (0.31.1-1) unstable; urgency=medium
+
+  * New upstream bugfix release.
+    - Portability / C++20 fixes
+    - Fix initialization bug in library config handling.
+
+ -- David Bremner <bremner@debian.org>  Sun, 08 Nov 2020 07:48:22 -0400
+
+notmuch (0.31-1) unstable; urgency=medium
+
+  * New upstream release
+  * Compatibility fixes for Emacs 27.1
+
+ -- David Bremner <bremner@debian.org>  Sat, 05 Sep 2020 21:47:42 -0300
+
+notmuch (0.31~rc2-1) experimental; urgency=medium
+
+  * New upstream release candidate
+  * Bug fix: "suggest elpa-mailscripts", thanks to Sean Whitton (Closes:
+    #944269).
+  * Bug fix: "suggest mailscripts", thanks to Sean Whitton (Closes:
+    #944270).
+  * Bug fix: "please drop transitional package notmuch-emacs from
+    src:notmuch", thanks to Holger Levsen (Closes: #940738).
+
+ -- David Bremner <bremner@debian.org>  Tue, 25 Aug 2020 07:51:33 -0300
+
+notmuch (0.31~rc1-1) experimental; urgency=medium
+
+  * Fix buggy test in T562-lib-database
+  * Clean up generated file in source package.
+
+ -- David Bremner <bremner@debian.org>  Mon, 17 Aug 2020 21:05:46 -0300
+
+notmuch (0.31~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate.
+  * Update notmuch-emacs for compatibility with GNU Emacs 27.1.
+
+ -- David Bremner <bremner@debian.org>  Sun, 16 Aug 2020 11:08:14 -0300
+
+notmuch (0.30-1) unstable; urgency=medium
+
+  * New upstream release
+  * Improvements to S/MIME handling
+  * Repairs to some mangled MIME messages
+  * New python bindings (notmuch2) compatible with current python 3
+
+ -- David Bremner <bremner@debian.org>  Fri, 10 Jul 2020 22:24:14 -0300
+
+notmuch (0.30~rc3-1) experimental; urgency=medium
+
+  * New upstream release candidate
+  * Mark two tests broken on legacy (32 bit time_t) architectures.
+  * Drop -std=c99
+
+ -- David Bremner <bremner@debian.org>  Fri, 03 Jul 2020 06:48:51 -0300
+
+notmuch (0.30~rc2-1) experimental; urgency=medium
+
+  * New upstream release candidate.
+  * Upstream fixes for new python bindings (python3-notmuch2).
+  * Update debian/copyright (one new author).
+
+ -- David Bremner <bremner@debian.org>  Tue, 16 Jun 2020 08:32:16 -0300
+
+notmuch (0.30~rc1-1) experimental; urgency=medium
+
+  * New upstream release candidate
+  * Update debian/changelog (new copyright holders)
+
+ -- David Bremner <bremner@debian.org>  Sat, 06 Jun 2020 08:06:56 -0300
+
+notmuch (0.30~rc0-2) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Mon, 01 Jun 2020 21:01:27 -0300
+
+notmuch (0.29.3-1) unstable; urgency=medium
+
+  * New upstream bugfix release.
+    - fix use-after-free bug in libnotmuch
+    - fix double close of file in "notmuch dump"
+
+ -- David Bremner <bremner@debian.org>  Wed, 27 Nov 2019 08:19:57 -0400
+
+notmuch (0.29.2-2) experimental; urgency=medium
+
+  * Drop python-notmuch binary package.
+  * Drop python2 build-dependency (Closes: #937161).
+  * Convert to pybuild for python bindings
+
+ -- David Bremner <bremner@debian.org>  Sat, 02 Nov 2019 18:21:06 -0300
+
+notmuch (0.29.2-1) unstable; urgency=medium
+
+  * New upstream bug fix release: fix file descriptor leak with gzipped
+    files. Thanks to James Troup for reporting and the fix.
+
+ -- David Bremner <bremner@debian.org>  Sat, 19 Oct 2019 07:23:21 -0300
+
+notmuch (0.29.1-2) unstable; urgency=medium
+
+  * Re-upload to unstable
+
+ -- David Bremner <bremner@debian.org>  Sun, 21 Jul 2019 11:43:40 -0300
+
+notmuch (0.29.1-1) experimental; urgency=medium
+
+  * New upstream bug fix release
+    - fix for building and installing without emacs, does not occur
+      in Debian builds.
+
+ -- David Bremner <bremner@debian.org>  Tue, 11 Jun 2019 20:16:03 -0300
+
+notmuch (0.29-2) experimental; urgency=medium
+
+  * New upstream feature release. See /usr/share/doc/notmuch/NEWS.gz for
+    details.
+  * Bug fix: "notmuch-hello screen should show notmuch logo", thanks to
+    Tim Retout (Closes: #918928).
+  * Bug fix: "Can't deal with compressed maildir files", thanks to
+    Joerg Jaspert (Closes: #688609).
+  * Bug fix: "Please ship notmuch-emacs-mua.desktop", thanks to Tim Retout
+    (Closes: #918975).
+
+ -- David Bremner <bremner@debian.org>  Thu, 06 Jun 2019 21:35:12 -0300
+
+notmuch (0.29~rc1-1) experimental; urgency=medium
+
+  * New upstream release candidate, with fix for parallel sphinx-build
+    invocation
+
+ -- David Bremner <bremner@debian.org>  Mon, 03 Jun 2019 08:09:38 -0300
+
+notmuch (0.29~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate.
+
+ -- David Bremner <bremner@debian.org>  Fri, 31 May 2019 08:22:21 -0300
+
+notmuch (0.28.4-1) unstable; urgency=medium
+
+  * New upstream bugfix release
+  * Fix for bug in 'notmuch show --raw' that causes spurious errors to be
+    reported when the mail file is a multiple of the libc buffer size
+    (e.g. 4096 bytes).
+
+ -- David Bremner <bremner@debian.org>  Sun, 05 May 2019 08:08:56 -0300
+
+notmuch (0.28.3-1) unstable; urgency=medium
+
+  * New upstream bugfix release.
+  * Fix for bug in message property search
+  * Fix for race condition leading to (very) occasional build failures
+    when building the documentation.
+
+ -- David Bremner <bremner@debian.org>  Tue, 05 Mar 2019 15:39:09 -0400
+
+notmuch (0.28.2-1) unstable; urgency=medium
+
+  * [notmuch-emacs] Invoke gpg from with --batch and --no-tty
+
+ -- David Bremner <bremner@debian.org>  Sun, 17 Feb 2019 07:30:33 -0400
+
+notmuch (0.28.1-1) unstable; urgency=medium
+
+  * New upstream bug fix release
+  * Bug fix: "muttprint/evince fails to show "print", thanks to
+    Joerg Jaspert (Closes: #920856).
+
+ -- David Bremner <bremner@debian.org>  Fri, 01 Feb 2019 08:05:05 -0400
+
+notmuch (0.28-2) unstable; urgency=medium
+
+  * Override location of bash, because /usr/bin/bash might exist
+    thanks to usrmerge.
+
+ -- David Bremner <bremner@debian.org>  Fri, 12 Oct 2018 20:54:00 -0300
+
+notmuch (0.28-1) unstable; urgency=medium
+
+  * New upstream releases.
+  * Includes threading fixes, support for relative database paths, and
+    rewritten zsh completion.
+
+ -- David Bremner <bremner@debian.org>  Fri, 12 Oct 2018 20:17:27 -0300
+
+notmuch (0.28~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate.
+
+ -- David Bremner <bremner@debian.org>  Wed, 03 Oct 2018 20:36:57 -0300
+
+notmuch (0.27-3) unstable; urgency=medium
+
+  * Update Vcs-Git to use https and specify correct branch
+  * Update Build-depends for unversioned emacs packages.
+
+ -- David Bremner <bremner@debian.org>  Sat, 08 Sep 2018 18:20:10 -0300
+
+notmuch (0.27-2) unstable; urgency=medium
+
+  * Add texinfo as a build-dep, build info version of documentation.
+
+ -- David Bremner <bremner@debian.org>  Thu, 28 Jun 2018 21:01:29 -0300
+
+notmuch (0.27-1) unstable; urgency=medium
+
+  * New upstream feature release
+    - thread subqueries (match terms in different messages of same thread)
+    - notmuch new --full-scan (ignore mtimes)
+    - notmuch show --decrypt=stash (decrypt and stash on first read)
+
+ -- David Bremner <bremner@debian.org>  Tue, 12 Jun 2018 22:39:33 -0300
+
+notmuch (0.27~rc1-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Thu, 31 May 2018 08:19:00 -0300
+
+notmuch (0.27~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Sat, 26 May 2018 09:12:37 -0700
+
+notmuch (0.26.2-2) unstable; urgency=medium
+
+  * Mark gdb and dtach as <!nocheck>, meaning they are only needed for
+    the test suite.
+  * Re-enable gdb based tests on s390x, ppc64el, armel, mipsel, they
+    pass on porterbox. Leave disabled on mipsel64 (gdb tests still
+    failing), and mips (many tests fail on porterbox).
+
+ -- David Bremner <bremner@debian.org>  Sun, 06 May 2018 08:36:52 -0300
+
+notmuch (0.26.2-1) unstable; urgency=medium
+
+  * Upstream bugfix release
+    - Break reference loops when indexing, fixes INTERNAL_ERROR in "notmuch show"
+    - Don't call get_mset(0,0,X), fixes crash on some gcc8 using distros
+
+ -- David Bremner <bremner@debian.org>  Sat, 28 Apr 2018 08:10:24 -0300
+
+notmuch (0.26.1-1) unstable; urgency=medium
+
+  * Bump LIBRARY_MINOR_VERSION to 1, for new functions in 0.26
+
+ -- David Bremner <bremner@debian.org>  Mon, 02 Apr 2018 08:08:01 -0300
+
+notmuch (0.26-1) unstable; urgency=medium
+
+  [ Daniel Kahn Gillmor ]
+  * build against python3-sphinx instead of python-sphinx
+  * d/changelog: strip trailing whitespace
+  * move to debhelper 10
+  * Standards-Version: bump to 4.1.3 (drop priority: extra
+    from transitional packages)
+
+  [ David Bremner ]
+  * New upstream release (see /usr/share/doc/notmuch/NEWS.gz)
+    - new command 'notmuch reindex'
+    - optional indexing of encrypted emails.
+    - indexing of files with duplicate message-id
+  * update symbols
+
+ -- David Bremner <bremner@debian.org>  Tue, 09 Jan 2018 07:13:21 -0400
+
+notmuch (0.26~rc2-1) experimental; urgency=medium
+
+  * Third upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Tue, 09 Jan 2018 07:13:11 -0400
+
+notmuch (0.26~rc1-1) experimental; urgency=medium
+
+  * Second upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Mon, 01 Jan 2018 21:17:39 -0400
+
+notmuch (0.26~rc0-1) experimental; urgency=medium
+
+  * Upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Thu, 28 Dec 2017 10:21:08 -0400
+
+notmuch (0.25.3-1) unstable; urgency=medium
+
+  * Upstream bugfix release. Fix for OpenPGP UID validity reporting,
+    and build failure with GMime 3.0.3+.
+  * Bug fix: "notmuch FTBFS on Alpha due to broken gdb", thanks to
+    Michael Cree (Closes: #881028).
+
+ -- David Bremner <bremner@debian.org>  Fri, 08 Dec 2017 21:08:00 -0400
+
+notmuch (0.25.2-1) unstable; urgency=medium
+
+  * New upstream bugfix release: fix for segfault when compiled
+    against gmime-2.6
+
+ -- David Bremner <bremner@debian.org>  Sun, 05 Nov 2017 20:05:49 -0400
+
+notmuch (0.25.1-1) unstable; urgency=medium
+
+  * new upstream bugfix release: mitigation for emacs bug 28350
+  * remove obsolete lintian override
+  * reformat debian/NEWS
+
+ -- David Bremner <bremner@debian.org>  Mon, 11 Sep 2017 22:20:48 -0300
+
+notmuch (0.25-6) unstable; urgency=medium
+
+  * Bug fix: "deletes shipped file on reinstall:
+    /etc/emacs/site-start.d/50notmuch.el", thanks to Andreas Beckmann
+    (Closes: #872197).
+
+ -- David Bremner <bremner@debian.org>  Tue, 15 Aug 2017 07:52:21 -0300
+
+notmuch (0.25-5) unstable; urgency=medium
+
+  * Bug fix: "dependency on elpa-emacs doesn't seem right", thanks
+    to Jiri Palecek (Closes: #871642).
+
+ -- David Bremner <bremner@debian.org>  Thu, 10 Aug 2017 06:42:50 -0400
+
+notmuch (0.25-4) unstable; urgency=medium
+
+  * Recommend elpa-emacs instead emacs-notmuch
+
+ -- David Bremner <bremner@debian.org>  Fri, 04 Aug 2017 18:11:35 -0400
+
+notmuch (0.25-3) unstable; urgency=medium
+
+  * Remove old startup file /etc/emacs/site-start.d/50notmuch.el
+
+ -- David Bremner <bremner@debian.org>  Thu, 03 Aug 2017 09:26:00 -0400
+
+notmuch (0.25-2) unstable; urgency=medium
+
+  * Drop build-dep on libgmime-2.4-dev, long unsupported upstream
+  * Bug fix: "please transition to gmime 3.0", thanks to Daniel Kahn
+    Gillmor (Closes: #867353).
+
+ -- David Bremner <bremner@debian.org>  Wed, 26 Jul 2017 10:59:14 -0400
+
+notmuch (0.25-1) unstable; urgency=medium
+
+  * New upstream release
+    - regexp search for mid, paths, tags
+    - drop inline images when indexing html
+
+ -- David Bremner <bremner@debian.org>  Tue, 25 Jul 2017 08:28:20 -0300
+
+notmuch (0.25~rc1-2) unstable; urgency=low
+
+  * upload to unstable
+
+ -- David Bremner <bremner@debian.org>  Tue, 18 Jul 2017 19:47:45 -0300
+
+notmuch (0.25~rc1-1) experimental; urgency=medium
+
+  * Bump standards version to 4.0.0 (no changes needed)
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Tue, 18 Jul 2017 07:04:14 -0300
+
+notmuch (0.25~rc0-2) experimental; urgency=medium
+
+  * Fix compilation on 32 bit architectures (time_t vs. gint64)
+
+ -- David Bremner <bremner@debian.org>  Mon, 17 Jul 2017 08:49:32 -0300
+
+notmuch (0.25~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate
+  * Drop notmuch-dbg, use notmuch-dbgsym if debug symbols are needed.
+  * [lib] Bump SONAME to libnotmuch.so.5
+  * Bug fix: "doesn't check gpg/pgp signatures by default", thanks
+    to Vagrant Cascadian (Closes: #755544).
+  * Bug fix: "allow separation of command-line options and their
+    values: --option foo", thanks to Paul Wise (Closes: #825886).
+
+ -- David Bremner <bremner@debian.org>  Sun, 16 Jul 2017 08:48:59 -0300
+
+notmuch (0.24.2-2) unstable; urgency=medium
+
+  * rebuild for unstable
+
+ -- David Bremner <bremner@debian.org>  Sun, 02 Jul 2017 12:48:46 -0300
+
+notmuch (0.24.2-1) experimental; urgency=medium
+
+  * Fix dump output to not include tags when not asked for
+  * Fix file name stashing in emacs tree view.
+
+ -- David Bremner <bremner@debian.org>  Thu, 01 Jun 2017 07:24:55 -0300
+
+notmuch (0.24.1-1) experimental; urgency=medium
+
+  * Restore Xapian wildcard queries to from: and subject:
+  * Handle empty queries for from: and subject:
+  * Memory leaks in notmuch show fixed
+  * Fix bug notmuch dump header generation
+
+ -- David Bremner <bremner@debian.org>  Sat, 01 Apr 2017 09:17:47 -0300
+
+notmuch (0.24-1) experimental; urgency=medium
+
+  * New upstream release
+    - regexp search for from: and subject:
+    - Emacs interface improvements:
+      - draft handling
+      - don't automatically expand application/*
+      - jump (shortcut) menu for tagging.
+      - fold long headers when sending
+    - library improvements
+      - catch some stray DatabaseModifiedErrors
+      - make exclude handling non-destructive.
+
+ -- David Bremner <bremner@debian.org>  Sun, 12 Mar 2017 22:14:25 -0300
+
+notmuch (0.24~rc1-1) experimental; urgency=medium
+
+  * New upstream release candidate
+  * upstream release notes
+  * One library internals fix/optimization for regexp processing.
+
+ -- David Bremner <bremner@debian.org>  Wed, 08 Mar 2017 08:08:34 -0400
+
+notmuch (0.24~rc0-1) experimental; urgency=medium
+
+  * New upstream feature release (candidate).
+
+ -- David Bremner <bremner@debian.org>  Sun, 05 Mar 2017 19:32:08 -0400
+
+notmuch (0.23.7-3) unstable; urgency=medium
+
+  * Cherry pick fixes to dump header from 0.24.1
+
+ -- David Bremner <bremner@debian.org>  Sat, 01 Apr 2017 21:09:36 -0300
+
+notmuch (0.23.7-2) unstable; urgency=medium
+
+  * Cherry pick 06adc276, fix use after free in libnotmuch4
+
+ -- David Bremner <bremner@debian.org>  Sun, 19 Mar 2017 09:38:17 -0300
+
+notmuch (0.23.7-1) unstable; urgency=medium
+
+  * Move test suite $GNUPGHOME to /tmp to avoid problems with long build paths.
+  * Fix read-after-free bug in `notmuch new`.
+
+ -- David Bremner <bremner@debian.org>  Tue, 28 Feb 2017 20:39:30 -0400
+
+notmuch (0.23.5-1) unstable; urgency=medium
+
+  * Remove RUNPATH from /usr/bin/notmuch
+
+ -- David Bremner <bremner@debian.org>  Mon, 09 Jan 2017 06:24:39 -0400
+
+notmuch (0.23.4-1) unstable; urgency=medium
+
+  * Improve error handling in notmuch insert
+  * Restore autoload cookie for notmuch-search (notmuch-emacs)
+
+ -- David Bremner <bremner@debian.org>  Sat, 24 Dec 2016 17:47:48 +0900
+
+notmuch (0.23.3-3) unstable; urgency=medium
+
+  * Disable gdb using tests on kfreebsd-*, due to apparent gdb breakage
+
+ -- David Bremner <bremner@debian.org>  Mon, 05 Dec 2016 08:25:32 -0400
+
+notmuch (0.23.3-2) unstable; urgency=medium
+
+  * Add missing depends to notmuch-emacs. Thanks to micah for the
+    report.
+
+ -- David Bremner <bremner@debian.org>  Thu, 01 Dec 2016 08:06:59 -0400
+
+notmuch (0.23.3-1) unstable; urgency=medium
+
+  * Re-enable test suite
+  * Fix test suite compatibility with gnupg 2.1.16. Fixes: "FTBFS:
+    Tests failures", thanks to Lucas Nussbaum (Closes: #844915).
+  * Bug fix: "race condition in `notmuch new`?", thanks to Paul Wise
+    (Closes: #843127).
+
+ -- David Bremner <bremner@debian.org>  Sat, 26 Nov 2016 08:37:39 -0400
+
+notmuch (0.23.2-1) unstable; urgency=medium
+
+  * New upstream bugfix release
+  * Convert notmuch-emacs to dh-elpa, new binary package elpa-notmuch
+  * Bug fix: "maintainer script(s) do not start on #!", thanks to
+    treinen@debian.org; (Closes: #843287).
+
+ -- David Bremner <bremner@debian.org>  Thu, 10 Nov 2016 22:36:04 -0400
+
+notmuch (0.23.1-1) unstable; urgency=medium
+
+  * New upstream bugfix release
+  * Fix test suite for Emacs 25.1
+  * Fix some Emacs customization regressions introduced in 0.23
+  * Bug fix: "testsuite fails with TERM=unknown", thanks to Gianfranco
+    Costamagna (Closes: #841319).
+
+ -- David Bremner <bremner@debian.org>  Sun, 23 Oct 2016 22:06:12 -0300
+
+notmuch (0.23-2) unstable; urgency=medium
+
+  * upload to unstable
+
+ -- David Bremner <bremner@debian.org>  Wed, 05 Oct 2016 21:27:00 -0300
+
+notmuch (0.23-1) experimental; urgency=medium
+
+  * New upstream release
+  * Bump minor version of libnotmuch4 because of new symbols.
+  * Several new features, see /usr/share/doc/notmuch/NEWS.gz
+
+ -- David Bremner <bremner@debian.org>  Mon, 03 Oct 2016 22:46:26 -0300
+
+notmuch (0.23~rc1-1) experimental; urgency=medium
+
+  * New upstream release candidate
+  * Make configure more robust on "unknown" platforms. Fixes FTBFS on
+    kfreebsd.
+
+ -- David Bremner <bremner@debian.org>  Fri, 30 Sep 2016 07:19:26 -0300
+
+notmuch (0.23~rc0-1) experimental; urgency=medium
+
+  * New upstream release candidate
+  * Bug fix: "Calls to notmuch_directory_get_mtime() don't return
+    the recently set mtime", thanks to Lars Luthman (Closes: #826881).
+  * Bug fix: "Please document compact command", thanks to Olivier
+    Berger (Closes: #825884).
+
+ -- David Bremner <bremner@debian.org>  Mon, 26 Sep 2016 07:28:06 -0300
+
+notmuch (0.22.2-1) unstable; urgency=medium
+
+  * Fix test suite compatibility with GnuPG 2.1.15.  Bug fix: "FTBFS:
+    Tests failures", thanks to Lucas Nussbaum (Closes: #837013).
+
+ -- David Bremner <bremner@debian.org>  Thu, 08 Sep 2016 19:09:53 -0300
+
+notmuch (0.22.1-3) unstable; urgency=medium
+
+  * Gag gdb even more. Bug fix: "FTBFS in testing", thanks to Santiago
+    Vila (Closes: #834271).
+
+ -- David Bremner <bremner@debian.org>  Sun, 14 Aug 2016 13:31:13 +0900
+
+notmuch (0.22.1-2) unstable; urgency=medium
+
+  * Add explicit build-depends on gnupg, for the test suite.
+
+ -- David Bremner <bremner@debian.org>  Tue, 19 Jul 2016 08:50:13 -0300
+
+notmuch (0.22.1-1) unstable; urgency=medium
+
+  * Correct the definition of `LIBNOTMUCH_CHECK_VERSION`.
+  * Document the (lack of) operations permitted on a closed database
+    (Closes: #826843).
+  * Fix race condition in dump / restore tests.
+  * [notmuch-emacs] Tell `message-mode` mode that outgoing messages are mail
+  * [notmuch-emacs] Respect charset of MIME parts when reading them
+
+ -- David Bremner <bremner@debian.org>  Tue, 19 Jul 2016 06:42:09 -0300
+
+notmuch (0.22.1~rc0-1) experimental; urgency=medium
+
+  * release candidate for bugfix release
+
+ -- David Bremner <bremner@debian.org>  Thu, 30 Jun 2016 21:28:13 +0200
+
+notmuch (0.22-1) unstable; urgency=medium
+
+  * New upstream release.  See /usr/share/doc/notmuch/NEWS for new
+    features and bug fixes.
+
+ -- David Bremner <bremner@debian.org>  Tue, 26 Apr 2016 21:31:44 -0300
+
+notmuch (0.22~rc1-1) experimental; urgency=medium
+
+  * Upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Sun, 24 Apr 2016 18:03:15 -0300
+
+notmuch (0.22~rc0-1) experimental; urgency=medium
+
+  * Upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Sat, 16 Apr 2016 08:45:32 -0300
+
+notmuch (0.21-3) unstable; urgency=medium
+
+  * Add mips and mips64el to gdb build-dep blacklist
+
+ -- David Bremner <bremner@debian.org>  Sat, 14 Nov 2015 19:07:06 -0400
+
+notmuch (0.21-2) unstable; urgency=medium
+
+  * Build-conflict with gdb on ppc64el and mipsel. Workaround gdb breakage on those
+    architectures (Closes: #804792).
+
+ -- David Bremner <bremner@debian.org>  Thu, 12 Nov 2015 08:54:23 -0400
+
+notmuch (0.21-1) unstable; urgency=medium
+
+  * New upstream release. Highlights include
+    - revision tracking for metadata
+    - new features and bug fixes for emacs interface
+    See /usr/share/doc/notmuch/NEWS for more details.
+
+ -- David Bremner <bremner@debian.org>  Thu, 29 Oct 2015 20:04:42 -0300
+
+notmuch (0.21~rc3-3) experimental; urgency=medium
+
+  * Build-conflict with gdb-minimal. gdb python scripts are needed for
+    the test suite
+
+ -- David Bremner <bremner@debian.org>  Sun, 25 Oct 2015 22:08:56 -0300
+
+notmuch (0.21~rc3-2) experimental; urgency=medium
+
+  * Bug fix: "reply-to encrypted messages in tree view fails to quote
+    and defaults to unencrypted message", thanks to Vagrant Cascadian
+    (Closes: #795243).
+  * Bug fix: "install/notmuch-emacs may interact with console, fail
+    emacs24 upgrade", thanks to Hilko Bengen (Closes: #802952).
+
+ -- David Bremner <bremner@debian.org>  Sun, 25 Oct 2015 13:42:57 -0300
+
+notmuch (0.21~rc3-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Thu, 22 Oct 2015 09:19:02 -0300
+
+notmuch (0.21~rc2-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Mon, 19 Oct 2015 07:25:10 -0300
+
+notmuch (0.21~rc1-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Thu, 15 Oct 2015 08:08:17 -0300
+
+notmuch (0.20.2-2) unstable; urgency=medium
+
+  * Fix linking in emacsen-install script. The previous version can
+    break an emacs upgrade.
+
+ -- David Bremner <bremner@debian.org>  Sat, 26 Sep 2015 09:26:41 -0300
+
+notmuch (0.20.2-1) unstable; urgency=medium
+
+  * Bug fix: "notmuch-tree does not mark messages as read", thanks to
+    Raúl Benencia (Closes: #789693).
+
+ -- David Bremner <bremner@debian.org>  Sat, 27 Jun 2015 15:03:33 +0200
+
+notmuch (0.20.1-1) unstable; urgency=medium
+
+  * Bug fix: "FTBFS on arm64", thanks to Edmund Grimley Evans (Closes:
+    #787341).
+
+ -- David Bremner <bremner@debian.org>  Mon, 01 Jun 2015 21:58:54 +0200
+
+notmuch (0.20-1) unstable; urgency=medium
+
+  * New upstream release
+    - new mimetype search prefix
+    - improvements to emacs, vim, and mutt front-ends
+    - undeprecate single message mboxes.
+    - reduced noise on stderr from the library
+    - improved API for notmuch_query_search_{messages, thread}
+
+ -- David Bremner <bremner@debian.org>  Sun, 31 May 2015 11:21:14 +0200
+
+notmuch (0.20~rc2-1) experimental; urgency=medium
+
+  * New upstream release candidate.
+  * Fix breakage of python bindings under python3
+
+ -- David Bremner <bremner@debian.org>  Sat, 23 May 2015 21:05:03 +0200
+
+notmuch (0.20~rc1-1) experimental; urgency=medium
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Mon, 04 May 2015 08:08:00 +0200
+
+notmuch (0.19-1) experimental; urgency=medium
+
+  * New upstream release.
+    - Improvements to reliability of 'notmuch dump' and the error
+    handling for 'notmuch insert'.
+    - The new 'notmuch address' command is intended to make searching
+    for email addresses more convenient.
+    - At the library level the revised handling of missing messages
+    fixes at least one bug in threading.
+    - Interface improvements to the emacs interface, most notably the
+    ability to bindkeyboard shortcuts to saved searches.
+
+ -- David Bremner <bremner@debian.org>  Fri, 14 Nov 2014 19:34:12 +0100
+
+notmuch (0.19~rc2-1) experimental; urgency=medium
+
+  * New upstream release candidate
+  * Updated defaults for "notmuch address"
+  * Assert compliance with policy 3.9.6
+
+ -- David Bremner <bremner@debian.org>  Sun, 09 Nov 2014 16:46:31 +0100
+
+notmuch (0.19~rc1-1) experimental; urgency=low
+
+  * New upstream release candidate
+  * Bump libnotmuch SONAME because of API changes
+
+ -- David Bremner <bremner@debian.org>  Thu, 06 Nov 2014 00:30:39 +0100
+
+notmuch (0.18.2-1) unstable; urgency=medium
+
+  * Rebuild for unstable.
+  * Translate T380-atomicity to use gdb/python
+  * Emacs 24.4 related bug fixes
+  * Simplify T360-symbol-hiding, port to ppc64el
+
+ -- David Bremner <bremner@debian.org>  Sat, 25 Oct 2014 18:19:53 +0200
+
+notmuch (0.18.2~rc1-1) experimental; urgency=medium
+
+  * Test suite bug and portability fix release.
+
+ -- David Bremner <bremner@debian.org>  Sat, 25 Oct 2014 10:48:16 +0200
+
+notmuch (0.18.1-2) unstable; urgency=medium
+
+  * Update build-deps to use emacs24 on buildd (Closes: #756085)
+  * Disable gdb atomicity test for arm64 as gdb is currently broken on
+    the (unofficial) buildds
+  * Re-enable atomicity test on armhf; upstream fix seems to have
+    worked.
+
+ -- David Bremner <bremner@debian.org>  Sat, 09 Aug 2014 11:48:10 -0300
+
+notmuch (0.18.1-1) unstable; urgency=medium
+
+  * New upstream bug fix release
+    - Re-enable support for single-message mbox files
+    - Fix for phrase indexing
+    - Make tagging empty query in Emacs harmless
+
+ -- David Bremner <bremner@debian.org>  Wed, 25 Jun 2014 07:20:45 -0300
+
+notmuch (0.18.1~rc0-1) experimental; urgency=medium
+
+  * New upstream bug fix release (candidate)
+  * Tighten dependence of python packages on libnotmuch
+    (Closes: #749881).
+  * Redo emacsen-install script from sample in emacsen-common
+    (Closes: #739839).
+
+ -- David Bremner <bremner@debian.org>  Sat, 14 Jun 2014 07:50:28 -0300
+
+notmuch (0.18-3) unstable; urgency=medium
+
+  * Disable atomicity tests on armel.
+
+ -- David Bremner <bremner@debian.org>  Thu, 08 May 2014 14:26:45 +0900
+
+notmuch (0.18-2) unstable; urgency=medium
+
+  * Disable atomicity tests on armhf. These should be re-enabled when
+    upstream releases a fix for this (in progress).
+
+ -- David Bremner <bremner@debian.org>  Thu, 08 May 2014 08:28:33 +0900
+
+notmuch (0.18-1) unstable; urgency=medium
+
+  * New upstream release. For detailed release notes see
+    see /usr/share/doc/notmuch/NEWS.gz. Some highlights:
+    - Changes/enhancements to searching for messages by filesystem
+      location ('folder:' and 'path:' prefixes).
+    - Saved searches in Emacs have also been enhanced to allow
+      distinct search orders for each one.
+    - Another enhancement to the Emacs interface is that replies to
+      encrypted messages are now encrypted, reducing the risk of
+      unintentional information disclosure.
+    - The default dump output format has changed to the more robust
+      'batch-tag' format.
+    - The previously deprecated parsing of single message mboxes has
+      been removed.
+
+ -- David Bremner <bremner@debian.org>  Tue, 06 May 2014 16:20:39 +0900
+
+notmuch (0.18~rc1-1) experimental; urgency=low
+
+  * Upstream release candidate
+    - include encoding fix for vim client.
+
+ -- David Bremner <bremner@debian.org>  Sun, 04 May 2014 07:29:51 +0900
+
+notmuch (0.18~rc0-1) experimental; urgency=low
+
+  * Upstream release candidate.
+  * Bug fix: "insufficient sanitization of arguments to notmuch CLI",
+    thanks to Antoine Beaupré (Closes: #737496).
+  * Bug fix: "notmuch(1) manpage: typo: int -> in", thanks to Jakub
+    Wilk (Closes: #739556).
+  * Bug fix: "get a quiet option", thanks to Joerg Jaspert (Closes:
+    #666027).
+  * Bug fix: "Please remove me from Uploaders", thanks to martin f
+    krafft (Closes: #719100).
+  * Bug fix: "M-x notmuch-show-reply on an encrypted message should
+    insert encryption tags into the mml buffer", thanks to Daniel Kahn
+    Gillmor (Closes: #704648).
+
+ -- David Bremner <bremner@debian.org>  Tue, 22 Apr 2014 09:27:29 +0900
+
+notmuch (0.17-5) unstable; urgency=medium
+
+  * Bug fix: "unowned directory after purge: /0755/", thanks to
+    Andreas Beckmann (Closes: #740325).
+
+ -- David Bremner <bremner@debian.org>  Mon, 03 Mar 2014 07:29:06 -0400
+
+notmuch (0.17-4) unstable; urgency=medium
+
+  * Bug fix: "Please update ruby binary extension install path",
+    thanks to Christian Hofstaedtler (Closes: #739120).
+
+ -- David Bremner <bremner@debian.org>  Tue, 18 Feb 2014 21:37:44 -0400
+
+notmuch (0.17-3) unstable; urgency=medium
+
+  * update notmuch-emacs for debian emacs policy 2.0.6
+  * Update emacs test suite for Hurd compatibility
+
+ -- David Bremner <bremner@debian.org>  Sun, 12 Jan 2014 17:07:16 -0400
+
+notmuch (0.17-2) unstable; urgency=medium
+
+  * Bug fix: "package should warn in a NEWS.Debian file about possible
+    pre-upgrade action", thanks to Jonas Smedegaard (Closes: #733853).
+
+ -- David Bremner <bremner@debian.org>  Wed, 01 Jan 2014 07:44:25 -0400
+
+notmuch (0.17-1) unstable; urgency=low
+
+  * New upstream feature release. See /usr/share/doc/notmuch/NEWS.gz
+    for details.  Highlights include:
+    - notmuch compact command (Closes: #720543).
+    - emacs "tree" view
+  * Remove madduck from uploaders (Closes: #719100).
+
+ -- David Bremner <bremner@debian.org>  Mon, 30 Dec 2013 20:28:20 -0400
+
+notmuch (0.17~rc4-1) experimental; urgency=low
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Sat, 28 Dec 2013 18:30:06 -0400
+
+notmuch (0.17~rc3-1) experimental; urgency=low
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Sat, 07 Dec 2013 17:05:11 +0800
+
+notmuch (0.17~rc2-1) experimental; urgency=low
+
+  * New upstream release candidate
+  * Remove gdb as build-dep on s390x. This implicitly disables failing
+    atomicity test.  For more information, see #728705
+
+ -- David Bremner <bremner@debian.org>  Sun, 24 Nov 2013 19:34:28 -0400
+
+notmuch (0.17~rc1-1) experimental; urgency=low
+
+  * New upstream release candidate
+
+ -- David Bremner <bremner@debian.org>  Wed, 20 Nov 2013 19:27:48 -0400
+
+notmuch (0.16-1) unstable; urgency=low
+
+  * New upstream feature release
+    - 'notmuch insert' command replaces notmuch-deliver (Closes: #692889).
+    - New ruby based vim interface (Closes: 616692, 636707).
+  * Provide a notmuch-dbg package, thanks to Daniel Kahn Gillmor
+    (Closes: #717339).
+  * Include alot to the list of recommended interfaces, thanks to
+    Simon Chopin (Closes: #709832).
+
+ -- David Bremner <bremner@debian.org>  Sat, 03 Aug 2013 08:28:39 -0300
+
+notmuch (0.15.2-2) unstable; urgency=low
+
+  * Bug fix: tighten dependence of notmuch-mutt on notmuch,
+    thanks to Philippe Gimmel and Jameson Rollins (Closes: #703608).
+  * Bump Standards-Version to 3.9.4; no changes.
+
+ -- David Bremner <bremner@debian.org>  Sat, 25 May 2013 18:37:23 -0300
+
+notmuch (0.15.2-1) experimental; urgency=low
+
+  * Upstream bug fix release.
+    - Improve support for parallel building
+    - Update Emacs tests for portability, fix FTBFS on hurd-i386
+
+ -- David Bremner <bremner@debian.org>  Fri, 22 Mar 2013 20:42:42 -0400
+
+notmuch (0.15.1-1) experimental; urgency=low
+
+  * Upstream bug fix release: set default TERM for running tests.
+  * Re-enable build time self-tests.
+
+ -- David Bremner <bremner@debian.org>  Thu, 24 Jan 2013 07:19:45 -0400
+
+notmuch (0.15-2) experimental; urgency=low
+
+  * Disable tests until a proper fix for running tests without a
+    proper TERM value is developed (again).
+
+ -- David Bremner <bremner@debian.org>  Sun, 20 Jan 2013 18:36:16 -0400
+
+notmuch (0.15-1) experimental; urgency=low
+
+  * New upstream release.
+    - Date range search support
+    - Empty tag names and tags beginning with "-" are deprecated
+    - Support for single message mboxes is deprecated
+    - Fixed `notmuch new` to skip ignored broken symlinks
+    - New dump/restore format and tagging interface
+    - Emacs Interface
+      - Removal of the deprecated `notmuch-folders` variable
+      - Visibility of MIME parts can be toggled
+      - Emacs now buttonizes mid: links
+      - Improved text/calendar content handling
+      - Disabled coding conversions when reading
+      - Fixed errors with HTML email containing images in Emacs 24
+      - Fixed handling of tags with unusual characters in them
+      - Fixed buttonization of id: links without quote characters
+      - Automatic tag changes are now unified and customizable
+      - Support for stashing the thread id in show view
+      - New add-on tool: notmuch-pick
+
+ -- David Bremner <bremner@debian.org>  Fri, 18 Jan 2013 21:23:36 -0400
+
+notmuch (0.15~rc1-1) experimental; urgency=low
+
+  * New upstream release candidate.
+  * Change priority to optional (Closes: #687217).
+  * Remove Dm-Upload-Allowed field, as this is no longer used by
+    Debian.
+  * Add python3 bindings, thanks to Jakub Wilk (Closes: #683515).
+  * Bug fix: ".ical attachment problem", (Closes: #688747).
+
+ -- David Bremner <bremner@debian.org>  Wed, 16 Jan 2013 08:28:27 -0400
+
+notmuch (0.14-1) experimental; urgency=low
+
+  [ Stefano Zacchiroli ]
+  * notmuch-mutt: fix tag action invocation (Closes: #678012)
+  * Use notmuch-search-terms manpage in notmuch-mutt (Closes: #675073).
+
+  [ David Bremner ]
+  * Do a better job of cleaning up after configuration and testing
+    (Closes: #683505)
+  * Alternately depend on emacs24 instead of emacs23 (Closes: #677900).
+  * New upstream version
+    - incompatible changes to dump/restore syntax
+    - bug fixes for maildir synchronization
+
+ -- David Bremner <bremner@debian.org>  Tue, 21 Aug 2012 10:39:33 +0200
+
+notmuch (0.13.2-1) unstable; urgency=low
+
+  * Upstream bugfix release. No changes to binary packages.
+
+ -- David Bremner <bremner@debian.org>  Sat, 02 Jun 2012 18:16:01 -0300
+
+notmuch (0.13.1-1) unstable; urgency=low
+
+  * Upstream bugfix release.
+    - fix for encoding problems with reply in emacs
+    - notmuch_database_(get_directory|find_message_by_filename) now
+      work for read-only databases.
+
+ -- David Bremner <bremner@debian.org>  Fri, 25 May 2012 21:19:06 -0300
+
+notmuch (0.13-1) unstable; urgency=low
+
+  * New upstream release. See /usr/share/doc/notmuch/NEWS.gz for changes.
+
+  [ Stefano Zacchiroli ]
+  * Recommend all notmuch UI (including notmuch-mutt) as alternatives,
+    to avoid unneeded vim/emacs installation. Thanks Matteo F. Vescovi
+    for the patch. (Closes: #673011)
+
+ -- David Bremner <bremner@debian.org>  Tue, 15 May 2012 18:19:32 -0300
+
+notmuch (0.13~rc1-2) experimental; urgency=low
+
+  * New upstream pre-release
+  * new binary package "notmuch-mutt" for Mutt integration
+  * Bump libnotmuch SONAME because of API changes
+
+ -- David Bremner <bremner@debian.org>  Sat, 05 May 2012 10:26:47 -0300
+
+notmuch (0.12-1) unstable; urgency=low
+
+  * New upstream release
+    - Python 3.2 support
+    - GMime 2.6 support
+    - Many updates to emacs interface (see /usr/share/doc/notmuch/NEWS)
+    - Optionally ignore some files/directories within mail hierarchy
+
+ -- David Bremner <bremner@debian.org>  Tue, 20 Mar 2012 18:45:22 -0300
+
+notmuch (0.12~rc2-1) experimental; urgency=low
+
+  * Upstream pre-release
+  * New bug fixes since ~rc1
+    - fix for uninitialized variable
+    - fix for python bindings type signatures
+
+ -- David Bremner <bremner@debian.org>  Sun, 18 Mar 2012 08:10:35 -0300
+
+notmuch (0.12~rc1-1) experimental; urgency=low
+
+  * Upstream pre-release.
+  * Bump standards version to 3.9.3; no changes.
+
+ -- David Bremner <bremner@debian.org>  Thu, 01 Mar 2012 07:51:45 -0400
+
+notmuch (0.11.1-1) unstable; urgency=low
+
+  * Upstream bugfix release
+    - Fix error handling bug in python bindings
+    - Fix vulnerability in emacs reply handling
+
+ -- David Bremner <bremner@debian.org>  Fri, 03 Feb 2012 08:35:41 -0400
+
+notmuch (0.11-1) unstable; urgency=low
+
+  * New upstream release.
+    - New 'hook' feature for notmuch-new
+    - performance and memory use improvements
+    - new add-on tool notmuch-deliver
+
+ -- David Bremner <bremner@debian.org>  Fri, 13 Jan 2012 19:59:23 -0400
+
+notmuch (0.11~rc3-1) experimental; urgency=low
+
+  * New upstream release candidate
+    - Fix for uninitialized variable(s) in notmuch-reply
+
+ -- David Bremner <bremner@debian.org>  Mon, 09 Jan 2012 07:07:46 -0400
+
+notmuch (0.11~rc2-1) experimental; urgency=low
+
+  * New upstream release candidate.
+    - Includes fix for one python bindings segfault.
+
+ -- David Bremner <bremner@debian.org>  Mon, 02 Jan 2012 06:57:29 -0400
+
+notmuch (0.11~rc1-1) experimental; urgency=low
+
+  * New upstream release candidate.
+
+ -- David Bremner <bremner@debian.org>  Sun, 25 Dec 2011 23:07:08 -0400
+
+notmuch (0.10.2-1) unstable; urgency=low
+
+  * Upstream bug fix release
+    - Fix segfault in python bindings due to missing g_type_init call.
+
+ -- David Bremner <bremner@debian.org>  Sun, 04 Dec 2011 22:06:46 -0400
+
+notmuch (0.10.1-1) unstable; urgency=low
+
+  * Upstream bug fix release.
+    - Fix segfault on "notmuch --help"
+
+ -- David Bremner <bremner@debian.org>  Fri, 25 Nov 2011 12:11:30 -0500
+
+notmuch (0.10-1) unstable; urgency=low
+
+  * New upstream release
+    - search performance improvements
+    - emacs UI improvements
+    - new dump/restore features
+    - new script contrib/nmbug for sharing tags
+    - see /usr/share/doc/notmuch/NEWS for details
+
+ -- David Bremner <bremner@debian.org>  Wed, 23 Nov 2011 07:44:01 -0400
+
+notmuch (0.10~rc2-1) experimental; urgency=low
+
+  * New upstream release candidate
+    - includes patch to avoid long unix domain socket paths in tests
+
+ -- David Bremner <bremner@debian.org>  Sat, 19 Nov 2011 08:21:39 -0400
+
+notmuch (0.10~rc1-1) experimental; urgency=low
+
+  * New upstream release candidate.
+
+ -- David Bremner <bremner@debian.org>  Tue, 15 Nov 2011 19:46:55 -0400
+
+notmuch (0.9-1) unstable; urgency=low
+
+  * New upstream release.
+  * Only doc changes since last release candidate.
+  * Upload to unstable.
+
+ -- David Bremner <bremner@debian.org>  Tue, 11 Oct 2011 21:51:29 -0300
+
+notmuch (0.9~rc2-1) experimental; urgency=low
+
+  * Upstream release candidate
+  * API changes for n_d_find_message and n_d_find_message_by_filename.
+    - New SONAME for libnotmuch
+    - bindings changes for ruby and python
+  * Less non-text parts reported in replies.
+  * emacs: provide button action to fetch unknown gpg keys
+
+ -- David Bremner <bremner@debian.org>  Fri, 07 Oct 2011 18:53:04 -0300
+
+notmuch (0.9~rc1-1) experimental; urgency=low
+
+  * Upstream release candidate
+    - Atomicity improvements, thanks to Austin Clements
+    - Add missing call to g_type_init, thanks to Aaron Ecay
+  * notmuch-emacs: add versioned dependency on notmuch.
+    (Closes: #642240).
+
+ -- David Bremner <bremner@debian.org>  Sun, 25 Sep 2011 11:26:01 -0300
+
+notmuch (0.8-1) unstable; urgency=low
+
+  * New upstream version.
+    - Improved handling of message/rfc822 parts
+    - Improved Build system portability
+    - Documentation update for Ruby bindings
+    - Unicode, iterator, PEP8 changes for python bindings
+
+ -- David Bremner <bremner@debian.org>  Sat, 10 Sep 2011 08:53:55 -0300
+
+notmuch (0.8~rc1-1) experimental; urgency=low
+
+  * Upstream release candidate.
+
+ -- David Bremner <bremner@debian.org>  Tue, 06 Sep 2011 22:24:24 -0300
+
+notmuch (0.7-1) unstable; urgency=low
+
+  * New upstream release (no changes since 0.7~rc1).
+  * Upload to unstable.
+
+ -- David Bremner <bremner@debian.org>  Mon, 01 Aug 2011 21:46:26 +0200
+
+notmuch (0.7~rc1-1) experimental; urgency=low
+
+  * Upstream release candidate.
+  * Fix for notmuch.sym and parallel build (Thanks to
+    Thomas Jost)
+  * Bug fixes from Jason Woofenden for vim interface:
+    -  Fix "space (in show mode) mostly adds tag:inbox and tag:unread
+       instead of removing them"  (Closes: #633009).
+    -  Fix "says press 's'; to toggle signatures, but it's
+       really 'i'",  (Closes: #633115).
+    -  Fix "fix for from list on search pages", (Closes: #633045).
+  * Bug fixes for vim interface from Uwe Kleine-König
+    - use full path for sendmail/doc fix
+    - fix compose temp file name
+  * Python tag encoding fixes from Sebastian Spaeth.
+
+ -- David Bremner <bremner@debian.org>  Fri, 29 Jul 2011 12:16:56 +0200
+
+notmuch (0.6.1-1) unstable; urgency=low
+
+  * Properly install README.Debian in notmuch-vim (Closes: #632992).
+    Thanks to Jason Woofenden for the report.
+  * Force notmuch to depend on the same version of libnotmuch. Thanks to
+    Uwe Kleine-König for the patch.
+  * Export typeinfo for Xapian exceptions from libnotmuch. This fixes
+    certain mysterious uncaught exception problems.
+
+ -- David Bremner <bremner@debian.org>  Sun, 17 Jul 2011 10:20:42 -0300
+
+notmuch (0.6) unstable; urgency=low
+
+  * New upstream release; see /usr/share/doc/notmuch/NEWS for
+    details. Highlights include:
+    - Folder-based search (Closes: #597222)
+    - PGP/MIME decryption and verification
+  * Document strict dependency on emacs23 (Closes: #631994).
+
+ -- David Bremner <bremner@debian.org>  Fri, 01 Jul 2011 11:45:22 -0300
+
+notmuch (0.6~rc1) experimental; urgency=low
+
+  * Git snapshot 3f777b2
+  * Upstream release candidate.
+  * Fix description of notmuch-vim to mention vim, not emacs
+    (Closes: #631974)
+  * Install zsh completion as an example instead of into /usr/share/zsh to
+    avoid file conflict with zsh.
+
+ -- David Bremner <bremner@debian.org>  Thu, 30 Jun 2011 10:02:05 -0300
+
+notmuch (0.6~254) experimental; urgency=low
+
+  [David Bremner]
+  * Git snapshot fba968d
+  * Upstream release candidate
+  * Build binary package python-notmuch from upstream notmuch.
+  * Split off emacs and vim interfaces into their own packages.
+    (Closes: #578199)
+  * Hide Xapian exception symbols
+  * Build-depend on emacs23-nox instead of emacs
+
+  [ Jameson Rollins ]
+  * Bump standards version to 3.9.2 (No changes).
+
+ -- David Bremner <bremner@debian.org>  Thu, 23 Jun 2011 07:50:05 -0300
+
+notmuch (0.6~237) experimental; urgency=low
+
+  * Git snapshot 12d6f90
+  * Emacs: hide original message in top posted replies, isearch fix,
+    message display/hiding fixes/improvements.
+  * CLI: received header fixes.
+  * python: Improve docs, Remove Messages().__len__, Implement
+    Message.__cmp__ and __hash__, Message.tags_to_maildir_flags
+
+ -- David Bremner <bremner@debian.org>  Sat, 18 Jun 2011 11:14:51 -0300
+
+notmuch (0.6~215) experimental; urgency=low
+
+  * Git snapshot 5143e5e
+  * GMime: improve password handling, prevent premature closing stdout
+  * Emacs: sender address UI tweaks
+  * lib/message-file: plug three memleaks.
+  * Updated python bindings
+  * Sanitize "Subject:" and "Author:" fields in notmuch-search
+  * vim: new delete command, update mark as read command
+
+ -- David Bremner <bremner@debian.org>  Sat, 04 Jun 2011 08:37:36 -0300
+
+notmuch (0.6~180) experimental; urgency=low
+
+  * Git snapshot 1a96c40
+  * Fix corruption of binary parts
+    (see ML id:"874o4a1m74.fsf@yoom.home.cworth.org")
+
+ -- David Bremner <bremner@debian.org>  Tue, 31 May 2011 21:16:35 -0300
+
+notmuch (0.6~171) experimental; urgency=low
+
+  * Git snapshot cb8418784c2
+  * PGP/MIME handling in CLI and emacs front end.
+  * cli: Rewrite of multipart handling.
+  * emacs: Make the queries used in the all-tags section configurable
+  * emacs: Choose from address when composing/replying
+  * emacs: add notmuch-before- and notmuch-after-tag-hook
+  * notmuch reply: Avoid segmentation fault when printing multiple parts
+  * notmuch show: New part output handling.
+  * emacs: Show cleaner `From:' addresses in the summary line.
+  * emacs: Add custom `notmuch-show-elide-same-subject',
+    `notmuch-show-always-show-subject'
+  * emacs: Render text/x-vcalendar parts.
+  * emacs: Add `notmuch-show-multipart/alternative-discouraged'.
+  * vim: parse 'from' address, use sendmail directly, implement archive in
+    show view, refactor tagging stuff
+  * Eager metadata optimizations
+  * emacs: Fix notmuch-search-process-filter to handle incomplete lines
+  * Fix installation of zsh completion
+  * new: Enhance progress reporting
+  * Do not defer maildir flag synchronization for new messages
+  * vim: Get user email address from notmuch config file.
+  * lib: Save and restore term position in message while indexing.
+  * notmuch search: Clean up some memory leaks during search loop.
+  * New bindings for Go
+  * ruby: Add wrapper for message_get_filenames,  maildir sync. interface
+    query_get_s{ort,tring}
+  * Add support for folder-based searching.
+  * compatibility fixes for emacs22
+
+ -- David Bremner <bremner@debian.org>  Sat, 28 May 2011 07:25:49 -0300
+
+notmuch (0.5+nmu3) unstable; urgency=low
+
+  * Non-maintainer upload.
+  * Upload to unstable.
+
+ -- David Bremner <bremner@debian.org>  Sun, 01 May 2011 15:09:09 -0300
+
+notmuch (0.5+nmu2) experimental; urgency=low
+
+  * Non-maintainer upload.
+  * Second try at timeout for test. Put timeouts at top level.
+
+ -- David Bremner <bremner@debian.org>  Sun, 19 Dec 2010 21:40:08 -0400
+
+notmuch (0.5+nmu1) experimental; urgency=low
+
+  * Non-maintainer upload.
+  * Add a timeout to emacs tests to hopefully work around build failures.
+
+ -- David Bremner <bremner@debian.org>  Tue, 14 Dec 2010 22:23:51 -0400
+
+notmuch (0.5) unstable; urgency=low
+
+  * new: maildir-flag synchronization
+  * new: New "notmuch show --format=raw" (enables local emacs interface,
+    for example, to use remote notmuch via ssh)
+  * lib: Support for multiple files for a message
+    (notmuch_message_get_filenames)
+  * lib: Support for maildir-flag synchronization
+    (notmuch_message_tags_to_maildir_flags and
+    notmuch_message_maildir_flags_to_tags)
+  * emacs: Incompatible change to format of notmuch-fcc-dirs variable (for
+    users using the "fancy" configuration)
+  * emacs: Cleaner display of subject lines in thread views
+
+ -- Carl Worth <cworth@debian.org>  Thu, 11 Nov 2010 20:49:11 -0800
+
+notmuch (0.4) unstable; urgency=low
+
+  * new: notmuch search --output=(summary|threads|messages|tags|files)
+  * new: notmuch show --format=mbox <search-specification>
+  * new: notmuch config [get|set] <section>.<item> [value ...]
+  * lib: Add notmuch_query_get_query_string and notmuch_query_get_sort
+  * emacs: Enable Fcc of all sent messages by default (to "sent" directory)
+  * emacs: Ability to all open messages in a thread to a pipe
+  * emacs: Optional support for detecting inline patches
+  * emacs: Automatically tag messages as "replied" when sending a reply
+  * emacs: Allow search-result color specifications to overlay each other
+  * emacs: Make hidden author names still available for incremental search.
+  * emacs: New binding of Control-TAB (works like TAB in reverse)
+  * test: New modularization of test suite.
+  * test: New testing of emacs interface.
+  * bugfix: Avoid setting Bcc header in "notmuch reply"
+  * bugfix: Avoid corruption of database when "notmuch new " is interrupted.
+  * bugfix: Fix failure with extremely long message ID headers.
+  * bugfix: Fix for messages with "charset=unknown-8bit"
+  * bugfix: Fix notmuch_query_search_threads to return NULL on any exception
+  * bugfix: Fix "notmuch search" to return non-zero on any exception
+  * emacs bugfix: Fix for message with a subject containing, "[1234]"
+  * emacs bugfix: Fix to correctly handle message IDs containing ".."
+  * emacs bugfix: Fix initialization so "M-x notmuch" works by default.
+
+ -- Carl Worth <cworth@debian.org>  Mon, 01 Nov 2010 16:23:47 -0700
+
+notmuch (0.3.1) unstable; urgency=low
+
+  * Fix an infinite loop in "notmuch reply"
+  * Fix a potential SEGV in "notmuch search"
+  * emacs: Fix calculations for line wrapping in the "notmuch" view.
+  * emacs: Fix Fcc support to prompt to create a directory if necessary
+
+ -- Carl Worth <cworth@debian.org>  Tue, 27 Apr 2010 17:02:07 -0700
+
+notmuch (0.3) unstable; urgency=low
+
+  * User-configurable tags for new messages
+  * Threads search results named based on subjects that match search
+  * Faster operation of "notmuch tag" (avoid unneeded sorting)
+  * Even Better guessing of From: header for "notmuch reply"
+  * Indication of author names that match a search
+  * emacs: An entirely new initial view for notmuch, (friendly yet powerful)
+  * emacs: Full-featured "customize" support for configuring notmuch
+  * emacs: Support for doing tab-completion of email addresses
+  * emacs: Support for file-based (Fcc) delivery of sent messages
+  * emacs: New 'G' key binding to trigger mail refresh (G == "Get new mail")
+  * emacs: Implement emacs message display with the JSON output from notmuch
+  * emacs: Better handling of HTML/MIME attachments (inline images!)
+  * emacs: Customizable support for tidying of text/plain message content
+  * emacs: New support for searchable citations (even when hidden)
+  * emacs: More flexible handling of header visibility
+  * emacs: The Return key now toggles message visibility anywhere
+  * emacs: Customizable formatting of search results
+  * emacs: Generate nicer names for search buffers when using a saved search.
+  * emacs: Add a notmuch User-Agent header when sending mail from notmuch/emacs
+  * emacs: New keybinding (M-Ret) to open all collapsed messages in a thread
+  * libnotmuch1: Provide a new NOTMUCH_SORT_UNSORTED value for queries
+
+ -- Carl Worth <cworth@debian.org>  Tue, 27 Apr 2010 02:07:29 -0700
+
+notmuch (0.2) unstable; urgency=low
+
+  * Better guessing of From: header.
+  * Make "notmuch count" with no arguments count all messages
+  * Provide a new special-case search term of "*" to match all messages.
+  * Detect thread connections when a parent message is missing.
+  * Fix potential data loss in "notmuch new" with SIGINT
+  * Fix segfault when a message includes a MIME part that is empty.
+  * Fix handling of non-ASCII characters with --format=json
+  * Fix headers to be properly decoded in "notmuch reply"
+  * emacs: Show the last few lines of citations as well as the first few lines.
+  * emacs: The '+' and '-' commands can now add and remove tags by region.
+  * emacs: More meaningful buffer names for thread-view buffers.
+  * emacs: Customized colors of threads in search view based on tags.
+
+ -- Carl Worth <cworth@debian.org>  Fri, 16 Apr 2010 10:20:23 -0700
+
+notmuch (0.1-1) unstable; urgency=low
+
+  [ martin f. krafft ]
+  * Add suggestion to vim-addon-manager.
+
+  [ Carl Worth ]
+  * Improve package description (closes: #566282).
+  * New upstream version (0.1) (closes: #576647).
+  * New versioning to track upstream version scheme.
+  * Split packaging into notmuch, libnotmuch1, and libnotmuch-dev.
+  * Update to advertise conformance with policy 3.8.4 (no changes).
+  * Add a debian/watch file to notice upstream tar files.
+
+ -- Carl Worth <cworth@debian.org>  Tue, 06 Apr 2010 18:27:49 -0700
+
+notmuch (0.0+201001211401) unstable; urgency=low
+
+  * Upload to Debian (closes: #557354).
+  * New versioning scheme.
+  * Added emacs build dependency.
+  * Added Vcs-Browser field to debian/control.
+  * Downgrade recommendation for emacs to suggestion.
+  * Add vim to suggestions and enhancements.
+  * Put debian/* under separate copyright.
+  * Make Carl the maintainer.
+  * Add myself to uploaders.
+  * Install the vim plugin (using vim-addons).
+
+ -- martin f. krafft <madduck@debian.org>  Thu, 21 Jan 2010 14:00:54 +1300
+
+notmuch (0.0-1) unstable; urgency=low
+
+  * New Debian package.
+
+ -- Jameson Graef Rollins <jrollins@finestructure.net>  Fri, 27 Nov 2009 13:39:09 -0500
diff --git a/debian/control b/debian/control
new file mode 100644 (file)
index 0000000..135eb7c
--- /dev/null
@@ -0,0 +1,247 @@
+Source: notmuch
+Section: mail
+Priority: optional
+Maintainer: Carl Worth <cworth@debian.org>
+Uploaders:
+ Jameson Graef Rollins <jrollins@finestructure.net>,
+ David Bremner <bremner@debian.org>,
+Build-Conflicts:
+ gdb [ia64 mips mips64el hppa],
+ gdb-minimal,
+ ruby1.8,
+Build-Depends:
+ bash-completion (>=1.9.0~),
+ debhelper-compat (= 13),
+ dh-elpa (>= 1.3),
+ dh-python,
+ desktop-file-utils,
+ doxygen,
+ 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-dev,
+ libsexp-dev,
+ libtalloc-dev,
+ libxapian-dev,
+ libz-dev,
+ pkg-config,
+ python3,
+ python3-cffi,
+ python3-pytest,
+ python3-pytest-cov,
+ python3-setuptools,
+ python3-sphinx,
+ 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
+Vcs-Browser: https://git.notmuchmail.org/git/notmuch
+Rules-Requires-Root: no
+
+Package: notmuch
+Architecture: any
+Depends:
+ libnotmuch5 (= ${binary:Version}),
+ ${misc:Depends},
+ ${shlibs:Depends},
+Recommends:
+ elpa-notmuch | notmuch-vim | notmuch-mutt | alot,
+ gnupg-agent,
+ gpgsm,
+Suggests:
+ 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
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package contains the notmuch command-line interface
+
+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: libnotmuch5
+Section: libs
+Architecture: any
+Depends:
+ ${misc:Depends},
+ ${shlibs:Depends},
+Pre-Depends:
+ ${misc:Pre-Depends},
+Description: thread-based email index, search and tagging (runtime)
+ 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 runtime library, necessary to run
+ applications using libnotmuch.
+
+Package: libnotmuch-dev
+Section: libdevel
+Architecture: any
+Depends:
+ libnotmuch5 (= ${binary:Version}),
+ ${misc:Depends},
+Description: thread-based email index, search and tagging (development)
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package provides the necessary development libraries and header
+ files to allow you to develop new software using libnotmuch.
+
+Package: python3-notmuch
+Architecture: all
+Section: python
+Depends:
+ libnotmuch5 (>= ${source:Version}),
+ ${misc:Depends},
+ ${python3:Depends},
+Description: Python 3 legacy interface to the notmuch mail search and index library
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package provides a legacy Python 3 interface to the notmuch
+ functionality, directly interfacing with a shared notmuch library.
+ .
+ New projects are encouraged to use python3-notmuch2 instead.
+
+Package: python3-notmuch2
+Architecture: any
+Section: python
+Depends:
+ libnotmuch5 (>= ${source:Version}),
+ ${misc:Depends},
+ ${python3:Depends},
+ ${shlibs:Depends},
+Description: Python 3 interface to the notmuch mail search and index library
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package provides a Python 3 interface to the notmuch
+ functionality using CFFI bindings, which interface with a shared
+ notmuch library.
+ .
+ This is the preferred way to use notmuch via Python.
+
+Package: ruby-notmuch
+Architecture: any
+Section: ruby
+Depends:
+ ${misc:Depends},
+ ${shlibs:Depends},
+Description: Ruby interface to the notmuch mail search and index library
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package provides a Ruby interface to the notmuch
+ functionality, directly interfacing with a shared notmuch library.
+
+Package: elpa-notmuch
+Architecture: all
+Depends:
+ ${elpa:Depends},
+ ${misc:Depends},
+Suggests: elpa-mailscripts
+Description: thread-based email index, search and tagging (emacs interface)
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package provides an emacs based mail user agent based on
+ notmuch.
+
+Package: notmuch-vim
+Architecture: all
+Breaks:
+ notmuch (<<0.6~254~),
+Replaces:
+ notmuch (<<0.6~254~),
+Depends:
+ notmuch,
+ ruby-notmuch,
+ vim-addon-manager,
+ vim-ruby,
+ ${misc:Depends},
+Recommends:
+ ruby-mail,
+Description: thread-based email index, search and tagging (vim interface)
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package provides a vim based mail user agent based on
+ notmuch.
+
+Package: notmuch-mutt
+Architecture: all
+Depends:
+ libmail-box-perl,
+ libmailtools-perl,
+ libterm-readline-gnu-perl,
+ notmuch (>= 0.4),
+ ${misc:Depends},
+ ${perl:Depends},
+Recommends:
+ mutt,
+Enhances:
+ mutt,
+ notmuch,
+Description: thread-based email index, search and tagging (Mutt interface)
+ notmuch-mutt provides integration among the Mutt mail user agent and
+ the Notmuch mail indexer.
+ .
+ notmuch-mutt offer two main integration features. The first one is
+ the ability of stating a search query interactively and then jump to
+ a fresh Maildir containing its search results only. The second one is
+ the ability to reconstruct threads on the fly starting from the
+ current highlighted mail.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644 (file)
index 0000000..ba221e6
--- /dev/null
@@ -0,0 +1,112 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: notmuch
+Source: https://git.notmuchmail.org/git/notmuch
+Upstream-Contact: Notmuch Mailing List <notmuch@notmuchmail.org>
+
+Files: *
+Copyright:  Copyright 2009-2020
+ David Bremner
+ Carl Worth
+ Jani Nikula
+ Austin Clements
+ Daniel Kahn Gillmor
+ Mark Walters
+ Floris Bruynooghe
+ David Edmondson
+ Tomi Ollila
+ Sebastian Spaeth
+ Ali Polatel
+ Michal Sojka
+ Justus Winter
+ Sebastien Binet
+ W. Trevor King
+ Jameson Graef Rollins
+ Felipe Contreras
+ Pieter Praet
+ Peter Feigl
+ Dmitry Kurochkin
+ Peter Wang
+ Daniel Schoepe
+ Gregor Zattler
+ Keith Packard
+ Adam Wolfe Gordon
+ Stefano Zacchiroli
+ Vincent Breitmoser
+ laochailan
+ Ben Gamari
+ Aaron Ecay
+ Jesse Rosenthal
+ l-m-h@web.de
+ Thomas Jost
+ Dirk Hohndel
+ Blake Jones
+ Jonas Bernoulli
+ Damien Cassou
+ Vladimir Panteleev
+ Anton Khirnov
+ Matt Armstrong
+ Örjan Ekeberg
+ Jan Janak
+ Patrick Totzke
+ Chunyang Xu
+ rhn
+ Ruben Pollan
+ Ioan-Adrian Ratiu
+ Ethan Glasser-Camp
+ Todd
+ Chris Wilson
+ William Casarin
+ Yuri Volchkov
+ Cédric Cabessa
+ Mark Anderson
+ Jed Brown
+ Maxime Coste
+ Ludovic LANGE
+ Sebastian Poeplau
+ Mikhail
+ Gaute Hope
+ Keith Amidon
+ martin f. krafft
+ Jeffrey C. Ollie
+ Bart Trojanowski
+ Jameson Rollins
+ Scott Henson
+ Vladimir Marek
+ Servilio Afre Puentes
+ Kevin McCarthy
+ Tomas Carnecky
+ Kevin J. McCarthy
+ Scott Robinson
+ Wael M. Nasreddine
+ Charles Celerier
+ Olly Betts
+ Istvan Marko
+ Florian Klink
+ Thibaut Horel
+ Joel Borggrén-Franck
+ Ingmar Vanhassel
+ Olivier Taïbi
+ Ian Main
+ Alexander Botero-Lowry
+ Luis Ressel
+ Sergei Shilovsky
+ Trevor Jim
+ Jinwoo Lee
+ Uli Scholler
+ Matthew Lear
+ Amadeusz Żołnowski
+License: GPL-3+
+
+Files: debian/*
+Copyright:  Copyright 2010 Jameson Graef Rollins <jrollins@finestructure.net>
+ martin f. krafft <madduck@debian.org>
+License: GPL-3+
+
+License: GPL-3+
+ This package 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.
+ .
+ On Debian systems, the complete text of the GNU General Public License
+ version 3 can be found in file "/usr/share/common-licenses/GPL-3".
diff --git a/debian/elpa-notmuch.elpa b/debian/elpa-notmuch.elpa
new file mode 100644 (file)
index 0000000..7b3ce0f
--- /dev/null
@@ -0,0 +1,3 @@
+debian/tmp/usr/share/emacs/site-lisp/*.el
+debian/tmp/usr/share/emacs/site-lisp/notmuch-logo.svg
+emacs/notmuch-pkg.el
diff --git a/debian/elpa-notmuch.info b/debian/elpa-notmuch.info
new file mode 100644 (file)
index 0000000..0ac0fbf
--- /dev/null
@@ -0,0 +1 @@
+usr/share/info/*.info
diff --git a/debian/elpa-notmuch.lintian-overrides b/debian/elpa-notmuch.lintian-overrides
new file mode 100644 (file)
index 0000000..aa275ed
--- /dev/null
@@ -0,0 +1,4 @@
+# elpa-notmuch is an elisp plugin for dealing with e-mail.  We can
+# already tell from the package name that it is an elisp package, so
+# it belongs in Section: mail, and lintian is being too strict here.
+elpa-notmuch: wrong-section-according-to-package-name elpa-notmuch => lisp
diff --git a/debian/elpa-test b/debian/elpa-test
new file mode 100644 (file)
index 0000000..e3346c1
--- /dev/null
@@ -0,0 +1 @@
+disable=true
diff --git a/debian/gbp.conf b/debian/gbp.conf
new file mode 100644 (file)
index 0000000..a1c8735
--- /dev/null
@@ -0,0 +1,14 @@
+# Configuration file for git-buildpackage
+
+[DEFAULT]
+# The default branch for upstream sources
+upstream-branch = master
+
+# The default branch for the debian patch (no patch in our case)
+debian-branch = master
+
+# Format for upstream tags
+upstream-tag = %(version)s
+
+# Format for the debian tag
+debian-tag = debian/%(version)s
diff --git a/debian/libnotmuch-dev.install b/debian/libnotmuch-dev.install
new file mode 100644 (file)
index 0000000..96bbd63
--- /dev/null
@@ -0,0 +1,2 @@
+usr/include
+usr/lib/*/libnotmuch.so
diff --git a/debian/libnotmuch-dev.manpages b/debian/libnotmuch-dev.manpages
new file mode 100644 (file)
index 0000000..9c4bd5d
--- /dev/null
@@ -0,0 +1 @@
+usr/share/man/man3/notmuch.3.gz
diff --git a/debian/libnotmuch5.install b/debian/libnotmuch5.install
new file mode 100644 (file)
index 0000000..a513b47
--- /dev/null
@@ -0,0 +1 @@
+usr/lib/*/libnotmuch.so.*
diff --git a/debian/libnotmuch5.symbols b/debian/libnotmuch5.symbols
new file mode 100644 (file)
index 0000000..e6d2b7c
--- /dev/null
@@ -0,0 +1,166 @@
+libnotmuch.so.5 libnotmuch5 #MINVER#
+* Build-Depends-Package: libnotmuch-dev
+ notmuch_built_with@Base 0.23~rc0
+ notmuch_config_get@Base 0.32~rc0
+ notmuch_config_get_bool@Base 0.32~rc0
+ notmuch_config_get_pairs@Base 0.32~rc0
+ notmuch_config_get_values@Base 0.32~rc0
+ notmuch_config_get_values_string@Base 0.32~rc0
+ notmuch_config_list_destroy@Base 0.23~rc0
+ notmuch_config_list_key@Base 0.23~rc0
+ notmuch_config_list_move_to_next@Base 0.23~rc0
+ notmuch_config_list_valid@Base 0.23~rc0
+ notmuch_config_list_value@Base 0.23~rc0
+ notmuch_config_pairs_destroy@Base 0.32~rc0
+ notmuch_config_pairs_key@Base 0.32~rc0
+ notmuch_config_pairs_move_to_next@Base 0.32~rc0
+ notmuch_config_pairs_valid@Base 0.32~rc0
+ notmuch_config_pairs_value@Base 0.32~rc0
+ notmuch_config_path@Base 0.32~rc0
+ notmuch_config_set@Base 0.32~rc0
+ notmuch_config_values_destroy@Base 0.32~rc0
+ notmuch_config_values_get@Base 0.32~rc0
+ notmuch_config_values_move_to_next@Base 0.32~rc0
+ notmuch_config_values_start@Base 0.32~rc0
+ notmuch_config_values_valid@Base 0.32~rc0
+ notmuch_database_add_message@Base 0.3
+ notmuch_database_begin_atomic@Base 0.9~rc1
+ notmuch_database_close@Base 0.13~rc1
+ notmuch_database_compact@Base 0.17~rc1
+ notmuch_database_compact_db@Base 0.32~rc0
+ notmuch_database_create@Base 0.3
+ notmuch_database_create_verbose@Base 0.20~rc1
+ notmuch_database_create_with_config@Base 0.32~rc0
+ notmuch_database_destroy@Base 0.13~rc1
+ notmuch_database_end_atomic@Base 0.9~rc1
+ notmuch_database_find_message@Base 0.9~rc2
+ notmuch_database_find_message_by_filename@Base 0.9~rc2
+ notmuch_database_get_all_tags@Base 0.3
+ notmuch_database_get_config@Base 0.23~rc0
+ notmuch_database_get_config_list@Base 0.23~rc0
+ notmuch_database_get_default_indexopts@Base 0.26~rc0
+ notmuch_database_get_directory@Base 0.3
+ notmuch_database_get_path@Base 0.3
+ notmuch_database_get_revision@Base 0.21~rc1
+ notmuch_database_get_version@Base 0.3
+ notmuch_database_index_file@Base 0.26~rc0
+ notmuch_database_load_config@Base 0.32~rc0
+ notmuch_database_needs_upgrade@Base 0.3
+ notmuch_database_open@Base 0.3
+ notmuch_database_open_verbose@Base 0.20~rc1
+ notmuch_database_open_with_config@Base 0.32~rc0
+ notmuch_database_remove_message@Base 0.3
+ notmuch_database_reopen@Base 0.32~rc0
+ notmuch_database_set_config@Base 0.23~rc0
+ notmuch_database_status_string@Base 0.20~rc1
+ notmuch_database_upgrade@Base 0.3
+ notmuch_directory_delete@Base 0.21~rc1
+ notmuch_directory_destroy@Base 0.3
+ notmuch_directory_get_child_directories@Base 0.3
+ notmuch_directory_get_child_files@Base 0.3
+ notmuch_directory_get_mtime@Base 0.3
+ notmuch_directory_set_mtime@Base 0.3
+ notmuch_filenames_destroy@Base 0.3
+ notmuch_filenames_get@Base 0.3
+ notmuch_filenames_move_to_next@Base 0.3
+ notmuch_filenames_valid@Base 0.3
+ notmuch_indexopts_destroy@Base 0.26~rc0
+ notmuch_indexopts_get_decrypt_policy@Base 0.26~rc0
+ notmuch_indexopts_set_decrypt_policy@Base 0.26~rc0
+ notmuch_message_add_property@Base 0.23~rc0
+ notmuch_message_add_tag@Base 0.3
+ notmuch_message_count_files@Base 0.26~rc0
+ notmuch_message_count_properties@Base 0.27~rc0
+ notmuch_message_destroy@Base 0.3
+ notmuch_message_freeze@Base 0.3
+ notmuch_message_get_database@Base 0.27~rc0
+ notmuch_message_get_date@Base 0.3
+ notmuch_message_get_filename@Base 0.3
+ notmuch_message_get_filenames@Base 0.5
+ notmuch_message_get_flag@Base 0.3
+ notmuch_message_get_flag_st@Base 0.31~rc0
+ notmuch_message_get_header@Base 0.3
+ notmuch_message_get_message_id@Base 0.3
+ notmuch_message_get_properties@Base 0.23~rc0
+ notmuch_message_get_property@Base 0.23~rc0
+ notmuch_message_get_replies@Base 0.3
+ notmuch_message_get_tags@Base 0.3
+ notmuch_message_get_thread_id@Base 0.3
+ notmuch_message_has_maildir_flag@Base 0.26~rc0
+ notmuch_message_has_maildir_flag_st@Base 0.31~rc0
+ notmuch_message_maildir_flags_to_tags@Base 0.5
+ notmuch_message_properties_destroy@Base 0.23~rc0
+ notmuch_message_properties_key@Base 0.23~rc0
+ notmuch_message_properties_move_to_next@Base 0.23~rc0
+ notmuch_message_properties_valid@Base 0.23~rc0
+ notmuch_message_properties_value@Base 0.23~rc0
+ notmuch_message_reindex@Base 0.26~rc0
+ notmuch_message_remove_all_properties@Base 0.23~rc0
+ notmuch_message_remove_all_properties_with_prefix@Base 0.26~rc0
+ notmuch_message_remove_all_tags@Base 0.3
+ notmuch_message_remove_property@Base 0.23~rc0
+ notmuch_message_remove_tag@Base 0.3
+ notmuch_message_set_flag@Base 0.3
+ notmuch_message_tags_to_maildir_flags@Base 0.5
+ notmuch_message_thaw@Base 0.3
+ notmuch_messages_collect_tags@Base 0.3
+ notmuch_messages_destroy@Base 0.3
+ notmuch_messages_get@Base 0.3
+ notmuch_messages_move_to_next@Base 0.3
+ notmuch_messages_valid@Base 0.3
+ notmuch_query_add_tag_exclude@Base 0.12~rc1
+ notmuch_query_count_messages@Base 0.3
+ notmuch_query_count_messages_st@Base 0.21~rc1
+ notmuch_query_count_threads@Base 0.10~rc1
+ notmuch_query_count_threads_st@Base 0.21~rc1
+ notmuch_query_create@Base 0.3
+ notmuch_query_create_with_syntax@Base 0.34~rc0
+ notmuch_query_destroy@Base 0.3
+ notmuch_query_get_database@Base 0.21~rc1
+ notmuch_query_get_query_string@Base 0.4
+ notmuch_query_get_sort@Base 0.4
+ notmuch_query_search_messages@Base 0.3
+ notmuch_query_search_messages_st@Base 0.20~rc1
+ notmuch_query_search_threads@Base 0.3
+ notmuch_query_search_threads_st@Base 0.20~rc1
+ notmuch_query_set_omit_excluded@Base 0.13~rc1
+ notmuch_query_set_sort@Base 0.3
+ notmuch_status_to_string@Base 0.3
+ notmuch_tags_destroy@Base 0.3
+ notmuch_tags_get@Base 0.3
+ notmuch_tags_move_to_next@Base 0.3
+ notmuch_tags_valid@Base 0.3
+ notmuch_thread_destroy@Base 0.3
+ notmuch_thread_get_authors@Base 0.3
+ notmuch_thread_get_matched_messages@Base 0.3
+ notmuch_thread_get_messages@Base 0.16
+ notmuch_thread_get_newest_date@Base 0.3
+ notmuch_thread_get_oldest_date@Base 0.3
+ notmuch_thread_get_subject@Base 0.3
+ notmuch_thread_get_tags@Base 0.3
+ notmuch_thread_get_thread_id@Base 0.3
+ notmuch_thread_get_toplevel_messages@Base 0.3
+ notmuch_thread_get_total_files@Base 0.26~rc0
+ notmuch_thread_get_total_messages@Base 0.3
+ notmuch_threads_destroy@Base 0.3
+ notmuch_threads_get@Base 0.3
+ notmuch_threads_move_to_next@Base 0.3
+ notmuch_threads_valid@Base 0.3
+ (c++)"typeinfo for Xapian::LogicError@Base" 0.6.1
+ (c++)"typeinfo for Xapian::RuntimeError@Base" 0.6.1
+ (c++)"typeinfo for Xapian::DocNotFoundError@Base" 0.6.1
+ (c++)"typeinfo for Xapian::InvalidArgumentError@Base" 0.6.1
+ (c++)"typeinfo for Xapian::Error@Base" 0.6.1
+ (c++)"typeinfo for Xapian::DatabaseError@Base" 0.24~rc0
+ (c++)"typeinfo for Xapian::DatabaseModifiedError@Base" 0.24~rc0
+ (c++)"typeinfo for Xapian::DatabaseOpeningError@Base" 0.32~rc0
+ (c++|optional=present with Xapian 1.4)"typeinfo for Xapian::QueryParserError@Base" 0.23~rc0
+ (c++)"typeinfo name for Xapian::LogicError@Base" 0.6.1
+ (c++)"typeinfo name for Xapian::RuntimeError@Base" 0.6.1
+ (c++)"typeinfo name for Xapian::DocNotFoundError@Base" 0.6.1
+ (c++)"typeinfo name for Xapian::InvalidArgumentError@Base" 0.6.1
+ (c++)"typeinfo name for Xapian::Error@Base" 0.6.1
+ (c++)"typeinfo name for Xapian::DatabaseError@Base" 0.24~rc0
+ (c++)"typeinfo name for Xapian::DatabaseModifiedError@Base" 0.24~rc0
+ (c++)"typeinfo name for Xapian::DatabaseOpeningError@Base" 0.32~rc0
+ (c++|optional=present with Xapian 1.4)"typeinfo name for Xapian::QueryParserError@Base" 0.23~rc0
diff --git a/debian/not-installed b/debian/not-installed
new file mode 100644 (file)
index 0000000..fd92945
--- /dev/null
@@ -0,0 +1,3 @@
+usr/share/applications/mimeinfo.cache
+usr/share/info/dir
+usr/share/emacs/site-lisp/*.elc
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
diff --git a/debian/notmuch-emacs.README.Debian b/debian/notmuch-emacs.README.Debian
new file mode 100644 (file)
index 0000000..33f2d99
--- /dev/null
@@ -0,0 +1,4 @@
+* For help in getting started with notmuch and the emacs interface,
+  see /usr/share/doc/notmuch/README.
+
+ -- David Bremner <bremner@debian.org>, Wed, 27 Nov 2013 08:17:11 -0400
diff --git a/debian/notmuch-emacs.maintscript b/debian/notmuch-emacs.maintscript
new file mode 100644 (file)
index 0000000..8e3004e
--- /dev/null
@@ -0,0 +1 @@
+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
diff --git a/debian/notmuch-mutt.docs b/debian/notmuch-mutt.docs
new file mode 100644 (file)
index 0000000..f3d25cd
--- /dev/null
@@ -0,0 +1 @@
+contrib/notmuch-mutt/README
diff --git a/debian/notmuch-mutt.install b/debian/notmuch-mutt.install
new file mode 100644 (file)
index 0000000..8314f88
--- /dev/null
@@ -0,0 +1,2 @@
+etc/Muttrc.d/notmuch-mutt.rc
+usr/bin/notmuch-mutt
diff --git a/debian/notmuch-mutt.manpages b/debian/notmuch-mutt.manpages
new file mode 100644 (file)
index 0000000..a14ed69
--- /dev/null
@@ -0,0 +1 @@
+usr/share/man/man1/notmuch-mutt.1
diff --git a/debian/notmuch-vim.README.Debian b/debian/notmuch-vim.README.Debian
new file mode 100644 (file)
index 0000000..ec052ee
--- /dev/null
@@ -0,0 +1,8 @@
+notmuch for Debian
+==================
+
+To use the vim plugin, please install it using vim-addons(1)
+
+See also /usr/share/doc/notmuch/README.
+
+ -- David Bremner <bremner@debian.org>, Fri,  1 Jul 2011 11:34:39 -0300
diff --git a/debian/notmuch-vim.dirs b/debian/notmuch-vim.dirs
new file mode 100644 (file)
index 0000000..2b53131
--- /dev/null
@@ -0,0 +1,4 @@
+usr/share/vim/addons/doc
+usr/share/vim/addons/plugin
+usr/share/vim/addons/syntax
+usr/share/vim/registry
diff --git a/debian/notmuch-vim.docs b/debian/notmuch-vim.docs
new file mode 100644 (file)
index 0000000..d1496b5
--- /dev/null
@@ -0,0 +1 @@
+vim/README
diff --git a/debian/notmuch-vim.install b/debian/notmuch-vim.install
new file mode 100644 (file)
index 0000000..cf89873
--- /dev/null
@@ -0,0 +1,4 @@
+vim/notmuch.txt usr/share/vim/addons/doc
+vim/notmuch.vim usr/share/vim/addons/plugin
+vim/notmuch.yaml usr/share/vim/registry
+vim/syntax/notmuch-*.vim usr/share/vim/addons/syntax
diff --git a/debian/notmuch.dirs b/debian/notmuch.dirs
new file mode 100644 (file)
index 0000000..e772481
--- /dev/null
@@ -0,0 +1 @@
+usr/bin
diff --git a/debian/notmuch.docs b/debian/notmuch.docs
new file mode 100644 (file)
index 0000000..50bd824
--- /dev/null
@@ -0,0 +1,2 @@
+NEWS
+README
diff --git a/debian/notmuch.install b/debian/notmuch.install
new file mode 100644 (file)
index 0000000..60f0971
--- /dev/null
@@ -0,0 +1,5 @@
+usr/bin/notmuch
+usr/bin/notmuch-emacs-mua
+usr/share/applications/notmuch-emacs-mua.desktop
+usr/share/bash-completion
+usr/share/zsh/vendor-completions
diff --git a/debian/notmuch.maintscript b/debian/notmuch.maintscript
new file mode 100644 (file)
index 0000000..8e3004e
--- /dev/null
@@ -0,0 +1 @@
+rm_conffile /etc/emacs/site-start.d/50notmuch.el 0.33-1~
diff --git a/debian/notmuch.manpages b/debian/notmuch.manpages
new file mode 100644 (file)
index 0000000..d7e8cf5
--- /dev/null
@@ -0,0 +1,20 @@
+usr/share/man/man1/notmuch-address.1.gz
+usr/share/man/man1/notmuch-compact.1.gz
+usr/share/man/man1/notmuch-config.1.gz
+usr/share/man/man1/notmuch-count.1.gz
+usr/share/man/man1/notmuch-dump.1.gz
+usr/share/man/man1/notmuch-emacs-mua.1.gz
+usr/share/man/man1/notmuch-insert.1.gz
+usr/share/man/man1/notmuch-new.1.gz
+usr/share/man/man1/notmuch-reindex.1.gz
+usr/share/man/man1/notmuch-reply.1.gz
+usr/share/man/man1/notmuch-restore.1.gz
+usr/share/man/man1/notmuch-search.1.gz
+usr/share/man/man1/notmuch-setup.1.gz
+usr/share/man/man1/notmuch-show.1.gz
+usr/share/man/man1/notmuch-tag.1.gz
+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
diff --git a/debian/ruby-notmuch.install b/debian/ruby-notmuch.install
new file mode 100644 (file)
index 0000000..d3f2105
--- /dev/null
@@ -0,0 +1 @@
+usr/lib/*/*ruby/*/*/notmuch.so
diff --git a/debian/rules b/debian/rules
new file mode 100755 (executable)
index 0000000..a77ffa1
--- /dev/null
@@ -0,0 +1,40 @@
+#!/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,sphinxdoc
+
+override_dh_auto_configure:
+       BASHCMD=/bin/bash ./configure --prefix=/usr \
+               --libdir=/usr/lib/${DEB_TARGET_MULTIARCH} \
+               --includedir=/usr/include \
+               --mandir=/usr/share/man \
+               --infodir=/usr/share/info \
+               --sysconfdir=/etc \
+               --zshcompletiondir=/usr/share/zsh/vendor-completions \
+               --localstatedir=/var
+
+override_dh_auto_build:
+       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
+
+override_dh_auto_clean:
+       dh_auto_clean
+       PYBUILD_NAME=notmuch dh_auto_clean --buildsystem=pybuild --sourcedirectory bindings/python
+       dh_auto_clean --sourcedirectory bindings/ruby
+       $(MAKE) -C contrib/notmuch-mutt clean
+
+override_dh_auto_install:
+       dh_auto_install
+       PYBUILD_NAME=notmuch dh_auto_install --buildsystem=pybuild --sourcedirectory bindings/python
+       PYBUILD_NAME=notmuch2 dh_auto_install --buildsystem=pybuild --sourcedirectory bindings/python-cffi
+       $(MAKE) -C contrib/notmuch-mutt DESTDIR=$(CURDIR)/debian/tmp install
+       dh_auto_install --sourcedirectory bindings/ruby
diff --git a/debian/source/format b/debian/source/format
new file mode 100644 (file)
index 0000000..163aaf8
--- /dev/null
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/debian/source/options b/debian/source/options
new file mode 100644 (file)
index 0000000..cc76e91
--- /dev/null
@@ -0,0 +1,3 @@
+single-debian-patch
+tar-ignore=.git
+tar-ignore=performance-test/download/*.tar.xz
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
diff --git a/debian/upstream/metadata b/debian/upstream/metadata
new file mode 100644 (file)
index 0000000..8f266aa
--- /dev/null
@@ -0,0 +1,6 @@
+Bug-Database: https://nmbug.notmuchmail.org/status/
+Bug-Submit: mailto:notmuch@notmuchmail.org
+FAQ: https://notmuchmail.org/faq/
+Repository: https://git.notmuchmail.org/git/notmuch
+Repository-Browse: https://git.notmuchmail.org/git/notmuch
+Screenshots: https://notmuchmail.org/screenshots/
diff --git a/debugger.c b/debugger.c
new file mode 100644 (file)
index 0000000..5f47a1d
--- /dev/null
@@ -0,0 +1,47 @@
+/* debugger.c - Some debugger utilities for the notmuch mail library
+ *
+ * Copyright © 2009 Chris Wilson
+ *
+ * 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: Chris Wilson <chris@chris-wilson.co.uk>
+ */
+
+#include "notmuch-client.h"
+
+#include <libgen.h>
+
+#if HAVE_VALGRIND
+#include <valgrind.h>
+#else
+#define RUNNING_ON_VALGRIND 0
+#endif
+
+bool
+debugger_is_active (void)
+{
+    char buf[1024];
+    char buf2[1024];
+
+    if (RUNNING_ON_VALGRIND)
+       return true;
+
+    sprintf (buf, "/proc/%d/exe", getppid ());
+    if (readlink (buf, buf2, sizeof (buf2)) != -1 &&
+       strncmp (basename (buf2), "gdb", 3) == 0) {
+       return true;
+    }
+
+    return false;
+}
diff --git a/devel/RELEASING b/devel/RELEASING
new file mode 100644 (file)
index 0000000..88dab04
--- /dev/null
@@ -0,0 +1,113 @@
+Here are the steps to follow to create a new notmuch release.
+
+These steps assume that a process (not described here) has already
+been followed to determine the features and bug fixes to be included
+in a release, and that adequate testing by the community has already
+been performed. The little bit of testing performed here is a safety
+check, and not a substitute for wider testing.
+
+OK, so the code to be released is present and committed to your git
+repository. From here, there are just a few steps to release:
+
+1) Verify that the NEWS file is up to date.
+
+       Read through the entry at the top of the NEWS file and see if
+       you are aware of any major features recently added that are
+       not mentioned there. If so, please add them, (and ask the
+       authors of the commits to update NEWS in the future).
+
+2) Verify that the library version in lib/Makefile.local is correct
+
+       See the instructions there for how to increment it.
+
+       The version should have been updated with any commits that
+       added API _in a non-upwardly compatible_ way, but do check
+       that that is the case. The command below can be useful for
+       inspecting header-file changes since the last release X.Y:
+
+               git diff X.Y..HEAD -- lib/notmuch.h
+
+       Commit this change, if any.
+
+3) Update the debian/libnotmuchX.symbols file
+
+       If the library version changed at all (step 2) it probably
+       means that symbols have changed/been added, in which case the
+       debian symbols file also needs to be updated:
+
+              dpkg-buildpackage -uc -us
+              dpkg-gensymbols -plibnotmuchX | patch -p0
+
+       Carefully review the changes to debian/libnotmuch1.symbols to
+       make sure there are no unexpected changes.  Remove any debian
+       versions from symbols.
+
+       Commit this change, if any.
+
+4) Upgrade the version in the file "version"
+
+       The scheme for the release number is as follows:
+
+       A major milestone in usability causes an increase in the major
+       number, yielding a two-component version with a minor number
+       of 0, (such as "1.0" or "2.0").
+
+       Otherwise, releases with changes in features cause an increase
+       in the minor number, yielding a two-component version, (such
+       as "1.1" or "1.2").
+
+       Finally, releases that do not change "features" but are merely
+       bug fixes either increase the micro number or add it (starting
+       at ".1" if not present). So a bug-fix release from "1.0" would
+       be "1.0.1" and a subsequent bug-fix release would be "1.0.2"
+       etc.
+
+       When you are happy with the file 'version', run
+
+            make update-versions
+
+       to propagate the version to the other places needed.
+
+       Commit these changes.
+
+5) Create an entry for the new release in debian/changelog
+
+       The syntax of this file is tightly restricted, but the
+       available emacs mode (see the dpkg-dev-el package) helps.
+       The entries here will be the Debian-relevant single-line
+       description of changes from the NEWS entry. And the version
+       must match the version in the next step.
+
+       Commit this change.
+
+       XXX: It would be great if this step were automated as part of
+       release, (taking entries from NEWS and the version from the
+       version file, and creating a new commit, etc.)
+
+6) Run "make release" which will perform the following steps.
+
+   Note: in order to really upload anything, set the make variable
+   REALLY_UPLOAD=yes
+
+       * Ensure that the version consists only of digits and periods
+       * Ensure that version and debian/changelog have the same version
+       * Verify that the source tree is clean
+       * Compile the current notmuch code (aborting release if it fails)
+       * Run the notmuch test suite (aborting release if it fails)
+       * Check that no release exists with the current version
+       * Make a signed tag
+       * Generate a tar file from this tag
+       * Generate a .sha1 sum file for the tar file and GPG sign it.
+       * Commit a (delta for a) copy of the tar file using pristine-tar
+       * Tag for the debian version
+       * if REALLY_UPLOAD=yes
+         - push the signed tag to the origin
+           XXX FIXME push debian tag
+         - scp tarball to web site
+       * Provide some text for the release announcement (see below).
+
+7) Send a message to notmuch@notmuchmail.org to announce the release.
+
+       Use the text provided from "make release" above, (if for some
+       reason you lose this message, "make release-message" prints
+       it again for you.
diff --git a/devel/STYLE b/devel/STYLE
new file mode 100644 (file)
index 0000000..b18a957
--- /dev/null
@@ -0,0 +1,118 @@
+C/C++ coding style
+==================
+
+Tools
+-----
+
+There is a file uncrustify.cfg in this directory that can be used to
+approximate the prevailing code style. You can run it with e.g.
+
+   uncrustify --replace -c devel/uncrustify.cfg foo.c
+
+You still have to use your judgement about accepting or rejecting the
+changes uncrustify makes. With a nice git frontend, you can add the
+lines you agree with and reject the rest.
+
+For Emacs users, the file .dir-locals.el in the top level source
+directory will configure c-mode to automatically meet most of the
+basic layout rules.  I
+
+Indentation, Whitespace, and Layout
+-----------------------------------
+
+The following nonsense code demonstrates many aspects of the style:
+
+static some_type
+function (param_type param, param_type param)
+{
+   for (int i = 0; i < 10; i++) {
+       int j;
+
+       j = i + 10;
+
+       some_other_func (j, i);
+   }
+}
+
+* Indent is 4 spaces with mixed tab/spaces and a tab width of 8.
+  (Specifically, a line should begin with zero or more tabs followed
+  by fewer than eight spaces.)
+
+* Use copious whitespace.  In particular
+   - there is a space between the function name and the open paren in a call.
+   - likewise, there is a space following keywords such as if and while
+   - every binary operator should have space on either side.
+
+* No trailing whitespace. Please enable the standard pre-commit hook in git
+  (or an equivalent hook). The standard pre-commit hook is enabled by simply
+  renaming file '.git/hooks/pre-commit.sample' to '.git/hooks/pre-commit' .
+
+* The name in a function prototype should start at the beginning of a line.
+
+* Opening braces "cuddle" (they are on the same line as the
+  if/for/while test) and are preceded by a space. The opening brace of
+  functions is the exception, and starts on a new line.
+
+* Opening parens also cuddle, even if the first argument does not fit
+  on the same line.
+
+* Ternary operators that span a line should be parenthesized like as
+  "a ? (\n b ) : c". This is mainly to keep the indentation tools
+  happy.
+
+* Comments are always C-style /* */ block comments, with a leading *
+  each line.  They should start with a capital letter and generally be
+  written in complete sentences.  Public library functions are
+  documented immediately before their prototype in lib/notmuch.h.
+  Internal functions are typically documented immediately before their
+  definition.
+
+* Code lines should be less than 80 columns and comments should be
+  wrapped at 70 columns.
+
+* Variable declarations should be at the top of a block; C99 style
+  control variable declarations in for loops are also OK.
+
+Naming
+------
+
+* Use lowercase_with_underscores for function, variable, and type
+  names.
+
+* Except for variables with extremely small scope, and perhaps loop
+  indices, when naming variables and functions, err on the side of
+  verbosity.
+
+* All structs should be typedef'd to a name ending with _t.  If the
+  struct has a tag, it should be the same as the typedef name, minus
+  the trailing _t.
+
+CLI conventions
+---------------
+
+* Any changes to the JSON output format of search or show need an
+  accompanying change to devel/schemata.
+
+libnotmuch conventions
+----------------------------------
+
+* Functions starting with notmuch_ in lib/notmuch.h are public and are
+  automatically exported from the shared library.  Private library
+  functions should generally either be static or, if they are shared
+  between compilation units, start with _notmuch.
+
+* Functions in libnotmuch must not access user configuration files
+  (i.e. .notmuch-config)
+
+* Code which needs to be accessed from both the CLI and from
+  libnotmuch should be factored out into libutil (under util/).
+
+* Deprecated functions should be marked with the NOTMUCH_DEPRECATED
+  macro which generates run time warnings with gcc and clang. In order
+  not to confuse doxygen this should go at the beginning of the
+  declaration like:
+
+  NOTMUCH_DEPRECATED(major,minor) notmuch_status_t notmuch_dwim(void *arg);
+
+  The @deprecated doxygen command can be used to generate markup in
+  the API docs.
diff --git a/devel/TODO b/devel/TODO
new file mode 100644 (file)
index 0000000..116194d
--- /dev/null
@@ -0,0 +1,255 @@
+Fix the things that are causing the most pain to new users
+----------------------------------------------------------
+1. A new import is tagging all messages as "inbox" -- total pain
+
+Emacs interface (notmuch.el)
+----------------------------
+Add notmuch-bcc and notmuch-cc for setting default Bcc and Cc values,
+(should affect the message-setup-hook).
+
+Switch the notmuch-search view to use "notmuch search --format=json"
+to fix large classes of bugs regarding poorly-escaped output and lame
+regular expressions. (The most recently found, unfixed example is the
+sender's name containing ';' which causes emacs to drop a search
+result.) This may require removing the outer array from the current
+"notmuch search --format=json" results.
+
+Add a global keybinding table for notmuch, and then view-specific
+tables that add to it.
+       
+Add a '|' binding from the search view.
+
+Add support for choosing from one of the user's configured email
+addresses for the From line.
+
+Make 'notmuch-show-pipe-message have a private history.
+
+Add support for a delete keybinding that adds a "deleted" tag to the
+current message/thread and make searches not return deleted messages
+by default, (unless the user asks explicitly for deleted messages in
+the search query).
+
+Add support to "mute" a thread (add a "muted" tag and then don't
+display threads in searches by default where any message of the thread
+has the "muted" tag).
+
+Make '=' count from the end rather than from the beginning if more
+than half-way through the buffer.
+
+Fix to automatically wrap long headers (for RFC compliance) before
+sending. This should probably just be fixed in message-mode itself,
+(but perhaps we can have a notmuch-message-mode that layers this on
+top).
+
+Stop hiding the headers so much in the thread-view mode.
+
+Allow opening a message in thread-view mode by clicking on either
+line.
+
+Automatically open a message when navigating to it with N or P.
+
+Change 'a' command in thread-view mode to only archive open messages.
+
+notmuch command-line tool
+-------------------------
+Add support to "notmuch search" and "notmuch show" to allow for
+listing of duplicate messages, (distinct filenames with the same
+Message-ID). I'm not sure what the option should be named. Perhaps
+--with-duplicates ?
+
+Replace "notmuch reply" with "notmuch compose --reply <search-terms>".
+This would enable a plain "notmuch compose" to be used to construct an
+initial message, (which would then have the properly configured name
+and email address in the From: line. We could also then easily support
+"notmuch compose --from <something>" to support getting at alternate
+email addresses.
+
+Implement "notmuch search --exclude-threads=<search-terms>" to allow
+for excluding muted threads, (and any other negative, thread-based
+filtering that the user wants to do).
+
+Fix "notmuch show" so that the UI doesn't fail to show a thread that
+is visible in a search buffer, but happens to no longer match the
+current search. (Perhaps add a --matching=<secondary-search-terms>
+option (or similar) to "notmuch show".) For now, this is being worked
+around in the emacs interface by noticing that "notmuch show" returns
+nothing and re-rerunning the command without the extra arguments.
+
+Add a "--format" option to "notmuch search", (something printf-like
+for selecting what gets printed).
+
+Give "notmuch restore" some progress indicator.
+
+Fix "notmuch restore" to operate in a single pass much like "notmuch
+dump" does, rather than doing N searches into the database, each
+matching 1/N messages.
+
+Allow configuration for filename patterns that should be ignored when
+indexing.
+
+Fix to avoid this ugly message:
+
+       (process:17197): gmime-CRITICAL **: g_mime_message_get_mime_part: assertion `GMIME_IS_MESSAGE (message)' failed
+       Warning: Not indexing empty mime part.
+
+  This probably means adding a test case to generate that message,
+  filing an upstream bug against GMime, and then silencing the
+  notmuch-generated portion of the warning (so that once GMime is
+  fixed, this is all silent).
+
+Simplify notmuch-reply to simply print the headers (we have the
+original values) rather than calling GMime (which encodes) and adding
+the confusing gmime-filter-headers.c code (which decodes).
+
+Properly handle replying to multiple messages. Currently, the JSON
+reply format only supports a single message, but the default reply
+format accepts searches returning multiple messages. The expected
+behavior of replying to multiple messages is not obvious, and there
+are multiple ideas that might make sense. Some consensus needs to be
+reached on this issue, and then both reply formats should be updated
+to be consistent.
+
+Return docid-based queries in thread search results, instead of
+message-id-based queries.  These are much more compact and much faster
+to later query.  This will require support from the library, both for
+retrieving docids (probably as an opaque "get a query that identifies
+this message") and for docid queries.
+
+notmuch library
+---------------
+Add support for custom flag<->tag mappings. In the notmuch
+configuration file this could be
+
+       [maildir]
+       synchronize_flags = R:replied; D*:deleted; S:~unread;
+
+In the library interface this could be implemented with an array of
+structures to define the mapping (flag character, tag name,
+inverse-sense bit (~ above), and tag-when-any-file-flagged
+vs. tag-when-all-files-flagged (* above)).
+
+Add an interface to accept a "key" and a byte stream, rather than a
+filename.
+
+Improve syntax for date ranges queries. date:expr should be
+interpreted as date:expr..expr so that, for example, "date:2013-01-22"
+would cover the whole of the specified day (currently that's not even
+recognized as a date range expression). It might be nice to be able to
+use things like "since:2013-01-22" and "until:2013-01-22" as synonyms
+to "date:2013-01-22.." and "date:..2013-01-22", respectively. To do
+any of this we're probably going to need to break down and write our
+own parser for the query string rather than using Xapian's QueryParser
+class.
+
+Make failure to read a file (such as a permissions problem) a warning
+rather than an error (should be similar to the existing warning for a
+non-mail file).
+
+Fix to use the *last* Message-ID header if multiple such headers are
+encountered, (I noticed this is one thing that kept me from seeing the
+same message-ID values as sup).
+
+Add support for configuring "virtual tags" which are a tuple of
+(tag-name, search-specification). The database is responsible for
+ensuring that the virtual tag is always consistent.
+
+Indicate to the user if two files with the same message ID have
+content that is actually different in some interesting way. Perhaps
+notmuch initially sees all changes as interesting, and quickly learns
+from the user which changes are not interesting (such as the very
+common mailing-list footer).
+
+Fix notmuch_query_count_messages to share code with
+notmuch_query_search_messages rather than duplicating code. (And
+consider renaming it as well.)
+
+Provide a mechanism for doing automatic address completion based on
+notmuch searches. Here was one proposal made in IRC:
+
+       <cworth> I guess all it would really have to be would be a way
+                to configure a series of searches to try in turn,
+                (presenting ambiguities at a given single level, and
+                advancing to the next level only if one level
+                returned no matches).
+       <cworth> So then I might have a series that looks like this:
+       <cworth> notmuch search --output=address_from tag:address_book_alias
+       <cworth> notmuch search --output=address_to tag:sent
+       <cworth> notmuch search --output=address_from
+       <cworth> I think I might like that quite a bit.
+       <cworth> And then we have a story for an address book for
+                non-emacs users.
+
+Provide a ~me Xapian synonym for all of the user's configured email
+addresses.
+
+Add symbol hiding so that we don't risk leaking any private symbols
+into the shared-library interface.
+
+Audit all libnotmuch entry points to ensure that all Xapian calls are
+wrapped in a try/catch block.
+
+Fix the threading of a message that has a References: header but no
+In-Reply-To: header (see id:"87lixxnxpb.fsf@yoom.home.cworth.org").
+
+Search syntax
+-------------
+Implement support for "tag:*" to expand to all tags.
+
+Fix "notmuch search to:" to be less confusing. Many users expect this
+to search for all messages with a To: header, but it instead searches
+for all messages with the word "to". If we don't provide the first
+behavior, perhaps we should exit on an error when a configured prefix
+is provided with no value?
+
+Support "*" in all cases and not just as a special case. That is, "* "
+should also work, as well as "* and tag:inbox".
+
+Implement a syntax for requesting set-theoertic operations on results
+of multiple searches. For example, I would like to do:
+
+       "tag:inbox" SET-SUBTRACT "tag:muted"
+
+    as well as:
+
+       "tag:notmuch and <date-range>" SET-INTERSECT
+       "tag:notmuch and not (tag:merged or tag:postponed)"
+
+    See id:3wdpr282yz2.fsf@testarossa.amd.com for more details on the
+    use cases of the above.
+
+Database changes
+----------------
+Store a reference term for every message-id that appears in
+References. We just started doing this for newly-added documents, but
+at the next convenient database-schema upgrade, we should go back and
+fix old messages to be consistent.
+
+Start indexing the List-Id header, (and re-index this header for
+existing messages at the next database upgrade).
+
+Add support for the user to specify custom headers to be indexed (and
+re-index these for existing messages at the next database upgrade).
+
+Save filenames for files detected as "not an email file" in the
+database. This would allow for two things: 1. Optimizing "notmuch new"
+to not have to look at these files again (since they are potentially
+large so the detection could be potentially slow). 2. A "notmuch
+search" syntax could be added to allow the user to find these files,
+(and perhaps delete them or move them away as appropriate).
+
+Fix filesystem/notmuch-new race condition by not updating database
+mtime for a directory if it is the same as the current mtime.
+
+Test suite
+----------
+Achieve 100% test coverage with the test suite.
+
+General
+-------
+Audit everything for dealing with out-of-memory (and drop xutil.c).
+
+Investigate why the notmuch database is slightly larger than the sup
+database for the same corpus of email.
+
+Makefile should print message teaching user about LD_LIBRARY_PATH (or
+similar) if libdir is not set to a directory examined by ldconfig.
diff --git a/devel/author-scan.sh b/devel/author-scan.sh
new file mode 100644 (file)
index 0000000..23854f3
--- /dev/null
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+FILE_EXCLUDE='corpora'
+AUTHOR_EXCLUDE='uncrustify'
+# based on the FSF guideline, for want of a better idea.
+THRESHOLD=15
+
+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
diff --git a/devel/check-out-of-tree-build.sh b/devel/check-out-of-tree-build.sh
new file mode 100755 (executable)
index 0000000..3e443ea
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/sh
+# test out-of-tree builds in a temp directory
+# passes all args to make
+
+set -eu
+
+srcdir=$(cd "$(dirname "$0")"/.. && pwd)
+builddir=$(mktemp -d)
+
+cd "$builddir"
+
+"$srcdir"/configure
+make "$@"
+
+rm -rf "$builddir"
diff --git a/devel/emacs-keybindings.org b/devel/emacs-keybindings.org
new file mode 100644 (file)
index 0000000..ad7f72e
--- /dev/null
@@ -0,0 +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                            | 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/man-to-mdwn.pl b/devel/man-to-mdwn.pl
new file mode 100755 (executable)
index 0000000..a3c4069
--- /dev/null
@@ -0,0 +1,205 @@
+#!/usr/bin/perl
+#
+# Author: Tomi Ollila
+# License: same as notmuch
+#
+# This program is used to generate mdwn-formatted notmuch manual pages
+# for notmuch wiki. Example run:
+#
+# $ ./devel/man-to-mdwn.pl doc/_build/man ../notmuch-wiki
+#
+# In case taken into more generic use, modify these comments and examples.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+unless (@ARGV == 2) {
+    warn "\n$0 <source-directory> <destination-directory>\n\n";
+    # Remove/edit this comment if this script is taken into generic use.
+    warn "Example: ./devel/man-to-mdwn.pl doc/_build/man ../notmuch-wiki\n\n";
+    exit 1;
+}
+
+die "'$ARGV[0]': no such source directory\n" unless -d $ARGV[0];
+die "'$ARGV[1]': no such destination directory\n" unless -d $ARGV[1];
+
+#die "'manpages' exists\n" if -e 'manpages';
+#die "'manpages.mdwn' exists\n" if -e 'manpages.mdwn';
+
+die "Expecting '$ARGV[1]/manpages' to exist.\n" .
+  "Please create it first or adjust <destination-directory>.\n"
+  unless -d $ARGV[1] . '/manpages';
+
+my $ev = 0;
+my %fhash;
+
+open P, '-|', 'find', $ARGV[0], qw/-name *.[0-9] -print/;
+while (<P>)
+{
+    chomp;
+    next unless -f $_; # follows symlink.
+    $ev = 1, warn "'$_': no such file\n" unless -f $_;
+    my ($in, $on) = ($_, $_);
+    $on =~ s|.*/||; $on =~ tr/./-/;
+    my $f = $fhash{$on};
+    $ev = 1, warn "'$in' collides with '$f' ($on.mdwn)\n" if defined $f;
+    $fhash{$on} = $in;
+}
+close P;
+
+my %htmlqh = qw/& &amp;   < &lt;   > &gt;   ' &apos;   " &quot;/;
+# do html quotation to $_[0] (which is an alias to the given arg)
+sub htmlquote($)
+{
+    $_[0] =~ s/([&<>'"])/$htmlqh{$1}/ge;
+}
+
+sub maymakelink($);
+sub mayconvert($$);
+
+#warn keys %fhash, "\n";
+
+while (my ($k, $v) = each %fhash)
+{
+    #next if -l $v; # skip symlinks here. -- not... references there may be.
+
+    my @lines;
+    open I, '-|', qw/env -i/, "PATH=$ENV{PATH}",
+       qw/TERM=vt100 LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8/,
+       qw/GROFF_NO_SGR=1 MAN_KEEP_FORMATTING=1 MANWIDTH=80/,
+       qw/man/, $v or die "$!";
+    binmode I, ':utf8';
+
+    my ($emptyline, $pre, $hl) = (0, 0, 'h1');
+    while (<I>) {
+       if (/^\s*$/) {
+           $emptyline = 1;
+           next;
+       }
+       # keep only leftmost in lines like 'NOTMUCH(1)   notmuch   NOTMUCH(1)'
+       s/\S\K\s{8,}\S.+\s{8,}\S.*//; # $hl = 'h1' if s/(?<=\S)\s{8,}.*//;
+       s/[_&]\010&/&/g;
+       s/((?:_\010[^_])+)/\001u\002$1\001\/u\002/g;
+       s/_\010(.)/$1/g;
+       s/((?:.\010.)+)/\001b\002$1\001\/b\002/g;
+       s/.\010(.)/$1/g;
+       htmlquote $_;
+       s/\001/</g; s/\002/>/g;
+
+       if (/^\S/) {
+           $pre = 0, push @lines, "</pre>\n" if $pre;
+           s/<\/?b>//g;
+           chomp;
+           $_ = "\n<$hl>$_</$hl>\n";
+           $hl = 'h2';
+           $emptyline = 0;
+       }
+       elsif (/^\s\s\s\S/) {
+           $pre = 0, push @lines, "</pre>\n" if $pre;
+           s/(?:^\s+)?<\/?b>//g;
+           chomp;
+           $_ = "\n<h3> &nbsp; $_</h3>\n";
+           $emptyline = 0;
+       }
+       else {
+           $pre = 1, push @lines, "<pre>\n" unless $pre;
+           $emptyline = 0, push @lines, "\n" if $emptyline;
+       }
+       push @lines, $_;
+    }
+    $lines[0] =~ s/^\n//;
+    $k = "$ARGV[1]/manpages/$k.mdwn";
+    open O, '>', $k or die;
+    binmode O, ':utf8';
+    print STDOUT 'Writing ', "'$k'\n";
+    select O;
+    my ($pe, $hyphen) = ('', '');
+    foreach (@lines) {
+       #print $_; next;
+       if ($pe) {
+           if (s/^(\s+)<b>([^<]+)\((\d+)\)<\/b>//) {
+               my $link = maymakelink "$pe-$2-$3";
+               $link = maymakelink "$pe$2-$3" unless $link;
+               if ($link) {
+                   print "<a href='$link'>$pe$hyphen</a>\n";
+                   print "$1<a href='$link'>$2</a>($3)";
+               }
+               else {
+                   print "<b>$pe-</b>\n";
+                   print "$1<b>$2</b>($3)";
+               }
+           } else {
+               print "<b>$pe-</b>\n";
+           }
+           $pe = '';
+       }
+       s/<b>([^<]+)\((\d+)\)<\/b>/mayconvert($1, $2)/ge;
+       ($pe, $hyphen) = ($1, $2) if s/<b>([^<]+)([-\x{2010}])<\/b>\s*$//;
+       print $_;
+    }
+}
+
+sub maymakelink($)
+{
+#    warn "$_[0]\n";
+    return "../$_[0]/" if exists $fhash{$_[0]};
+    return '';
+}
+
+sub mayconvert($$)
+{
+    my $f = "$_[0]-$_[1]";
+#    warn "$f\n";
+    return "<a href='../$f/'>$_[0]</a>($_[1])" if exists $fhash{$f};
+    return "<b>$_[0]</b>($_[1])";
+}
+
+# Finally, make manpages.mdwn
+
+open O, '>', $ARGV[1] . '/manpages.mdwn' or die $!;
+print STDOUT "Writing '$ARGV[1]/manpages.mdwn'\n";
+select O;
+print "Manual page index\n";
+print "=================\n\n";
+
+sub srt { my ($x, $y) = ($a, $b); $x =~ tr/./-/; $y =~ tr/./-/; $x cmp $y; }
+
+foreach (sort srt values %fhash)
+{
+    my $in = $_;
+    open I, '<', $in or die $!;
+    my $s;
+    while (<I>) {
+       if (/^\s*[.]TH\s+\S+\s+"?(\S+?)"?\s/) {
+           $s = $1;
+           last;
+       }
+    }
+    while (<I>) {
+       last if /^\s*[.]SH NAME/
+    }
+    my $line = '';
+    while (<I>) {
+       tr/\\//d;
+       if (/\s*(\S+)\s+(.*)/) {
+           my $e = $2;
+           # Ignoring the NAME in file, get from file name instead.
+           #my $on = (-l $in)? readlink $in: $in;
+           my $on = $in;
+           $on =~ tr/./-/; $on =~ s|.*/||;
+           my $n = $in; $n =~ s|.*/||; $n =~ tr/./-/; $n =~ s/-[^-]+$//;
+           $line = "<a href='$on/'>$n</a>($s) $e\n";
+           last;
+       }
+    }
+    die "No NAME in '$in'\n" unless $line;
+    print "* $line";
+    #warn $line;
+}
+print <<'EOF';
+
+The manual pages are licensed under
+[the GNU General Public License](https://www.gnu.org/licenses/gpl.txt),
+either version 3.0 or at your option any later version.
+EOF
diff --git a/devel/news2wiki.pl b/devel/news2wiki.pl
new file mode 100755 (executable)
index 0000000..d966bab
--- /dev/null
@@ -0,0 +1,101 @@
+#!/usr/bin/perl
+#
+# Author: Tomi Ollila
+# License: same as notmuch
+
+# This program is used to split NEWS file to separate (mdwn) files
+# for notmuch wiki. Example run:
+#
+# $ ./devel/news2wiki.pl NEWS ../notmuch-wiki/news
+#
+# In case taken into more generic use, modify these comments and examples.
+
+use strict;
+use warnings;
+
+unless (@ARGV == 2) {
+    warn "\n$0 <source-file> <destination-directory>\n\n";
+    warn "Example: ./devel/news2wiki.pl NEWS ../notmuch-wiki/news\n\n";
+    exit 1;
+}
+
+die "'$ARGV[0]': no such file\n" unless -f $ARGV[0];
+die "'$ARGV[1]': no such directory\n" unless -d $ARGV[1];
+
+open I, '<', $ARGV[0] or die "Cannot open '$ARGV[0]': $!\n";
+
+open O, '>', '/dev/null' or die $!;
+my @emptylines = ();
+my $cln;
+print "\nWriting to $ARGV[1]:\n";
+while (<I>)
+{
+    warn "$ARGV[0]:$.: tab(s) in line!\n" if /\t/;
+    warn "$ARGV[0]:$.: trailing whitespace\n" if /\s\s$/;
+    if (/^Notmuch\s+(\S+)\s+\((\d\d\d\d-\d\d-\d\d|UNRELEASED)\)\s*$/) {
+       # open O... autocloses previously opened file.
+       open O, '>', "$ARGV[1]/release-$1.mdwn" or die $!;
+       print "+ release-$1.mdwn...\n";
+       print O "[[!meta date=\"$2\"]]\n\n";
+       @emptylines = ();
+    }
+
+    last if /^<!--\s*$/; # Local variables block at the end (as of now).
+
+    # Buffer "trailing" empty lines -- dropped at end of file.
+    push(@emptylines, $_), next if s/^\s*$/\n/;
+    if (@emptylines) {
+       print O @emptylines;
+       @emptylines = ();
+    }
+
+    # Convert '*' to '`*`' and "*" to "`*`" so that * is not considered
+    # as starting emphasis character there. We're a bit opportunistic
+    # there -- some single * does not cause problems and, on the other
+    # hand, this would not regognize already 'secured' *:s.
+    s/'[*]'/'`*`'/g; s/"[*]"/"`*`"/g;
+
+    # Convert nonindented lines that aren't already headers or
+    # don't contain periods (.) or '!'s to level 4 header.
+    if ( /^[^\s-]/ ) {
+       my $tbc = ! /[.!]\s/;
+       chomp;
+       my @l = $_;
+       $cln = $.;
+       while (<I>) {
+           last if /^\s*$/;
+           #$cln = 0 if /^---/ or /^===/; # used for debugging.
+           $tbc = 0 if /[.!]\s/ or /^---/ or /^===/;
+           chomp; s/^\s+//;
+           push @l, $_;
+       }
+       if ($tbc) {
+           print O "### ", (join ' ', @l), "\n";
+       }
+       else {
+           #print "$ARGV[0]:$cln: skip level 4 header conversion\n" if $cln;
+           print O (join "\n", @l), "\n";
+       }
+       @emptylines = ( "\n" );
+       next;
+    }
+
+    # Markdown doc specifies that list item may have paragraphs if those
+    # are indented by 4 spaces (or a tab) from current list item marker
+    # indentation (paragraph meaning there is empty line in between).
+    # If there is empty line and next line is not indented 4 chars then
+    # that should end the above list. This doesn't happen in all markdown
+    # implementations.
+    # In our NEWS case this problem exists in release 0.6 documentation.
+    # It can be avoided by removing 2 leading spaces in lines that are not
+    # list items and requiring all that indents are 0, 2, and 4+ (to make
+    # regexp below work).
+    # Nested lists are supported but one needs to be more careful with
+    # markup there (as the hack below works only on first level).
+
+    s/^[ ][ ]// unless /^[ ][ ](?:[\s*+-]|\d+\.)\s/;
+
+    print O $_;
+}
+print "\ndone.\n";
+close O;
diff --git a/devel/nmbug/doc/.gitignore b/devel/nmbug/doc/.gitignore
new file mode 100644 (file)
index 0000000..f25d695
--- /dev/null
@@ -0,0 +1,2 @@
+*.pyc
+/_build
diff --git a/devel/nmbug/doc/Makefile b/devel/nmbug/doc/Makefile
new file mode 100644 (file)
index 0000000..7ea3ae7
--- /dev/null
@@ -0,0 +1,38 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+DOCBUILDDIR   := _build
+
+SRCDIR ?= .
+ALLSPHINXOPTS := -d $(DOCBUILDDIR)/doctrees $(SPHINXOPTS) $(SRCDIR)
+
+MAN_RST_FILES := $(shell find $(SRCDIR)/man* -name '*.rst')
+MAN_ROFF_FILES := $(patsubst $(SRCDIR)/man%.rst,$(DOCBUILDDIR)/man/man%,$(MAN_RST_FILES))
+MAN_GZIP_FILES := $(addsuffix .gz,$(MAN_ROFF_FILES))
+
+.PHONY: build-man
+build-man: $(MAN_GZIP_FILES)
+
+%.gz: %
+       rm -f $@ && gzip --stdout $^ > $@
+
+$(MAN_ROFF_FILES): $(DOCBUILDDIR)/.roff.stamp
+
+# By using $(DOCBUILDDIR)/.roff.stamp instead of $(MAN_ROFF_FILES), we
+# convey to make that a single invocation of this recipe builds all
+# of the roff files.  This prevents parallel make from starting an
+# instance of this recipe for each roff file.
+$(DOCBUILDDIR)/.roff.stamp $(MAN_ROFF_FILES): $(MAN_RST_FILES)
+       mkdir -p $(DOCBUILDDIR)
+       touch $(DOCBUILDDIR)/.roff.stamp
+       $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(DOCBUILDDIR)/man
+       for section in 1 5; do \
+           mkdir -p $(DOCBUILDDIR)/man/man$${section}; \
+           mv $(DOCBUILDDIR)/man/*.$${section} $(DOCBUILDDIR)/man/man$${section}; \
+       done
+
+clean:
+       rm -rf $(DOCBUILDDIR) $(SRCDIR)/conf.pyc
diff --git a/devel/nmbug/doc/conf.py b/devel/nmbug/doc/conf.py
new file mode 100644 (file)
index 0000000..29379d0
--- /dev/null
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+
+import os.path
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'notmuch'
+authors = 'Carl Worth and many others'
+copyright = '2009-2015, {0}'.format(authors)
+
+location = os.path.dirname(__file__)
+
+dirname = location
+while True:
+    version_file = os.path.join(dirname, 'version')
+    if os.path.exists(version_file):
+        with open(version_file,'r') as f:
+            version = f.read().strip()
+            break
+    if dirname == '/':
+        raise ValueError(
+            'no version file found in this directory or its ancestors')
+    dirname = os.path.dirname(dirname)
+
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+
+man_pages = [
+    ('man1/notmuch-report.1', 'notmuch-report',
+     'generate reports from notmuch queries', [authors], 1),
+    ('man5/notmuch-report.json.5', 'notmuch-report.json',
+     'configure notmuch-report', [authors], 5),
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+texinfo_no_detailmenu = True
+
+texinfo_documents = [
+    ('man1/notmuch-report.1', 'notmuch-report',
+     'generate reports from notmuch queries', authors, 'notmuch-report',
+     'generate reports from notmuch queries', 'Miscellaneous'),
+    ('man5/notmuch-report.json.5', 'notmuch-report.json',
+     'configure notmuch-report', authors, 'notmuch-report.json',
+     'configure notmuch-report', 'Miscellaneous'),
+]
diff --git a/devel/nmbug/doc/index.rst b/devel/nmbug/doc/index.rst
new file mode 100644 (file)
index 0000000..51ac59e
--- /dev/null
@@ -0,0 +1,17 @@
+Welcome to notmuch's dev-tool documentation!
+============================================
+
+Contents:
+
+.. toctree::
+   :titlesonly:
+
+   man1/notmuch-report.1
+   man5/notmuch-report.json.5
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/devel/nmbug/doc/man1/notmuch-report.1.rst b/devel/nmbug/doc/man1/notmuch-report.1.rst
new file mode 100644 (file)
index 0000000..dfd82df
--- /dev/null
@@ -0,0 +1,54 @@
+==============
+notmuch-report
+==============
+
+SYNOPSIS
+========
+
+**notmuch-report** [options ...]
+
+DESCRIPTION
+===========
+
+Generate HTML or plain-text reports showing query results.
+
+OPTIONS
+=======
+
+  ``-h``, ``--help``
+
+    Show a help message, including a list of available options, and
+    exit.
+
+  ``--text``
+    Output plain text instead of HTML.
+
+  ``--config`` <PATH>
+    Load config from given file.  The format is described in
+    **notmuch-report.json(5)**.  If this option is not set,
+    **notmuch-report** loads the config from the Git repository at
+    ``NMBGIT``.  See :ref:`NMBGIT <NMBGIT>` for details.
+
+  ``--list-views``
+    List available views (by title) and exit.
+
+  ``--get-query`` <VIEW>
+    Print the configured query for view matching the given title.
+
+ENVIRONMENT
+===========
+
+.. _NMBGIT:
+
+  ``NMBGIT``
+    If ``--config PATH`` is not set, **notmuch-report** will attempt
+    to load a config file named ``notmuch-report.json`` from the
+    ``config`` branch of the ``NMBGIT`` repository (defaulting to
+    ``~/.nmbug``).
+
+SEE ALSO
+========
+
+**notmuch(1)**, **notmuch-report.json(5)**, **notmuch-search(1)**,
+ **notmuch-tag(1)**
+
diff --git a/devel/nmbug/doc/man5/notmuch-report.json.5.rst b/devel/nmbug/doc/man5/notmuch-report.json.5.rst
new file mode 100644 (file)
index 0000000..98d2d2e
--- /dev/null
@@ -0,0 +1,129 @@
+==============
+notmuch-report
+==============
+
+NAME
+====
+
+notmuch-report.json - configure output for **notmuch-report(1)**
+
+DESCRIPTION
+===========
+
+The config file is JSON_ with the following fields:
+
+meta
+  An object with page-wide information
+
+  title
+    Page title used in the default header.
+
+  blurb
+    Introduction paragraph used in the default header.
+
+  header
+    `Python format string`_ for the HTML header.  Optional.  It is
+    formatted with the following context:
+
+    date
+      The current UTC date.
+
+    datetime
+      The current UTC date-time.
+
+    title
+      The **meta.title** value.
+
+    blurb
+      The **meta.blurb** value.
+
+    encoding
+      The encoding used for the output file.
+
+    inter_message_padding
+      0.25em, for consistent CSS generation.
+
+    border_radius
+      0.5em, for consistent CSS generation.
+
+  footer
+    `Python format string`_ for the HTML footer.  It is formatted with
+    the same context used for **meta.header**.  Optional.
+
+  message-url
+    `Python format string`_ for message-linking URLs.  Optional.
+    Defaults to linking Gmane_.  It is formatted with the following
+    context:
+
+    message-id
+      The quoted_ message ID.
+
+    subject
+      The message subject.
+
+views
+  An array of view objects, where each object has the following
+  fields:
+
+  title
+    Header text for the view.
+
+  comment
+    Paragraph describing the view in more detail.  Optional.
+
+  id
+    Anchor string for the view.  Optional, defaulting to a slugged
+    form of the view title
+
+  query
+    An array of strings, which will be joined with 'and' to form the
+    view query.
+
+.. _Gmane: https://gmane.org/
+.. _JSON: https://json.org/
+.. _Python format string: https://docs.python.org/3/library/string.html#formatstrings
+.. _quoted: https://docs.python.org/3/library/urllib.parse.html#urllib.parse.quote
+
+EXAMPLE
+=======
+
+::
+
+  {
+    "meta": {
+      "title": "Notmuch Patches",
+      "blurb": "For more information see <a href=\"https://notmuchmail.org/nmbug\">nmbug</a>",
+      "header": "<html><head></head><body><h1>{title}</h1><p>{blurb}</p><h2>Views</h2>",
+      "footer": "<hr><p>Generated: {datetime}</p></html>",
+      "message-url": "https://mid.gmane.org/{message-id}"
+    },
+    "views": [
+      {
+        "title": "Bugs",
+        "comment": "Unresolved bugs.",
+        "query": [
+          "tag:notmuch::bug",
+          "not tag:notmuch::fixed",
+          "not tag:notmuch::wontfix"
+        ]
+      },
+      {
+        "title": "Review",
+        "comment": "These patches are under review, or waiting for feedback.",
+        "id": "under-review",
+        "query": [
+          "tag:notmuch::patch",
+          "not tag:notmuch::pushed",
+          "not tag:notmuch::obsolete",
+          "not tag:notmuch::stale",
+          "not tag:notmuch::wontfix",
+          "(tag:notmuch::moreinfo or tag:notmuch::needs-review)"
+        ]
+      }
+    ]
+  }
+
+SEE ALSO
+========
+
+**notmuch(1)**, **notmuch-report(1)**, **notmuch-search(1)**, **notmuch-tag(1)**
diff --git a/devel/nmbug/notmuch-report b/devel/nmbug/notmuch-report
new file mode 100755 (executable)
index 0000000..9a6a31c
--- /dev/null
@@ -0,0 +1,441 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2011-2012 David Bremner <david@tethera.net>
+#
+# dependencies
+#       - python3 or python2.7
+#
+# 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/ .
+
+"""Generate text and/or HTML for one or more notmuch searches.
+
+Messages matching each search are grouped by thread.  Each message
+that contains both a subject and message-id will have the displayed
+subject link to an archive view of the message (defaulting to Gmane).
+"""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import codecs
+import collections
+import datetime
+import email.utils
+try:  # Python 3
+    from urllib.parse import quote
+except ImportError:  # Python 2
+    from urllib import quote
+import json
+import argparse
+import os
+import re
+import sys
+import subprocess
+import xml.sax.saxutils
+
+
+_ENCODING = 'UTF-8'
+_PAGES = {}
+
+
+if not hasattr(collections, 'OrderedDict'):  # Python 2.6 or earlier
+    class _OrderedDict (dict):
+        "Just enough of a stub to get through Page._get_threads"
+        def __init__(self, *args, **kwargs):
+            super(_OrderedDict, self).__init__(*args, **kwargs)
+            self._keys = []  # record key order
+
+        def __setitem__(self, key, value):
+            super(_OrderedDict, self).__setitem__(key, value)
+            self._keys.append(key)
+
+        def values(self):
+            for key in self._keys:
+                yield self[key]
+
+
+    collections.OrderedDict = _OrderedDict
+
+
+class ConfigError (Exception):
+    """Errors with config file usage
+    """
+    pass
+
+
+def read_config(path=None, encoding=None):
+    "Read config from json file"
+    if not encoding:
+        encoding = _ENCODING
+    if path:
+        try:
+            with open(path, 'rb') as f:
+                config_bytes = f.read()
+        except IOError as e:
+            raise ConfigError('Could not read config from {}'.format(path))
+    else:
+        nmbhome = os.getenv('NMBGIT', os.path.expanduser('~/.nmbug'))
+        branch = 'config'
+        filename = 'notmuch-report.json'
+
+        # read only the first line from the pipe
+        sha1_bytes = subprocess.Popen(
+            ['git', '--git-dir', nmbhome, 'show-ref', '-s', '--heads', branch],
+            stdout=subprocess.PIPE).stdout.readline()
+        sha1 = sha1_bytes.decode(encoding).rstrip()
+        if not sha1:
+            raise ConfigError(
+                ("No local branch '{branch}' in {nmbgit}.  "
+                 'Checkout a local {branch} branch or explicitly set --config.'
+                ).format(branch=branch, nmbgit=nmbhome))
+
+        p = subprocess.Popen(
+            ['git', '--git-dir', nmbhome, 'cat-file', 'blob',
+             '{}:{}'.format(sha1, filename)],
+            stdout=subprocess.PIPE)
+        config_bytes, err = p.communicate()
+        status = p.wait()
+        if status != 0:
+            raise ConfigError(
+                ("Missing {filename} in branch '{branch}' of {nmbgit}.  "
+                 'Add the file or explicitly set --config.'
+                ).format(filename=filename, branch=branch, nmbgit=nmbhome))
+
+    config_json = config_bytes.decode(encoding)
+    try:
+        return json.loads(config_json)
+    except ValueError as e:
+        if not path:
+            path = "{} in branch '{}' of {}".format(
+                filename, branch, nmbhome)
+        raise ConfigError(
+            'Could not parse JSON from the config file {}:\n{}'.format(
+                path, e))
+
+
+class Thread (list):
+    def __init__(self):
+        self.running_data = {}
+
+
+class Page (object):
+    def __init__(self, header=None, footer=None):
+        self.header = header
+        self.footer = footer
+
+    def write(self, database, views, stream=None):
+        if not stream:
+            try:  # Python 3
+                byte_stream = sys.stdout.buffer
+            except AttributeError:  # Python 2
+                byte_stream = sys.stdout
+            stream = codecs.getwriter(encoding=_ENCODING)(stream=byte_stream)
+        self._write_header(views=views, stream=stream)
+        for view in views:
+            self._write_view(database=database, view=view, stream=stream)
+        self._write_footer(views=views, stream=stream)
+
+    def _write_header(self, views, stream):
+        if self.header:
+            stream.write(self.header)
+
+    def _write_footer(self, views, stream):
+        if self.footer:
+            stream.write(self.footer)
+
+    def _write_view(self, database, view, stream):
+        # sort order, default to oldest-first
+        sort_key = view.get('sort', 'oldest-first')
+        # dynamically accept all values in Query.SORT
+        sort_attribute = sort_key.upper().replace('-', '_')
+        try:
+            sort = getattr(notmuch.Query.SORT, sort_attribute)
+        except AttributeError:
+            raise ConfigError('Invalid sort setting for {}: {!r}'.format(
+                view['title'], sort_key))
+        if 'query-string' not in view:
+            query = view['query']
+            view['query-string'] = ' and '.join(
+                '( {} )'.format(q) for q in query)
+        q = notmuch.Query(database, view['query-string'])
+        q.set_sort(sort)
+        threads = self._get_threads(messages=q.search_messages())
+        self._write_view_header(view=view, stream=stream)
+        self._write_threads(threads=threads, stream=stream)
+
+    def _get_threads(self, messages):
+        threads = collections.OrderedDict()
+        for message in messages:
+            thread_id = message.get_thread_id()
+            if thread_id in threads:
+                thread = threads[thread_id]
+            else:
+                thread = Thread()
+                threads[thread_id] = thread
+            thread.running_data, display_data = self._message_display_data(
+                running_data=thread.running_data, message=message)
+            thread.append(display_data)
+        return list(threads.values())
+
+    def _write_view_header(self, view, stream):
+        pass
+
+    def _write_threads(self, threads, stream):
+        for thread in threads:
+            for message_display_data in thread:
+                stream.write(
+                    ('{date:10.10s} {from:20.20s} {subject:40.40s}\n'
+                     '{message-id-term:>72}\n'
+                     ).format(**message_display_data))
+            if thread != threads[-1]:
+                stream.write('\n')
+
+    def _message_display_data(self, running_data, message):
+        headers = ('thread-id', 'message-id', 'date', 'from', 'subject')
+        data = {}
+        for header in headers:
+            if header == 'thread-id':
+                value = message.get_thread_id()
+            elif header == 'message-id':
+                value = message.get_message_id()
+                data['message-id-term'] = 'id:"{0}"'.format(value)
+            elif header == 'date':
+                value = str(datetime.datetime.utcfromtimestamp(
+                    message.get_date()).date())
+            else:
+                value = message.get_header(header)
+            if header == 'from':
+                (value, addr) = email.utils.parseaddr(value)
+                if not value:
+                    value = addr.split('@')[0]
+            data[header] = value
+        next_running_data = data.copy()
+        for header, value in data.items():
+            if header in ['message-id', 'subject']:
+                continue
+            if value == running_data.get(header, None):
+                data[header] = ''
+        return (next_running_data, data)
+
+
+class HtmlPage (Page):
+    _slug_regexp = re.compile('\W+')
+
+    def __init__(self, message_url_template, **kwargs):
+        self.message_url_template = message_url_template
+        super(HtmlPage, self).__init__(**kwargs)
+
+    def _write_header(self, views, stream):
+        super(HtmlPage, self)._write_header(views=views, stream=stream)
+        stream.write('<ul>\n')
+        for view in views:
+            if 'id' not in view:
+                view['id'] = self._slug(view['title'])
+            stream.write(
+                '<li><a href="#{id}">{title}</a></li>\n'.format(**view))
+        stream.write('</ul>\n')
+
+    def _write_view_header(self, view, stream):
+        stream.write('<h3 id="{id}">{title}</h3>\n'.format(**view))
+        stream.write('<p>\n')
+        if 'comment' in view:
+            stream.write(view['comment'])
+            stream.write('\n')
+        for line in [
+                'The view is generated from the following query:',
+                '</p>',
+                '<p>',
+                '  <code>',
+                view['query-string'],
+                '  </code>',
+                '</p>',
+                ]:
+            stream.write(line)
+            stream.write('\n')
+
+    def _write_threads(self, threads, stream):
+        if not threads:
+            return
+        stream.write('<table>\n')
+        for thread in threads:
+            stream.write('  <tbody>\n')
+            for message_display_data in thread:
+                stream.write((
+                    '    <tr class="message-first">\n'
+                    '      <td>{date}</td>\n'
+                    '      <td><code>{message-id-term}</code></td>\n'
+                    '    </tr>\n'
+                    '    <tr class="message-last">\n'
+                    '      <td>{from}</td>\n'
+                    '      <td>{subject}</td>\n'
+                    '    </tr>\n'
+                    ).format(**message_display_data))
+            stream.write('  </tbody>\n')
+            if thread != threads[-1]:
+                stream.write(
+                    '  <tbody><tr><td colspan="2"><br /></td></tr></tbody>\n')
+        stream.write('</table>\n')
+
+    def _message_display_data(self, *args, **kwargs):
+        running_data, display_data = super(
+            HtmlPage, self)._message_display_data(
+                *args, **kwargs)
+        if 'subject' in display_data and 'message-id' in display_data:
+            d = {
+                'message-id': quote(display_data['message-id']),
+                'subject': xml.sax.saxutils.escape(display_data['subject']),
+                }
+            d['url'] = self.message_url_template.format(**d)
+            display_data['subject'] = (
+                '<a href="{url}">{subject}</a>'
+                ).format(**d)
+        for key in ['message-id', 'from']:
+            if key in display_data:
+                display_data[key] = xml.sax.saxutils.escape(display_data[key])
+        return (running_data, display_data)
+
+    def _slug(self, string):
+        return self._slug_regexp.sub('-', string)
+
+parser = argparse.ArgumentParser(description=__doc__)
+parser.add_argument(
+    '--text', action='store_true', help='output plain text format')
+parser.add_argument(
+    '--config', metavar='PATH',
+    help='load config from given file.  '
+        'The format is described in notmuch-report.json(5).')
+parser.add_argument(
+    '--list-views', action='store_true', help='list views')
+parser.add_argument(
+    '--get-query', metavar='VIEW', help='get query for view')
+
+
+args = parser.parse_args()
+
+try:
+    config = read_config(path=args.config)
+except ConfigError as e:
+    print(e, file=sys.stderr)
+    sys.exit(1)
+
+header_template = config['meta'].get('header', '''<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset={encoding}" />
+  <title>{title}</title>
+  <style media="screen" type="text/css">
+    h1 {{
+      font-size: 1.5em;
+    }}
+    h2 {{
+      font-size: 1.17em;
+    }}
+    h3 {{
+      font-size: 100%;
+    }}
+    table {{
+      border-spacing: 0;
+    }}
+    tr.message-first td {{
+      padding-top: {inter_message_padding};
+    }}
+    tr.message-last td {{
+      padding-bottom: {inter_message_padding};
+    }}
+    td {{
+      padding-left: {border_radius};
+      padding-right: {border_radius};
+    }}
+    tr:first-child td:first-child {{
+      border-top-left-radius: {border_radius};
+    }}
+    tr:first-child td:last-child {{
+      border-top-right-radius: {border_radius};
+    }}
+    tr:last-child td:first-child {{
+      border-bottom-left-radius: {border_radius};
+    }}
+    tr:last-child td:last-child {{
+      border-bottom-right-radius: {border_radius};
+    }}
+    tbody:nth-child(4n+1) tr td {{
+      color: #000;
+      background-color: #ffd96e;
+    }}
+    tbody:nth-child(4n+3) tr td {{
+      color: #000;
+      background-color: #bce;
+    }}
+    hr {{
+      border: 0;
+      height: 1px;
+      color: #ccc;
+      background-color: #ccc;
+    }}
+  </style>
+</head>
+<body>
+<h1>{title}</h1>
+<p>
+{blurb}
+</p>
+<h2>Views</h2>
+''')
+
+footer_template = config['meta'].get('footer', '''
+<hr>
+<p>Generated: {datetime}</p>
+</body>
+</html>
+''')
+
+now = datetime.datetime.utcnow()
+context = {
+    'date': now,
+    'datetime': now.strftime('%Y-%m-%d %H:%M:%SZ'),
+    'title': config['meta']['title'],
+    'blurb': config['meta']['blurb'],
+    'encoding': _ENCODING,
+    'inter_message_padding': '0.25em',
+    'border_radius': '0.5em',
+    }
+
+_PAGES['text'] = Page()
+_PAGES['html'] = HtmlPage(
+    header=header_template.format(**context),
+    footer=footer_template.format(**context),
+    message_url_template=config['meta'].get(
+        'message-url', 'https://mid.gmane.org/{message-id}'),
+    )
+
+if args.list_views:
+    for view in config['views']:
+        print(view['title'])
+    sys.exit(0)
+elif args.get_query != None:
+    for view in config['views']:
+        if args.get_query == view['title']:
+            print(' and '.join('( {} )'.format(q) for q in view['query']))
+    sys.exit(0)
+else:
+    # only import notmuch if needed
+    import notmuch
+
+if args.text:
+    page = _PAGES['text']
+else:
+    page = _PAGES['html']
+
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+page.write(database=db, views=config['views'])
diff --git a/devel/nmbug/notmuch-report.json b/devel/nmbug/notmuch-report.json
new file mode 100644 (file)
index 0000000..c5b35b1
--- /dev/null
@@ -0,0 +1,70 @@
+{
+    "meta": {
+        "title": "Notmuch Patches",
+        "blurb": "For more information see <a href=\"https://notmuchmail.org/nmbug\">nmbug</a>"
+    },
+
+    "views": [
+       {
+           "comment": "Unresolved bugs (or just need tag updating).",
+           "query": [
+               "tag:notmuch::bug",
+               "not tag:notmuch::fixed",
+               "not tag:notmuch::wontfix"
+           ],
+           "title": "Bugs"
+       },
+       {
+           "comment": "These patches are under consideration for pushing.",
+           "query": [
+               "tag:notmuch::patch and not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete and not tag:notmuch::wip",
+               "not tag:notmuch::stale and not tag:notmuch::contrib",
+               "not tag:notmuch::moreinfo",
+               "not tag:notmuch::python",
+               "not tag:notmuch::vim",
+               "not tag:notmuch::wontfix",
+               "not tag:notmuch::needs-review"
+           ],
+           "title": "Maybe Ready (Core and Emacs)"
+       },
+       {
+           "comment": "These python related patches might be ready to push, or they might just need updated tags.",
+           "query": [
+               "tag:notmuch::patch and not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete and not tag:notmuch::wip",
+               "not tag:notmuch::stale and not tag:notmuch::contrib",
+               "not tag:notmuch::moreinfo",
+               "not tag:notmuch::wontfix",
+               " tag:notmuch::python",
+               "not tag:notmuch::needs-review"
+           ],
+           "title": "Maybe Ready (Python)"
+       },
+       {
+           "comment": "These vim related patches might be ready to push, or they might just need updated tags.",
+           "query": [
+               "tag:notmuch::patch and not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete and not tag:notmuch::wip",
+               "not tag:notmuch::stale and not tag:notmuch::contrib",
+               "not tag:notmuch::moreinfo",
+               "not tag:notmuch::wontfix",
+               "tag:notmuch::vim",
+               "not tag:notmuch::needs-review"
+           ],
+           "title": "Maybe Ready (vim)"
+       },
+       {
+           "comment": "These patches are under review, or waiting for feedback.",
+           "query": [
+               "tag:notmuch::patch",
+               "not tag:notmuch::pushed",
+               "not tag:notmuch::obsolete",
+               "not tag:notmuch::stale",
+               "not tag:notmuch::wontfix",
+               "tag:notmuch::moreinfo or tag:notmuch::needs-review"
+           ],
+           "title": "Review"
+       }
+    ]
+}
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..b0d4d5c
--- /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))
+  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?
diff --git a/devel/release-checks.sh b/devel/release-checks.sh
new file mode 100755 (executable)
index 0000000..c0accf7
--- /dev/null
@@ -0,0 +1,208 @@
+#!/usr/bin/env bash
+
+set -eu
+#set -x # or enter bash -x ... on command line
+
+if [ x"${BASH_VERSION-}" = x ]
+then   echo
+       echo "Please execute this script using 'bash' interpreter"
+       echo
+       exit 1
+fi
+
+set -o posix
+set -o pipefail # bash feature
+
+readonly DEFAULT_IFS="$IFS" # Note: In this particular case quotes are needed.
+
+# Avoid locale-specific differences in output of executed commands
+LANG=C LC_ALL=C; export LANG LC_ALL
+
+readonly PV_FILE='bindings/python/notmuch/version.py'
+
+# Using array here turned out to be unnecessarily complicated
+emsgs=''
+emsg_count=0
+append_emsg ()
+{
+       emsg_count=$((emsg_count + 1))
+       emsgs="${emsgs:+$emsgs\n}  $1"
+}
+
+for f in ./version.txt debian/changelog NEWS "$PV_FILE"
+do
+       if   [ ! -f "$f" ]; then append_emsg "File '$f' is missing"
+       elif [ ! -r "$f" ]; then append_emsg "File '$f' is unreadable"
+       elif [ ! -s "$f" ]; then append_emsg "File '$f' is empty"
+       fi
+done
+
+if [ -n "$emsgs" ]
+then
+       echo 'Release files problems; fix these and try again:'
+       echo -e "$emsgs"
+       exit 1
+fi
+
+if read VERSION
+then
+       if read rest
+       then    echo "'version' file contains more than one line"
+               exit 1
+       fi
+else
+       echo "Reading './version' file failed (surprisingly!)"
+       exit 1
+fi < ./version.txt
+
+readonly VERSION
+
+# In the rest of this file, tests collect list of errors to be fixed
+
+printf %s "Checking that git working directory is clean... "
+git_status=`git status --porcelain`
+if [ "$git_status" = '' ]
+then
+       echo Yes.
+else
+       echo No.
+       append_emsg "Git working directory is not clean (git status --porcelain)."
+fi
+unset git_status
+
+verfail ()
+{
+       echo No.
+       append_emsg "$@"
+       append_emsg "  Please follow the instructions in RELEASING to choose a version"
+}
+
+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" ;;
+       .*)     verfail "'$VERSION' begins with a period" ;;
+       *.)     verfail "'$VERSION' ends with a period" ;;
+       *..*)   verfail "'$VERSION' contains two consecutive periods" ;;
+       *.*)    echo Yes. ;;
+       *)      verfail "'$VERSION' is a single number" ;;
+esac
+
+printf %s "Checking that this is Debian package for notmuch... "
+read deb_notmuch deb_version rest < debian/changelog
+if [ "$deb_notmuch" = 'notmuch' ]
+then
+       echo Yes.
+else
+       echo No.
+       append_emsg "Package name '$deb_notmuch' is not 'notmuch' in debian/changelog"
+fi
+
+printf %s "Checking that Debian package version is $VERSION-1... "
+
+if [ "$deb_version" = "($VERSION-1)" ]
+then
+       echo Yes.
+else
+       echo No.
+       append_emsg "Version '$deb_version' is not '($VERSION-1)' in debian/changelog"
+fi
+
+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
+       echo Yes.
+else
+       echo No.
+       append_emsg "Version '$py_version' is not '$VERSION' in $PV_FILE"
+fi
+
+printf %s "Checking that NEWS header is tidy... "
+if [ "`exec sed 's/./=/g; 1q' NEWS`" = "`exec sed '1d; 2q' NEWS`" ]
+then
+       echo Yes.
+else
+       echo No.
+       if [ "`exec sed '1d; s/=//g; 2q' NEWS`" != '' ]
+       then
+               append_emsg "Line 2 in NEWS file is not all '=':s"
+       else
+               append_emsg "Line 2 in NEWS file does not have the same length as line 1"
+       fi
+fi
+
+printf %s "Checking that this is Notmuch NEWS... "
+read news_notmuch news_version news_date < NEWS
+if [ "$news_notmuch" = "Notmuch" ]
+then
+       echo Yes.
+else
+       echo No.
+       append_emsg "First word '$news_notmuch' is not 'Notmuch' in NEWS file"
+fi
+
+printf %s "Checking that NEWS version is $VERSION... "
+if [ "$news_version" = "$VERSION" ]
+then
+       echo Yes.
+else
+       echo No.
+       append_emsg "Version '$news_version' in NEWS file is not '$VERSION'"
+fi
+
+#eval `date '+year=%Y mon=%m day=%d'`
+today0utc=`date --date=0Z +%s` # gnu date feature
+
+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`
+       ddiff=$((newsdate0utc - today0utc))
+       if [ $ddiff -lt -86400 ] # since beginning of yesterday...
+       then
+               echo No.
+               append_emsg "Date $news_date in NEWS file is too much in the past"
+       elif [ $ddiff -gt 172800 ] # up to end of tomorrow...
+       then
+               echo No.
+               append_emsg "Date $news_date in NEWS file is too much in the future"
+       else
+               echo Yes.
+       fi ;;
+ *)
+       echo No.
+       append_emsg "Date '$news_date' in NEWS file is not in format (yyyy-mm-dd)"
+esac
+
+year=`exec date +%Y`
+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
+       *2009-$year*)
+               echo Yes. ;;
+       *)
+               echo No.
+               append_emsg "The copyright in doc/conf.py line '$copyrightline' does not contain '2009-$year'"
+esac
+
+if [ -n "$emsgs" ]
+then
+       echo
+       echo 'Release check failed; check these issues:'
+       echo -e "$emsgs"
+       exit 1
+fi
+
+echo 'All checks this script executed completed successfully.'
+echo 'Make sure that everything else mentioned in RELEASING'
+echo 'file is in order, too.'
+
+
+# Local variables:
+# mode: shell-script
+# sh-basic-offset: 8
+# tab-width: 8
+# End:
+# vi: set sw=8 ts=8
diff --git a/devel/schemata b/devel/schemata
new file mode 100644 (file)
index 0000000..4e05cda
--- /dev/null
@@ -0,0 +1,246 @@
+This file describes the schemata used for notmuch's structured output
+format (currently JSON and S-Expressions).
+
+[]'s indicate lists.  List items can be marked with a '?', meaning
+they are optional; or a '*', meaning there can be zero or more of that
+item.  {}'s indicate an object that maps from field identifiers to
+values.  An object field marked '?' is optional; one marked with '*'
+can repeat (with a different name). |'s indicate alternates (e.g.,
+int|string means something can be an int or a string).
+
+For S-Expression output, lists are printed delimited by () instead of
+[]. Objects are printed as p-lists, i.e. lists where the keys and values
+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 5 of the structured output format.
+
+Version history
+---------------
+
+v1
+- First versioned schema release.
+- Added part.content-length and part.content-transfer-encoding fields.
+
+v2
+- Added the thread_summary.query field.
+
+v3
+- Replaced message.filename string with a list of filenames.
+- Added part.content-disposition field.
+
+v4
+- replace signature error integer bitmask with a set of flags for
+  individual errors.
+- (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
+--------------------
+
+# Number of seconds since the Epoch
+unix_time = int
+
+# Thread ID, sans "thread:"
+threadid = string
+
+# Message ID, sans "id:"
+messageid = string
+
+# E-mail header name, sans trailing colon, like "Subject" or "In-Reply-To"
+header_name = string
+
+notmuch show schema
+-------------------
+
+# A top-level set of threads (do_show)
+# Returned by notmuch show without a --part argument
+thread_set = [thread*]
+
+# Top-level messages in a thread (show_messages)
+thread = [thread_node*]
+
+# A message and its replies (show_messages)
+thread_node = [
+    message|null,             # null if not matched and not --entire-thread
+    [thread_node*]            # children of message
+]
+
+# A message (format_part_sprinter)
+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
+    tags:           [string*],
+
+    headers:        headers,
+    crypto:         crypto,
+    duplicate:      integer,
+    body?:          [part]    # omitted if --body=false
+}
+
+# when showing the message, was any or all of it decrypted?
+msgdecstatus: "full"|"partial"
+
+# The overall cryptographic state of the message as a whole:
+crypto = {
+    signed?:    {
+                  status:      sigstatus,
+                  # was the set of signatures described under encrypted cover?
+                  encrypted:   bool,
+                  # which of the headers is covered by sigstatus?
+                  headers:     [header_name*]
+                },
+    decrypted?: {
+                  status: msgdecstatus,
+                  # map encrypted headers that differed from the outside headers.
+                  # the value of each item in the map is what that field showed externally
+                  # (maybe null if it was not present in the external headers).
+                  header-mask:  { header_name*: string|null }
+                }
+}
+
+# A MIME part (format_part_sprinter)
+part = {
+    id:             int|string, # part id (currently DFS part number)
+
+    encstatus?:     encstatus,
+    sigstatus?:     sigstatus,
+
+    content-type:   string,
+    content-disposition?:       string,
+    content-id?:    string,
+    # if content-type starts with "multipart/":
+    content:        [part*],
+    # if content-type is "message/rfc822":
+    content:        [{headers: headers, body: [part]}],
+    # otherwise (leaf parts):
+    filename?:      string,
+    content-charset?: string,
+    # A leaf part's body content is optional, but may be included if
+    # it can be correctly encoded as a string.  Consumers should use
+    # this in preference to fetching the part content separately.
+    content?:       string,
+    # If a leaf part's body content is not included, the length of
+    # the encoded content (in bytes) may be given instead.
+    content-length?: int,
+    # If a leaf part's body content is not included, its transfer encoding
+    # may be given.  Using this and the encoded content length, it is
+    # possible for the consumer to estimate the decoded content length.
+    content-transfer-encoding?: string
+}
+
+# The headers of a message or part (format_headers_sprinter with reply = FALSE)
+headers = {
+    Subject:        string,
+    From:           string,
+    To?:            string,
+    Cc?:            string,
+    Bcc?:           string,
+    Reply-To?:      string,
+    Date:           string,
+    extra_header_pair*
+}
+
+extra_header_pair=  (header_name: string)
+# Encryption status (format_part_sprinter)
+encstatus = [{status: "good"|"bad"}]
+
+# Signature status (format_part_sigstatus_sprinter)
+sigstatus = [signature*]
+
+signature = {
+    # (signature_status_to_string)
+    status:         "good"|"bad"|"error"|"unknown",
+    # if status is "good":
+    fingerprint?:   string,
+    created?:       unix_time,
+    expires?:       unix_time,
+    userid?:        string
+    email?:         string
+    # if status is not "good":
+    keyid?:         string
+    errors?:       sig_errors
+}
+
+sig_errors = {
+    key-revoked?: bool,
+    key-expired?: bool,
+    sig-expired?: bool,
+    key-missing?: bool,
+    alg-unsupported?: bool,
+    crl-missing?: bool,
+    crl-too-old?: bool,
+    bad-policy?: bool,
+    sys-error?: bool,
+    tofu-conflict?: bool
+}
+
+notmuch search schema
+---------------------
+
+# --output=summary
+search_summary = [thread_summary*]
+
+# --output=threads
+search_threads = [threadid*]
+
+# --output=messages
+search_messages = [messageid*]
+
+# --output=files
+search_files = [string*]
+
+# --output=tags
+search_tags = [string*]
+
+thread_summary = {
+    thread:         threadid,
+    timestamp:      unix_time,
+    date_relative:  string,   # user-friendly timestamp
+    matched:        int,      # number of matched messages
+    total:          int,      # total messages in thread
+    authors:        string,   # comma-separated names with | between
+                              # matched and unmatched
+    subject:        string,
+    tags:           [string*],
+
+    # Two stable query strings identifying exactly the matched and
+    # unmatched messages currently in this thread.  The messages
+    # matched by these queries will not change even if more messages
+    # arrive in the thread.  If there are no matched or unmatched
+    # messages, the corresponding query will be null (there is no
+    # query that matches nothing).  (Added in schema version 2.)
+    query:          [string|null, string|null],
+}
+
+notmuch reply schema
+--------------------
+
+reply = {
+    # The headers of the constructed reply
+    reply-headers: reply_headers,
+
+    # As in the show format (format_part_sprinter)
+    original: message
+}
+
+# Reply headers (format_headers_sprinter with reply = TRUE)
+reply_headers = {
+    Subject:        string,
+    From:           string,
+    To?:            string,
+    Cc?:            string,
+    Bcc?:           string,
+    In-reply-to:    string,
+    References:     string
+}
diff --git a/devel/try-emacs-mua b/devel/try-emacs-mua
new file mode 100755 (executable)
index 0000000..585d624
--- /dev/null
@@ -0,0 +1,139 @@
+#!/bin/sh
+:; set -x; exec "${EMACS:-emacs}" --debug-init --load "$0" "$@"; exit
+;;
+;; Try the notmuch emacs client located in ../emacs/ directory
+;;
+;; Run this without arguments; emacs window opens with some usage information
+;;
+;; Authors: Tomi Ollila <tomi.ollila@iki.fi>
+;;
+;; https://www.emacswiki.org/emacs/EmacsScripts was a useful starting point...
+;;
+;; Licence: GPLv3+
+;;
+
+(message "Starting '%s'" load-file-name)
+
+(set-buffer "*scratch*")
+
+(setq initial-buffer-choice nil
+      inhibit-startup-screen t)
+
+(when (featurep 'notmuch)
+  (insert "
+Notmuch has been loaded to this emacs (during processing of the init file)
+which means it is (most probably) loaded from different source than expected.
+
+Please run \"" (file-name-nondirectory load-file-name)
+"\" with '-q' (or '-Q') as an argument, to disable
+processing of the init file -- you can load it after emacs has started\n
+exit emacs (y or n)? ")
+  (if (y-or-n-p "exit emacs")
+      (kill-emacs)
+    (error "Stopped reading %s" load-file-name)))
+
+(let ((pdir (file-name-directory
+            (directory-file-name (file-name-directory load-file-name)))))
+  (unless (file-exists-p (concat pdir "emacs/notmuch-lib.el"))
+    (insert "Cannot find notmuch-emacs source directory
+while looking at: " pdir "emacs\n\nexit emacs (y or n)? ")
+    (if (y-or-n-p "exit emacs")
+       (kill-emacs)
+      (error "Stopped reading %s" load-file-name)))
+  (setq try-notmuch-source-directory (directory-file-name pdir)
+       try-notmuch-emacs-directory (concat pdir "emacs/")
+       load-path (cons try-notmuch-emacs-directory load-path)))
+
+(define-advice require
+    (:before (feature &optional _filename _noerror) notmuch)
+  (unless (featurep feature)
+    (message "require: %s" feature)))
+
+(insert "Found notmuch emacs client in " try-notmuch-emacs-directory "\n")
+
+(let ((notmuch-path (executable-find "notmuch")))
+  (insert "Notmuch CLI executable "
+         (if notmuch-path (concat "is " notmuch-path) "not found!") "\n"))
+
+(condition-case err
+;; "opportunistic" load-prefer-newer -- will be effective since emacs 24.4
+    (let ((load-prefer-newer t)
+         (force-load-messages t))
+      (require 'notmuch))
+  ;; specifying `debug' here lets the debugger run
+  ;; if `debug-on-error' is non-nil.
+  ((debug error)
+   (let ((error-message-string (error-message-string err)))
+     (insert "\nLoading notmuch failed: " error-message-string "\n")
+     (message "Loading notmuch failed: %s" error-message-string)
+     (insert "See *Messages* buffer for more information.\n")
+     (if init-file-user
+        (message "Hint: %s -q (or -Q) may help" load-file-name))
+     (pop-to-buffer "*Messages*")
+     (error "Stopped reading %s" load-file-name))))
+
+(insert "
+Go to the end of the following lines and type C-x C-e to evaluate
+(or C-j which is shorter but inserts evaluation results into buffer)
+
+To \"disable\" mail sending, evaluate
+* (setq message-send-mail-function (lambda () t))
+")
+
+(if (file-exists-p (concat try-notmuch-source-directory "/notmuch"))
+    (insert "
+To use accompanied notmuch binary from the same source, evaluate
+* (setq exec-path (cons \"" try-notmuch-source-directory  "\" exec-path))
+Note: Evaluating the above may be followed by unintended database
+upgrade and getting back to old version may require dump & restore.
+"))
+
+(if init-file-user ;; nil, if '-q' or '-Q' is given, but no '-u' 'USER'
+    (insert "
+Your init file was processed during emacs startup. If you want to test
+notmuch emacs mail client without your emacs init file interfering, Run\n\""
+(file-name-nondirectory load-file-name) "\" with '-q' (or '-Q') as an argument.
+")
+  (let ((emacs-init-file-name) (notmuch-init-file-name))
+    ;; determining init file name in startup.el/command-line is too complicated
+    ;; to be duplicated here; these 3 file names covers most of the users
+    (mapc (lambda (fn) (if (file-exists-p fn) (setq emacs-init-file-name fn)))
+         '("~/.emacs.d/init.el" "~/.emacs" "~/.emacs.el"))
+    (setq notmuch-init-file-name "~/.emacs.d/notmuch-config.el")
+    (unless (file-exists-p notmuch-init-file-name)
+       (setq notmuch-init-file-name nil))
+    (if (and emacs-init-file-name notmuch-init-file-name)
+       (insert "
+If you want to load your initialization files now, evaluate\n* (progn")
+      (if (or emacs-init-file-name notmuch-init-file-name)
+         (insert "
+If you want to load your initialization file now, evaluate\n*")))
+    (if emacs-init-file-name
+       (insert " (load \"" emacs-init-file-name "\")"))
+    (if notmuch-init-file-name
+       (insert " (load \"" notmuch-init-file-name "\")"))
+    (if (and emacs-init-file-name notmuch-init-file-name)
+       (insert ")"))
+    (if (or emacs-init-file-name notmuch-init-file-name)
+       (insert "\n")))
+  (if (>= emacs-major-version 24)
+      (insert "
+If you want to use packages (e.g. company from elpa) evaluate
+* (progn (require 'package) (package-initialize))
+")))
+
+(insert "
+To start notmuch (hello) screen, evaluate
+* (notmuch-hello)")
+
+(add-hook 'emacs-startup-hook
+         (lambda ()
+           (with-current-buffer "*scratch*"
+             (lisp-interaction-mode)
+             (goto-char (point-min))
+             (forward-line 2)
+             (set-buffer-modified-p nil))))
+
+;; Local Variables:
+;; mode: emacs-lisp
+;; End:
diff --git a/devel/uncrustify.cfg b/devel/uncrustify.cfg
new file mode 100644 (file)
index 0000000..d203d4e
--- /dev/null
@@ -0,0 +1,127 @@
+#
+# Uncrustify config file for notmuch.
+# Based on uncrustify config file for the linux kernel
+#
+# $Id: linux-indent.cfg 488 2006-09-09 12:44:38Z bengardner $
+# Taken from the uncrustify distribution under license (GPL2+)
+#
+# Sample usage:
+#        uncrustify --replace -c uncrustify.cfg foo.c
+#
+
+indent_with_tabs       = 2             # 1=indent to level only, 2=indent with tabs
+align_with_tabs                = TRUE          # use tabs to align
+align_on_tabstop       = TRUE          # align on tabstops
+input_tab_size         = 8             # original tab size
+output_tab_size                = 8             # new tab size
+indent_columns         = 4
+
+indent_label           = -2            # pos: absolute col, neg: relative column
+
+indent_cmt_with_tabs   = false         # true would align to tabstop always...
+
+#
+# inter-symbol newlines
+#
+
+nl_enum_brace          = remove        # "enum {" vs "enum \n {"
+nl_union_brace         = remove        # "union {" vs "union \n {"
+nl_struct_brace                = remove        # "struct {" vs "struct \n {"
+nl_do_brace             = remove       # "do {" vs "do \n {"
+nl_if_brace             = remove       # "if () {" vs "if () \n {"
+nl_for_brace            = remove       # "for () {" vs "for () \n {"
+nl_else_brace           = remove       # "else {" vs "else \n {"
+nl_while_brace          = remove       # "while () {" vs "while () \n {"
+nl_switch_brace         = remove       # "switch () {" vs "switch () \n {"
+nl_brace_while         = remove        # "} while" vs "} \n while" - cuddle while
+nl_brace_else          = remove        # "} else" vs "} \n else" - cuddle else
+nl_func_var_def_blk    = 1
+nl_fcall_brace         = remove        # "list_for_each() {" vs "list_for_each()\n{"
+nl_fdef_brace          = force         # "int foo() {" vs "int foo()\n{"
+# nl_after_return              = TRUE;
+# nl_before_case       = 1
+
+# Add or remove newline between return type and function name in definition
+nl_func_type_name      = force
+nl_enum_leave_one_liners = True
+nl_enum_brace = Remove
+nl_after_struct = 0
+#
+# Source code modifications
+#
+
+# mod_paren_on_return  = remove        # "return 1;" vs "return (1);"
+# mod_full_brace_if    = remove        # "if (a) a--;" vs "if (a) { a--; }"
+# mod_full_brace_for   = remove        # "for () a--;" vs "for () { a--; }"
+# mod_full_brace_do    = remove        # "do a--; while ();" vs "do { a--; } while ();"
+# mod_full_brace_while = remove        # "while (a) a--;" vs "while (a) { a--; }"
+
+
+# In case some custom types aren't detected properly by uncrustify
+# add those to this section below. For example there are cases where
+# uncrustify doesn't know whether a 'token' is part of pointer type
+# or left operand of a binary multiplication operation.
+
+type FILE
+type GMimeObject GMimeCryptoContext GMimeCipherContext
+type mime_node_t notmuch_message_t notmuch_show_params_t
+type sprinter_t
+
+#
+# inter-character spacing options
+#
+
+sp_before_ptr_star     = force
+sp_between_ptr_star    = remove
+sp_after_ptr_star      = remove
+sp_not                 = force
+sp_pp_concat           = ignore        # XXX 'remove' drops leading space also
+sp_pp_stringify                = remove
+
+# sp _return_paren     = force         # "return (1);" vs "return(1);"
+sp_sizeof_paren                = force         # "sizeof (int)" vs "sizeof(int)"
+sp_before_sparen       = force         # "if (" vs "if("
+sp_after_sparen                = force         # "if () {" vs "if (){"
+sp_sparen_brace                = force
+sp_after_cast          = force         # "(int) a" vs "(int)a"
+sp_inside_braces       = add           # "{ 1 }" vs "{1}"
+sp_inside_braces_struct        = add           # "{ 1 }" vs "{1}"
+sp_inside_braces_enum  = add           # "{ 1 }" vs "{1}"
+sp_assign              = force
+sp_arith               = force
+sp_bool                        = add
+sp_compare             = add
+sp_assign              = add
+sp_after_comma         = add
+sp_func_def_paren      = force         # "int foo (){" vs "int foo(){"
+sp_func_call_paren     = force         # "foo (" vs "foo("
+sp_func_proto_paren    = force         # "int foo ();" vs "int foo();"
+sp_brace_else          = force         # "} else" vs "}else"
+sp_else_brace          = force         # "else {" vs "else{"
+#
+# Aligning stuff
+#
+
+align_enum_equ_span    = 4             # '=' in enum definition
+# align_nl_cont                = TRUE
+# align_var_def_span   = 2
+# align_var_def_inline = TRUE
+# align_var_def_star   = FALSE
+# align_var_def_colon  = TRUE
+# align_assign_span    = 1
+align_struct_init_span = 0             # align stuff in a structure init '= { }'
+align_right_cmt_span   = 8             # align comments span this much in func
+# align_pp_define_span = 8;
+# align_pp_define_gap  = 4;
+
+cmt_star_cont          = true
+
+# indent_brace         = 0
+
+indent_class = true
+
+# line width / line splitting
+code_width            102
+ls_for_split_full     True
+ls_func_split_full    True
+ls_code_width         True
diff --git a/doc/.gitignore b/doc/.gitignore
new file mode 100644 (file)
index 0000000..bbb749f
--- /dev/null
@@ -0,0 +1,3 @@
+*.pyc
+/_build
+/config.dox
diff --git a/doc/INSTALL b/doc/INSTALL
new file mode 100644 (file)
index 0000000..0585476
--- /dev/null
@@ -0,0 +1,11 @@
+This file contains some more detailed information about building and
+installing the documentation.
+
+- You need sphinx at least version 1.0.
+
+- You can build build and install man pages with 'make install-man'
+
+- You can build man, info, html, and pdf versions of the docs
+  (currently only the man pages) with
+
+     'make install-{man|info|html|pdf}'
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644 (file)
index 0000000..fa25832
--- /dev/null
@@ -0,0 +1,5 @@
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/doc/Makefile.local b/doc/Makefile.local
new file mode 100644 (file)
index 0000000..aafa77a
--- /dev/null
@@ -0,0 +1,163 @@
+# -*- makefile-gmake -*-
+
+dir := doc
+
+# You can set these variables from the command line.
+SPHINXOPTS    := -q
+SPHINXBUILD   = env LD_LIBRARY_PATH=${NOTMUCH_BUILDDIR}/lib sphinx-build
+DOCBUILDDIR      := $(dir)/_build
+
+# Internal variables.
+ALLSPHINXOPTS   := $(SPHINXOPTS) $(srcdir)/$(dir)
+APIMAN         := $(DOCBUILDDIR)/man/man3/notmuch.3
+DOXYFILE       := $(srcdir)/$(dir)/doxygen.cfg
+
+MAN1_RST := $(wildcard $(srcdir)/doc/man1/*.rst)
+MAN5_RST := $(wildcard $(srcdir)/doc/man5/*.rst)
+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)
+
+MAN_GZIP_FILES := $(addsuffix .gz,${MAN_ROFF_FILES})
+
+MAN1_TEXI := $(patsubst $(srcdir)/doc/man1/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$(MAN1_RST))
+MAN5_TEXI := $(patsubst $(srcdir)/doc/man5/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$(MAN5_RST))
+MAN7_TEXI := $(patsubst $(srcdir)/doc/man7/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$(MAN7_RST))
+INFO_TEXI_FILES := $(MAN1_TEXI) $(MAN5_TEXI) $(MAN7_TEXI)
+ifeq ($(WITH_EMACS),1)
+       INFO_TEXI_FILES += $(DOCBUILDDIR)/texinfo/notmuch-emacs.texi
+endif
+
+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
+
+.PHONY: install-man build-man apidocs install-apidocs
+
+%.gz: %
+       rm -f $@ && gzip --no-name --stdout $^ > $@
+
+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) $(DOC_PREREQS)
+       $(SPHINXBUILD) -b html -d $(DOCBUILDDIR)/html_doctrees $(ALLSPHINXOPTS) $(DOCBUILDDIR)/html
+       touch $@
+
+sphinx-texinfo: $(DOCBUILDDIR)/.texi.stamp
+
+$(DOCBUILDDIR)/.texi.stamp: $(ALL_RST_FILES) $(DOC_PREREQS)
+       $(SPHINXBUILD) -b texinfo -d $(DOCBUILDDIR)/texinfo_doctrees $(ALLSPHINXOPTS) $(DOCBUILDDIR)/texinfo
+       touch $@
+
+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.
+${MAN_ROFF_FILES}: $(DOCBUILDDIR)/.roff.stamp
+
+# By using $(DOCBUILDDIR)/.roff.stamp instead of ${MAN_ROFF_FILES}, we
+# convey to make that a single invocation of this recipe builds all
+# of the roff files.  This prevents parallel make from starting an
+# instance of this recipe for each roff file.
+$(DOCBUILDDIR)/.roff.stamp: ${MAN_RST_FILES}
+ifeq ($(HAVE_SPHINX),1)
+       $(SPHINXBUILD) -b man -d $(DOCBUILDDIR)/man_doctrees $(ALLSPHINXOPTS) $(DOCBUILDDIR)/man
+       for section in 1 5 7; do \
+           mkdir -p $(DOCBUILDDIR)/man/man$${section}; \
+           mv $(DOCBUILDDIR)/man/*.$${section} $(DOCBUILDDIR)/man/man$${section}; \
+       done
+else
+       @echo "Fatal: build dependency fail."
+       @false
+endif
+       touch $@
+
+install-man: install-apidocs
+
+ifeq ($(HAVE_DOXYGEN),1)
+MAN_GZIP_FILES += ${APIMAN}.gz
+apidocs: $(APIMAN)
+install-apidocs: ${APIMAN}.gz
+       mkdir -p "$(DESTDIR)$(mandir)/man3"
+       install -m0644 $(filter %.3.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man3
+
+$(APIMAN): $(dir)/config.dox $(srcdir)/$(dir)/doxygen.cfg $(srcdir)/lib/notmuch.h
+       mkdir -p $(DOCBUILDDIR)/man/man3
+       doxygen $(DOXYFILE)
+       rm -f $(DOCBUILDDIR)/man/man3/_*.3
+       perl -pi -e 's/^[.]RI "\\fI/.RI "\\fP/' $(APIMAN)
+else
+apidocs:
+install-apidocs:
+endif
+
+# Do not try to build or install man pages if a man page converter is
+# not available.
+ifeq ($(HAVE_SPHINX),0)
+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"
+       mkdir -m0755 -p "$(DESTDIR)$(mandir)/man5"
+       mkdir -m0755 -p "$(DESTDIR)$(mandir)/man7"
+       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
+endif
+
+ifneq ($(HAVE_SPHINX)$(HAVE_MAKEINFO),11)
+build-info:
+       @echo "Missing sphinx or makeinfo, not building info pages"
+else
+build-info: $(DOCBUILDDIR)/.info.stamp
+endif
+
+ifneq ($(HAVE_SPHINX)$(HAVE_MAKEINFO)$(HAVE_INSTALL_INFO),111)
+install-info:
+       @echo "Missing prerequisites, not installing info pages"
+else
+install-info: build-info
+       mkdir -m0755 -p "$(DESTDIR)$(infodir)"
+       install -m0644 $(INFO_INFO_FILES) $(DESTDIR)$(infodir)
+       for file in $(INFO_INFO_FILES); do install-info $$file $(DESTDIR)$(infodir)/dir; done
+endif
+
+$(dir)/config.dox: version.stamp
+       echo "PROJECT_NAME = \"Notmuch $(VERSION)\"" > $@
+       echo "INPUT=${srcdir}/lib/notmuch.h" >> $@
+
+CLEAN := $(CLEAN) $(DOCBUILDDIR) $(DOCBUILDDIR)/.roff.stamp $(DOCBUILDDIR)/.texi.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>
+   
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644 (file)
index 0000000..7ac13a5
--- /dev/null
@@ -0,0 +1,226 @@
+
+# -*- coding: utf-8 -*-
+
+import sys
+import os
+from pathlib import Path
+sys.path.append(str(Path(__file__).parent))
+
+extensions = [ 'sphinx.ext.autodoc', 'elisp' ]
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'notmuch'
+copyright = u'2009-2023, Carl Worth and many others'
+
+location = os.path.dirname(__file__)
+
+for pathdir in ['.', '..']:
+    version_file = os.path.join(location,pathdir,'version.txt')
+    if os.path.exists(version_file):
+        with open(version_file,'r') as infile:
+            version=infile.read().replace('\n','')
+
+# for autodoc
+sys.path.insert(0, os.path.join(location, '..', 'bindings', 'python-cffi', 'build', 'stage'))
+
+# read generated config
+for pathdir in ['.', '..']:
+    conf_file = os.path.join(location,pathdir,'sphinx.config')
+    if os.path.exists(conf_file):
+        with open(conf_file,'r') as infile:
+            exec(''.join(infile.readlines()))
+
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+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-hello.rsti', 'notmuch-show.rsti', 'notmuch-tag.rsti', 'notmuch-tree.rsti'):
+        lines.extend(open(rsti_dir+'/'+file))
+    rst_epilog = ''.join(lines)
+    del lines
+else:
+    # If we don't have emacs (or the user configured --without-emacs),
+    # don't build the notmuch-emacs docs, as they need emacs to generate
+    # the docstring include files
+    exclude_patterns.append('notmuch-emacs.rst')
+
+if not tags.has('WITH_PYTHON'):
+    exclude_patterns.append('python-bindings.rst')
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# 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,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = []
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'notmuchdoc'
+
+# Disable SmartyPants, as it mangles command lines.
+# 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
+# (source start file, name, description, authors, manual section).
+
+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',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-address', 'notmuch-address',
+     u'output addresses from matching messages',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-compact', 'notmuch-compact',
+     u'compact the notmuch database',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-config', 'notmuch-config',
+     u'access notmuch configuration file',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-count', 'notmuch-count',
+     u'count messages matching the given search terms',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-dump', 'notmuch-dump',
+     u'creates a plain-text dump of the tags of each message',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-emacs-mua', 'notmuch-emacs-mua',
+     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),
+
+    ('man1/notmuch-insert', 'notmuch-insert',
+     u'add a message to the maildir and notmuch database',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-new', 'notmuch-new',
+     u'incorporate new mail into the notmuch database',
+     [notmuch_authors], 1),
+
+    ('man7/notmuch-properties', 'notmuch-properties',
+     u'notmuch message property conventions and documentation',
+     [notmuch_authors], 7),
+
+    ('man1/notmuch-reindex', 'notmuch-reindex',
+     u're-index matching messages',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-reply', 'notmuch-reply',
+     u'constructs a reply template for a set of messages',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-restore', 'notmuch-restore',
+     u'restores the tags from the given file (see notmuch dump)',
+     [notmuch_authors], 1),
+
+    ('man1/notmuch-search', 'notmuch-search',
+     u'search for messages matching the given search terms',
+     [notmuch_authors], 1),
+
+    ('man7/notmuch-search-terms', 'notmuch-search-terms',
+     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),
+
+    ('man1/notmuch-tag', 'notmuch-tag',
+     u'add/remove tags for all messages matching the search terms',
+     [notmuch_authors], 1),
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+texinfo_no_detailmenu = True
+
+texinfo_documents = [
+    ('notmuch-emacs', 'notmuch-emacs', u'notmuch-emacs documentation',
+     notmuch_authors, 'notmuch-emacs',
+     'emacs based front-end for notmuch', 'Miscellaneous'),
+]
+
+# generate texinfo list from man page list
+texinfo_documents += [
+    (
+        x[0],                          # source start file
+        x[1],                          # target name
+        x[1] + u' documentation',      # title
+        x[3][0],                       # author
+        x[1],                          # dir menu entry
+        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/doxygen.cfg b/doc/doxygen.cfg
new file mode 100644 (file)
index 0000000..4a022de
--- /dev/null
@@ -0,0 +1,298 @@
+# Doxyfile 1.8.4
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+DOXYFILE_ENCODING      = UTF-8
+@INCLUDE              =  "doc/config.dox"
+PROJECT_NUMBER         =
+PROJECT_BRIEF          =
+PROJECT_LOGO           =
+OUTPUT_DIRECTORY       = doc/_build
+CREATE_SUBDIRS         = NO
+OUTPUT_LANGUAGE        = English
+BRIEF_MEMBER_DESC      = YES
+REPEAT_BRIEF           = YES
+ABBREVIATE_BRIEF       =
+ALWAYS_DETAILED_SEC    = NO
+INLINE_INHERITED_MEMB  = NO
+FULL_PATH_NAMES        = NO
+STRIP_FROM_PATH        =
+STRIP_FROM_INC_PATH    =
+SHORT_NAMES            = NO
+JAVADOC_AUTOBRIEF      = YES
+QT_AUTOBRIEF           = NO
+MULTILINE_CPP_IS_BRIEF = NO
+INHERIT_DOCS           = YES
+SEPARATE_MEMBER_PAGES  = NO
+TAB_SIZE               = 8
+ALIASES                =
+OPTIMIZE_OUTPUT_FOR_C  = YES
+OPTIMIZE_OUTPUT_JAVA   = NO
+OPTIMIZE_FOR_FORTRAN   = NO
+OPTIMIZE_OUTPUT_VHDL   = NO
+EXTENSION_MAPPING      =
+MARKDOWN_SUPPORT       = YES
+AUTOLINK_SUPPORT       = YES
+BUILTIN_STL_SUPPORT    = NO
+CPP_CLI_SUPPORT        = NO
+SIP_SUPPORT            = NO
+IDL_PROPERTY_SUPPORT   = YES
+DISTRIBUTE_GROUP_DOC   = NO
+SUBGROUPING            = YES
+INLINE_GROUPED_CLASSES = NO
+INLINE_SIMPLE_STRUCTS  = NO
+TYPEDEF_HIDES_STRUCT   = YES
+LOOKUP_CACHE_SIZE      = 0
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+EXTRACT_ALL            = NO
+EXTRACT_PRIVATE        = NO
+EXTRACT_PACKAGE        = NO
+EXTRACT_STATIC         = NO
+EXTRACT_LOCAL_CLASSES  = YES
+EXTRACT_LOCAL_METHODS  = NO
+EXTRACT_ANON_NSPACES   = NO
+HIDE_UNDOC_MEMBERS     = NO
+HIDE_UNDOC_CLASSES     = NO
+HIDE_FRIEND_COMPOUNDS  = NO
+HIDE_IN_BODY_DOCS      = NO
+INTERNAL_DOCS          = NO
+CASE_SENSE_NAMES       = YES
+HIDE_SCOPE_NAMES       = NO
+SHOW_INCLUDE_FILES     = NO
+FORCE_LOCAL_INCLUDES   = NO
+INLINE_INFO            = YES
+SORT_MEMBER_DOCS       = NO
+SORT_BRIEF_DOCS        = NO
+SORT_MEMBERS_CTORS_1ST = NO
+SORT_GROUP_NAMES       = NO
+SORT_BY_SCOPE_NAME     = NO
+STRICT_PROTO_MATCHING  = NO
+GENERATE_TODOLIST      = NO
+GENERATE_TESTLIST      = NO
+GENERATE_BUGLIST       = NO
+GENERATE_DEPRECATEDLIST= YES
+ENABLED_SECTIONS       =
+MAX_INITIALIZER_LINES  = 30
+SHOW_USED_FILES        = NO
+SHOW_FILES             = NO
+SHOW_NAMESPACES        = NO
+FILE_VERSION_FILTER    =
+LAYOUT_FILE            =
+CITE_BIB_FILES         =
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+QUIET                  = YES
+WARNINGS               = YES
+WARN_IF_UNDOCUMENTED   = YES
+WARN_IF_DOC_ERROR      = YES
+WARN_NO_PARAMDOC       = NO
+WARN_FORMAT            = "$file:$line: $text"
+WARN_LOGFILE           =
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+INPUT_ENCODING         = UTF-8
+FILE_PATTERNS          =
+RECURSIVE              = NO
+EXCLUDE                =
+EXCLUDE_SYMLINKS       = NO
+EXCLUDE_PATTERNS       =
+EXCLUDE_SYMBOLS        =
+EXAMPLE_PATH           =
+EXAMPLE_PATTERNS       =
+EXAMPLE_RECURSIVE      = NO
+IMAGE_PATH             =
+INPUT_FILTER           =
+FILTER_PATTERNS        =
+FILTER_SOURCE_FILES    = NO
+FILTER_SOURCE_PATTERNS =
+USE_MDFILE_AS_MAINPAGE =
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+SOURCE_BROWSER         = NO
+INLINE_SOURCES         = NO
+STRIP_CODE_COMMENTS    = YES
+REFERENCED_BY_RELATION = NO
+REFERENCES_RELATION    = NO
+REFERENCES_LINK_SOURCE = YES
+USE_HTAGS              = NO
+VERBATIM_HEADERS       = NO
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+ALPHABETICAL_INDEX     = NO
+COLS_IN_ALPHA_INDEX    = 5
+IGNORE_PREFIX          =
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+GENERATE_HTML          = NO
+HTML_OUTPUT            = html
+HTML_FILE_EXTENSION    = .html
+HTML_HEADER            =
+HTML_FOOTER            =
+HTML_STYLESHEET        =
+HTML_EXTRA_STYLESHEET  =
+HTML_EXTRA_FILES       =
+HTML_COLORSTYLE_HUE    = 220
+HTML_COLORSTYLE_SAT    = 100
+HTML_COLORSTYLE_GAMMA  = 80
+HTML_TIMESTAMP         = YES
+HTML_DYNAMIC_SECTIONS  = NO
+HTML_INDEX_NUM_ENTRIES = 100
+GENERATE_DOCSET        = NO
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+DOCSET_PUBLISHER_NAME  = Publisher
+GENERATE_HTMLHELP      = NO
+CHM_FILE               =
+HHC_LOCATION           =
+GENERATE_CHI           = NO
+CHM_INDEX_ENCODING     =
+BINARY_TOC             = NO
+TOC_EXPAND             = NO
+GENERATE_QHP           = NO
+QCH_FILE               =
+QHP_NAMESPACE          = org.doxygen.Project
+QHP_VIRTUAL_FOLDER     = doc
+QHP_CUST_FILTER_NAME   =
+QHP_CUST_FILTER_ATTRS  =
+QHP_SECT_FILTER_ATTRS  =
+QHG_LOCATION           =
+GENERATE_ECLIPSEHELP   = NO
+ECLIPSE_DOC_ID         = org.doxygen.Project
+DISABLE_INDEX          = NO
+GENERATE_TREEVIEW      = NO
+ENUM_VALUES_PER_LINE   = 4
+TREEVIEW_WIDTH         = 250
+EXT_LINKS_IN_WINDOW    = NO
+FORMULA_FONTSIZE       = 10
+FORMULA_TRANSPARENT    = YES
+USE_MATHJAX            = NO
+MATHJAX_FORMAT         = HTML-CSS
+MATHJAX_RELPATH        = https://cdn.mathjax.org/mathjax/latest
+MATHJAX_EXTENSIONS     =
+MATHJAX_CODEFILE       =
+SEARCHENGINE           = YES
+SERVER_BASED_SEARCH    = NO
+EXTERNAL_SEARCH        = NO
+SEARCHENGINE_URL       =
+SEARCHDATA_FILE        = searchdata.xml
+EXTERNAL_SEARCH_ID     =
+EXTRA_SEARCH_MAPPINGS  =
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+GENERATE_LATEX         = NO
+LATEX_OUTPUT           = latex
+LATEX_CMD_NAME         = latex
+MAKEINDEX_CMD_NAME     = makeindex
+COMPACT_LATEX          = NO
+PAPER_TYPE             = a4
+EXTRA_PACKAGES         =
+LATEX_HEADER           =
+LATEX_FOOTER           =
+LATEX_EXTRA_FILES      =
+PDF_HYPERLINKS         = YES
+USE_PDFLATEX           = YES
+LATEX_BATCHMODE        = NO
+LATEX_HIDE_INDICES     = NO
+LATEX_SOURCE_CODE      = NO
+LATEX_BIB_STYLE        = plain
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+GENERATE_RTF           = NO
+RTF_OUTPUT             = rtf
+COMPACT_RTF            = NO
+RTF_HYPERLINKS         = NO
+RTF_STYLESHEET_FILE    =
+RTF_EXTENSIONS_FILE    =
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+GENERATE_MAN           = YES
+MAN_OUTPUT             = man
+MAN_EXTENSION          = .3
+MAN_LINKS              = NO
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+GENERATE_XML           = NO
+XML_OUTPUT             = xml
+XML_PROGRAMLISTING     = YES
+#---------------------------------------------------------------------------
+# configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+GENERATE_DOCBOOK       = NO
+DOCBOOK_OUTPUT         = docbook
+#---------------------------------------------------------------------------
+# configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+GENERATE_AUTOGEN_DEF   = NO
+#---------------------------------------------------------------------------
+# configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+GENERATE_PERLMOD       = NO
+PERLMOD_LATEX          = NO
+PERLMOD_PRETTY         = YES
+PERLMOD_MAKEVAR_PREFIX =
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+ENABLE_PREPROCESSING   = YES
+MACRO_EXPANSION        = NO
+EXPAND_ONLY_PREDEF     = NO
+SEARCH_INCLUDES        = NO
+INCLUDE_PATH           =
+INCLUDE_FILE_PATTERNS  =
+PREDEFINED             = __DOXYGEN__
+EXPAND_AS_DEFINED      =
+SKIP_FUNCTION_MACROS   = YES
+#---------------------------------------------------------------------------
+# Configuration::additions related to external references
+#---------------------------------------------------------------------------
+TAGFILES               =
+GENERATE_TAGFILE       =
+ALLEXTERNALS           = NO
+EXTERNAL_GROUPS        = NO
+EXTERNAL_PAGES         = NO
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+CLASS_DIAGRAMS         = NO
+HIDE_UNDOC_RELATIONS   = YES
+HAVE_DOT               = NO
+DOT_NUM_THREADS        = 0
+DOT_FONTNAME           = Helvetica
+DOT_FONTSIZE           = 10
+DOT_FONTPATH           =
+CLASS_GRAPH            = YES
+COLLABORATION_GRAPH    = YES
+GROUP_GRAPHS           = YES
+UML_LOOK               = NO
+UML_LIMIT_NUM_FIELDS   = 10
+TEMPLATE_RELATIONS     = NO
+INCLUDE_GRAPH          = NO
+INCLUDED_BY_GRAPH      = NO
+CALL_GRAPH             = NO
+CALLER_GRAPH           = NO
+GRAPHICAL_HIERARCHY    = NO
+DIRECTORY_GRAPH        = NO
+DOT_IMAGE_FORMAT       = png
+INTERACTIVE_SVG        = NO
+DOT_PATH               =
+DOTFILE_DIRS           =
+MSCFILE_DIRS           =
+DOT_GRAPH_MAX_NODES    = 50
+MAX_DOT_GRAPH_DEPTH    = 0
+DOT_TRANSPARENT        = NO
+DOT_MULTI_TARGETS      = YES
+GENERATE_LEGEND        = NO
+DOT_CLEANUP            = YES
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}
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644 (file)
index 0000000..fec3e6c
--- /dev/null
@@ -0,0 +1,20 @@
+
+Welcome to notmuch's documentation!
+===================================
+
+Content
+-------
+
+.. toctree::
+   :titlesonly:
+
+   command-line
+   queries
+   notmuch-emacs
+   python-bindings
+   
+Index and search
+-----------------
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/doc/man1/notmuch-address.rst b/doc/man1/notmuch-address.rst
new file mode 100644 (file)
index 0000000..c70dde3
--- /dev/null
@@ -0,0 +1,141 @@
+.. _notmuch-address(1):
+
+===============
+notmuch-address
+===============
+
+SYNOPSIS
+========
+
+**notmuch** **address** [*option* ...] <*search-term*> ...
+
+DESCRIPTION
+===========
+
+Search for messages matching the given search terms, and display the
+addresses from them. Duplicate addresses are filtered out.
+
+See :any:`notmuch-search-terms(7)` for details of the supported syntax for
+<search-terms>.
+
+Supported options for **address** include
+
+.. 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
+===========
+
+This command supports the following special exit status codes
+
+``20``
+    The requested format version is too old.
+
+``21``
+    The requested format version is too new.
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-compact.rst b/doc/man1/notmuch-compact.rst
new file mode 100644 (file)
index 0000000..cb1c858
--- /dev/null
@@ -0,0 +1,56 @@
+.. _notmuch-compact(1):
+
+===============
+notmuch-compact
+===============
+
+SYNOPSIS
+========
+
+**notmuch** **compact** [--quiet] [--backup=<*directory*>]
+
+DESCRIPTION
+===========
+
+The **compact** command can be used to compact the notmuch database.
+This can both reduce the space required by the database and improve
+lookup performance.
+
+The compacted database is built in a temporary directory and is later
+moved into the place of the origin database. The original uncompacted
+database is discarded, unless the ``--backup=``\ <directory> option is
+used.
+
+Note that the database write lock will be held during the compaction
+process (which may be quite long) to protect data integrity.
+
+Supported options for **compact** include
+
+.. program:: compact
+
+.. option:: --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.
+
+.. option:: --quiet
+
+   Do not report database compaction progress to stdout.
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst
new file mode 100644 (file)
index 0000000..bd34afa
--- /dev/null
@@ -0,0 +1,385 @@
+.. _notmuch-config(1):
+
+==============
+notmuch-config
+==============
+
+SYNOPSIS
+========
+
+**notmuch** **config** **get** <*section*>.<*item*>
+
+**notmuch** **config** **set** [--database] <*section*>.<*item*> [*value* ...]
+
+**notmuch** **config** **list**
+
+DESCRIPTION
+===========
+
+The **config** command can be used to get or set settings in the notmuch
+configuration file and corresponding database.
+
+.. program:: config
+
+.. option:: 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.
+
+.. option:: 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.
+
+   If no values are provided, the specified configuration item will
+   be removed from the configuration file.
+
+   With the `--database` option, updates configuration metadata
+   stored in the database, rather than the default (text)
+   configuration file.
+
+.. option:: list
+
+   Every configuration item is printed to stdout, each on a separate
+   line of the form::
+
+     section.item=value
+
+   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: A sibling directory of the Xapian database called
+    `backups`.
+
+.. nmconfig:: database.hook_dir
+
+    Directory containing hooks run by notmuch commands. See
+    :any:`notmuch-hooks(5)`.
+
+    History: this configuration value was introduced in notmuch 0.32.
+
+    Default: See HOOKS, below.
+
+.. nmconfig:: 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.
+
+    History: this configuration value was introduced in notmuch 0.32.
+
+    Default: For compatibility with older configurations, the value of
+    database.path is used if :nmconfig:`database.mail_root` is unset.
+
+.. nmconfig:: database.path
+
+    Notmuch will store its database here, (in
+    sub-directory named ``.notmuch`` if :nmconfig:`database.mail_root`
+    is unset).
+
+    Default: see :ref:`database`
+
+.. nmconfig:: git.path
+
+    Default location for git repository for :any:`notmuch-git`.
+
+.. nmconfig:: git.safe_fraction
+
+   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".
+
+.. nmconfig:: git.tag_prefix
+
+    Default tag prefix (filter) for :any:`notmuch-git`.
+
+.. nmconfig:: index.as_text
+
+   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.
+
+   History: This configuration value was introduced in notmuch 0.38.
+
+.. nmconfig:: index.decrypt
+
+    Policy for decrypting encrypted messages during indexing.  Must be
+    one of: ``false``, ``auto``, ``nostash``, or ``true``.
+
+    When indexing an encrypted e-mail message, if this variable is set
+    to ``true``, notmuch will try to decrypt the message and index the
+    cleartext, stashing a copy of any discovered session keys for the
+    message.  If ``auto``, it will try to index the cleartext if a
+    stashed session key is already known for the message (e.g. from a
+    previous copy), but will not try to access your secret keys.  Use
+    ``false`` to avoid decrypting even when a stashed session key is
+    already present.
+
+    ``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 :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.
+
+    Here is a table that summarizes the functionality of each of these
+    policies:
+
+    +------------------------+-------+------+---------+------+
+    |                        | false | auto | nostash | true |
+    +========================+=======+======+=========+======+
+    | Index cleartext using  |       |  X   |    X    |  X   |
+    | stashed session keys   |       |      |         |      |
+    +------------------------+-------+------+---------+------+
+    | Index cleartext        |       |      |    X    |  X   |
+    | using secret keys      |       |      |         |      |
+    +------------------------+-------+------+---------+------+
+    | Stash session keys     |       |      |         |  X   |
+    +------------------------+-------+------+---------+------+
+    | Delete stashed session |   X   |      |         |      |
+    | keys on reindex        |       |      |         |      |
+    +------------------------+-------+------+---------+------+
+
+    Stashed session keys are kept in the database as properties
+    associated with the message.  See ``session-key`` in
+    :any:`notmuch-properties(7)` for more details about how they can be
+    useful.
+
+    Be aware that the notmuch 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
+    ``index.decrypt=true`` or ``index.decrypt=nostash`` without
+    considering the security of your index.
+
+    Default: ``auto``.
+
+.. _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 :any:`notmuch-search-terms(7)` for a list of existing
+    prefixes, and an explanation of probabilistic prefixes.
+
+.. 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>
+
+    Expansion for named query called <name>. See
+    :any:`notmuch-search-terms(7)` for more information about named
+    queries.
+
+.. nmconfig:: search.exclude_tags
+
+    A list of tags that will be excluded from search results by
+    default. Using an excluded tag in a query will override that
+    exclusion.
+
+    Default: empty list. Note that :any:`notmuch-setup(1)` puts
+    ``deleted;spam`` here when creating new configuration file.
+
+.. nmconfig:: show.extra_headers
+
+    By default :any:`notmuch-show(1)` includes the following headers
+    in structured output if they are present in the message:
+    `Subject`, `From`, `To`, `Cc`, `Bcc`, `Reply-To`, `Date`. This
+    option allows the specification of a list of further
+    headers to output.
+
+    History: This configuration value was introduced in notmuch 0.35.
+
+    Default: empty list.
+
+.. nmconfig:: squery.<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
+-------------
+
+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.
+
+4. ``$HOME/.notmuch-config.<profile>`` where ``<profile>`` is defined
+   by :envvar:`NOTMUCH_PROFILE` environment variable if set,
+   ``$HOME/.notmuch-config`` otherwise.
+
+.. _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
+-----
+
+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.
+
+3. ``<database.path>/.notmuch/hooks``
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-count.rst b/doc/man1/notmuch-count.rst
new file mode 100644 (file)
index 0000000..4c9c9a1
--- /dev/null
@@ -0,0 +1,81 @@
+.. _notmuch-count(1):
+
+=============
+notmuch-count
+=============
+
+SYNOPSIS
+========
+
+**notmuch** **count** [*option* ...] <*search-term*> ...
+
+DESCRIPTION
+===========
+
+Count messages matching the search terms.
+
+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 :any:`notmuch-search-terms(7)` for details of the supported syntax for
+<search-terms>.
+
+Supported options for **count** include
+
+.. 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.
+
+.. option:: --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.
+
+.. option:: --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>
+
+   Read input from given file, instead of from stdin. Implies
+   ``--batch``.
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-dump.rst b/doc/man1/notmuch-dump.rst
new file mode 100644 (file)
index 0000000..a7ca39d
--- /dev/null
@@ -0,0 +1,123 @@
+.. _notmuch-dump(1):
+
+============
+notmuch-dump
+============
+
+SYNOPSIS
+========
+
+**notmuch** **dump** [--gzip] [--format=(batch-tag|sup)] [--output=<*file*>] [--] [<*search-term*> ...]
+
+DESCRIPTION
+===========
+
+Dump tags for messages matching the given search terms.
+
+Output is to the given filename, if any, or to stdout.
+
+These tags are the only data in the notmuch database that can't be
+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 :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
+
+.. 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
+========
+
+: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)`
diff --git a/doc/man1/notmuch-emacs-mua.rst b/doc/man1/notmuch-emacs-mua.rst
new file mode 100644 (file)
index 0000000..d8af08b
--- /dev/null
@@ -0,0 +1,102 @@
+.. _notmuch-emacs-mua(1):
+
+=================
+notmuch-emacs-mua
+=================
+
+SYNOPSIS
+========
+
+**notmuch** **emacs-mua** [options ...] [<to-address> ... | <mailto-url>]
+
+DESCRIPTION
+===========
+
+Start composing an email in the Notmuch Emacs UI with the specified
+subject, recipients, and message body, or mailto: URL.
+
+Supported options for **emacs-mua** include
+
+.. 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>
+
+   Specify a carbon-copy (Cc) recipient.
+
+.. option:: -b, --bcc=<bcc-address>
+
+   Specify a blind-carbon-copy (Bcc) recipient.
+
+.. option:: -i, --body=<file>
+
+   Specify a file to include into the body of the message.
+
+.. option:: --hello
+
+   Go to the Notmuch hello screen instead of the message composition
+   window if no message composition parameters are given.
+
+.. option:: --no-window-system
+
+   Even if a window system is available, use the current terminal.
+
+.. option:: --client
+
+   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 :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
+=====================
+
+.. envvar:: EMACS
+
+   Name of emacs command to invoke. Defaults to "emacs".
+
+.. envvar:: EMACSCLIENT
+
+   Name of emacsclient command to invoke. Defaults to "emacsclient".
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-insert.rst b/doc/man1/notmuch-insert.rst
new file mode 100644 (file)
index 0000000..e05bd0b
--- /dev/null
@@ -0,0 +1,133 @@
+.. _notmuch-insert(1):
+
+==============
+notmuch-insert
+==============
+
+SYNOPSIS
+========
+
+**notmuch** **insert** [option ...] [+<*tag*>|-<*tag*> ...]
+
+DESCRIPTION
+===========
+
+**notmuch insert** reads a message from standard input and delivers it
+into the maildir directory given by configuration option
+:nmconfig:`database.mail_root`, then incorporates the message into the notmuch
+database. It is an alternative to using a separate tool to deliver the
+message then running :any:`notmuch-new(1)` afterwards.
+
+The new message will be tagged with the tags specified by the
+:nmconfig:`new.tags` configuration option, then by operations specified on the
+command-line: tags prefixed by '+' are added while those prefixed by '-'
+are removed.
+
+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 :any:`notmuch-hooks(5)` for
+more details on hooks.
+
+Option arguments must appear before any tag operation arguments.
+Supported options for **insert** include
+
+.. 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
+===========
+
+This command returns exit status 0 on successful mail delivery,
+non-zero otherwise. The default is to indicate failed mail delivery on
+any errors, including message file delivery to the filesystem, message
+indexing to Notmuch database, changing tags, and synchronizing tags to
+maildir flags. The ``--keep`` option may be used to settle for
+successful message file delivery.
+
+This command supports the following special exit status code for
+errors most likely to be temporary in nature, e.g. failure to get a
+database write lock.
+
+``75 (EX_TEMPFAIL)``
+    A temporary failure occurred; the user is invited to retry.
+
+The exit status of the **post-insert** hook does not affect the exit
+status of the **insert** command.
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-new.rst b/doc/man1/notmuch-new.rst
new file mode 100644 (file)
index 0000000..f0ed8eb
--- /dev/null
@@ -0,0 +1,113 @@
+.. _notmuch-new(1):
+
+===========
+notmuch-new
+===========
+
+SYNOPSIS
+========
+
+**notmuch** **new** [options]
+
+DESCRIPTION
+===========
+
+Find and import any new messages to the database.
+
+The **new** command scans all sub-directories of the database,
+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
+: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
+: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
+:any:`notmuch-config(1)` for details.
+
+The **new** command supports hooks. See :any:`notmuch-hooks(5)` for more
+details on hooks.
+
+Supported options for **new** include
+
+.. 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.
+
+   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.
+
+   See also ``index.decrypt`` in :any:`notmuch-config(1)`.
+
+.. option:: --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.
+
+CONFIGURATION
+=============
+
+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
+===========
+
+This command supports the following special exit status code
+
+``75 (EX_TEMPFAIL)``
+    A temporary failure occurred; the user is invited to retry.
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-reindex.rst b/doc/man1/notmuch-reindex.rst
new file mode 100644 (file)
index 0000000..85dad24
--- /dev/null
@@ -0,0 +1,105 @@
+.. _notmuch-reindex(1):
+
+===============
+notmuch-reindex
+===============
+
+SYNOPSIS
+========
+
+**notmuch** **reindex** [*option* ...] <*search-term*> ...
+
+DESCRIPTION
+===========
+
+Re-index all messages matching the search terms.
+
+See :any:`notmuch-search-terms(7)` for details of the supported syntax for
+<*search-term*\ >.
+
+The **reindex** command searches for all messages matching the
+supplied search terms, and re-creates the full-text index on these
+messages using the supplied options.
+
+Supported options for **reindex** include
+
+.. 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.
+
+   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.
+
+   See also ``index.decrypt`` in :any:`notmuch-config(1)`.
+
+EXAMPLES
+========
+
+A user just received an encrypted message without indexing its
+cleartext.  After reading it (via ``notmuch show --decrypt=true``),
+they decide that they want to index its cleartext so that they can
+easily find it later and read it without having to have access to
+their secret keys:
+
+::
+
+ notmuch reindex --decrypt=true id:1234567@example.com
+
+A user wants to change their policy going forward to start indexing
+cleartext.  But they also want indexed access to the cleartext of all
+previously-received encrypted messages.  Some messages might have
+already been indexed in the clear (as in the example above). They can
+ask notmuch to just reindex the not-yet-indexed messages:
+
+::
+
+  notmuch config set index.decrypt true
+  notmuch reindex tag:encrypted and not property:index.decryption=success
+
+Later, the user changes their mind, and wants to stop indexing
+cleartext (perhaps their threat model has changed, or their trust in
+their index store has been shaken).  They also want to clear all of
+their old cleartext from the index.  Note that they compact the
+database afterward as a workaround for
+https://trac.xapian.org/ticket/742:
+
+::
+
+  notmuch config set index.decrypt false
+  notmuch reindex property:index.decryption=success
+  notmuch compact
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-reply.rst b/doc/man1/notmuch-reply.rst
new file mode 100644 (file)
index 0000000..4f39a95
--- /dev/null
@@ -0,0 +1,143 @@
+.. _notmuch-reply(1):
+
+=============
+notmuch-reply
+=============
+
+SYNOPSIS
+========
+
+**notmuch** **reply** [option ...] <*search-term*> ...
+
+DESCRIPTION
+===========
+
+Constructs a reply template for a set of messages.
+
+To make replying to email easier, **notmuch reply** takes an existing
+set of messages and constructs a suitable mail template. Its To:
+address is set according to the original email in this way: if the
+Reply-to: header is present and different from any To:/Cc: address it
+is used, otherwise From: header is used. Unless
+``--reply-to=sender`` is specified, values from the To: and Cc: headers
+are copied, but not including any of the current user's email addresses
+(as configured in primary\_mail or other\_email in the .notmuch-config
+file) in the recipient list.
+
+It also builds a suitable new subject, including Re: at the front (if
+not already present), and adding the message IDs of the messages being
+replied to to the References list and setting the In-Reply-To: field
+correctly.
+
+Finally, the original contents of the emails are quoted by prefixing
+each line with '> ' and included in the body.
+
+The resulting message template is output to stdout.
+
+Supported options for **reply** include
+
+.. program:: reply
+
+.. option:: --duplicate=N
+
+   Reply to duplicate number N. The numbering starts from 1, and
+   matches the order used by :option:`show --duplicate` and
+   :option:`search --output=files <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.
+
+   headers-only
+     Only produces In-Reply-To, References, To, Cc, and Bcc
+     headers.
+
+.. 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:: --reply-to=(all|sender)
+
+   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.
+
+.. 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 ``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.
+
+   Non-automatic decryption expects a functioning
+   :manpage:`gpg-agent(1)` to provide any needed credentials. Without
+   one, the decryption will likely fail.
+
+   Default: ``auto``
+
+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
+matching a single message, (such as id:<message-id>), but it can be
+useful to reply to several messages at once. For example, when a series
+of patches are sent in a single thread, replying to the entire thread
+allows for the reply to comment on issues found in multiple patches. The
+default format supports replying to multiple messages at once, but the
+JSON and S-Expression formats do not.
+
+EXIT STATUS
+===========
+
+This command supports the following special exit status codes
+
+``20``
+    The requested format version is too old.
+
+``21``
+    The requested format version is too new.
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-restore.rst b/doc/man1/notmuch-restore.rst
new file mode 100644 (file)
index 0000000..ac6b424
--- /dev/null
@@ -0,0 +1,107 @@
+.. _notmuch-restore(1):
+
+===============
+notmuch-restore
+===============
+
+SYNOPSIS
+========
+
+**notmuch** **restore** [--accumulate] [--format=(auto|batch-tag|sup)] [--input=<*filename*>]
+
+DESCRIPTION
+===========
+
+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
+
+.. 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
+: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
+========
+
+: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)`
diff --git a/doc/man1/notmuch-search.rst b/doc/man1/notmuch-search.rst
new file mode 100644 (file)
index 0000000..b87737e
--- /dev/null
@@ -0,0 +1,191 @@
+.. _notmuch-search(1):
+
+==============
+notmuch-search
+==============
+
+SYNOPSIS
+========
+
+**notmuch** **search** [*option* ...] <*search-term*> ...
+
+DESCRIPTION
+===========
+
+Search for messages matching the given search terms, and display as
+results the threads containing the matched messages.
+
+The output consists of one line per thread, giving a thread ID, the date
+of the newest (or oldest, depending on the sort option) matched message
+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 :any:`notmuch-search-terms(7)` for details of the supported syntax for
+<search-terms>.
+
+Supported options for **search** include
+
+.. 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
+=======
+
+The following shows an example of the summary output format, with one
+message having multiple filenames.
+
+::
+
+  % notmuch search date:today.. and tag:bad-news
+  thread:0000000000063c10 Today [1/1] Some Persun; To the bone (bad-news inbox unread)
+  thread:0000000000063c25 Today [1/1(2)] Ann Other; Bears (bad-news inbox unread)
+  thread:0000000000063c00 Today [1/1] A Thurd; Bites, stings, sad feelings (bad-news unread)
+
+EXIT STATUS
+===========
+
+This command supports the following special exit status codes
+
+``20``
+    The requested format version is too old.
+
+``21``
+    The requested format version is too new.
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-show.rst b/doc/man1/notmuch-show.rst
new file mode 100644 (file)
index 0000000..c13d94d
--- /dev/null
@@ -0,0 +1,271 @@
+.. _notmuch-show(1):
+
+============
+notmuch-show
+============
+
+SYNOPSIS
+========
+
+**notmuch** **show** [*option* ...] <*search-term*> ...
+
+DESCRIPTION
+===========
+
+Shows all messages matching the search terms.
+
+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
+replies to a particular message will appear immediately after that
+message in date order). The output is not indented by default, but depth
+tags are printed so that proper indentation can be performed by a
+post-processor (such as the emacs interface to notmuch).
+
+Supported options for **show** include
+
+.. program:: show
+
+.. option:: --duplicate=N
+
+   Output duplicate number N. The numbering starts from 1, and matches
+   the order used by :option:`search --duplicate` and
+   :option:`search --output=files <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
+===========
+
+This command supports the following special exit status codes
+
+``20``
+    The requested format version is too old.
+
+``21``
+    The requested format version is too new.
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man1/notmuch-tag.rst b/doc/man1/notmuch-tag.rst
new file mode 100644 (file)
index 0000000..ae311a2
--- /dev/null
@@ -0,0 +1,121 @@
+.. _notmuch-tag(1):
+
+===========
+notmuch-tag
+===========
+
+SYNOPSIS
+========
+
+**notmuch** **tag** [options ...] +<*tag*>|-<*tag*> [--] <*search-term*> ...
+
+**notmuch** **tag** **--batch** [--input=<*filename*>]
+
+DESCRIPTION
+===========
+
+Add/remove tags for all messages matching the search terms.
+
+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.
+For each message, tag changes are applied in the order they appear on
+the command line.
+
+The beginning of the search terms is recognized by the first argument
+that begins with neither '+' nor '-'. Support for an initial search term
+beginning with '+' or '-' is provided by allowing the user to specify a
+"--" argument to separate the tags from the search terms.
+
+**notmuch tag** updates the maildir flags according to tag changes if
+the **maildir.synchronize\_flags** configuration option is enabled. See
+:any:`notmuch-config(1)` for details.
+
+Supported options for **tag** include
+
+.. 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.
+
+.. option:: --input=<filename>
+
+   Read input from given file, instead of from stdin. Implies
+   ``--batch``.
+
+TAG FILE FORMAT
+===============
+
+The input must consist of lines of the format:
+
++<*tag*\ >\|-<*tag*\ > [...] [--] <*query*\ >
+
+Each line is interpreted similarly to **notmuch tag** command line
+arguments. The delimiter is one or more spaces ' '. Any characters in
+<*tag*\ > **may** be hex-encoded with %NN where NN is the hexadecimal
+value of the character. To hex-encode a character with a multi-byte
+UTF-8 encoding, hex-encode each byte. Any spaces in <tag> **must** be
+hex-encoded as %20. Any characters that are not part of <*tag*\ > **must
+not** be hex-encoded.
+
+In the future tag:"tag with spaces" style quoting may be supported for
+<*tag*\ > as well; for this reason all double quote characters in
+<*tag*\ > **should** be hex-encoded.
+
+The <*query*\ > should be quoted using Xapian boolean term quoting
+rules: if a term contains whitespace or a close paren or starts with a
+double quote, it must be enclosed in double quotes (not including any
+prefix) and double quotes inside the term must be doubled (see below for
+examples).
+
+Leading and trailing space ' ' is ignored. Empty lines and lines
+beginning with '#' are ignored.
+
+EXAMPLE
+-------
+
+The following shows a valid input to batch tagging. Note that only the
+isolated '\*' acts as a wildcard. Also note the two different quotings
+of the tag **space in tags**
+
+::
+
+    +winner *
+    +foo::bar%25 -- (One and Two) or (One and tag:winner)
+    +found::it -- tag:foo::bar%
+    # ignore this line and the next
+
+    +space%20in%20tags -- Two
+    # add tag '(tags)', among other stunts.
+    +crazy{ +(tags) +&are +#possible\ -- tag:"space in tags"
+    +match*crazy -- tag:crazy{
+    +some_tag -- id:"this is ""nauty)"""
+
+SEE ALSO
+========
+
+: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)`,
diff --git a/doc/man1/notmuch.rst b/doc/man1/notmuch.rst
new file mode 100644 (file)
index 0000000..c488f12
--- /dev/null
@@ -0,0 +1,237 @@
+.. _notmuch(1):
+.. _notmuch-setup(1):
+
+=======
+notmuch
+=======
+
+SYNOPSIS
+========
+
+**notmuch** [option ...] **command** [arg ...]
+
+DESCRIPTION
+===========
+
+Notmuch is a command-line based program for indexing, searching,
+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
+: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
+you through the process of indexing your mail.
+
+NOTE
+====
+
+While the command-line program ``notmuch`` provides powerful
+functionality, it does not provide the most convenient interface for
+that functionality. More sophisticated interfaces are expected to be
+built on top of either the command-line interface, or more likely, on
+top of the notmuch library interface. See https://notmuchmail.org for
+more about alternate interfaces to notmuch. The emacs-based interface to
+notmuch (available under **emacs/** in the Notmuch source distribution)
+is probably the most widely used at this time.
+
+OPTIONS
+=======
+
+Supported global options for ``notmuch`` include
+
+.. 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
+
+   Print the installed version of notmuch, and exit.
+
+.. option:: --config=FILE
+
+   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
+to ``notmuch --uuid=HEX subcommand``.
+
+COMMANDS
+========
+
+SETUP
+-----
+
+The **notmuch setup** command is used to configure Notmuch for first
+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 :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
+configuration.
+
+The mail directory you specify can contain any number of sub-directories
+and should primarily contain only files with individual email messages
+(eg. maildir or mh archives are perfect). If there are other, non-email
+files (such as indexes maintained by other email programs) then notmuch
+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 :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.
+
+OTHER COMMANDS
+--------------
+
+Several of the notmuch commands accept search terms with a common
+syntax. See :any:`notmuch-search-terms(7)` for more details on the
+supported syntax.
+
+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 :any:`notmuch-reply(1)` command is useful for preparing a template
+for an email reply.
+
+The :any:`notmuch-tag(1)` command is the only command available for
+manipulating database contents.
+
+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 :any:`notmuch-config(1)` command can be used to get or set
+settings in the notmuch configuration file.
+
+EXTERNAL COMMANDS
+-----------------
+
+If the given command is not known to notmuch, notmuch tries to execute
+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.  The environment variable
+:envvar:`NOTMUCH_CONFIG` will be set according to :option:`--config`,
+if the latter is present.
+
+OPTION SYNTAX
+-------------
+
+All options accepting an argument can be used with '=' or ':' as a
+separator. Except for boolean options (which would be ambiguous), a
+space can also be used as a separator. The following are all
+equivalent:
+
+::
+
+   notmuch --config=alt-config config get user.name
+   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.
+
+.. 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.
+
+.. envvar:: NOTMUCH_DEBUG_QUERY
+
+   If set to a non-empty value, the notmuch library will print (to
+   stderr) Xapian queries it constructs.
+
+SEE ALSO
+========
+
+: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**
+
+CONTACT
+=======
+
+Feel free to send questions, comments, or kudos to the notmuch mailing
+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.libera.chat, channel: #notmuch).
diff --git a/doc/man5/notmuch-hooks.rst b/doc/man5/notmuch-hooks.rst
new file mode 100644 (file)
index 0000000..d778bdb
--- /dev/null
@@ -0,0 +1,64 @@
+.. _notmuch-hooks(5):
+
+=============
+notmuch-hooks
+=============
+
+SYNOPSIS
+========
+
+<hook_dir>/{pre-new, post-new, post-insert}
+
+DESCRIPTION
+===========
+
+Hooks are scripts (or arbitrary executables or symlinks to such) that
+notmuch invokes before and after certain actions. These scripts reside
+in 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 :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 :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 :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.
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man7/notmuch-properties.rst b/doc/man7/notmuch-properties.rst
new file mode 100644 (file)
index 0000000..ff79f4c
--- /dev/null
@@ -0,0 +1,148 @@
+.. _notmuch-properties(7):
+
+==================
+notmuch-properties
+==================
+
+SYNOPSIS
+========
+
+**notmuch** **count** **property:**\ <*key*>=<*value*>
+
+**notmuch** **search** **property:**\ <*key*>=<*value*>
+
+**notmuch** **show** **property:**\ <*key*>=<*value*>
+
+**notmuch** **reindex** **property:**\ <*key*>=<*value*>
+
+**notmuch** **tag** +<*tag*> **property:**\ <*key*>=<*value*>
+
+
+**notmuch** **dump** **--include=properties**
+
+**notmuch** **restore** **--include=properties**
+
+DESCRIPTION
+===========
+
+Several notmuch commands can search for, modify, add or remove
+properties associated with specific messages.  Properties are
+key/value pairs, and a message can have more than one key/value pair
+for the same key.
+
+While users can select based on a specific property in their search
+terms with the prefix **property:**, the notmuch command-line
+interface does not provide mechanisms for modifying properties
+directly to the user.
+
+Instead, message properties are expected to be set and used
+programmatically, according to logic in notmuch itself, or in
+extensions to it.
+
+Extensions to notmuch which make use of properties are encouraged to
+report the specific properties used to the upstream notmuch project,
+as a way of avoiding collisions in the property namespace.
+
+CONVENTIONS
+===========
+
+Any property with a key that starts with "index." will be removed (and
+possibly re-set) upon reindexing (see :any:`notmuch-reindex(1)`).
+
+MESSAGE PROPERTIES
+==================
+
+The following properties are set by notmuch internally in the course
+of its normal activity.
+
+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
+    indexed.  If notmuch attempts to decrypt any part of a message
+    during indexing and that decryption attempt fails, it will add the
+    property ``index.decryption=failure`` to the message.
+
+    Note that it's possible for a single message to have both
+    ``index.decryption=success`` and ``index.decryption=failure``.
+    Consider an encrypted e-mail message that contains another
+    encrypted e-mail message as an attachment -- if the outer message
+    can be decrypted, but the attached part cannot, then both
+    properties will be set on the message as a whole.
+
+    If notmuch never tried to decrypt an encrypted message during
+    indexing (which is the default, see ``index.decrypt`` in
+    :any:`notmuch-config(1)`), then this property will not be set on that
+    message.
+
+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``.
+
+    Using a stashed session key with "notmuch show" will speed up
+    rendering of long encrypted threads.  It also allows the user to
+    destroy the secret part of any expired encryption-capable subkey
+    while still being able to read any retained messages for which
+    they have stashed the session key.  This enables truly deletable
+    e-mail, since (once the session key and asymmetric subkey are both
+    destroyed) there are no keys left that can be used to decrypt any
+    copy of the original message previously stored by an adversary.
+
+    However, access to the stashed session key for an encrypted message
+    permits full byte-for-byte reconstruction of the cleartext
+    message.  This includes attachments, cryptographic signatures, and
+    other material that cannot be reconstructed from the index alone.
+
+    See ``index.decrypt`` in :any:`notmuch-config(1)` for more
+    details about how to set notmuch's policy on when to store session
+    keys.
+
+    The session key should be in the ASCII text form produced by
+    GnuPG.  For OpenPGP, that consists of a decimal representation of
+    the hash algorithm used (identified by number from RFC 4880,
+    e.g. 9 means AES-256) followed by a colon, followed by a
+    hexadecimal representation of the algorithm-specific key.  For
+    example, an AES-128 key might be stashed in a notmuch property as:
+    ``session-key=7:14B16AF65536C28AF209828DFE34C9E0``.
+
+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
+    knows how to both detect and repair such a problematic message, it
+    will do so during indexing.
+
+    If it applies a message repair during indexing, it will use the
+    ``index.repaired`` property to note the type of repair(s) it
+    performed.
+
+    ``index.repaired=skip-protected-headers-legacy-display`` indicates
+    that when indexing the cleartext of an encrypted message, notmuch
+    skipped over a "legacy-display" text/rfc822-headers part that it
+    found in that message, since it was able to index the built-in
+    protected headers directly.
+
+    ``index.repaired=mixedup`` indicates the repair of a "Mixed Up"
+    encrypted PGP/MIME message, a mangling typically produced by
+    Microsoft's Exchange MTA.  See
+    https://tools.ietf.org/html/draft-dkg-openpgp-pgpmime-message-mangling
+    for more information.
+
+SEE ALSO
+========
+
+: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)`
diff --git a/doc/man7/notmuch-search-terms.rst b/doc/man7/notmuch-search-terms.rst
new file mode 100644 (file)
index 0000000..acc1c96
--- /dev/null
@@ -0,0 +1,481 @@
+.. _notmuch-search-terms(7):
+
+====================
+notmuch-search-terms
+====================
+
+SYNOPSIS
+========
+
+**notmuch** **count** [option ...] <*search-term*> ...
+
+**notmuch** **dump** [--gzip] [--format=(batch-tag|sup)] [--output=<*file*>] [--] [<*search-term*> ...]
+
+**notmuch** **reindex** [option ...] <*search-term*> ...
+
+**notmuch** **search** [option ...] <*search-term*> ...
+
+**notmuch** **show** [option ...] <*search-term*> ...
+
+**notmuch** **tag** +<*tag*> ... -<*tag*> [--] <*search-term*> ...
+
+DESCRIPTION
+===========
+
+Several notmuch commands accept a common syntax for search terms.
+
+The search terms can consist of free-form text (and quoted phrases)
+which will match all messages that contain all of the given
+terms/phrases in the body, the subject, or any of the sender or
+recipient headers.
+
+As a special case, a search string consisting of exactly a single
+asterisk ("\*") will match all messages.
+
+Search prefixes
+---------------
+
+In addition to free text, the following prefixes can be used to force
+terms to match against specific portions of an email, (where <brackets>
+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
+:manpage:`regex(7)`) delimited with //, for example::
+
+   notmuch search 'from:"/bob@.*[.]example[.]com/"'
+
+body:<word-or-quoted-phrase>
+    Match terms in the body of messages.
+
+from:<name-or-address> or from:/<regex>/
+    The **from:** prefix is used to match the name or address of
+    the sender of an email message.
+
+to:<name-or-address>
+    The **to:** prefix is used to match the names or addresses of any
+    recipient of an email message, (whether To, Cc, or Bcc).
+
+subject:<word-or-quoted-phrase> or subject:/<regex>/
+    Any term prefixed with **subject:** will match only text from the
+    subject of an email. Searching for a phrase in the subject is
+    supported by including quotation marks around the phrase,
+    immediately following **subject:**.
+
+attachment:<word>
+    The **attachment:** prefix can be used to search for specific
+    filenames (or extensions) of attachments to email messages.
+
+mimetype:<word>
+    The **mimetype:** prefix will be used to match text from the
+    content-types of MIME parts within email messages (as specified by
+    the sender).
+
+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
+    :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
+    contents of the Message-ID: header of email messages, but without
+    the '<', '>' delimiters.
+
+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 :any:`notmuch-search(1)`
+
+thread:{<notmuch query>}
+    Threads may be searched for indirectly by providing an arbitrary
+    notmuch query in **{}**. For example, the following returns
+    threads containing a message from mallory and one (not necessarily
+    the same message) with Subject containing the word "crypto".
+
+    ::
+
+       % notmuch search 'thread:"{from:mallory}" and thread:"{subject:crypto}"'
+
+    The performance of such queries can vary wildly. To understand
+    this, the user should think of the query **thread:{<something>}**
+    as expanding to all of the thread IDs which match **<something>**;
+    notmuch then performs a second search using the expanded query.
+
+path:<directory-path> or path:<directory-path>/** or path:/<regex>/
+    The **path:** prefix searches for email messages that are in
+    particular directories within the mail store. The directory must
+    be specified relative to the top-level maildir (and without the
+    leading slash). By default, **path:** matches messages in the
+    specified directory only. The "/\*\*" suffix can be used to match
+    messages in the specified directory and all its subdirectories
+    recursively. **path:""** matches messages in the root of the mail
+    store and, likewise, **path:\*\*** matches all messages.
+
+    **path:** will find a message if *any* copy of that message is in
+    the specific directory.
+
+folder:<maildir-folder> or folder:/<regex>/
+    The **folder:** prefix searches for email messages by maildir or
+    MH folder. For MH-style folders, this is equivalent to
+    **path:**. For maildir, this includes messages in the "new" and
+    "cur" subdirectories. The exact syntax for maildir folders depends
+    on your mail configuration. For maildir++, **folder:""** matches
+    the inbox folder (which is the root in maildir++), other folder
+    names always start with ".", and nested folders are separated by
+    "."s, such as **folder:.classes.topology**. For "file system"
+    maildir, the inbox is typically **folder:INBOX** and nested
+    folders are separated by slashes, such as
+    **folder:classes/topology**.
+
+    **folder:** will find a message if *any* copy of that message is
+    in the specific folder.
+
+date:<since>..<until> or date:<date>
+    The **date:** prefix can be used to restrict the results to only
+    messages within a particular time range (based on the Date:
+    header).
+
+    See **DATE AND TIME SEARCH** below for details on the range
+    expression, and supported syntax for <since> and <until> date and
+    time expressions.
+
+    The time range can also be specified using timestamps without
+    including the date prefix using a syntax of:
+
+    <initial-timestamp>..<final-timestamp>
+
+    Each timestamp is a number representing the number of seconds
+    since 1970-01-01 00:00:00 UTC. Specifying a time range this way
+    is considered legacy and predates the date prefix.
+
+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). 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 :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 :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 :any:`notmuch-config(1)` for
+details.
+
+Operators
+---------
+
+In addition to individual terms, multiple terms can be combined with
+Boolean operators (**and**, **or**, **not**, and **xor**). Each term
+in the query will be implicitly connected by a logical AND if no
+explicit operator is provided (except that terms with a common prefix
+will be implicitly combined with OR).  The shorthand '-<term>' can be
+used for 'not <term>' but unfortunately this does not work at the
+start of an expression.  Parentheses can also be used to control the
+combination of the Boolean operators, but will have to be protected
+from interpretation by the shell, (such as by putting quotation marks
+around any parenthesized expression).
+
+In addition to the standard boolean operators, Xapian provides several
+operators specific to text searching.
+
+::
+
+        notmuch search term1 NEAR term2
+
+will return results where term1 is within 10 words of term2. The
+threshold can be set like this:
+
+::
+
+        notmuch search term1 NEAR/2 term2
+
+The search
+
+::
+
+        notmuch search term1 ADJ term2
+
+will return results where term1 is within 10 words of term2, but in the
+same order as in the query. The threshold can be set the same as with
+NEAR:
+
+::
+
+        notmuch search term1 ADJ/7 term2
+
+
+Stemming
+--------
+
+**Stemming** in notmuch means that these searches
+
+::
+
+        notmuch search detailed
+        notmuch search details
+        notmuch search detail
+
+will all return identical results, because Xapian first "reduces" the
+term to the common stem (here 'detail') and then performs the search.
+
+There are two ways to turn this off: a search for a capitalized word
+will be performed unstemmed, so that one can search for "John" and not
+get results for "Johnson"; phrase searches are also unstemmed (see
+below for details).  Stemming is currently only supported for
+English. Searches for words in other languages will be performed unstemmed.
+
+Wildcards
+---------
+
+It is possible to use a trailing '\*' as a wildcard. A search for
+'wildc\*' will match 'wildcard', 'wildcat', etc.
+
+
+Boolean and Probabilistic Prefixes
+----------------------------------
+
+Xapian (and hence notmuch) prefixes are either **boolean**, supporting
+exact matches like "tag:inbox" or **probabilistic**, supporting a more
+flexible **term** based searching. Certain **special** prefixes are
+processed by notmuch in a way not strictly fitting either of Xapian's
+built in styles. The prefixes currently supported by notmuch are as
+follows.
+
+Boolean
+   **tag:**, **id:**, **thread:**, **folder:**, **path:**, **property:**
+Probabilistic
+  **body:**, **to:**, **attachment:**, **mimetype:**
+Special
+   **from:**, **query:**, **subject:**, **sexp:**
+
+Terms and phrases
+-----------------
+
+In general Xapian distinguishes between lists of terms and
+**phrases**. Phrases are indicated by double quotes (but beware you
+probably need to protect those from your shell) and insist that those
+unstemmed words occur in that order. One useful, but initially
+surprising feature is that the following are equivalent ways to write
+the same phrase.
+
+- "a list of words"
+- a-list-of-words
+- a/list/of/words
+- a.list.of.words
+
+Both parenthesised lists of terms and quoted phrases are ok with
+probabilistic prefixes such as **to:**, **from:**, and **subject:**.
+For prefixes supporting regex search, the parenthesised list should be
+quoted.  In particular
+
+::
+
+   subject:"(pizza free)"
+
+is equivalent to
+
+::
+
+   subject:pizza and subject:free
+
+Both of these will match a subject "Free Delicious Pizza" while
+
+::
+
+   subject:"pizza free"
+
+will not.
+
+.. _quoting:
+
+Quoting
+-------
+
+Double quotes are also used by the notmuch query parser to protect
+boolean terms, regular expressions, or subqueries containing spaces or
+other special characters, e.g.
+
+::
+
+   tag:"a tag"
+
+::
+
+   folder:"/^.*/(Junk|Spam)$/"
+
+::
+
+   thread:"{from:mallory and date:2009}"
+
+As with phrases, you need to protect the double quotes from the shell
+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
+====================
+
+notmuch understands a variety of standard and natural ways of expressing
+dates and times, both in absolute terms ("2012-10-24") and in relative
+terms ("yesterday"). Any number of relative terms can be combined ("1
+hour 25 minutes") and an absolute date/time can be combined with
+relative terms to further adjust it. A non-exhaustive description of the
+syntax supported for absolute and relative terms is given below.
+
+The range expression
+--------------------
+
+date:<since>..<until>
+
+The above expression restricts the results to only messages from <since>
+to <until>, based on the Date: header.
+
+<since> and <until> can describe imprecise times, such as "yesterday".
+In this case, <since> is taken as the earliest time it could describe
+(the beginning of yesterday) and <until> is taken as the latest time it
+could describe (the end of yesterday). Similarly, date:january..february
+matches from the beginning of January to the end of February.
+
+If specifying a time range using timestamps in conjunction with the
+date prefix, each timestamp must be preceded by @ (ASCII hex 40). As
+above, each timestamp is a number representing the number of seconds
+since 1970-01-01 00:00:00 UTC. For example:
+
+    date:@<initial-timestamp>..@<final-timestamp>
+
+Currently, spaces in range expressions are not supported. You can
+replace the spaces with '\_', or (in most cases) '-', or (in some cases)
+leave the spaces out altogether. Examples in this man page use spaces
+for clarity.
+
+Open-ended ranges are supported. I.e. it's possible to specify
+date:..<until> or date:<since>.. to not limit the start or
+end time, respectively.
+
+Single expression
+-----------------
+
+date:<expr> works as a shorthand for date:<expr>..<expr>.
+For example, date:monday matches from the beginning of Monday until
+the end of Monday.
+
+Relative date and time
+----------------------
+
+[N\|number]
+(years\|months\|weeks\|days\|hours\|hrs\|minutes\|mins\|seconds\|secs)
+[...]
+
+All refer to past, can be repeated and will be accumulated.
+
+Units can be abbreviated to any length, with the otherwise ambiguous
+single m being m for minutes and M for months.
+
+Number can also be written out one, two, ..., ten, dozen, hundred.
+Additionally, the unit may be preceded by "last" or "this" (e.g., "last
+week" or "this month").
+
+When combined with absolute date and time, the relative date and time
+specification will be relative from the specified absolute date and
+time.
+
+Examples: 5M2d, two weeks
+
+Supported absolute time formats
+-------------------------------
+
+-  H[H]:MM[:SS] [(am\|a.m.\|pm\|p.m.)]
+
+-  H[H] (am\|a.m.\|pm\|p.m.)
+
+-  HHMMSS
+
+-  now
+
+-  noon
+
+-  midnight
+
+-  Examples: 17:05, 5pm
+
+Supported absolute date formats
+-------------------------------
+
+-  YYYY-MM[-DD]
+
+-  DD-MM[-[YY]YY]
+
+-  MM-YYYY
+
+-  M[M]/D[D][/[YY]YY]
+
+-  M[M]/YYYY
+
+-  D[D].M[M][.[YY]YY]
+
+-  D[D][(st\|nd\|rd\|th)] Mon[thname] [YYYY]
+
+-  Mon[thname] D[D][(st\|nd\|rd\|th)] [YYYY]
+
+-  Wee[kday]
+
+Month names can be abbreviated at three or more characters.
+
+Weekday names can be abbreviated at three or more characters.
+
+Examples: 2012-07-31, 31-07-2012, 7/31/2012, August 3
+
+Time zones
+----------
+
+-  (+\|-)HH:MM
+
+-  (+\|-)HH[MM]
+
+Some time zone codes, e.g. UTC, EET.
+
+SEE ALSO
+========
+
+: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`
diff --git a/doc/notmuch-emacs.rst b/doc/notmuch-emacs.rst
new file mode 100644 (file)
index 0000000..7dff7d6
--- /dev/null
@@ -0,0 +1,785 @@
+.. _notmuch-emacs:
+
+==========================
+Emacs Frontend for Notmuch
+==========================
+
+About this Manual
+=================
+
+This manual covers only the Emacs interface to Notmuch. For information
+on the command line interface, see section “Description” in the Notmuch
+Manual Pages. To save typing, we will sometimes use *notmuch* in this
+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 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
+description in the variables' docstring.
+
+notmuch-hello
+=============
+
+.. index::
+   single: notmuch-hello
+   single: notmuch
+
+``notmuch-hello`` is the main entry point for Notmuch. You can start it
+with ``M-x notmuch`` or ``M-x notmuch-hello``. The startup screen looks
+something like the following. There are some hints at the bottom of the
+screen. There are three main parts to the notmuch-hello screen,
+discussed below. The **bold** text indicates buttons you can click with
+a mouse or by positioning the cursor and pressing ``<return>``
+
+|   Welcome to **notmuch** You have 52 messages.
+|
+| Saved searches: **[edit]**
+|
+|        52 **inbox**           52 **unread**
+|
+| Search: ____________________________________
+|
+| All tags: **[show]**
+|
+|       Hit \`?' for context-sensitive help in any Notmuch screen.
+|                    Customize Notmuch or this page.
+
+You can change the overall appearance of the notmuch-hello screen by
+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
+--------------------------
+
+.. el:define-key:: <tab>
+
+    Move to the next widget (button or text entry field)
+
+.. el:define-key:: <backtab>
+
+    Move to the previous widget.
+
+.. el:define-key:: <return>
+
+    Activate the current widget.
+
+.. el:define-key:: g
+                   =
+
+    Refresh the buffer; mainly update the counts of messages for various
+    saved searches.
+
+.. el:define-key:: G
+
+    Import mail, See :ref:`importing`
+
+.. el:define-key:: m
+
+    Compose a message
+
+.. el:define-key:: s
+
+    Search the notmuch database using :ref:`notmuch-search`
+
+.. el:define-key:: v
+
+    Print notmuch version
+
+.. el:define-key:: q
+
+    Quit
+
+.. _saved-searches:
+
+Saved Searches
+--------------
+
+Since notmuch is entirely search-based, it's often useful to organize
+mail around common searches.  To facilitate this, the first section of
+notmuch-hello presents a customizable set of saved searches.  Saved
+searches can also be accessed from anywhere in notmuch by pressing
+``j`` to access :ref:`notmuch-jump`.
+
+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:
+
+.. el:defcustom:: notmuch-saved-searches
+
+    The list of saved searches, including names, queries, and
+    additional per-query options.
+
+.. 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’.
+
+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 `notmuch-hello-recent-searches-max`.
+
+.. el:defcustom:: notmuch-hello-recent-searches-max
+
+              |docstring::notmuch-hello-recent-searches-max|
+
+Known Tags
+----------
+
+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.
+
+.. el:defcustom:: notmuch-hello-tag-list-make-query
+
+    Control how to construct a search (“virtual folder”) from a given
+    tag.
+
+.. el:defcustom:: notmuch-hello-hide-tags
+
+    Which tags not to display at all.
+
+.. _notmuch-search:
+
+notmuch-search
+==============
+
+``notmuch-search-mode`` is used to display the results from executing
+a query via ``notmuch-search``. The syntax for these queries is the
+the same as :ref:`saved-searches`. For details of this syntax see
+info:notmuch-search-terms
+
+By default the output approximates that of the command line See section
+“Description” in notmuch search command.
+
+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.
+
+.. el:define-key:: n
+   C-n
+   <down>
+
+    Move to next line
+
+.. el:define-key::
+   p
+   C-p
+   <up>
+
+    Move to previous line
+
+.. el:define-key:: <return>
+
+    Open thread on current line in :ref:`notmuch-show` mode
+
+.. el:define-key:: g
+   =
+
+    Refresh the buffer
+
+.. el:define-key:: ?
+
+    Display full set of key bindings
+
+The presentation of results can be controlled by the following
+variables.
+
+.. 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
+
+    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
+============
+
+``notmuch-show-mode`` is used to display a single thread of email from
+your email archives.
+
+By default, various components of email messages, (citations,
+signatures, already-read messages), are hidden. You can make
+these parts visible by clicking with the mouse button or by
+pressing RET after positioning the cursor on a hidden part.
+
+.. el:define-key:: <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).
+
+.. el:define-key:: c
+
+    :ref:`show-copy`
+
+.. el:define-key:: N
+
+    Move to next message
+
+.. el:define-key:: P
+
+    Move to previous message (or start of current message)
+
+.. el:define-key:: n
+
+    Move to next matching message
+
+.. 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; see also :ref:`show-large`.
+
+.. el:defcustom:: notmuch-message-headers
+
+       |docstring::notmuch-message-headers|
+
+.. 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
+-----------------
+
+You can use the usually Emacs ways of copying text to the kill-ring,
+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`.
+
+.. el:define-key:: c F
+   M-x notmuch-show-stash-filename
+
+   |docstring::notmuch-show-stash-filename|
+
+.. el:define-key:: c G
+   M-x notmuch-show-stash-git-send-email
+
+   |docstring::notmuch-show-stash-git-send-email|
+
+.. el:define-key:: c I
+   M-x notmuch-show-stash-message-id-stripped
+
+   |docstring::notmuch-show-stash-message-id-stripped|
+
+.. el:define-key:: c L
+   M-x notmuch-show-stash-mlarchive-link-and-go
+
+   |docstring::notmuch-show-stash-mlarchive-link-and-go|
+
+.. el:define-key:: c T
+   M-x notmuch-show-stash-tags
+
+   |docstring::notmuch-show-stash-tags|
+
+.. el:define-key:: c c
+   M-x notmuch-show-stash-cc
+
+   |docstring::notmuch-show-stash-cc|
+
+.. el:define-key:: c d
+   M-x notmuch-show-stash-date
+
+   |docstring::notmuch-show-stash-date|
+
+.. el:define-key:: c f
+   M-x notmuch-show-stash-from
+
+   |docstring::notmuch-show-stash-from|
+
+.. el:define-key:: c i
+   M-x notmuch-show-stash-message-id
+
+   |docstring::notmuch-show-stash-message-id|
+
+.. el:define-key:: c l
+   M-x notmuch-show-stash-mlarchive-link
+
+   |docstring::notmuch-show-stash-mlarchive-link|
+
+.. el:define-key:: c s
+   M-x notmuch-show-stash-subject
+
+   |docstring::notmuch-show-stash-subject|
+
+.. el:define-key:: c t
+   M-x notmuch-show-stash-to
+
+   |docstring::notmuch-show-stash-to|
+
+.. 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:
+
+notmuch-tree
+============
+
+``notmuch-tree-mode`` displays the results of a "notmuch tree" of your
+email archives. Each line in the buffer represents a single
+message giving the relative date, the author, subject, and any
+tags.
+
+.. el:define-key:: c
+
+    :ref:`show-copy`
+
+.. el:define-key:: <return>
+
+   Displays that message.
+
+.. el:define-key:: N
+
+    Move to next message
+
+.. el:define-key:: P
+
+    Move to previous message
+
+.. el:define-key:: n
+
+    Move to next matching message
+
+.. el:define-key:: p
+
+    Move to previous matching message
+
+.. 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:: 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 most places in notmuch through the
+following key bindings:
+
+.. el:define-key:: j
+
+    Jump to saved searches using :ref:`notmuch-jump`.
+
+.. 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
+------------
+
+Saved searches configured through :ref:`saved-searches` can
+include a "shortcut key" that's accessible through notmuch-jump.
+Pressing ``j`` anywhere in notmuch followed by the configured shortcut
+key of a saved search will immediately jump to that saved search.  For
+example, in the default configuration ``j i`` jumps immediately to the
+inbox search.  When you press ``j``, notmuch-jump shows the saved
+searches and their shortcut keys in the mini-buffer.
+
+.. _notmuch-tag-jump:
+
+notmuch-tag-jump
+----------------
+
+Tagging operations configured through ``notmuch-tagging-keys`` can
+be accessed via :kbd:`k` in :ref:`notmuch-show`,
+:ref:`notmuch-search` and :ref:`notmuch-tree`.  With a
+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.
+
+.. 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
+=================
+
+.. el:define-key:: M-x notmuch-cycle-notmuch-buffers
+
+   |docstring::notmuch-cycle-notmuch-buffers|
+
+Configuration
+=============
+
+.. _importing:
+
+Importing Mail
+--------------
+
+.. el:define-key:: M-x notmuch-poll
+
+   |docstring::notmuch-poll|
+
+.. 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
+---------
+
+When Notmuch is loaded, it will read the ``notmuch-init-file``
+(``~/.emacs.d/notmuch-config`` by default) file. This is normal Emacs Lisp
+file and can be used to avoid cluttering your ``~/.emacs`` with Notmuch
+stuff. If the file with ``.elc``, ``.elc.gz``, ``.el`` or ``.el.gz``
+suffix exist it will be read instead (just one of these, chosen in this
+order). Most often users create ``~/.emacs.d/notmuch-config.el`` and just
+work with it. If Emacs was invoked with the ``-q`` or ``--no-init-file``
+options, ``notmuch-init-file`` is not read.
diff --git a/doc/python-bindings.rst b/doc/python-bindings.rst
new file mode 100644 (file)
index 0000000..e1ad26a
--- /dev/null
@@ -0,0 +1,5 @@
+Python Bindings
+===============
+
+.. automodule:: notmuch2
+   :members:
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
diff --git a/emacs/.gitignore b/emacs/.gitignore
new file mode 100644 (file)
index 0000000..b9873b0
--- /dev/null
@@ -0,0 +1,5 @@
+/.eldeps*
+/*.elc
+/*.rsti
+/notmuch-version.el
+/notmuch-pkg.el
diff --git a/emacs/Makefile b/emacs/Makefile
new file mode 100644 (file)
index 0000000..de492a7
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/emacs/Makefile.local b/emacs/Makefile.local
new file mode 100644 (file)
index 0000000..0f1f0eb
--- /dev/null
@@ -0,0 +1,133 @@
+# -*- makefile-gmake -*-
+
+dir := emacs
+emacs_sources := \
+       $(dir)/notmuch-lib.el \
+       $(dir)/notmuch-compat.el \
+       $(dir)/notmuch-parser.el \
+       $(dir)/notmuch.el \
+       $(dir)/notmuch-query.el \
+       $(dir)/notmuch-show.el \
+       $(dir)/notmuch-tree.el \
+       $(dir)/notmuch-wash.el \
+       $(dir)/notmuch-hello.el \
+       $(dir)/notmuch-mua.el \
+       $(dir)/notmuch-address.el \
+       $(dir)/notmuch-maildir-fcc.el \
+       $(dir)/notmuch-message.el \
+       $(dir)/notmuch-crypto.el \
+       $(dir)/notmuch-tag.el \
+       $(dir)/coolj.el \
+       $(dir)/notmuch-print.el \
+       $(dir)/notmuch-version.el \
+       $(dir)/notmuch-jump.el \
+       $(dir)/notmuch-company.el \
+       $(dir)/notmuch-draft.el
+
+elpa_sources := ${emacs_sources} $(dir)/notmuch-pkg.el
+
+$(dir)/notmuch-version.el: $(dir)/Makefile.local version.stamp
+$(dir)/notmuch-version.el: $(srcdir)/$(dir)/notmuch-version.el.tmpl
+       @sed -e 's/%AG%/Generated file (from $(<F)) -- do not edit!/' \
+            -e 's/%VERSION%/"$(VERSION)"/' $< > $@
+
+$(dir)/notmuch-pkg.el: $(srcdir)/$(dir)/notmuch-pkg.el.tmpl
+       @sed -e 's/%AG%/Generated file (from $(<F)) -- do not edit!/' \
+            -e 's/%VERSION%/"$(ELPA_VERSION)"/' $< > $@
+
+all: $(dir)/notmuch-pkg.el
+install-emacs: $(dir)/notmuch-pkg.el
+
+emacs_mua := $(dir)/notmuch-emacs-mua
+emacs_mua_desktop := $(dir)/notmuch-emacs-mua.desktop
+
+emacs_images := \
+       $(srcdir)/$(dir)/notmuch-logo.svg
+
+emacs_bytecode = $(emacs_sources:.el=.elc)
+emacs_docstrings = $(emacs_sources:.el=.rsti)
+
+ifneq ($(HAVE_SPHINX)$(WITH_EMACS),11)
+docstring.stamp:
+       @echo "Missing prerequisites, not collecting docstrings"
+else
+docstring.stamp: ${emacs_docstrings}
+       touch $@
+endif
+
+# Because of defmacro's and defsubst's, we have to account for load
+# dependencies between Elisp files when byte compiling.  Otherwise,
+# the byte compiler may load an old .elc file when processing a
+# "require" or we may fail to rebuild a .elc that depended on a macro
+# from an updated file.
+ifeq ($(WITH_EMACS),1)
+$(dir)/.eldeps: $(dir)/Makefile.local $(dir)/make-deps.el $(emacs_sources)
+       $(call quiet,EMACS) --directory emacs -batch -l make-deps.el \
+               -f batch-make-deps $(emacs_sources) > $@.tmp && \
+               mv $@.tmp $@
+# We could include .eldeps directly, but that would cause a make
+# restart whenever any .el file was modified, even if dependencies
+# didn't change, because the mtime of .eldeps will change.  Instead,
+# we include a second file, .eldeps.x, which we ensure always has the
+# same content as .eldeps, but its mtime only changes when dependency
+# information changes, in which case a make restart is necessary
+# anyway.
+$(dir)/.eldeps.x: $(dir)/.eldeps
+       @cmp -s $^ $@ || cp $^ $@
+-include $(dir)/.eldeps.x
+
+# Add the one dependency make-deps.el does not have visibility to.
+$(dir)/notmuch-lib.elc: $(dir)/notmuch-version.elc
+
+endif
+CLEAN+=$(dir)/.eldeps $(dir)/.eldeps.tmp $(dir)/.eldeps.x
+
+ifeq ($(WITH_EMACS),1)
+%.elc: %.el $(global_deps)
+       $(call quiet,EMACS) --directory emacs -batch -f batch-byte-compile $<
+%.rsti: %.el
+       $(call quiet,EMACS) -batch -L emacs -l rstdoc -f rstdoc-batch-extract $< $@
+endif
+
+elpa: $(ELPA_FILE)
+
+ELPA_DIR=.elpa-build/notmuch-${ELPA_VERSION}
+notmuch-emacs-%.tar: ${elpa_sources} build-info
+       mkdir -p .elpa-build/notmuch-${ELPA_VERSION}
+       cp ${elpa_sources} ${ELPA_DIR}
+ifeq ($(HAVE_SPHINX)$(HAVE_MAKEINFO)$(HAVE_INSTALL_INFO),111)
+       cp ${INFO_INFO_FILES} ${ELPA_DIR}
+       for file in ${INFO_INFO_FILES}; do install-info $$file ${ELPA_DIR}/dir; done
+endif
+       tar -C .elpa-build -cf $@ notmuch-${ELPA_VERSION}
+       rm -r .elpa-build
+
+ifeq ($(WITH_EMACS),1)
+all: $(emacs_bytecode) $(emacs_docstrings)
+install-emacs: $(emacs_bytecode)
+
+install: install-emacs
+endif
+
+.PHONY: install-emacs
+install-emacs: $(emacs_sources) $(emacs_images)
+       mkdir -p "$(DESTDIR)$(emacslispdir)"
+       install -m0644 $(emacs_sources) "$(DESTDIR)$(emacslispdir)"
+ifeq ($(WITH_EMACS),1)
+       install -m0644 $(emacs_bytecode) "$(DESTDIR)$(emacslispdir)"
+endif
+       mkdir -p "$(DESTDIR)$(emacsetcdir)"
+       install -m0644 $(emacs_images) "$(DESTDIR)$(emacsetcdir)"
+       mkdir -p "$(DESTDIR)$(prefix)/bin/"
+ifeq ($(HAVE_BASH),1)
+       sed "1s|^#!.*|#! $(BASH_ABSOLUTE)|" < $(emacs_mua) > $(DESTDIR)$(prefix)/bin/notmuch-emacs-mua
+       chmod 755 $(DESTDIR)$(prefix)/bin/notmuch-emacs-mua
+endif
+ifeq ($(WITH_DESKTOP),1)
+       mkdir -p "$(DESTDIR)$(desktop_dir)"
+       desktop-file-install --mode 0644 --dir "$(DESTDIR)$(desktop_dir)" $(emacs_mua_desktop)
+       -update-desktop-database "$(DESTDIR)$(desktop_dir)"
+endif
+
+CLEAN := $(CLEAN) $(emacs_bytecode) $(dir)/notmuch-version.el $(dir)/notmuch-pkg.el \
+       $(emacs_docstrings) docstring.stamp
diff --git a/emacs/coolj.el b/emacs/coolj.el
new file mode 100644 (file)
index 0000000..79d2a1b
--- /dev/null
@@ -0,0 +1,145 @@
+;;; coolj.el --- automatically wrap long lines  -*- lexical-binding: t; coding: utf-8 -*-
+
+;; Copyright (C) 2000, 2001, 2004-2009 Free Software Foundation, Inc.
+
+;; Authors:    Kai Grossjohann <Kai.Grossjohann@CS.Uni-Dortmund.DE>
+;;             Alex Schroeder <alex@gnu.org>
+;;             Chong Yidong <cyd@stupidchicken.com>
+;; Maintainer: David Edmondson <dme@dme.org>
+;; Keywords: convenience, wp
+
+;; This file is not part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This is a simple derivative of some functionality from
+;; `longlines.el'. The key difference is that this version will
+;; insert a prefix at the head of each wrapped line. The prefix is
+;; calculated from the originating long line.
+
+;; No minor-mode is provided, the caller is expected to call
+;; `coolj-wrap-region' to wrap the region of interest.
+
+;;; Code:
+
+(defgroup coolj nil
+  "Wrapping of long lines with prefix."
+  :group 'fill)
+
+(defcustom coolj-wrap-follows-window-size t
+  "Non-nil means wrap text to the window size.
+Otherwise respect `fill-column'."
+  :group 'coolj
+  :type 'boolean)
+
+(defcustom coolj-line-prefix-regexp "^\\(>+ ?\\)*"
+  "Regular expression that matches line prefixes."
+  :group 'coolj
+  :type 'regexp)
+
+(defvar-local coolj-wrap-point nil)
+
+(defun coolj-determine-prefix ()
+  "Determine the prefix for the current line."
+  (save-excursion
+    (beginning-of-line)
+    (if (re-search-forward coolj-line-prefix-regexp nil t)
+       (buffer-substring (match-beginning 0) (match-end 0))
+      "")))
+
+(defun coolj-wrap-buffer ()
+  "Wrap the current buffer."
+  (coolj-wrap-region (point-min) (point-max)))
+
+(defun coolj-wrap-region (beg end)
+  "Wrap each successive line, starting with the line before BEG.
+Stop when we reach lines after END that don't need wrapping, or the
+end of the buffer."
+  (setq fill-column (if coolj-wrap-follows-window-size
+                       (window-width)
+                     fill-column))
+  (let ((mod (buffer-modified-p)))
+    (setq coolj-wrap-point (point))
+    (goto-char beg)
+    (forward-line -1)
+    ;; Two successful coolj-wrap-line's in a row mean successive
+    ;; lines don't need wrapping.
+    (while (null (and (coolj-wrap-line)
+                     (or (eobp)
+                         (and (>= (point) end)
+                              (coolj-wrap-line))))))
+    (goto-char coolj-wrap-point)
+    (set-buffer-modified-p mod)))
+
+(defun coolj-wrap-line ()
+  "If the current line needs to be wrapped, wrap it and return nil.
+If wrapping is performed, point remains on the line.  If the line does
+not need to be wrapped, move point to the next line and return t."
+  (let ((prefix (coolj-determine-prefix)))
+    (if (coolj-set-breakpoint prefix)
+       (progn
+         (insert-before-markers ?\n)
+         (backward-char 1)
+         (delete-char -1)
+         (forward-char 1)
+         (insert-before-markers prefix)
+         nil)
+      (forward-line 1)
+      t)))
+
+(defun coolj-set-breakpoint (prefix)
+  "Place point where we should break the current line, and return t.
+If the line should not be broken, return nil; point remains on the
+line."
+  (move-to-column fill-column)
+  (and (re-search-forward "[^ ]" (line-end-position) 1)
+       (> (current-column) fill-column)
+       ;; This line is too long.  Can we break it?
+       (or (coolj-find-break-backward prefix)
+          (progn (move-to-column fill-column)
+                 (coolj-find-break-forward)))))
+
+(defun coolj-find-break-backward (prefix)
+  "Move point backward to the first available breakpoint and return t.
+If no breakpoint is found, return nil."
+  (let ((end-of-prefix (+ (line-beginning-position) (length prefix))))
+    (and (search-backward " " end-of-prefix 1)
+        (save-excursion
+          (skip-chars-backward " " end-of-prefix)
+          (null (bolp)))
+        (progn (forward-char 1)
+               (if (and fill-nobreak-predicate
+                        (run-hook-with-args-until-success
+                         'fill-nobreak-predicate))
+                   (progn (skip-chars-backward " " end-of-prefix)
+                          (coolj-find-break-backward prefix))
+                 t)))))
+
+(defun coolj-find-break-forward ()
+  "Move point forward to the first available breakpoint and return t.
+If no break point is found, return nil."
+  (and (search-forward " " (line-end-position) 1)
+       (progn (skip-chars-forward " " (line-end-position))
+             (null (eolp)))
+       (if (and fill-nobreak-predicate
+               (run-hook-with-args-until-success
+                'fill-nobreak-predicate))
+          (coolj-find-break-forward)
+        t)))
+
+(provide 'coolj)
+
+;;; coolj.el ends here
diff --git a/emacs/make-deps.el b/emacs/make-deps.el
new file mode 100644 (file)
index 0000000..8c9e0a2
--- /dev/null
@@ -0,0 +1,69 @@
+;;; make-deps.el --- compute make dependencies for Elisp sources  -*- lexical-binding: t -*-
+;;
+;; Copyright © Austin Clements
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Austin Clements <aclements@csail.mit.edu>
+
+;;; Code:
+
+(defun batch-make-deps ()
+  "Invoke `make-deps' for each file on the command line."
+  (setq debug-on-error t)
+  (dolist (file command-line-args-left)
+    (let ((default-directory command-line-default-directory))
+      (find-file-literally file))
+    (make-deps command-line-default-directory))
+  (kill-emacs))
+
+(defun make-deps (&optional dir)
+  "Print make dependencies for the current buffer.
+
+This prints make dependencies to `standard-output' based on the
+top-level `require' expressions in the current buffer.  Paths in
+rules will be given relative to DIR, or `default-directory'."
+  (unless dir
+    (setq dir default-directory))
+  (save-excursion
+    (goto-char (point-min))
+    (condition-case nil
+       (while t
+         (let ((form (read (current-buffer))))
+           ;; Is it a (require 'x) form?
+           (when (and (listp form) (= (length form) 2)
+                      (eq (car form) 'require)
+                      (listp (cadr form)) (= (length (cadr form)) 2)
+                      (eq (car (cadr form)) 'quote)
+                      (symbolp (cadr (cadr form))))
+             ;; Find the required library
+             (let* ((name (cadr (cadr form)))
+                    (fname (locate-library (symbol-name name))))
+               ;; Is this file and the library in the same directory?
+               ;; If not, assume it's a system library and don't
+               ;; bother depending on it.
+               (when (and fname
+                          (string= (file-name-directory (buffer-file-name))
+                                   (file-name-directory fname)))
+                 ;; Print the dependency
+                 (princ (format "%s.elc: %s.elc\n"
+                                (file-name-sans-extension
+                                 (file-relative-name (buffer-file-name) dir))
+                                (file-name-sans-extension
+                                 (file-relative-name fname dir)))))))))
+      (end-of-file nil))))
+
+;;; make-deps.el ends here
diff --git a/emacs/notmuch-address.el b/emacs/notmuch-address.el
new file mode 100644 (file)
index 0000000..1a4cdda
--- /dev/null
@@ -0,0 +1,437 @@
+;;; notmuch-address.el --- address completion with notmuch  -*- lexical-binding: t -*-
+;;
+;; Copyright © David Edmondson
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: David Edmondson <dme@dme.org>
+
+;;; Code:
+
+(require 'message)
+(require 'notmuch-parser)
+(require 'notmuch-lib)
+(require 'notmuch-company)
+
+(declare-function company-manual-begin "company")
+
+;;; Cache internals
+
+(defvar notmuch-address-last-harvest 0
+  "Time of last address harvest.")
+
+(defvar notmuch-address-completions (make-hash-table :test 'equal)
+  "Hash of email addresses for completion during email composition.
+This variable is set by calling `notmuch-address-harvest'.")
+
+(defvar notmuch-address-full-harvest-finished nil
+  "Whether full completion address harvesting has finished.
+Use `notmuch-address--harvest-ready' to access as that will load
+a saved hash if necessary (and available).")
+
+(defun notmuch-address--harvest-ready ()
+  "Return t if there is a full address hash available.
+
+If the hash is not present it attempts to load a saved hash."
+  (or notmuch-address-full-harvest-finished
+      (notmuch-address--load-address-hash)))
+
+;;; Options
+
+(defcustom notmuch-address-command 'internal
+  "Determines how address completion candidates are generated.
+
+If this is a string, then that string should be an external
+program, which must take a single argument (searched string)
+and output a list of completion candidates, one per line.
+
+If this is the symbol `internal', then an implementation is used
+that relies on the \"notmuch address\" command, but does not use
+any third-party (i.e. \"external\") programs.
+
+If this is the symbol `as-is', then Notmuch does not modify the
+value of `message-completion-alist'. This option has to be set to
+this value before `notmuch' is loaded, otherwise the modification
+to `message-completion-alist' may already have taken place. This
+setting obviously does not prevent `message-completion-alist'
+from being modified at all; the user or some third-party package
+may still modify it.
+
+Finally, if this is nil, then address completion is disabled."
+  :type '(radio
+         (const  :tag "Use internal address completion" internal)
+         (string :tag "Use external completion command")
+         (const  :tag "Disable address completion" nil)
+         (const  :tag "Use default or third-party mechanism" as-is))
+  :group 'notmuch-send
+  :group 'notmuch-address
+  :group 'notmuch-external)
+
+(defcustom notmuch-address-internal-completion '(sent nil)
+  "Determines how internal address completion generates candidates.
+
+This should be a list of the form (DIRECTION FILTER), where
+DIRECTION is either sent or received and specifies whether the
+candidates are searched in messages sent by the user or received
+by the user (note received by is much faster), and FILTER is
+either nil or a filter-string, such as \"date:1y..\" to append to
+the query."
+  :type '(list :tag "Use internal address completion"
+              (radio
+               :tag "Base completion on messages you have"
+               :value sent
+               (const :tag "sent (more accurate)" sent)
+               (const :tag "received (faster)" received))
+              (radio :tag "Filter messages used for completion"
+                     (const :tag "Use all messages" nil)
+                     (string :tag "Filter query")))
+  ;; We override set so that we can clear the cache when this changes
+  :set (lambda (symbol value)
+        (set-default symbol value)
+        (setq notmuch-address-last-harvest 0)
+        (setq notmuch-address-completions (clrhash notmuch-address-completions))
+        (setq notmuch-address-full-harvest-finished nil))
+  :group 'notmuch-send
+  :group 'notmuch-address
+  :group 'notmuch-external)
+
+(defcustom notmuch-address-save-filename nil
+  "Filename to save the cached completion addresses.
+
+All the addresses notmuch uses for address completion will be
+cached in this file.  This has obvious privacy implications so
+you should make sure it is not somewhere publicly readable."
+  :type '(choice (const :tag "Off" nil)
+                (file :tag "Filename"))
+  :group 'notmuch-send
+  :group 'notmuch-address
+  :group 'notmuch-external)
+
+(defcustom notmuch-address-selection-function 'notmuch-address-selection-function
+  "The function to select address from given list.
+
+The function is called with PROMPT, COLLECTION, and INITIAL-INPUT
+as arguments (subset of what `completing-read' can be called
+with).  While executed the value of `completion-ignore-case'
+is t.  See documentation of function
+`notmuch-address-selection-function' to know how address
+selection is made by default."
+  :type 'function
+  :group 'notmuch-send
+  :group 'notmuch-address
+  :group 'notmuch-external)
+
+(defcustom notmuch-address-post-completion-functions nil
+  "Functions called after completing address.
+
+The completed address is passed as an argument to each function.
+Note that this hook will be invoked for completion in headers
+matching `notmuch-address-completion-headers-regexp'."
+  :type 'hook
+  :group 'notmuch-address
+  :group 'notmuch-hooks)
+
+(defcustom notmuch-address-use-company t
+  "If available, use company mode for address completion."
+  :type 'boolean
+  :group 'notmuch-send
+  :group 'notmuch-address)
+
+;;; Setup
+
+(defun notmuch-address-selection-function (prompt collection initial-input)
+  "Call (`completing-read'
+      PROMPT COLLECTION nil nil INITIAL-INPUT 'notmuch-address-history)"
+  (completing-read
+   prompt collection nil nil initial-input 'notmuch-address-history))
+
+(defvar notmuch-address-completion-headers-regexp
+  "^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):")
+
+(defvar notmuch-address-history nil)
+
+(defun notmuch-address-message-insinuate ()
+  (message "calling notmuch-address-message-insinuate is no longer needed"))
+
+(defun notmuch-address-setup ()
+  (unless (eq notmuch-address-command 'as-is)
+    (when (and notmuch-address-use-company
+              (require 'company nil t))
+      (notmuch-company-setup))
+    (cl-pushnew (cons notmuch-address-completion-headers-regexp
+                     #'notmuch-address-expand-name)
+               message-completion-alist :test #'equal)))
+
+(defun notmuch-address-toggle-internal-completion ()
+  "Toggle use of internal completion for current buffer.
+
+This overrides the global setting for address completion and
+toggles the setting in this buffer."
+  (interactive)
+  (if (local-variable-p 'notmuch-address-command)
+      (kill-local-variable 'notmuch-address-command)
+    (setq-local notmuch-address-command 'internal))
+  (when (boundp 'company-idle-delay)
+    (if (local-variable-p 'company-idle-delay)
+       (kill-local-variable 'company-idle-delay)
+      (setq-local company-idle-delay nil))))
+
+;;; Completion
+
+(defun notmuch-address-matching (substring)
+  "Returns a list of completion candidates matching SUBSTRING.
+The candidates are taken from `notmuch-address-completions'."
+  (let ((candidates)
+       (re (regexp-quote substring)))
+    (maphash (lambda (key _val)
+              (when (string-match re key)
+                (push key candidates)))
+            notmuch-address-completions)
+    candidates))
+
+(defun notmuch-address-options (original)
+  "Return a list of completion candidates.
+Use either elisp-based implementation or older implementation
+requiring external commands."
+  (cond
+   ((eq notmuch-address-command 'internal)
+    (unless (notmuch-address--harvest-ready)
+      ;; First, run quick synchronous harvest based on what the user
+      ;; entered so far.
+      (notmuch-address-harvest original t))
+    (prog1 (notmuch-address-matching original)
+      ;; Then start the (potentially long-running) full asynchronous
+      ;; harvest if necessary.
+      (notmuch-address-harvest-trigger)))
+   (t
+    (notmuch--process-lines notmuch-address-command original))))
+
+(defun notmuch-address-expand-name ()
+  (cond
+   ((and (eq notmuch-address-command 'internal)
+        notmuch-address-use-company
+        (bound-and-true-p company-mode))
+    (company-manual-begin))
+   (notmuch-address-command
+    (let* ((end (point))
+          (beg (save-excursion
+                 (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
+                 (goto-char (match-end 0))
+                 (point)))
+          (orig (buffer-substring-no-properties beg end))
+          (completion-ignore-case t)
+          (options (with-temp-message "Looking for completion candidates..."
+                     (notmuch-address-options orig)))
+          (num-options (length options))
+          (chosen (cond
+                   ((eq num-options 0)
+                    nil)
+                   ((eq num-options 1)
+                    (car options))
+                   (t
+                    (funcall notmuch-address-selection-function
+                             (format "Address (%s matches): " num-options)
+                             options
+                             orig)))))
+      (if chosen
+         (progn
+           (push chosen notmuch-address-history)
+           (delete-region beg end)
+           (insert chosen)
+           (run-hook-with-args 'notmuch-address-post-completion-functions
+                               chosen))
+       (message "No matches.")
+       (ding))))
+   (t nil)))
+
+;;; Harvest
+
+(defun notmuch-address-harvest-addr (result)
+  (puthash (plist-get result :name-addr)
+          t notmuch-address-completions))
+
+(defun notmuch-address-harvest-filter (proc string)
+  (when (buffer-live-p (process-buffer proc))
+    (with-current-buffer (process-buffer proc)
+      (save-excursion
+       (goto-char (point-max))
+       (insert string))
+      (notmuch-sexp-parse-partial-list
+       'notmuch-address-harvest-addr (process-buffer proc)))))
+
+(defvar notmuch-address-harvest-procs '(nil . nil)
+  "The currently running harvests.
+
+The car is a partial harvest, and the cdr is a full harvest.")
+
+(defun notmuch-address-harvest (&optional addr-prefix synchronous callback)
+  "Collect addresses completion candidates.
+
+It queries the notmuch database for messages sent/received (as
+configured with `notmuch-address-command') by the user, collects
+destination/source addresses from those messages and stores them
+in `notmuch-address-completions'.
+
+If ADDR-PREFIX is not nil, only messages with to/from addresses
+matching ADDR-PREFIX*' are queried.
+
+Address harvesting may take some time so the address collection runs
+asynchronously unless SYNCHRONOUS is t. In case of asynchronous
+execution, CALLBACK is called when harvesting finishes."
+  (let* ((sent (eq (car notmuch-address-internal-completion) 'sent))
+        (config-query (cadr notmuch-address-internal-completion))
+        (prefix-query (and addr-prefix
+                           (format "%s:%s*"
+                                   (if sent "to" "from")
+                                   addr-prefix)))
+        (from-or-to-me-query
+         (mapconcat (lambda (x)
+                      (concat (if sent "from:" "to:") x))
+                    (notmuch-user-emails) " or "))
+        (query (if (or prefix-query config-query)
+                   (concat (format "(%s)" from-or-to-me-query)
+                           (and prefix-query
+                                (format " and (%s)" prefix-query))
+                           (and config-query
+                                (format " and (%s)" config-query)))
+                 from-or-to-me-query))
+        (args `("address" "--format=sexp" "--format-version=5"
+                ,(if sent "--output=recipients" "--output=sender")
+                "--deduplicate=address"
+                ,query)))
+    (if synchronous
+       (mapc #'notmuch-address-harvest-addr
+             (apply 'notmuch-call-notmuch-sexp args))
+      ;; Asynchronous
+      (let* ((current-proc (if addr-prefix
+                              (car notmuch-address-harvest-procs)
+                            (cdr notmuch-address-harvest-procs)))
+            (proc-name (format "notmuch-address-%s-harvest"
+                               (if addr-prefix "partial" "full")))
+            (proc-buf (concat " *" proc-name "*")))
+       ;; Kill any existing process
+       (when current-proc
+         (kill-buffer (process-buffer current-proc))) ; this also kills the process
+       (setq current-proc
+             (apply 'notmuch-start-notmuch proc-name proc-buf
+                    callback                           ; process sentinel
+                    args))
+       (set-process-filter current-proc 'notmuch-address-harvest-filter)
+       (set-process-query-on-exit-flag current-proc nil)
+       (if addr-prefix
+           (setcar notmuch-address-harvest-procs current-proc)
+         (setcdr notmuch-address-harvest-procs current-proc)))))
+  ;; return value
+  nil)
+
+(defvar notmuch-address--save-hash-version 1
+  "Version format of the save hash.")
+
+(defun notmuch-address--get-address-hash ()
+  "Return the saved address hash as a plist.
+
+Returns nil if the save file does not exist, or it does not seem
+to be a saved address hash."
+  (and notmuch-address-save-filename
+       (condition-case nil
+          (with-temp-buffer
+            (insert-file-contents notmuch-address-save-filename)
+            (let ((name (read (current-buffer)))
+                  (plist (read (current-buffer))))
+              ;; We do two simple sanity checks on the loaded file.
+              ;; We just check a version is specified, not that
+              ;; it is the current version, as we are allowed to
+              ;; over-write and a save-file with an older version.
+              (and (string= name "notmuch-address-hash")
+                   (plist-get plist :version)
+                   plist)))
+        ;; The error case catches any of the reads failing.
+        (error nil))))
+
+(defun notmuch-address--load-address-hash ()
+  "Read the saved address hash and set the corresponding variables."
+  (let ((load-plist (notmuch-address--get-address-hash)))
+    (when (and load-plist
+              ;; If the user's setting have changed, or the version
+              ;; has changed, return nil to make sure the new settings
+              ;; take effect.
+              (equal (plist-get load-plist :completion-settings)
+                     notmuch-address-internal-completion)
+              (equal (plist-get load-plist :version)
+                     notmuch-address--save-hash-version))
+      (setq notmuch-address-last-harvest (plist-get load-plist :last-harvest))
+      (setq notmuch-address-completions (plist-get load-plist :completions))
+      (setq notmuch-address-full-harvest-finished t)
+      ;; Return t to say load was successful.
+      t)))
+
+(defun notmuch-address--save-address-hash ()
+  (when notmuch-address-save-filename
+    (if (or (not (file-exists-p notmuch-address-save-filename))
+           ;; The file exists, check it is a file we saved.
+           (notmuch-address--get-address-hash))
+       (with-temp-file notmuch-address-save-filename
+         (let ((save-plist
+                (list :version notmuch-address--save-hash-version
+                      :completion-settings notmuch-address-internal-completion
+                      :last-harvest notmuch-address-last-harvest
+                      :completions notmuch-address-completions)))
+           (print "notmuch-address-hash" (current-buffer))
+           (print save-plist (current-buffer))))
+      (message "\
+Warning: notmuch-address-save-filename %s exists but doesn't
+appear to be an address savefile.  Not overwriting."
+              notmuch-address-save-filename))))
+
+(defun notmuch-address-harvest-trigger ()
+  (let ((now (float-time)))
+    (when (> (- now notmuch-address-last-harvest) 86400)
+      (setq notmuch-address-last-harvest now)
+      (notmuch-address-harvest
+       nil nil
+       (lambda (_proc event)
+        ;; If harvest fails, we want to try
+        ;; again when the trigger is next called.
+        (if (string= event "finished\n")
+            (progn
+              (notmuch-address--save-address-hash)
+              (setq notmuch-address-full-harvest-finished t))
+          (setq notmuch-address-last-harvest 0)))))))
+
+;;; Standalone completion
+
+(defun notmuch-address-from-minibuffer (prompt)
+  (if (not notmuch-address-command)
+      (read-string prompt)
+    (let ((rmap (copy-keymap minibuffer-local-map))
+         (omap minibuffer-local-map))
+      ;; Configure TAB to start completion when executing read-string.
+      ;; "Original" minibuffer keymap is restored just before calling
+      ;; notmuch-address-expand-name as it may also use minibuffer-local-map
+      ;; (completing-read probably does not but if something else is used there).
+      (define-key rmap (kbd "TAB") (lambda ()
+                                    (interactive)
+                                    (let ((enable-recursive-minibuffers t)
+                                          (minibuffer-local-map omap))
+                                      (notmuch-address-expand-name))))
+      (let ((minibuffer-local-map rmap))
+       (read-string prompt)))))
+
+;;; _
+
+(provide 'notmuch-address)
+
+;;; notmuch-address.el ends here
diff --git a/emacs/notmuch-company.el b/emacs/notmuch-company.el
new file mode 100644 (file)
index 0000000..7e05dc8
--- /dev/null
@@ -0,0 +1,106 @@
+;;; notmuch-company.el --- Mail address completion for notmuch via company-mode  -*- lexical-binding: t -*-
+;;
+;; Copyright © Trevor Jim
+;; Copyright © Michal Sojka
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Trevor Jim <tjim@mac.com>
+;;         Michal Sojka <sojkam1@fel.cvut.cz>
+;; Keywords: mail, completion
+
+;;; Commentary:
+
+;; Mail address completion for notmuch via company-mode.  To enable
+;; this, install company mode from <https://company-mode.github.io/>.
+;;
+;; NB company-minimum-prefix-length defaults to 3 so you don't get
+;; completion unless you type 3 characters.
+
+;;; Code:
+
+(require 'notmuch-lib)
+
+(defvar-local notmuch-company-last-prefix nil)
+
+(declare-function company-begin-backend "company")
+(declare-function company-grab "company")
+(declare-function company-mode "company")
+(declare-function company-manual-begin "company")
+(defvar company-backends)
+(defvar company-idle-delay)
+
+(declare-function notmuch-address-harvest "notmuch-address")
+(declare-function notmuch-address-harvest-trigger "notmuch-address")
+(declare-function notmuch-address-matching "notmuch-address")
+(declare-function notmuch-address--harvest-ready "notmuch-address")
+(defvar notmuch-address-completion-headers-regexp)
+(defvar notmuch-address-command)
+
+;;;###autoload
+(defun notmuch-company-setup ()
+  (company-mode)
+  (setq-local company-backends '(notmuch-company))
+  ;; Disable automatic company completion unless an internal
+  ;; completion method is configured. Company completion (using
+  ;; internal completion) can still be accessed via standard company
+  ;; functions, e.g., company-complete.
+  (unless (eq notmuch-address-command 'internal)
+    (setq-local company-idle-delay nil)))
+
+;;;###autoload
+(defun notmuch-company (command &optional arg &rest _ignore)
+  "`company-mode' completion back-end for `notmuch'."
+  (interactive (list 'interactive))
+  (require 'company)
+  (let ((case-fold-search t)
+       (completion-ignore-case t))
+    (cl-case command
+      (interactive (company-begin-backend 'notmuch-company))
+      (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))
+                  (setq notmuch-company-last-prefix
+                        (company-grab "[:,][ \t]*\\(.*\\)" 1 (point-at-bol)))))
+      (candidates (cond
+                  ((notmuch-address--harvest-ready)
+                   ;; Update harvested addressed from time to time
+                   (notmuch-address-harvest-trigger)
+                   (notmuch-address-matching arg))
+                  (t
+                   (cons :async
+                         (lambda (callback)
+                           ;; First run quick asynchronous harvest
+                           ;; based on what the user entered so far
+                           (notmuch-address-harvest
+                            arg nil
+                            (lambda (_proc _event)
+                              (funcall callback (notmuch-address-matching arg))
+                              ;; Then start the (potentially long-running)
+                              ;; full asynchronous harvest if necessary
+                              (notmuch-address-harvest-trigger))))))))
+      (match (if (string-match notmuch-company-last-prefix arg)
+                (match-end 0)
+              0))
+      (post-completion
+       (run-hook-with-args 'notmuch-address-post-completion-functions arg))
+      (no-cache t))))
+
+(provide 'notmuch-company)
+
+;;; notmuch-company.el ends here
diff --git a/emacs/notmuch-compat.el b/emacs/notmuch-compat.el
new file mode 100644 (file)
index 0000000..179bf59
--- /dev/null
@@ -0,0 +1,58 @@
+;;; notmuch-compat.el --- compatibility functions for earlier versions of emacs  -*- lexical-binding: t -*-
+;;
+;; The functions in this file are copied from more modern versions of
+;; emacs and are Copyright (C) 1985-1986, 1992, 1994-1995, 1999-2017
+;; Free Software Foundation, Inc.
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+;; Before Emacs 26.1 lines that are longer than 998 octets were not.
+;; folded. Commit 77bbca8c82f6e553c42abbfafca28f55fc995d00 fixed
+;; that. Until we drop support for Emacs 25 we have to backport that
+;; fix. To avoid interfering with Gnus we only run the hook when
+;; called from notmuch-message-mode.
+
+(declare-function mail-header-fold-field "mail-parse" nil)
+
+(defun notmuch-message--fold-long-headers ()
+  (when (eq major-mode 'notmuch-message-mode)
+    (goto-char (point-min))
+    (while (not (eobp))
+      (when (and (looking-at "[^:]+:")
+                (> (- (line-end-position) (point)) 998))
+       (mail-header-fold-field))
+      (forward-line 1))))
+
+(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
diff --git a/emacs/notmuch-crypto.el b/emacs/notmuch-crypto.el
new file mode 100644 (file)
index 0000000..a1cf3dd
--- /dev/null
@@ -0,0 +1,272 @@
+;;; notmuch-crypto.el --- functions for handling display of cryptographic metadata  -*- lexical-binding: t -*-
+;;
+;; Copyright © Jameson Rollins
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Jameson Rollins <jrollins@finestructure.net>
+
+;;; Code:
+
+(require 'epg)
+(require 'notmuch-lib)
+
+(declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare))
+
+;;; Options
+
+(defcustom notmuch-crypto-process-mime t
+  "Whether to process cryptographic MIME parts.
+
+If this variable is non-nil signatures in multipart/signed
+messages will be verified and multipart/encrypted parts will be
+decrypted.  The result of the crypto operation will be displayed
+in a specially colored header button at the top of the processed
+part.  Signed parts will have variously colored headers depending
+on the success or failure of the verification process and on the
+validity of user ID of the signer.
+
+The effect of setting this variable can be seen temporarily by
+providing a prefix when viewing a signed or encrypted message, or
+by providing a prefix when reloading the message in notmuch-show
+mode."
+  :type 'boolean
+  :package-version '(notmuch . "0.25")
+  :group 'notmuch-crypto)
+
+(defcustom notmuch-crypto-get-keys-asynchronously t
+  "Whether to retrieve openpgp keys asynchronously."
+  :type 'boolean
+  :group 'notmuch-crypto)
+
+(defcustom notmuch-crypto-gpg-program epg-gpg-program
+  "The gpg executable."
+  :type 'string
+  :group 'notmuch-crypto)
+
+;;; Faces
+
+(defface notmuch-crypto-part-header
+  '((((class color)
+      (background dark))
+     (:foreground "LightBlue1"))
+    (((class color)
+      (background light))
+     (:foreground "blue")))
+  "Face used for crypto parts headers."
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
+
+(defface notmuch-crypto-signature-good
+  '((t (:background "green" :foreground "black")))
+  "Face used for good signatures."
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
+
+(defface notmuch-crypto-signature-good-key
+  '((t (:background "orange" :foreground "black")))
+  "Face used for good signatures."
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
+
+(defface notmuch-crypto-signature-bad
+  '((t (:background "red" :foreground "black")))
+  "Face used for bad signatures."
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
+
+(defface notmuch-crypto-signature-unknown
+  '((t (:background "red" :foreground "black")))
+  "Face used for signatures of unknown status."
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
+
+(defface notmuch-crypto-decryption
+  '((t (:background "purple" :foreground "black")))
+  "Face used for encryption/decryption status messages."
+  :group 'notmuch-crypto
+  :group 'notmuch-faces)
+
+;;; Functions
+
+(define-button-type 'notmuch-crypto-status-button-type
+  'action (lambda (button) (message "%s" (button-get button 'help-echo)))
+  'follow-link t
+  'help-echo "Set notmuch-crypto-process-mime to process cryptographic mime parts."
+  :supertype 'notmuch-button-type)
+
+(defun notmuch-crypto-insert-sigstatus-button (sigstatus from)
+  "Insert a button describing the signature status SIGSTATUS sent by user FROM."
+  (let* ((status (plist-get sigstatus :status))
+        (show-button t)
+        (face 'notmuch-crypto-signature-unknown)
+        (button-action (lambda (button) (message (button-get button 'help-echo))))
+        (keyid (concat "0x" (plist-get sigstatus :keyid)))
+        label help-msg)
+    (cond
+     ((string= status "good")
+      (let ((fingerprint (concat "0x" (plist-get sigstatus :fingerprint)))
+           (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")
+      (setq label (concat "Unknown key ID " keyid " or unsupported algorithm"))
+      (setq button-action 'notmuch-crypto-sigstatus-error-callback)
+      (setq help-msg (concat "Click to retrieve key ID " keyid
+                            " from key server.")))
+     ((string= status "bad")
+      (setq label (concat "Bad signature (claimed key ID " keyid ")"))
+      (setq face 'notmuch-crypto-signature-bad))
+     (status
+      (setq label (concat "Unknown signature status: " status)))
+     (t
+      (setq show-button nil)))
+    (when show-button
+      (insert-button
+       (concat "[ " label " ]")
+       :type 'notmuch-crypto-status-button-type
+       'help-echo help-msg
+       'face face
+       'mouse-face face
+       'action button-action
+       :notmuch-sigstatus sigstatus
+       :notmuch-from from)
+      (insert "\n"))))
+
+(defun notmuch-crypto-sigstatus-good-callback (button)
+  (let* ((id (notmuch-show-get-message-id))
+        (sigstatus (button-get button :notmuch-sigstatus))
+        (fingerprint (concat "0x" (plist-get sigstatus :fingerprint)))
+        (buffer (get-buffer-create "*notmuch-crypto-gpg-out*"))
+        (window (display-buffer buffer)))
+    (with-selected-window window
+      (with-current-buffer buffer
+       (goto-char (point-max))
+       (insert (format "-- Key %s in message %s:\n"
+                       fingerprint id))
+       (notmuch--call-process notmuch-crypto-gpg-program nil t t
+                     "--batch" "--no-tty" "--list-keys" fingerprint))
+      (recenter -1))))
+
+(declare-function notmuch-show-refresh-view "notmuch-show" (&optional reset-state))
+(declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare))
+
+(defun notmuch-crypto--async-key-sentinel (process _event)
+  "When the user asks for a GPG key to be retrieved
+asynchronously, handle completion of that task.
+
+If the retrieval is successful, the thread where the retrieval
+was initiated is still displayed and the cursor has not moved,
+redisplay the thread."
+  (let ((status (process-status process))
+       (exit-status (process-exit-status process))
+       (keyid (process-get process :gpg-key-id)))
+    (when (memq status '(exit signal))
+      (message "Getting the GPG key %s asynchronously...%s."
+              keyid
+              (if (= exit-status 0)
+                  "completed"
+                "failed"))
+      ;; If the original buffer is still alive and point didn't move
+      ;; (i.e. the user didn't move on or away), refresh the buffer to
+      ;; show the updated signature status.
+      (let ((show-buffer (process-get process :notmuch-show-buffer))
+           (show-point (process-get process :notmuch-show-point)))
+       (when (and (bufferp show-buffer)
+                  (buffer-live-p show-buffer)
+                  (= show-point
+                     (with-current-buffer show-buffer
+                       (point))))
+         (with-current-buffer show-buffer
+           (notmuch-show-refresh-view)))))))
+
+(defun notmuch-crypto--set-button-label (button label)
+  "Set the text displayed in BUTTON to LABEL."
+  (save-excursion
+    (let ((inhibit-read-only t))
+      ;; This knows rather too much about how we typically format
+      ;; buttons.
+      (goto-char (button-start button))
+      (forward-char 2)
+      (delete-region (point) (- (button-end button) 2))
+      (insert label))))
+
+(defun notmuch-crypto-sigstatus-error-callback (button)
+  "When signature validation has failed, try to retrieve the
+corresponding key when the status button is pressed."
+  (let* ((sigstatus (button-get button :notmuch-sigstatus))
+        (keyid (concat "0x" (plist-get sigstatus :keyid)))
+        (buffer (get-buffer-create "*notmuch-crypto-gpg-out*")))
+    (if notmuch-crypto-get-keys-asynchronously
+       (progn
+         (notmuch-crypto--set-button-label
+          button (format "Retrieving key %s asynchronously..." keyid))
+         (with-current-buffer buffer
+           (goto-char (point-max))
+           (insert (format "--- Retrieving key %s:\n" keyid)))
+         (let ((p (notmuch--make-process
+                   :name "notmuch GPG key retrieval"
+                   :connection-type 'pipe
+                   :buffer buffer
+                   :stderr buffer
+                   :command (list notmuch-crypto-gpg-program "--recv-keys" keyid)
+                   :sentinel #'notmuch-crypto--async-key-sentinel)))
+           (process-put p :gpg-key-id keyid)
+           (process-put p :notmuch-show-buffer (current-buffer))
+           (process-put p :notmuch-show-point (point))
+           (message "Getting the GPG key %s asynchronously..." keyid)))
+      (let ((window (display-buffer buffer)))
+       (with-selected-window window
+         (with-current-buffer buffer
+           (goto-char (point-max))
+           (insert (format "--- Retrieving key %s:\n" keyid))
+           (notmuch--call-process notmuch-crypto-gpg-program nil t t "--recv-keys" keyid)
+           (insert "\n")
+           (notmuch--call-process notmuch-crypto-gpg-program nil t t "--list-keys" keyid))
+         (recenter -1))
+       (notmuch-show-refresh-view)))))
+
+(defun notmuch-crypto-insert-encstatus-button (encstatus)
+  "Insert a button describing the encryption status ENCSTATUS."
+  (insert-button
+   (concat "[ "
+          (let ((status (plist-get encstatus :status)))
+            (cond
+             ((string= status "good")
+              "Decryption successful")
+             ((string= status "bad")
+              "Decryption error")
+             (t
+              (concat "Unknown encryption status"
+                      (and status (concat ": " status))))))
+          " ]")
+   :type 'notmuch-crypto-status-button-type
+   'face 'notmuch-crypto-decryption
+   'mouse-face 'notmuch-crypto-decryption)
+  (insert "\n"))
+
+;;; _
+
+(provide 'notmuch-crypto)
+
+;;; notmuch-crypto.el ends here
diff --git a/emacs/notmuch-draft.el b/emacs/notmuch-draft.el
new file mode 100644 (file)
index 0000000..fcc4550
--- /dev/null
@@ -0,0 +1,287 @@
+;;; notmuch-draft.el --- functions for postponing and editing drafts  -*- lexical-binding: t -*-
+;;
+;; Copyright © Mark Walters
+;; Copyright © David Bremner
+;; Copyright © Leo Gaspard
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Mark Walters <markwalters1009@gmail.com>
+;;         David Bremner <david@tethera.net>
+;;         Leo Gaspard <leo@gaspard.io>
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'pcase)
+(require 'subr-x)
+
+(require 'notmuch-maildir-fcc)
+(require 'notmuch-tag)
+
+(declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare))
+(declare-function notmuch-message-mode "notmuch-mua")
+
+;;; Options
+
+(defgroup notmuch-draft nil
+  "Saving and editing drafts in Notmuch."
+  :group 'notmuch)
+
+(defcustom notmuch-draft-tags '("+draft")
+  "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 \"-\"
+will be removed from the message being stored.
+
+For example, if you wanted to give the message a \"draft\" tag
+but not the (normally added by default) \"inbox\" tag, you would
+set:
+    (\"+draft\" \"-inbox\")"
+  :type '(repeat string)
+  :group 'notmuch-draft)
+
+(defcustom notmuch-draft-folder "drafts"
+  "Folder to save draft messages in.
+
+This should be specified relative to the root of the notmuch
+database. It will be created if necessary."
+  :type 'string
+  :group 'notmuch-draft)
+
+(defcustom notmuch-draft-quoted-tags '()
+  "Mml tags to quote.
+
+This should be a list of mml tags to quote before saving. You do
+not need to include \"secure\" as that is handled separately.
+
+If you include \"part\" then attachments will not be saved with
+the draft -- if not then they will be saved with the draft. The
+former means the attachments may not still exist when you resume
+the message, the latter means that the attachments as they were
+when you postponed will be sent with the resumed message.
+
+Note you may get strange results if you change this between
+postponing and resuming a message."
+  :type '(repeat string)
+  :group 'notmuch-send)
+
+(defcustom notmuch-draft-save-plaintext 'ask
+  "Whether to allow saving plaintext when it seems encryption is intended.
+When a message contains mml tags, then that suggest it is
+intended to be encrypted.  If the user requests that such a
+message is saved locally, then this option controls whether
+that is allowed.  Beside a boolean, this can also be `ask'."
+  :type '(radio
+         (const :tag "Never" nil)
+         (const :tag "Ask every time" ask)
+         (const :tag "Always" t))
+  :group 'notmuch-draft
+  :group 'notmuch-crypto)
+
+;;; Internal
+
+(defvar notmuch-draft-encryption-tag-regex
+  "<#\\(part encrypt\\|secure.*mode=.*encrypt>\\)"
+  "Regular expression matching mml tags indicating encryption of part or message.")
+
+(defvar-local notmuch-draft-id nil
+  "Message-id of the most recent saved draft of this message.")
+
+(defun notmuch-draft--mark-deleted ()
+  "Tag the last saved draft deleted.
+
+Used when a new version is saved, or the message is sent."
+  (when notmuch-draft-id
+    (notmuch-tag notmuch-draft-id '("+deleted"))))
+
+(defun notmuch-draft-quote-some-mml ()
+  "Quote the mml tags in `notmuch-draft-quoted-tags'."
+  (save-excursion
+    ;; First we deal with any secure tag separately.
+    (message-goto-body)
+    (when (looking-at "<#secure[^\n]*>\n")
+      (let ((secure-tag (match-string 0)))
+       (delete-region (match-beginning 0) (match-end 0))
+       (message-add-header (concat "X-Notmuch-Emacs-Secure: " secure-tag))))
+    ;; This is copied from mml-quote-region but only quotes the
+    ;; specified tags.
+    (when notmuch-draft-quoted-tags
+      (let ((re (concat "<#!*/?\\("
+                       (mapconcat 'regexp-quote notmuch-draft-quoted-tags "\\|")
+                       "\\)")))
+       (message-goto-body)
+       (while (re-search-forward re nil t)
+         ;; Insert ! after the #.
+         (goto-char (+ (match-beginning 0) 2))
+         (insert "!"))))))
+
+(defun notmuch-draft-unquote-some-mml ()
+  "Unquote the mml tags in `notmuch-draft-quoted-tags'."
+  (save-excursion
+    (when notmuch-draft-quoted-tags
+      (let ((re (concat "<#!+/?\\("
+                       (mapconcat 'regexp-quote notmuch-draft-quoted-tags "\\|")
+                       "\\)")))
+       (message-goto-body)
+       (while (re-search-forward re nil t)
+         ;; Remove one ! from after the #.
+         (goto-char (+ (match-beginning 0) 2))
+         (delete-char 1))))
+    (let (secure-tag)
+      (save-restriction
+       (message-narrow-to-headers)
+       (setq secure-tag (message-fetch-field "X-Notmuch-Emacs-Secure" t))
+       (message-remove-header "X-Notmuch-Emacs-Secure"))
+      (message-goto-body)
+      (when secure-tag
+       (insert secure-tag "\n")))))
+
+(defun notmuch-draft--has-encryption-tag ()
+  "Return non-nil if there is an mml secure tag."
+  (save-excursion
+    (message-goto-body)
+    (re-search-forward notmuch-draft-encryption-tag-regex nil t)))
+
+(defun notmuch-draft--query-encryption ()
+  "Return non-nil if we should save a message that should be encrypted.
+
+`notmuch-draft-save-plaintext' controls the behaviour."
+  (cl-case notmuch-draft-save-plaintext
+    ((ask)
+     (unless (yes-or-no-p
+             "(Customize `notmuch-draft-save-plaintext' to avoid this warning)
+This message contains mml tags that suggest it is intended to be encrypted.
+Really save and index an unencrypted copy? ")
+       (error "Save aborted")))
+    ((nil)
+     (error "Refusing to save draft with encryption tags (see `%s')"
+           'notmuch-draft-save-plaintext))
+    ((t)
+     (ignore))))
+
+(defun notmuch-draft--make-message-id ()
+  ;; message-make-message-id gives the id inside a "<" ">" pair,
+  ;; but notmuch doesn't want that form, so remove them.
+  (concat "draft-" (substring (message-make-message-id) 1 -1)))
+
+;;; Commands
+
+(defun notmuch-draft-save ()
+  "Save the current draft message in the notmuch database.
+
+This saves the current message in the database with tags
+`notmuch-draft-tags' (in addition to any default tags
+applied to newly inserted messages)."
+  (interactive)
+  (when (notmuch-draft--has-encryption-tag)
+    (notmuch-draft--query-encryption))
+  (let ((id (notmuch-draft--make-message-id)))
+    (with-temporary-notmuch-message-buffer
+     ;; We insert a Date header and a Message-ID header, the former
+     ;; so that it is easier to search for the message, and the
+     ;; latter so we have a way of accessing the saved message (for
+     ;; example to delete it at a later time). We check that the
+     ;; user has these in `message-deletable-headers' (the default)
+     ;; as otherwise they are doing something strange and we
+     ;; shouldn't interfere. Note, since we are doing this in a new
+     ;; buffer we don't change the version in the compose buffer.
+     (cond
+      ((member 'Message-ID message-deletable-headers)
+       (message-remove-header "Message-ID")
+       (message-add-header (concat "Message-ID: <" id ">")))
+      (t
+       (message "You have customized emacs so Message-ID is not a %s"
+               "deletable header, so not changing it")
+       (setq id nil)))
+     (cond
+      ((member 'Date message-deletable-headers)
+       (message-remove-header "Date")
+       (message-add-header (concat "Date: " (message-make-date))))
+      (t
+       (message "You have customized emacs so Date is not a deletable %s"
+               "header, so not changing it")))
+     (message-add-header "X-Notmuch-Emacs-Draft: True")
+     (notmuch-draft-quote-some-mml)
+     (notmuch-maildir-setup-message-for-saving)
+     (notmuch-maildir-notmuch-insert-current-buffer
+      notmuch-draft-folder t notmuch-draft-tags))
+    ;; We are now back in the original compose buffer. Note the
+    ;; function notmuch-call-notmuch-process (called by
+    ;; notmuch-maildir-notmuch-insert-current-buffer) signals an error
+    ;; on failure, so to get to this point it must have
+    ;; succeeded. Also, notmuch-draft-id is still the id of the
+    ;; previous draft, so it is safe to mark it deleted.
+    (notmuch-draft--mark-deleted)
+    (setq notmuch-draft-id (concat "id:" id))
+    (set-buffer-modified-p nil)))
+
+(defun notmuch-draft-postpone ()
+  "Save the draft message in the notmuch database and exit buffer."
+  (interactive)
+  (notmuch-draft-save)
+  (kill-buffer))
+
+(defun notmuch-draft-resume (id)
+  "Resume editing of message with id ID."
+  ;; Used by command `notmuch-show-resume-message'.
+  (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
+             (yes-or-no-p "Message does not appear to be a draft: edit as new? "))
+      (pop-to-buffer-same-window
+       (get-buffer-create (concat "*notmuch-draft-" id "*")))
+      (setq buffer-read-only nil)
+      (erase-buffer)
+      (let ((coding-system-for-read 'no-conversion))
+       (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)
+       (replace-match mail-header-separator t t))
+      ;; Remove the Date and Message-ID headers (unless the user has
+      ;; explicitly customized emacs to tell us not to) as they will
+      ;; be replaced when the message is sent.
+      (save-restriction
+       (message-narrow-to-headers)
+       (when (member 'Message-ID message-deletable-headers)
+         (message-remove-header "Message-ID"))
+       (when (member 'Date message-deletable-headers)
+         (message-remove-header "Date"))
+       (unless draft (notmuch-fcc-header-setup))
+       ;; The X-Notmuch-Emacs-Draft header is a more reliable
+       ;; indication of whether the message really is a draft.
+       (setq draft (> (message-remove-header "X-Notmuch-Emacs-Draft") 0)))
+      ;; If the message is not a draft we should not unquote any mml.
+      (when draft
+       (notmuch-draft-unquote-some-mml))
+      (notmuch-message-mode)
+      (message-goto-body)
+      (set-buffer-modified-p nil)
+      ;; If the resumed message was a draft then set the draft
+      ;; message-id so that we can delete the current saved draft if the
+      ;; message is resaved or sent.
+      (setq notmuch-draft-id (and draft id)))))
+
+;;; _
+
+(add-hook 'message-send-hook 'notmuch-draft--mark-deleted)
+
+(provide 'notmuch-draft)
+
+;;; notmuch-draft.el ends here
diff --git a/emacs/notmuch-emacs-mua b/emacs/notmuch-emacs-mua
new file mode 100755 (executable)
index 0000000..254e640
--- /dev/null
@@ -0,0 +1,185 @@
+#!/usr/bin/env bash
+#
+# notmuch-emacs-mua - start composing a mail on the command line
+#
+# Copyright © 2014 Jani Nikula
+#
+# 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/ .
+#
+# Authors: Jani Nikula <jani@nikula.org>
+#
+
+set -eu
+
+# escape: "expand" '\' as '\\' and '"' as '\"'
+# calling convention: escape -v var "$arg" (like in bash printf).
+escape ()
+{
+    local __escape_arg__=${3//\\/\\\\}
+    printf -v $2 '%s' "${__escape_arg__//\"/\\\"}"
+}
+
+EMACS=${EMACS:-emacs}
+EMACSCLIENT=${EMACSCLIENT:-emacsclient}
+
+PRINT_ONLY=
+NO_WINDOW=
+USE_EMACSCLIENT=
+AUTO_DAEMON=
+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
+    # Handle errors and long options.
+    case "${opt}" in
+       :)
+           echo "$0: short option -${OPTARG} requires an argument." >&2
+           exit 1
+           ;;
+       \?)
+           opt=$1
+           if [ "${OPTARG}" != "-" ]; then
+               echo "$0: unknown short option -${OPTARG}." >&2
+               exit 1
+           fi
+
+           case "${opt}" in
+               # Long options with arguments.
+               --subject=*|--to=*|--cc=*|--bcc=*|--body=*)
+                   OPTARG=${opt#--*=}
+                   opt=${opt%%=*}
+                   ;;
+               # Long options without arguments.
+               --help|--print|--no-window-system|--client|--auto-daemon|--create-frame|--hello)
+                   ;;
+               *)
+                   echo "$0: unknown long option ${opt}, or argument mismatch." >&2
+                   exit 1
+                   ;;
+           esac
+           # getopts does not do this for what it considers errors.
+           OPTIND=$((OPTIND + 1))
+           ;;
+    esac
+
+    escape -v OPTARG "${OPTARG-none}"
+
+    case "${opt}" in
+       --help|h)
+           exec man notmuch-emacs-mua
+           ;;
+       --subject|s)
+           ELISP="${ELISP} (message-goto-subject) (insert \"${OPTARG}\")"
+           ;;
+       --to)
+           ELISP="${ELISP} (message-goto-to) (insert \"${TO_SEP}${OPTARG}\")"
+           TO_SEP=", "
+           ;;
+       --cc|c)
+           ELISP="${ELISP} (message-goto-cc) (insert \"${CC_SEP}${OPTARG}\")"
+           CC_SEP=", "
+           ;;
+       --bcc|b)
+           ELISP="${ELISP} (message-goto-bcc) (insert \"${BCC_SEP}${OPTARG}\")"
+           BCC_SEP=", "
+           ;;
+       --body|i)
+           ELISP="${ELISP} (message-goto-body) (insert-file \"${OPTARG}\")"
+           ;;
+       --print)
+           PRINT_ONLY=1
+           ;;
+       --no-window-system)
+           NO_WINDOW="-nw"
+           ;;
+       --client)
+           USE_EMACSCLIENT="yes"
+           ;;
+       --auto-daemon)
+           AUTO_DAEMON="--alternate-editor="
+           CREATE_FRAME="-c"
+           ;;
+       --create-frame)
+           CREATE_FRAME="-c"
+           ;;
+       --hello)
+           HELLO=1
+           ;;
+       *)
+           # We should never end up here.
+           echo "$0: internal error (option ${opt})." >&2
+           exit 1
+           ;;
+    esac
+
+    shift $((OPTIND - 1))
+    OPTIND=1
+done
+
+# Positional parameters.
+for arg; do
+    escape -v arg "${arg}"
+    case $arg in
+       mailto:*)
+           if [ -n "${MAILTO}" ]; then
+               echo "$0: more than one mailto: argument." >&2
+               exit 1
+           fi
+           MAILTO="${arg}"
+           ;;
+       *)
+           ELISP="${ELISP} (message-goto-to) (insert \"${arg}, \")"
+           ;;
+    esac
+done
+
+if [ -n "${MAILTO}" ]; then
+    if [ -n "${ELISP}" ]; then
+       echo "$0: mailto: is not compatible with other message parameters." >&2
+       exit 1
+    fi
+    ELISP="(browse-url-mail \"${MAILTO}\")"
+elif [ -z "${ELISP}" -a -n "${HELLO}" ]; then
+    ELISP="(notmuch)"
+else
+    ELISP="(notmuch-mua-new-mail) ${ELISP}"
+fi
+
+# Kill the terminal/frame if we're creating one.
+if [ -z "$USE_EMACSCLIENT" -o -n "$CREATE_FRAME" -o -n "$NO_WINDOW" ]; then
+    ELISP="${ELISP} (message-add-action #'save-buffers-kill-terminal 'exit)"
+fi
+
+escape -v pwd "$PWD"
+
+# The crux of it all: construct an elisp progn and eval it.
+ELISP="(prog1 'done (require 'notmuch) (cd \"$pwd\") ${ELISP})"
+
+if [ -n "$PRINT_ONLY" ]; then
+    echo ${ELISP}
+    exit 0
+fi
+
+if [ -n "$USE_EMACSCLIENT" ]; then
+    # Evaluate the progn.
+    exec ${EMACSCLIENT} ${NO_WINDOW} ${CREATE_FRAME} ${AUTO_DAEMON} --eval "${ELISP}"
+else
+    exec ${EMACS} ${NO_WINDOW} --eval "${ELISP}"
+fi
diff --git a/emacs/notmuch-emacs-mua.desktop b/emacs/notmuch-emacs-mua.desktop
new file mode 100644 (file)
index 0000000..752a1d7
--- /dev/null
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Name=Notmuch (emacs interface)
+GenericName=Email Client
+Comment=Emacs based email client
+Exec=notmuch-emacs-mua --hello %u
+MimeType=x-scheme-handler/mailto;
+Icon=emblem-mail
+Terminal=false
+Type=Application
+Categories=Network;Email;
+Keywords=Mail;E-mail;Email;
diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el
new file mode 100644 (file)
index 0000000..4662e70
--- /dev/null
@@ -0,0 +1,1010 @@
+;;; notmuch-hello.el --- welcome to notmuch, a frontend  -*- lexical-binding: t -*-
+;;
+;; Copyright © David Edmondson
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: David Edmondson <dme@dme.org>
+
+;;; Code:
+
+(require 'widget)
+(require 'wid-edit) ; For `widget-forward'.
+
+(require 'notmuch-lib)
+(require 'notmuch-mua)
+
+(declare-function notmuch-search "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 parent-buffer oldest-first))
+(declare-function notmuch-unthreaded "notmuch-tree"
+                 (&optional query query-context target buffer-name
+                            open-target))
+
+
+;;; Options
+
+(defun notmuch-saved-search-get (saved-search field)
+  "Get FIELD from SAVED-SEARCH.
+
+If SAVED-SEARCH is a plist, this is just `plist-get', but for
+backwards compatibility, this also deals with the two other
+possible formats for SAVED-SEARCH: cons cells (NAME . QUERY) and
+lists (NAME QUERY COUNT-QUERY)."
+  (cond
+   ((keywordp (car saved-search))
+    (plist-get saved-search field))
+   ;; It is not a plist so it is an old-style entry.
+   ((consp (cdr saved-search))
+    (pcase-let ((`(,name ,query ,count-query) saved-search))
+      (cl-case field
+       (:name name)
+       (:query query)
+       (:count-query count-query)
+       (t nil))))
+   (t
+    (pcase-let ((`(,name . ,query) saved-search))
+      (cl-case field
+       (:name name)
+       (:query query)
+       (t nil))))))
+
+(defun notmuch-hello-saved-search-to-plist (saved-search)
+  "Return a copy of SAVED-SEARCH in plist form.
+
+If saved search is a plist then just return a copy. In other
+cases, for backwards compatibility, convert to plist form and
+return that."
+  (if (keywordp (car saved-search))
+      (copy-sequence saved-search)
+    (let ((fields (list :name :query :count-query))
+         plist-search)
+      (dolist (field fields plist-search)
+       (let ((string (notmuch-saved-search-get saved-search field)))
+         (when string
+           (setq plist-search (append plist-search (list field string)))))))))
+
+(defun notmuch-hello--saved-searches-to-plist (symbol)
+  "Extract a saved-search variable into plist form.
+
+The new style saved search is just a plist, but for backwards
+compatibility we use this function to extract old style saved
+searches so they still work in customize."
+  (let ((saved-searches (default-value symbol)))
+    (mapcar #'notmuch-hello-saved-search-to-plist saved-searches)))
+
+(define-widget 'notmuch-saved-search-plist 'list
+  "A single saved search property list."
+  :tag "Saved Search"
+  :args '((list :inline t
+               :format "%v"
+               (group :format "%v" :inline t
+                      (const :format "   Name: " :name)
+                      (string :format "%v"))
+               (group :format "%v" :inline t
+                      (const :format "  Query: " :query)
+                      (string :format "%v")))
+         (checklist :inline t
+                    :format "%v"
+                    (group :format "%v" :inline t
+                           (const :format "Shortcut key: " :key)
+                           (key-sequence :format "%v"))
+                    (group :format "%v" :inline t
+                           (const :format "Count-Query: " :count-query)
+                           (string :format "%v"))
+                    (group :format "%v" :inline t
+                           (const :format "" :sort-order)
+                           (choice :tag " Sort Order"
+                                   (const :tag "Default" nil)
+                                   (const :tag "Oldest-first" oldest-first)
+                                   (const :tag "Newest-first" newest-first)))
+                    (group :format "%v" :inline t
+                           (const :format "" :search-type)
+                           (choice :tag " Search Type"
+                                   (const :tag "Search mode" nil)
+                                   (const :tag "Tree mode" tree)
+                                   (const :tag "Unthreaded mode" unthreaded))))))
+
+(defcustom notmuch-saved-searches
+  `((:name "inbox" :query "tag:inbox" :key ,(kbd "i"))
+    (:name "unread" :query "tag:unread" :key ,(kbd "u"))
+    (:name "flagged" :query "tag:flagged" :key ,(kbd "f"))
+    (:name "sent" :query "tag:sent" :key ,(kbd "t"))
+    (:name "drafts" :query "tag:draft" :key ,(kbd "d"))
+    (:name "all mail" :query "*" :key ,(kbd "a")))
+  "A list of saved searches to display.
+
+The saved search can be given in 3 forms. The preferred way is as
+a plist. Supported properties are
+
+  :name            Name of the search (required).
+  :query           Search to run (required).
+  :key             Optional shortcut key for `notmuch-jump-search'.
+  :count-query     Optional extra query to generate the count
+                   shown. If not present then the :query property
+                   is used.
+  :sort-order      Specify the sort order to be used for the search.
+                   Possible values are `oldest-first', `newest-first'
+                   or nil. Nil means use the default sort order.
+  :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.
+
+Other accepted forms are a cons cell of the form (NAME . QUERY)
+or a list of the form (NAME QUERY COUNT-QUERY)."
+  ;; The saved-search format is also used by the all-tags notmuch-hello
+  ;; section. This section generates its own saved-search list in one of
+  ;; the latter two forms.
+  :get 'notmuch-hello--saved-searches-to-plist
+  :type '(repeat notmuch-saved-search-plist)
+  :tag "List of Saved Searches"
+  :group 'notmuch-hello)
+
+(defcustom notmuch-hello-recent-searches-max 10
+  "The number of recent searches to display."
+  :type 'integer
+  :group 'notmuch-hello)
+
+(defcustom notmuch-show-empty-saved-searches nil
+  "Should saved searches with no messages be listed?"
+  :type 'boolean
+  :group 'notmuch-hello)
+
+(defun notmuch-sort-saved-searches (saved-searches)
+  "Generate an alphabetically sorted saved searches list."
+  (sort (copy-sequence saved-searches)
+       (lambda (a b)
+         (string< (notmuch-saved-search-get a :name)
+                  (notmuch-saved-search-get b :name)))))
+
+(defcustom notmuch-saved-search-sort-function nil
+  "Function used to sort the saved searches for the notmuch-hello view.
+
+This variable controls how saved searches should be sorted. No
+sorting (nil) displays the saved searches in the order they are
+stored in `notmuch-saved-searches'. Sort alphabetically sorts the
+saved searches in alphabetical order. Custom sort function should
+be a function or a lambda expression that takes the saved
+searches list as a parameter, and returns a new saved searches
+list to be used. For compatibility with the various saved-search
+formats it should use notmuch-saved-search-get to access the
+fields of the search."
+  :type '(choice (const :tag "No sorting" nil)
+                (const :tag "Sort alphabetically" notmuch-sort-saved-searches)
+                (function :tag "Custom sort function"
+                          :value notmuch-sort-saved-searches))
+  :group 'notmuch-hello)
+
+(defvar notmuch-hello-indent 4
+  "How much to indent non-headers.")
+
+(defimage notmuch-hello-logo ((:type svg :file "notmuch-logo.svg")))
+
+(defcustom notmuch-show-logo t
+  "Should the notmuch logo be shown?"
+  :type 'boolean
+  :group 'notmuch-hello)
+
+(defcustom notmuch-show-all-tags-list nil
+  "Should all tags be shown in the notmuch-hello view?"
+  :type 'boolean
+  :group 'notmuch-hello)
+
+(defcustom notmuch-hello-tag-list-make-query nil
+  "Function or string to generate queries for the all tags list.
+
+This variable controls which query results are shown for each tag
+in the \"all tags\" list. If nil, it will use all messages with
+that tag. If this is set to a string, it is used as a filter for
+messages having that tag (equivalent to \"tag:TAG and (THIS-VARIABLE)\").
+Finally this can be a function that will be called for each tag and
+should return a filter for that tag, or nil to hide the tag."
+  :type '(choice (const :tag "All messages" nil)
+                (const :tag "Unread messages" "tag:unread")
+                (string :tag "Custom filter"
+                        :value "tag:unread")
+                (function :tag "Custom filter function"))
+  :group 'notmuch-hello)
+
+(defcustom notmuch-hello-hide-tags nil
+  "List of tags to be hidden in the \"all tags\"-section."
+  :type '(repeat string)
+  :group 'notmuch-hello)
+
+(defface notmuch-hello-logo-background
+  '((((class color)
+      (background dark))
+     (:background "#5f5f5f"))
+    (((class color)
+      (background light))
+     (:background "white")))
+  "Background colour for the notmuch logo."
+  :group 'notmuch-hello
+  :group 'notmuch-faces)
+
+(defcustom notmuch-column-control t
+  "Controls the number of columns for saved searches/tags in notmuch view.
+
+This variable has three potential sets of values:
+
+- t: automatically calculate the number of columns possible based
+  on the tags to be shown and the window width,
+- an integer: a lower bound on the number of characters that will
+  be used to display each column,
+- a float: a fraction of the window width that is the lower bound
+  on the number of characters that should be used for each
+  column.
+
+So:
+- if you would like two columns of tags, set this to 0.5.
+- if you would like a single column of tags, set this to 1.0.
+- if you would like tags to be 30 characters wide, set this to
+  30.
+- if you don't want to worry about all of this nonsense, leave
+  this set to `t'."
+  :type '(choice
+         (const :tag "Automatically calculated" t)
+         (integer :tag "Number of characters")
+         (float :tag "Fraction of window"))
+  :group 'notmuch-hello)
+
+(defcustom notmuch-hello-thousands-separator " "
+  "The string used as a thousands separator.
+
+Typically \",\" in the US and UK and \".\" or \" \" in Europe.
+The latter is recommended in the SI/ISO 31-0 standard and by the
+International Bureau of Weights and Measures."
+  :type 'string
+  :group 'notmuch-hello)
+
+(defcustom notmuch-hello-mode-hook nil
+  "Functions called after entering `notmuch-hello-mode'."
+  :type 'hook
+  :group 'notmuch-hello
+  :group 'notmuch-hooks)
+
+(defcustom notmuch-hello-refresh-hook nil
+  "Functions called after updating a `notmuch-hello' buffer."
+  :type 'hook
+  :group 'notmuch-hello
+  :group 'notmuch-hooks)
+
+(defconst notmuch-hello-url "https://notmuchmail.org"
+  "The `notmuch' web site.")
+
+(defvar notmuch-hello-custom-section-options
+  '((:filter (string :tag "Filter for each tag"))
+    (:filter-count (string :tag "Different filter to generate message counts"))
+    (:initially-hidden (const :tag "Hide this section on startup" t))
+    (:show-empty-searches (const :tag "Show queries with no matching messages" t))
+    (:hide-if-empty (const :tag "Hide this section if all queries are empty
+\(and not shown by show-empty-searches)" t)))
+  "Various customization-options for notmuch-hello-tags/query-section.")
+
+(define-widget 'notmuch-hello-tags-section 'lazy
+  "Customize-type for notmuch-hello tag-list sections."
+  :tag "Customized tag-list section (see docstring for details)"
+  :type
+  `(list :tag ""
+        (const :tag "" notmuch-hello-insert-tags-section)
+        (string :tag "Title for this section")
+        (plist
+         :inline t
+         :options
+         ,(append notmuch-hello-custom-section-options
+                  '((:hide-tags (repeat :tag "Tags that will be hidden"
+                                        string)))))))
+
+(define-widget 'notmuch-hello-query-section 'lazy
+  "Customize-type for custom saved-search-like sections"
+  :tag "Customized queries section (see docstring for details)"
+  :type
+  `(list :tag ""
+        (const :tag "" notmuch-hello-insert-searches)
+        (string :tag "Title for this section")
+        (repeat :tag "Queries"
+                (cons (string :tag "Name") (string :tag "Query")))
+        (plist :inline t :options ,notmuch-hello-custom-section-options)))
+
+(defcustom notmuch-hello-sections
+  (list #'notmuch-hello-insert-header
+       #'notmuch-hello-insert-saved-searches
+       #'notmuch-hello-insert-search
+       #'notmuch-hello-insert-recent-searches
+       #'notmuch-hello-insert-alltags
+       #'notmuch-hello-insert-footer)
+  "Sections for notmuch-hello.
+
+The list contains functions which are used to construct sections in
+notmuch-hello buffer.  When notmuch-hello buffer is constructed,
+these functions are run in the order they appear in this list.  Each
+function produces a section simply by adding content to the current
+buffer.  A section should not end with an empty line, because a
+newline will be inserted after each section by `notmuch-hello'.
+
+Each function should take no arguments. The return value is
+ignored.
+
+For convenience an element can also be a list of the form (FUNC ARG1
+ARG2 .. ARGN) in which case FUNC will be applied to the rest of the
+list.
+
+A \"Customized tag-list section\" item in the customize-interface
+displays a list of all tags, optionally hiding some of them. It
+is also possible to filter the list of messages matching each tag
+by an additional filter query. Similarly, the count of messages
+displayed next to the buttons can be generated by applying a
+different filter to the tag query. These filters are also
+supported for \"Customized queries section\" items."
+  :group 'notmuch-hello
+  :type
+  '(repeat
+    (choice (function-item notmuch-hello-insert-header)
+           (function-item notmuch-hello-insert-saved-searches)
+           (function-item notmuch-hello-insert-search)
+           (function-item notmuch-hello-insert-recent-searches)
+           (function-item notmuch-hello-insert-alltags)
+           (function-item notmuch-hello-insert-footer)
+           (function-item notmuch-hello-insert-inbox)
+           notmuch-hello-tags-section
+           notmuch-hello-query-section
+           (function :tag "Custom section"))))
+
+(defcustom notmuch-hello-auto-refresh t
+  "Automatically refresh when returning to the notmuch-hello buffer."
+  :group 'notmuch-hello
+  :type 'boolean)
+
+;;; Internal variables
+
+(defvar notmuch-hello-hidden-sections nil
+  "List of sections titles whose contents are hidden.")
+
+(defvar notmuch-hello-first-run t
+  "True if `notmuch-hello' is run for the first time, set to nil afterwards.")
+
+;;; Widgets for inserters
+
+(define-widget 'notmuch-search-item 'item
+  "A recent search."
+  :format "%v\n"
+  :value-create 'notmuch-search-item-value-create)
+
+(defun notmuch-search-item-value-create (widget)
+  (let ((value (widget-get widget :value)))
+    (widget-insert (make-string notmuch-hello-indent ?\s))
+    (widget-create 'editable-field
+                  :size (widget-get widget :size)
+                  :parent widget
+                  :action #'notmuch-hello-search
+                  value)
+    (widget-insert " ")
+    (widget-create 'push-button
+                  :parent widget
+                  :notify #'notmuch-hello-add-saved-search
+                  "save")
+    (widget-insert " ")
+    (widget-create 'push-button
+                  :parent widget
+                  :notify #'notmuch-hello-delete-search-from-history
+                  "del")))
+
+(defun notmuch-search-item-field-width ()
+  (max 8 ; Don't let the search boxes be less than 8 characters wide.
+       (- (window-width)
+         notmuch-hello-indent ; space at bol
+         notmuch-hello-indent ; space at eol
+         1    ; for the space before the [save] button
+         6    ; for the [save] button
+         1    ; for the space before the [del] button
+         5))) ; for the [del] button
+
+;;; Widget actions
+
+(defun notmuch-hello-search (widget &rest _event)
+  (let ((search (widget-value widget)))
+    (when search
+      (setq search (string-trim search))
+      (let ((history-delete-duplicates t))
+       (add-to-history 'notmuch-search-history search)))
+    (notmuch-search search notmuch-search-oldest-first)))
+
+(defun notmuch-hello-add-saved-search (widget &rest _event)
+  (let ((search (widget-value (widget-get widget :parent)))
+       (name (completing-read "Name for saved search: "
+                              notmuch-saved-searches)))
+    ;; If an existing saved search with this name exists, remove it.
+    (setq notmuch-saved-searches
+         (cl-loop for elem in notmuch-saved-searches
+                  unless (equal name (notmuch-saved-search-get elem :name))
+                  collect elem))
+    ;; Add the new one.
+    (customize-save-variable 'notmuch-saved-searches
+                            (add-to-list 'notmuch-saved-searches
+                                         (list :name name :query search) t))
+    (message "Saved '%s' as '%s'." search name)
+    (notmuch-hello-update)))
+
+(defun notmuch-hello-delete-search-from-history (widget &rest _event)
+  (when (y-or-n-p "Are you sure you want to delete this search? ")
+    (let ((search (widget-value (widget-get widget :parent))))
+      (setq notmuch-search-history
+           (delete search notmuch-search-history)))
+    (notmuch-hello-update)))
+
+;;; Button utilities
+
+;; `notmuch-hello-query-counts', `notmuch-hello-nice-number' and
+;; `notmuch-hello-insert-buttons' are used outside this section.
+;; All other functions that are defined in this section are only
+;; used by these two functions.
+
+(defun notmuch-hello-longest-label (searches-alist)
+  (or (cl-loop for elem in searches-alist
+              maximize (length (notmuch-saved-search-get elem :name)))
+      0))
+
+(defun notmuch-hello-reflect-generate-row (ncols nrows row list)
+  (let ((len (length list)))
+    (cl-loop for col from 0 to (- ncols 1)
+            collect (let ((offset (+ (* nrows col) row)))
+                      (if (< offset len)
+                          (nth offset list)
+                        ;; Don't forget to insert an empty slot in the
+                        ;; output matrix if there is no corresponding
+                        ;; value in the input matrix.
+                        nil)))))
+
+(defun notmuch-hello-reflect (list ncols)
+  "Reflect a `ncols' wide matrix represented by `list' along the
+diagonal."
+  ;; Not very lispy...
+  (let ((nrows (ceiling (length list) ncols)))
+    (cl-loop for row from 0 to (- nrows 1)
+            append (notmuch-hello-reflect-generate-row ncols nrows row list))))
+
+(defun notmuch-hello-widget-search (widget &rest _ignore)
+  (cl-case (widget-get widget :notmuch-search-type)
+   (tree
+    (let ((n (notmuch-search-format-buffer-name (widget-value widget) "tree" t)))
+      (notmuch-tree (widget-get widget :notmuch-search-terms)
+                   nil nil n nil nil nil
+                   (widget-get widget :notmuch-search-oldest-first))))
+   (unthreaded
+    (let ((n (notmuch-search-format-buffer-name (widget-value widget)
+                                               "unthreaded" t)))
+      (notmuch-unthreaded (widget-get widget :notmuch-search-terms) nil nil n)))
+   (t
+    (notmuch-search (widget-get widget :notmuch-search-terms)
+                   (widget-get widget :notmuch-search-oldest-first)))))
+
+(defun notmuch-saved-search-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
+should be. Returns a cons cell `(tags-per-line width)'."
+  (let ((tags-per-line
+        (cond
+         ((integerp notmuch-column-control)
+          (max 1
+               (/ (- (window-width) notmuch-hello-indent)
+                  ;; Count is 9 wide (8 digits plus space), 1 for the space
+                  ;; after the name.
+                  (+ 9 1 (max notmuch-column-control widest)))))
+         ((floatp notmuch-column-control)
+          (let* ((available-width (- (window-width) notmuch-hello-indent))
+                 (proposed-width (max (* available-width notmuch-column-control)
+                                      widest)))
+            (floor available-width proposed-width)))
+         (t
+          (max 1
+               (/ (- (window-width) notmuch-hello-indent)
+                  ;; Count is 9 wide (8 digits plus space), 1 for the space
+                  ;; after the name.
+                  (+ 9 1 widest)))))))
+    (cons tags-per-line (/ (max 1
+                               (- (window-width) notmuch-hello-indent
+                                  ;; Count is 9 wide (8 digits plus
+                                  ;; space), 1 for the space after the
+                                  ;; name.
+                                  (* tags-per-line (+ 9 1))))
+                          tags-per-line))))
+
+(defun notmuch-hello-filtered-query (query filter)
+  "Constructs a query to search all messages matching QUERY and FILTER.
+
+If FILTER is a string, it is directly used in the returned query.
+
+If FILTER is a function, it is called with QUERY as a parameter and
+the string it returns is used as the query. If nil is returned,
+the entry is hidden.
+
+Otherwise, FILTER is ignored."
+  (cond
+   ((functionp filter) (funcall filter query))
+   ((stringp filter)
+    (concat "(" query ") and (" filter ")"))
+   (t query)))
+
+(defun notmuch-hello-query-counts (query-list &rest options)
+  "Compute list of counts of matched messages from QUERY-LIST.
+
+QUERY-LIST must be a list of saved-searches. Ideally each of
+these is a plist but other options are available for backwards
+compatibility: see `notmuch-saved-searches' for details.
+
+The result is a list of plists each of which includes the
+properties :name NAME, :query QUERY and :count COUNT, together
+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'. :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)
+                            (notmuch-saved-search-get elem :query))))
+       (insert
+        (replace-regexp-in-string
+         "\n" " "
+         (notmuch-hello-filtered-query count-query
+                                       (or (plist-get options :filter-count)
+                                           (plist-get options :filter))))
+        "\n")))
+    (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
+--batch'. In general we recommend running matching versions of
+the CLI and emacs interface."))
+    (goto-char (point-min))
+    (cl-mapcan
+     (lambda (elem)
+       (let* ((elem-plist (notmuch-hello-saved-search-to-plist elem))
+             (search-query (plist-get elem-plist :query))
+             (filtered-query (notmuch-hello-filtered-query
+                              search-query (plist-get options :filter)))
+             (message-count (prog1 (read (current-buffer))
+                              (forward-line 1))))
+        (when (and filtered-query (or (plist-get options :show-empty-searches)
+                                      (> message-count 0)))
+          (setq elem-plist (plist-put elem-plist :query filtered-query))
+          (list (plist-put elem-plist :count message-count)))))
+     query-list)))
+
+(defun notmuch-hello-nice-number (n)
+  (let (result)
+    (while (> n 0)
+      (push (% n 1000) result)
+      (setq n (/ n 1000)))
+    (setq result (or result '(0)))
+    (apply #'concat
+          (number-to-string (car result))
+          (mapcar (lambda (elem)
+                    (format "%s%03d" notmuch-hello-thousands-separator elem))
+                  (cdr result)))))
+
+(defun notmuch-hello-insert-buttons (searches)
+  "Insert buttons for SEARCHES.
+
+SEARCHES must be a list of plists each of which should contain at
+least the properties :name NAME :query QUERY and :count COUNT,
+where QUERY is the query to start when the button for the
+corresponding entry is activated, and COUNT should be the number
+of messages matching the query.  Such a plist can be computed
+with `notmuch-hello-query-counts'."
+  (let* ((widest (notmuch-hello-longest-label searches))
+        (tags-and-width (notmuch-hello-tags-per-line widest))
+        (tags-per-line (car tags-and-width))
+        (column-width (cdr tags-and-width))
+        (column-indent 0)
+        (count 0)
+        (reordered-list (notmuch-hello-reflect searches tags-per-line))
+        ;; Hack the display of the buttons used.
+        (widget-push-button-prefix "")
+        (widget-push-button-suffix ""))
+    ;; dme: It feels as though there should be a better way to
+    ;; implement this loop than using an incrementing counter.
+    (mapc (lambda (elem)
+           ;; (not elem) indicates an empty slot in the matrix.
+           (when elem
+             (when (> column-indent 0)
+               (widget-insert (make-string column-indent ? )))
+             (let* ((name (plist-get elem :name))
+                    (query (plist-get elem :query))
+                    (oldest-first (cl-case (plist-get elem :sort-order)
+                                    (newest-first nil)
+                                    (oldest-first t)
+                                    (otherwise notmuch-search-oldest-first)))
+                    (search-type (plist-get elem :search-type))
+                    (msg-count (plist-get elem :count)))
+               (widget-insert (format "%8s "
+                                      (notmuch-hello-nice-number msg-count)))
+               (widget-create 'push-button
+                              :notify #'notmuch-hello-widget-search
+                              :notmuch-search-terms query
+                              :notmuch-search-oldest-first oldest-first
+                              :notmuch-search-type search-type
+                              name)
+               (setq column-indent
+                     (1+ (max 0 (- column-width (length name)))))))
+           (cl-incf count)
+           (when (eq (% count tags-per-line) 0)
+             (setq column-indent 0)
+             (widget-insert "\n")))
+         reordered-list)
+    ;; If the last line was not full (and hence did not include a
+    ;; carriage return), insert one now.
+    (unless (eq (% count tags-per-line) 0)
+      (widget-insert "\n"))))
+
+;;; Mode
+
+(defun notmuch-hello-update ()
+  "Update the notmuch-hello buffer."
+  ;; Lazy - rebuild everything.
+  (interactive)
+  (notmuch-hello t))
+
+(defun notmuch-hello-window-configuration-change ()
+  "Hook function to update the hello buffer when it is switched to."
+  (let ((hello-buf (get-buffer "*notmuch-hello*"))
+       (do-refresh nil))
+    ;; Consider all windows in the currently selected frame, since
+    ;; that's where the configuration change happened.  This also
+    ;; refreshes our snapshot of all windows, so we have to do this
+    ;; even if we know we won't refresh (e.g., hello-buf is null).
+    (dolist (window (window-list))
+      (let ((last-buf (window-parameter window 'notmuch-hello-last-buffer))
+           (cur-buf (window-buffer window)))
+       (unless (eq last-buf cur-buf)
+         ;; This window changed or is new.  Update recorded buffer
+         ;; for next time.
+         (set-window-parameter window 'notmuch-hello-last-buffer cur-buf)
+         (when (and (eq cur-buf hello-buf) last-buf)
+           ;; The user just switched to hello in this window (hello
+           ;; is currently visible, was not visible on the last
+           ;; configuration change, and this is not a new window)
+           (setq do-refresh t)))))
+    (when (and do-refresh notmuch-hello-auto-refresh)
+      ;; Refresh hello as soon as we get back to redisplay.  On Emacs
+      ;; 24, we can't do it right here because something in this
+      ;; hook's call stack overrides hello's point placement.
+      ;; FIXME And on Emacs releases that we still support?
+      (run-at-time nil nil #'notmuch-hello t))
+    (unless hello-buf
+      ;; Clean up hook
+      (remove-hook 'window-configuration-change-hook
+                  #'notmuch-hello-window-configuration-change))))
+
+(defvar notmuch-hello-mode-map
+  ;; Inherit both widget-keymap and notmuch-common-keymap.  We have
+  ;; to use make-sparse-keymap to force this to be a new keymap (so
+  ;; that when we modify map it does not modify widget-keymap).
+  (let ((map (make-composed-keymap (list (make-sparse-keymap) widget-keymap))))
+    (set-keymap-parent map notmuch-common-keymap)
+    ;; 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.
+
+Saved searches are \"bookmarks\" for arbitrary queries. Hit RET
+or click on a saved search to view matching threads. Edit saved
+searches with the `edit' button. Type `\\[notmuch-jump-search]'
+in any Notmuch screen for quick access to saved searches that
+have shortcut keys.
+
+Type new searches in the search box and hit RET to view matching
+threads. Hit RET in a recent search box to re-submit a previous
+search. Edit it first if you like. Save a recent search to saved
+searches with the `save' button.
+
+Hit `\\[notmuch-search]' or `\\[notmuch-tree]' in any Notmuch
+screen to search for messages and view matching threads or
+messages, respectively. Recent searches are available in the
+minibuffer history.
+
+Expand the all tags view with the `show' button (and collapse
+again with the `hide' button). Hit RET or click on a tag name to
+view matching threads.
+
+Hit `\\[notmuch-refresh-this-buffer]' to refresh the screen and
+`\\[notmuch-bury-or-kill-this-buffer]' to quit.
+
+The screen may be customized via `\\[customize]'.
+
+Complete list of currently available key bindings:
+
+\\{notmuch-hello-mode-map}"
+  (setq notmuch-buffer-refresh-function #'notmuch-hello-update))
+
+;;; Inserters
+
+(defun notmuch-hello-generate-tag-alist (&optional hide-tags)
+  "Return an alist from tags to queries to display in the all-tags section."
+  (cl-mapcan (lambda (tag)
+              (and (not (member tag hide-tags))
+                   (list (cons tag
+                               (concat "tag:"
+                                       (notmuch-escape-boolean-term tag))))))
+            (notmuch--process-lines notmuch-command "search" "--output=tags" "*")))
+
+(defun notmuch-hello-insert-header ()
+  "Insert the default notmuch-hello header."
+  (when notmuch-show-logo
+    (let ((image notmuch-hello-logo))
+      ;; The notmuch logo uses transparency. That can display poorly
+      ;; when inserting the image into an emacs buffer (black logo on
+      ;; a black background), so force the background colour of the
+      ;; image. We use a face to represent the colour so that
+      ;; `defface' can be used to declare the different possible
+      ;; colours, which depend on whether the frame has a light or
+      ;; dark background.
+      (setq image (cons 'image
+                       (append (cdr image)
+                               (list :background
+                                     (face-background
+                                      'notmuch-hello-logo-background)))))
+      (insert-image image))
+    (widget-insert "  "))
+
+  (widget-insert "Welcome to ")
+  ;; Hack the display of the links used.
+  (let ((widget-link-prefix "")
+       (widget-link-suffix ""))
+    (widget-create 'link
+                  :notify (lambda (&rest _ignore)
+                            (browse-url notmuch-hello-url))
+                  :help-echo "Visit the notmuch website."
+                  "notmuch")
+    (widget-insert ". ")
+    (widget-insert "You have ")
+    (widget-create 'link
+                  :notify (lambda (&rest _ignore)
+                            (notmuch-hello-update))
+                  :help-echo "Refresh"
+                  (notmuch-hello-nice-number
+                   (string-to-number
+                    (car (notmuch--process-lines notmuch-command "count" "--exclude=false")))))
+    (widget-insert " messages.\n")))
+
+(defun notmuch-hello-insert-saved-searches ()
+  "Insert the saved-searches section."
+  (let ((searches (notmuch-hello-query-counts
+                  (if notmuch-saved-search-sort-function
+                      (funcall notmuch-saved-search-sort-function
+                               notmuch-saved-searches)
+                    notmuch-saved-searches)
+                  :show-empty-searches notmuch-show-empty-saved-searches)))
+    (when searches
+      (widget-insert "Saved searches: ")
+      (widget-create 'push-button
+                    :notify (lambda (&rest _ignore)
+                              (customize-variable 'notmuch-saved-searches))
+                    "edit")
+      (widget-insert "\n\n")
+      (let ((start (point)))
+       (notmuch-hello-insert-buttons searches)
+       (indent-rigidly start (point) notmuch-hello-indent)))))
+
+(defun notmuch-hello-insert-search ()
+  "Insert a search widget."
+  (widget-insert "Search: ")
+  (widget-create 'editable-field
+                ;; Leave some space at the start and end of the
+                ;; search boxes.
+                :size (max 8 (- (window-width) notmuch-hello-indent
+                                (length "Search: ")))
+                :action #'notmuch-hello-search)
+  ;; Add an invisible dot to make `widget-end-of-line' ignore
+  ;; trailing spaces in the search widget field.  A dot is used
+  ;; instead of a space to make `show-trailing-whitespace'
+  ;; happy, i.e. avoid it marking the whole line as trailing
+  ;; spaces.
+  (widget-insert (propertize "." 'invisible t))
+  (widget-insert "\n"))
+
+(defun notmuch-hello-insert-recent-searches ()
+  "Insert recent searches."
+  (when notmuch-search-history
+    (widget-insert "Recent searches: ")
+    (widget-create
+     'push-button
+     :notify (lambda (&rest _ignore)
+              (when (y-or-n-p "Are you sure you want to clear the searches? ")
+                (setq notmuch-search-history nil)
+                (notmuch-hello-update)))
+     "clear")
+    (widget-insert "\n\n")
+    (let ((width (notmuch-search-item-field-width)))
+      (dolist (search (seq-take notmuch-search-history
+                               notmuch-hello-recent-searches-max))
+       (widget-create 'notmuch-search-item :value search :size width)))))
+
+(defun notmuch-hello-insert-searches (title query-list &rest options)
+  "Insert a section with TITLE showing a list of buttons made from QUERY-LIST.
+
+QUERY-LIST should ideally be a plist but for backwards
+compatibility other forms are also accepted (see
+`notmuch-saved-searches' for details).  The plist should
+contain keys :name and :query; if :count-query is also present
+then it specifies an alternate query to be used to generate the
+count for the associated search.
+
+Supports the following entries in OPTIONS as a plist:
+:initially-hidden - if non-nil, section will be hidden on startup
+: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."
+  (widget-insert title ": ")
+  (when (and notmuch-hello-first-run (plist-get options :initially-hidden))
+    (add-to-list 'notmuch-hello-hidden-sections title))
+  (let ((is-hidden (member title notmuch-hello-hidden-sections))
+       (start (point)))
+    (if is-hidden
+       (widget-create 'push-button
+                      :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 (&rest _ignore)
+                              (add-to-list 'notmuch-hello-hidden-sections
+                                           title)
+                              (notmuch-hello-update))
+                    "hide"))
+    (widget-insert "\n")
+    (unless is-hidden
+      (let ((searches (apply 'notmuch-hello-query-counts query-list options)))
+       (when (or (not (plist-get options :hide-if-empty))
+                 searches)
+         (widget-insert "\n")
+         (notmuch-hello-insert-buttons searches)
+         (indent-rigidly start (point) notmuch-hello-indent))))))
+
+(defun notmuch-hello-insert-tags-section (&optional title &rest options)
+  "Insert a section displaying all tags with message counts.
+
+TITLE defaults to \"All tags\".
+Allowed options are those accepted by `notmuch-hello-insert-searches' and the
+following:
+
+:hide-tags - List of tags that should be excluded."
+  (apply 'notmuch-hello-insert-searches
+        (or title "All tags")
+        (notmuch-hello-generate-tag-alist (plist-get options :hide-tags))
+        options))
+
+(defun notmuch-hello-insert-inbox ()
+  "Show an entry for each saved search and inboxed messages for each tag."
+  (notmuch-hello-insert-searches "What's in your inbox"
+                                (append
+                                 notmuch-saved-searches
+                                 (notmuch-hello-generate-tag-alist))
+                                :filter "tag:inbox"))
+
+(defun notmuch-hello-insert-alltags ()
+  "Insert a section displaying all tags and associated message counts."
+  (notmuch-hello-insert-tags-section
+   nil
+   :initially-hidden (not notmuch-show-all-tags-list)
+   :hide-tags notmuch-hello-hide-tags
+   :filter notmuch-hello-tag-list-make-query
+   :disable-excludes t))
+
+(defun notmuch-hello-insert-footer ()
+  "Insert the notmuch-hello footer."
+  (let ((start (point)))
+    (widget-insert "Hit `?' for context-sensitive help in any Notmuch screen.\n")
+    (widget-insert "Customize ")
+    (widget-create 'link
+                  :notify (lambda (&rest _ignore)
+                            (customize-group 'notmuch))
+                  :button-prefix "" :button-suffix ""
+                  "Notmuch")
+    (widget-insert " or ")
+    (widget-create 'link
+                  :notify (lambda (&rest _ignore)
+                            (customize-variable 'notmuch-hello-sections))
+                  :button-prefix "" :button-suffix ""
+                  "this page.")
+    (let ((fill-column (- (window-width) notmuch-hello-indent)))
+      (center-region start (point)))))
+
+;;; Hello!
+
+;;;###autoload
+(defun notmuch-hello (&optional no-display)
+  "Run notmuch and display saved searches, known tags, etc."
+  (interactive)
+  (notmuch-assert-cli-sane)
+  ;; This may cause a window configuration change, so if the
+  ;; auto-refresh hook is already installed, avoid recursive refresh.
+  (let ((notmuch-hello-auto-refresh nil))
+    (if no-display
+       (set-buffer "*notmuch-hello*")
+      (pop-to-buffer-same-window "*notmuch-hello*")))
+  ;; Install auto-refresh hook
+  (when notmuch-hello-auto-refresh
+    (add-hook 'window-configuration-change-hook
+             #'notmuch-hello-window-configuration-change))
+  (let ((target-line (line-number-at-pos))
+       (target-column (current-column))
+       (inhibit-read-only t))
+    ;; Delete all editable widget fields.  Editable widget fields are
+    ;; tracked in a buffer local variable `widget-field-list' (and
+    ;; others).  If we do `erase-buffer' without properly deleting the
+    ;; widgets, some widget-related functions are confused later.
+    (mapc 'widget-delete widget-field-list)
+    (erase-buffer)
+    (unless (eq major-mode 'notmuch-hello-mode)
+      (notmuch-hello-mode))
+    (let ((all (overlay-lists)))
+      ;; Delete all the overlays.
+      (mapc 'delete-overlay (car all))
+      (mapc 'delete-overlay (cdr all)))
+    (mapc
+     (lambda (section)
+       (let ((point-before (point)))
+        (if (functionp section)
+            (funcall section)
+          (apply (car section) (cdr section)))
+        ;; don't insert a newline when the previous section didn't
+        ;; show anything.
+        (unless (eq (point) point-before)
+          (widget-insert "\n"))))
+     notmuch-hello-sections)
+    (widget-setup)
+    ;; Move point back to where it was before refresh. Use line and
+    ;; column instead of point directly to be insensitive to additions
+    ;; and removals of text within earlier lines.
+    (goto-char (point-min))
+    (forward-line (1- target-line))
+    (move-to-column target-column))
+  (run-hooks 'notmuch-hello-refresh-hook)
+  (setq notmuch-hello-first-run nil))
+
+;;; _
+
+(provide 'notmuch-hello)
+
+;;; notmuch-hello.el ends here
diff --git a/emacs/notmuch-jump.el b/emacs/notmuch-jump.el
new file mode 100644 (file)
index 0000000..6a27692
--- /dev/null
@@ -0,0 +1,210 @@
+;;; notmuch-jump.el --- User-friendly shortcut keys  -*- lexical-binding: t -*-
+;;
+;; Copyright © Austin Clements
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Austin Clements <aclements@csail.mit.edu>
+;;          David Edmondson <dme@dme.org>
+
+;;; Code:
+
+(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.
+
+This prompts for and performs a saved search using the shortcut
+keys configured in the :key property of `notmuch-saved-searches'.
+Typically these shortcuts are a single key long, so this is a
+fast way to jump to a saved search from anywhere in Notmuch."
+  (interactive)
+  ;; Build the action map
+  (let (action-map)
+    (dolist (saved-search notmuch-saved-searches)
+      (let* ((saved-search (notmuch-hello-saved-search-to-plist saved-search))
+            (key (plist-get saved-search :key)))
+       (when key
+         (let ((name (plist-get saved-search :name))
+               (query (plist-get saved-search :query))
+               (oldest-first
+                (cl-case (plist-get saved-search :sort-order)
+                  (newest-first nil)
+                  (oldest-first t)
+                  (otherwise (default-value 'notmuch-search-oldest-first)))))
+           (push (list key name
+                       (cond
+                        ((eq (plist-get saved-search :search-type) 'tree)
+                         (lambda () (notmuch-tree query)))
+                        ((eq (plist-get saved-search :search-type) 'unthreaded)
+                         (lambda () (notmuch-unthreaded query)))
+                        (t
+                         (lambda () (notmuch-search query oldest-first)))))
+                 action-map)))))
+    (setq action-map (nreverse action-map))
+    (if action-map
+       (notmuch-jump action-map "Search: ")
+      (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
+(defun notmuch-jump (action-map prompt)
+  "Interactively prompt for one of the keys in ACTION-MAP.
+
+Displays a summary of all bindings in ACTION-MAP in the
+minibuffer, reads a key from the minibuffer, and performs the
+corresponding action.  The prompt can be canceled with C-g or
+RET.  PROMPT must be a string to use for the prompt.  PROMPT
+should include a space at the end.
+
+ACTION-MAP must be a list of triples of the form
+  (KEY LABEL ACTION)
+where KEY is a key binding, LABEL is a string label to display in
+the buffer, and ACTION is a nullary function to call.  LABEL may
+be null, in which case the action will still be bound, but will
+not appear in the pop-up buffer."
+  (let* ((items (notmuch-jump--format-actions action-map))
+        ;; Format the table of bindings and the full prompt
+        (table
+         (with-temp-buffer
+           (notmuch-jump--insert-items (window-body-width) items)
+           (buffer-string)))
+        (full-prompt
+         (concat table "\n\n"
+                 (propertize prompt 'face 'minibuffer-prompt)))
+        ;; By default, the minibuffer applies the minibuffer face to
+        ;; the entire prompt.  However, we want to clearly
+        ;; distinguish bindings (which we put in the prompt face
+        ;; ourselves) from their labels, so disable the minibuffer's
+        ;; own re-face-ing.
+        (minibuffer-prompt-properties
+         (notmuch-plist-delete
+          (copy-sequence minibuffer-prompt-properties)
+          'face))
+        ;; Build the keymap with our bindings
+        (minibuffer-map (notmuch-jump--make-keymap action-map prompt))
+        ;; The bindings save the the action in notmuch-jump--action
+        (notmuch-jump--action nil))
+    ;; Read the action
+    (read-from-minibuffer full-prompt nil minibuffer-map)
+    ;; If we got an action, do it
+    (when notmuch-jump--action
+      (funcall notmuch-jump--action))))
+
+(defun notmuch-jump--format-actions (action-map)
+  "Format the actions in ACTION-MAP.
+
+Returns a list of strings, one for each item with a label in
+ACTION-MAP.  These strings can be inserted into a tabular
+buffer."
+  ;; Compute the maximum key description width
+  (let ((key-width 1))
+    (pcase-dolist (`(,key ,_desc) action-map)
+      (setq key-width
+           (max key-width
+                (string-width (format-kbd-macro key)))))
+    ;; Format each action
+    (mapcar (pcase-lambda (`(,key ,desc))
+             (setq key (format-kbd-macro key))
+             (concat (propertize key 'face 'notmuch-jump-key)
+                     (make-string (- key-width (length key)) ? )
+                     " " desc))
+           action-map)))
+
+(defun notmuch-jump--insert-items (width items)
+  "Make a table of ITEMS up to WIDTH wide in the current buffer."
+  (let* ((nitems (length items))
+        (col-width (+ 3 (apply #'max (mapcar #'string-width items))))
+        (ncols (if (> (* col-width nitems) width)
+                   (max 1 (/ width col-width))
+                 ;; Items fit on one line.  Space them out
+                 (setq col-width (/ width nitems))
+                 (length items))))
+    (while items
+      (dotimes (col ncols)
+       (when items
+         (let ((item (pop items)))
+           (insert item)
+           (when (and items (< col (- ncols 1)))
+             (insert (make-string (- col-width (string-width item)) ? ))))))
+      (when items
+       (insert "\n")))))
+
+(defvar notmuch-jump-minibuffer-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map minibuffer-local-map)
+    ;; Make this like a special-mode keymap, with no self-insert-command
+    (suppress-keymap map)
+    (define-key map (kbd "DEL") 'exit-minibuffer)
+    map)
+  "Base keymap for notmuch-jump's minibuffer keymap.")
+
+(defun notmuch-jump--make-keymap (action-map prompt)
+  "Translate ACTION-MAP into a minibuffer keymap."
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map notmuch-jump-minibuffer-map)
+    (pcase-dolist (`(,key ,_name ,fn) action-map)
+      (when (= (length key) 1)
+       (define-key map key
+         (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.
+    (pcase-dolist (`(,key ,_name ,_fn) action-map)
+      (when (> (length key) 1)
+       (let* ((key (elt key 0))
+              (keystr (string key))
+              (new-prompt (concat prompt (format-kbd-macro keystr) " "))
+              (action-submap nil))
+         (unless (lookup-key map keystr)
+           (pcase-dolist (`(,k ,n ,f) action-map)
+             (when (= key (elt k 0))
+               (push (list (substring k 1) n f) action-submap)))
+           ;; We deal with backspace specially
+           (push (list (kbd "DEL")
+                       "Backup"
+                       (apply-partially #'notmuch-jump action-map prompt))
+                 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)))))))
+    map))
+
+(provide 'notmuch-jump)
+
+;;; notmuch-jump.el ends here
diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el
new file mode 100644 (file)
index 0000000..a09f4ab
--- /dev/null
@@ -0,0 +1,1075 @@
+;;; notmuch-lib.el --- common variables, functions and function declarations  -*- lexical-binding: t -*-
+;;
+;; Copyright © Carl Worth
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Carl Worth <cworth@cworth.org>
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'pcase)
+(require 'subr-x)
+
+(require 'mm-util)
+(require 'mm-view)
+(require 'mm-decode)
+
+(require 'notmuch-compat)
+
+(unless (require 'notmuch-version nil t)
+  (defconst notmuch-emacs-version "unknown"
+    "Placeholder variable when notmuch-version.el[c] is not available."))
+
+;;; Groups
+
+(defgroup notmuch nil
+  "Notmuch mail reader for Emacs."
+  :group 'mail)
+
+(defgroup notmuch-hello nil
+  "Overview of saved searches, tags, etc."
+  :group 'notmuch)
+
+(defgroup notmuch-search nil
+  "Searching and sorting mail."
+  :group 'notmuch)
+
+(defgroup notmuch-show nil
+  "Showing messages and threads."
+  :group 'notmuch)
+
+(defgroup notmuch-send nil
+  "Sending messages from Notmuch."
+  :group 'notmuch
+  :group 'message)
+
+(defgroup notmuch-tag nil
+  "Tags and tagging in Notmuch."
+  :group 'notmuch)
+
+(defgroup notmuch-crypto nil
+  "Processing and display of cryptographic MIME parts."
+  :group 'notmuch)
+
+(defgroup notmuch-hooks nil
+  "Running custom code on well-defined occasions."
+  :group 'notmuch)
+
+(defgroup notmuch-external nil
+  "Running external commands from within Notmuch."
+  :group 'notmuch)
+
+(defgroup notmuch-address nil
+  "Address completion."
+  :group 'notmuch)
+
+(defgroup notmuch-faces nil
+  "Graphical attributes for displaying text"
+  :group 'notmuch)
+
+;;; Options
+
+(defcustom notmuch-command "notmuch"
+  "Name of the notmuch binary.
+
+This can be a relative or absolute path to the notmuch binary.
+If this is a relative path, it will be searched for in all of the
+directories given in `exec-path' (which is, by default, based on
+$PATH)."
+  :type 'string
+  :group 'notmuch-external)
+
+(defcustom notmuch-search-oldest-first t
+  "Show the oldest mail first when searching.
+
+This variable defines the default sort order for displaying
+search results. Note that any filtered searches created by
+`notmuch-search-filter' retain the search order of the parent
+search."
+  :type 'boolean
+  :group 'notmuch-search)
+(make-variable-buffer-local 'notmuch-search-oldest-first)
+
+(defcustom notmuch-poll-script nil
+  "[Deprecated] Command to run to incorporate new mail into the notmuch database.
+
+This option has been deprecated in favor of \"notmuch new\"
+hooks (see man notmuch-hooks).  To change the path to the notmuch
+binary, customize `notmuch-command'.
+
+This variable controls the action invoked by
+`notmuch-poll-and-refresh-this-buffer' (bound by default to 'G')
+to incorporate new mail into the notmuch database.
+
+If set to nil (the default), new mail is processed by invoking
+\"notmuch new\". Otherwise, this should be set to a string that
+gives the name of an external script that processes new mail. If
+set to the empty string, no command will be run.
+
+The external script could do any of the following depending on
+the user's needs:
+
+1. Invoke a program to transfer mail to the local mail store
+2. Invoke \"notmuch new\" to incorporate the new mail
+3. Invoke one or more \"notmuch tag\" commands to classify the mail"
+  :type '(choice (const :tag "notmuch new" nil)
+                (const :tag "Disabled" "")
+                (string :tag "Custom script"))
+  :group 'notmuch-external)
+
+(defcustom notmuch-archive-tags '("-inbox")
+  "List of tag changes to apply to a message or a thread when it is archived.
+
+Tags starting with \"+\" (or not starting with either \"+\" or
+\"-\") in the list will be added, and tags starting with \"-\"
+will be removed from the message or thread being archived.
+
+For example, if you wanted to remove an \"inbox\" tag and add an
+\"archived\" tag, you would set:
+    (\"-inbox\" \"+archived\")"
+  :type '(repeat string)
+  :group 'notmuch-search
+  :group 'notmuch-show)
+
+;;; Variables
+
+(defvar notmuch-search-history nil
+  "Variable to store notmuch searches history.")
+
+(defvar notmuch-common-keymap
+  (let ((map (make-sparse-keymap)))
+    (define-key map "?" 'notmuch-help)
+    (define-key map "v" 'notmuch-version)
+    (define-key map "q" 'notmuch-bury-or-kill-this-buffer)
+    (define-key map "s" 'notmuch-search)
+    (define-key map "t" 'notmuch-search-by-tag)
+    (define-key map "z" 'notmuch-tree)
+    (define-key map "u" 'notmuch-unthreaded)
+    (define-key map "m" 'notmuch-mua-new-mail)
+    (define-key map "g" 'notmuch-refresh-this-buffer)
+    (define-key map "=" 'notmuch-refresh-this-buffer)
+    (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.")
+
+;; By default clicking on a button does not select the window
+;; containing the button (as opposed to clicking on a widget which
+;; does). This means that the button action is then executed in the
+;; current selected window which can cause problems if the button
+;; changes the buffer (e.g., id: links) or moves point.
+;;
+;; This provides a button type which overrides mouse-action so that
+;; the button's window is selected before the action is run. Other
+;; notmuch buttons can get the same behaviour by inheriting from this
+;; button type.
+(define-button-type 'notmuch-button-type
+  'mouse-action (lambda (button)
+                 (select-window (posn-window (event-start last-input-event)))
+                 (button-activate button)))
+
+;;; CLI Utilities
+
+(defun notmuch-command-to-string (&rest args)
+  "Synchronously invoke \"notmuch\" with the given list of arguments.
+
+If notmuch exits with a non-zero status, output from the process
+will appear in a buffer named \"*Notmuch errors*\" and an error
+will be signaled.
+
+Otherwise the output will be returned."
+  (with-temp-buffer
+    (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)))
+
+(defvar notmuch--cli-sane-p nil
+  "Cache whether the CLI seems to be configured sanely.")
+
+(defun notmuch-cli-sane-p ()
+  "Return t if the cli seems to be configured sanely."
+  (unless notmuch--cli-sane-p
+    (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)
+
+(defun notmuch-assert-cli-sane ()
+  (unless (notmuch-cli-sane-p)
+    (notmuch-logged-error
+     "notmuch cli seems misconfigured or unconfigured."
+     "Perhaps you haven't run \"notmuch setup\" yet? Try running this
+on the command line, and then retry your notmuch command")))
+
+(defun notmuch-cli-version ()
+  "Return a string with the notmuch cli command version number."
+  (let ((long-string
+        ;; Trim off the trailing newline.
+        (substring (notmuch-command-to-string "--version") 0 -1)))
+    (if (string-match "^notmuch\\( version\\)? \\(.*\\)$"
+                     long-string)
+       (match-string 2 long-string)
+      "unknown")))
+
+(defvar notmuch-emacs-version)
+
+(defun notmuch-version ()
+  "Display the notmuch version.
+The versions of the Emacs package and the `notmuch' executable
+should match, but if and only if they don't, then this command
+displays both values separately."
+  (interactive)
+  (let ((cli-version (notmuch-cli-version)))
+    (message "notmuch version %s"
+            (if (string= notmuch-emacs-version cli-version)
+                cli-version
+              (concat cli-version
+                      " (emacs mua version " notmuch-emacs-version ")")))))
+
+;;; Notmuch Configuration
+
+(defun notmuch-config-get (item)
+  "Return a value from the notmuch configuration."
+  (let* ((val (notmuch-command-to-string "config" "get" item))
+        (len (length val)))
+    ;; Trim off the trailing newline (if the value is empty or not
+    ;; configured, there will be no newline).
+    (if (and (> len 0)
+            (= (aref val (- len 1)) ?\n))
+       (substring val 0 -1)
+      val)))
+
+(defun notmuch-database-path ()
+  "Return the database.path value from the notmuch configuration."
+  (notmuch-config-get "database.path"))
+
+(defun notmuch-user-name ()
+  "Return the user.name value from the notmuch configuration."
+  (notmuch-config-get "user.name"))
+
+(defun notmuch-user-primary-email ()
+  "Return the user.primary_email value from the notmuch configuration."
+  (notmuch-config-get "user.primary_email"))
+
+(defun notmuch-user-other-email ()
+  "Return the user.other_email value (as a list) from the notmuch configuration."
+  (split-string (notmuch-config-get "user.other_email") "\n" t))
+
+(defun notmuch-user-emails ()
+  (cons (notmuch-user-primary-email) (notmuch-user-other-email)))
+
+;;; Commands
+
+(defun notmuch-poll ()
+  "Run \"notmuch new\" or an external script to import mail.
+
+Invokes `notmuch-poll-script', \"notmuch new\", or does nothing
+depending on the value of `notmuch-poll-script'."
+  (interactive)
+  (message "Polling mail...")
+  (if (stringp notmuch-poll-script)
+      (unless (string-empty-p notmuch-poll-script)
+       (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"))
+
+(defun notmuch-bury-or-kill-this-buffer ()
+  "Undisplay the current buffer.
+
+Bury the current buffer, unless there is only one window showing
+it, in which case it is killed."
+  (interactive)
+  (if (> (length (get-buffer-window-list nil nil t)) 1)
+      (bury-buffer)
+    (kill-buffer)))
+
+;;; Describe Key Bindings
+
+(defun notmuch-prefix-key-description (key)
+  "Given a prefix key code, return a human-readable string representation.
+
+This is basically just `format-kbd-macro' but we also convert ESC to M-."
+  (let* ((key-vector (if (vectorp key) key (vector key)))
+        (desc (format-kbd-macro key-vector)))
+    (if (string= desc "ESC")
+       "M-"
+      (concat desc " "))))
+
+(defun notmuch-describe-key (actual-key binding prefix ua-keys tail)
+  "Prepend cons cells describing prefix-arg ACTUAL-KEY and ACTUAL-KEY to TAIL.
+
+It does not prepend if ACTUAL-KEY is already listed in TAIL."
+  (let ((key-string (concat prefix (key-description actual-key))))
+    ;; We don't include documentation if the key-binding is
+    ;; over-ridden. Note, over-riding a binding automatically hides the
+    ;; prefixed version too.
+    (unless (assoc key-string tail)
+      (when (and ua-keys (symbolp binding)
+                (get binding 'notmuch-prefix-doc))
+       ;; Documentation for prefixed command
+       (let ((ua-desc (key-description ua-keys)))
+         (push (cons (concat ua-desc " " prefix (format-kbd-macro actual-key))
+                     (get binding 'notmuch-prefix-doc))
+               tail)))
+      ;; Documentation for command
+      (push (cons key-string
+                 (or (and (symbolp binding)
+                          (get binding 'notmuch-doc))
+                     (and (functionp binding)
+                          (let ((doc (documentation binding)))
+                            (and doc
+                                 (string-match "\\`.+" doc)
+                                 (match-string 0 doc))))))
+           tail)))
+  tail)
+
+(defun notmuch-describe-remaps (remap-keymap ua-keys base-keymap prefix tail)
+  ;; Remappings are represented as a binding whose first "event" is
+  ;; 'remap.  Hence, if the keymap has any remappings, it will have a
+  ;; binding whose "key" is 'remap, and whose "binding" is itself a
+  ;; keymap that maps not from keys to commands, but from old (remapped)
+  ;; functions to the commands to use in their stead.
+  (map-keymap (lambda (command binding)
+               (mapc (lambda (actual-key)
+                       (setq tail
+                             (notmuch-describe-key actual-key binding
+                                                   prefix ua-keys tail)))
+                     (where-is-internal command base-keymap)))
+             remap-keymap)
+  tail)
+
+(defun notmuch-describe-keymap (keymap ua-keys base-keymap &optional prefix tail)
+  "Return a list of cons cells, each describing one binding in KEYMAP.
+
+Each cons cell consists of a string giving a human-readable
+description of the key, and a one-line description of the bound
+function.  See `notmuch-help' for an overview of how this
+documentation is extracted.
+
+UA-KEYS should be a key sequence bound to `universal-argument'.
+It will be used to describe bindings of commands that support a
+prefix argument.  PREFIX and TAIL are used internally."
+  (map-keymap
+   (lambda (key binding)
+     (cond ((mouse-event-p key) nil)
+          ((keymapp binding)
+           (setq tail
+                 (if (eq key 'remap)
+                     (notmuch-describe-remaps
+                      binding ua-keys base-keymap prefix tail)
+                   (notmuch-describe-keymap
+                    binding ua-keys base-keymap
+                    (notmuch-prefix-key-description key)
+                    tail))))
+          (binding
+           (setq tail
+                 (notmuch-describe-key (vector key)
+                                       binding prefix ua-keys tail)))))
+   keymap)
+  tail)
+
+(defun notmuch-substitute-command-keys (doc)
+  "Like `substitute-command-keys' but with documentation, not function names."
+  (let ((beg 0))
+    (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
+      (let ((desc
+            (save-match-data
+              (let* ((keymap-name (substring doc
+                                             (match-beginning 1)
+                                             (match-end 1)))
+                     (keymap (symbol-value (intern keymap-name)))
+                     (ua-keys (where-is-internal 'universal-argument keymap t))
+                     (desc-alist (notmuch-describe-keymap keymap ua-keys keymap))
+                     (desc-list (mapcar (lambda (arg)
+                                          (concat (car arg) "\t" (cdr arg)))
+                                        desc-alist)))
+                (mapconcat #'identity desc-list "\n")))))
+       (setq doc (replace-match desc 1 1 doc)))
+      (setq beg (match-end 0)))
+    doc))
+
+(defun notmuch-help ()
+  "Display help for the current notmuch mode.
+
+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.
+A command that supports a prefix argument can explicitly document
+its prefixed behavior by setting the 'notmuch-prefix-doc property
+of its command symbol."
+  (interactive)
+  (let ((doc (substitute-command-keys
+             (notmuch-substitute-command-keys
+              (documentation major-mode t)))))
+    (with-current-buffer (generate-new-buffer "*notmuch-help*")
+      (insert doc)
+      (goto-char (point-min))
+      (set-buffer-modified-p nil)
+      (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
+
+(defun notmuch-subkeymap-help ()
+  "Show help for a subkeymap."
+  (interactive)
+  (let* ((key (this-command-keys-vector))
+        (prefix (make-vector (1- (length key)) nil))
+        (i 0))
+    (while (< i (length prefix))
+      (aset prefix i (aref key i))
+      (cl-incf i))
+    (let* ((subkeymap (key-binding prefix))
+          (ua-keys (where-is-internal 'universal-argument nil t))
+          (prefix-string (notmuch-prefix-key-description prefix))
+          (desc-alist (notmuch-describe-keymap
+                       subkeymap ua-keys subkeymap prefix-string))
+          (desc-list (mapcar (lambda (arg) (concat (car arg) "\t" (cdr arg)))
+                             desc-alist))
+          (desc (mapconcat #'identity desc-list "\n")))
+      (with-help-window (help-buffer)
+       (with-current-buffer standard-output
+         (insert "\nPress 'q' to quit this window.\n\n")
+         (insert desc)))
+      (pop-to-buffer (help-buffer)))))
+
+;;; Refreshing Buffers
+
+(defvar-local notmuch-buffer-refresh-function nil
+  "Function to call to refresh the current buffer.")
+
+(defun notmuch-refresh-this-buffer ()
+  "Refresh the current buffer."
+  (interactive)
+  (when notmuch-buffer-refresh-function
+    ;; Pass prefix argument, etc.
+    (call-interactively notmuch-buffer-refresh-function)))
+
+(defun notmuch-poll-and-refresh-this-buffer ()
+  "Invoke `notmuch-poll' to import mail, then refresh the current buffer."
+  (interactive)
+  (notmuch-poll)
+  (notmuch-refresh-this-buffer))
+
+(defun notmuch-refresh-all-buffers ()
+  "Invoke `notmuch-refresh-this-buffer' on all notmuch major-mode buffers.
+
+The buffers are silently refreshed, i.e. they are not forced to
+be displayed."
+  (interactive)
+  (dolist (buffer (buffer-list))
+    (let ((buffer-mode (buffer-local-value 'major-mode buffer)))
+      (when (memq buffer-mode '(notmuch-show-mode
+                               notmuch-tree-mode
+                               notmuch-search-mode
+                               notmuch-hello-mode))
+       (with-current-buffer buffer
+         (notmuch-refresh-this-buffer))))))
+
+;;; String Utilities
+
+(defun notmuch-prettify-subject (subject)
+  ;; This function is used by `notmuch-search-process-filter',
+  ;; which requires that we not disrupt its matching state.
+  (save-match-data
+    (if (and subject
+            (string-match "^[ \t]*$" subject))
+       "[No Subject]"
+      subject)))
+
+(defun notmuch-sanitize (str)
+  "Sanitize control character in STR.
+
+This includes newlines, tabs, and other funny characters."
+  (replace-regexp-in-string "[[:cntrl:]\x7f\u2028\u2029]+" " " str))
+
+(defun notmuch-escape-boolean-term (term)
+  "Escape a boolean term for use in a query.
+
+The caller is responsible for prepending the term prefix and a
+colon.  This performs minimal escaping in order to produce
+user-friendly queries."
+  (save-match-data
+    (if (or (equal term "")
+           ;; To be pessimistic, only pass through terms composed
+           ;; entirely of ASCII printing characters other than ", (,
+           ;; and ).
+           (string-match "[^!#-'*-~]" term))
+       ;; Requires escaping
+       (concat "\"" (replace-regexp-in-string "\"" "\"\"" term t t) "\"")
+      term)))
+
+(defun notmuch-id-to-query (id)
+  "Return a query that matches the message with id ID."
+  (concat "id:" (notmuch-escape-boolean-term id)))
+
+(defun notmuch-hex-encode (str)
+  "Hex-encode STR (e.g., as used by batch tagging).
+
+This replaces spaces, percents, and double quotes in STR with
+%NN where NN is the hexadecimal value of the character."
+  (replace-regexp-in-string
+   "[ %\"]" (lambda (match) (format "%%%02x" (aref match 0))) str))
+
+(defun notmuch-common-do-stash (text)
+  "Common function to stash text in kill ring, and display in minibuffer."
+  (if text
+      (progn
+       (kill-new text)
+       (message "Stashed: %s" text))
+    ;; There is nothing to stash so stash an empty string so the user
+    ;; doesn't accidentally paste something else somewhere.
+    (kill-new "")
+    (message "Nothing to stash!")))
+
+;;; Generic Utilities
+
+(defun notmuch-plist-delete (plist property)
+  (let (p)
+    (while plist
+      (unless (eq property (car plist))
+       (setq p (plist-put p (car plist) (cadr plist))))
+      (setq plist (cddr plist)))
+    p))
+
+;;; MML Utilities
+
+(defun notmuch-match-content-type (t1 t2)
+  "Return t if t1 and t2 are matching content types.
+Take wildcards into account."
+  (and (stringp t1)
+       (stringp t2)
+       (let ((st1 (split-string t1 "/"))
+            (st2 (split-string t2 "/")))
+        (if (or (string= (cadr st1) "*")
+                (string= (cadr st2) "*"))
+            ;; Comparison of content types should be case insensitive.
+            (string= (downcase (car st1))
+                     (downcase (car st2)))
+          (string= (downcase t1)
+                   (downcase t2))))))
+
+(defcustom notmuch-multipart/alternative-discouraged
+  '(;; Avoid HTML parts.
+    "text/html"
+    ;; multipart/related usually contain a text/html part and some
+    ;; associated graphics.
+    "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."
+  ;; If a function, return the result of calling it.
+  (if (functionp notmuch-multipart/alternative-discouraged)
+      (funcall notmuch-multipart/alternative-discouraged msg)
+    ;; Otherwise simply return the value of the variable, which is
+    ;; assumed to be a list of discouraged alternatives. This is the
+    ;; default behaviour.
+    notmuch-multipart/alternative-discouraged))
+
+(defun notmuch-multipart/alternative-choose (msg types)
+  "Return a list of preferred types from the given list of types
+for this message, if present."
+  ;; Based on `mm-preferred-alternative-precedence'.
+  (let ((discouraged (notmuch-multipart/alternative-determine-discouraged msg))
+       (seq types))
+    (dolist (pref (reverse discouraged))
+      (dolist (elem (copy-sequence seq))
+       (when (string-match pref elem)
+         (setq seq (nconc (delete elem seq) (list elem))))))
+    seq))
+
+(defun notmuch-parts-filter-by-type (parts type)
+  "Given a list of message parts, return a list containing the ones matching
+the given type."
+  (cl-remove-if-not
+   (lambda (part) (notmuch-match-content-type (plist-get part :content-type) type))
+   parts))
+
+(defun notmuch--get-bodypart-raw (msg part process-crypto binaryp cache)
+  (let* ((plist-elem (if binaryp :content-binary :content))
+        (data (or (plist-get part plist-elem)
+                  (with-temp-buffer
+                    ;; Emacs internally uses a UTF-8-like multibyte string
+                    ;; representation by default (regardless of the coding
+                    ;; system, which only affects how it goes from outside data
+                    ;; to this internal representation).  This *almost* never
+                    ;; matters.  Annoyingly, it does matter if we use this data
+                    ;; in an image descriptor, since Emacs will use its internal
+                    ;; data buffer directly and this multibyte representation
+                    ;; corrupts binary image formats.  Since the caller is
+                    ;; asking for binary data, a unibyte string is a more
+                    ;; appropriate representation anyway.
+                    (when binaryp
+                      (set-buffer-multibyte nil))
+                    (let ((args `("show" "--format=raw"
+                                  ,(format "--part=%s" (plist-get part :id))
+                                  ,@(and process-crypto '("--decrypt=true"))
+                                  ,(notmuch-id-to-query (plist-get msg :id))))
+                          (coding-system-for-read
+                           (if binaryp
+                               'no-conversion
+                             (let ((coding-system
+                                    (mm-charset-to-coding-system
+                                     (plist-get part :content-charset))))
+                               ;; Sadly,
+                               ;; `mm-charset-to-coding-system' seems
+                               ;; to return things that are not
+                               ;; considered acceptable values for
+                               ;; `coding-system-for-read'.
+                               (if (coding-system-p coding-system)
+                                   coding-system
+                                 ;; RFC 2047 says that the default
+                                 ;; charset is US-ASCII. RFC6657
+                                 ;; complicates this somewhat.
+                                 'us-ascii)))))
+                      (apply #'notmuch--call-process
+                             notmuch-command nil '(t nil) nil args)
+                      (buffer-string))))))
+    (when (and cache data)
+      (plist-put part plist-elem data))
+    data))
+
+(defun notmuch-get-bodypart-binary (msg part process-crypto &optional cache)
+  "Return the unprocessed content of PART in MSG as a unibyte string.
+
+This returns the \"raw\" content of the given part after content
+transfer decoding, but with no further processing (see the
+discussion of --format=raw in man notmuch-show).  In particular,
+this does no charset conversion.
+
+If CACHE is non-nil, the content of this part will be saved in
+MSG (if it isn't already)."
+  (notmuch--get-bodypart-raw msg part process-crypto t cache))
+
+(defun notmuch-get-bodypart-text (msg part process-crypto &optional cache)
+  "Return the text content of PART in MSG.
+
+This returns the content of the given part as a multibyte Lisp
+string after performing content transfer decoding and any
+necessary charset decoding.
+
+If CACHE is non-nil, the content of this part will be saved in
+MSG (if it isn't already)."
+  (notmuch--get-bodypart-raw msg part process-crypto nil cache))
+
+(defun notmuch-mm-display-part-inline (msg part content-type process-crypto)
+  "Use the mm-decode/mm-view functions to display a part in the
+current buffer, if possible."
+  (let ((display-buffer (current-buffer)))
+    (with-temp-buffer
+      ;; In case we already have :content, use it and tell mm-* that
+      ;; it's already been charset-decoded by using the fake
+      ;; `gnus-decoded' charset.  Otherwise, we'll fetch the binary
+      ;; part content and let mm-* decode it.
+      (let* ((have-content (plist-member part :content))
+            (charset (if have-content
+                         'gnus-decoded
+                       (plist-get part :content-charset)))
+            (handle (mm-make-handle (current-buffer)
+                                    `(,content-type (charset . ,charset)))))
+       ;; If the user wants the part inlined, insert the content and
+       ;; test whether we are able to inline it (which includes both
+       ;; capability and suitability tests).
+       (when (mm-inlined-p handle)
+         (if have-content
+             (insert (notmuch-get-bodypart-text msg part process-crypto))
+           (insert (notmuch-get-bodypart-binary msg part process-crypto)))
+         (when (mm-inlinable-p handle)
+           (set-buffer display-buffer)
+           (mm-display-part handle)
+           (plist-put part :undisplayer (mm-handle-undisplayer handle))
+           t))))))
+
+;;; Generic Utilities
+
+;; Converts a plist of headers to an alist of headers. The input plist should
+;; have symbols of the form :Header as keys, and the resulting alist will have
+;; symbols of the form 'Header as keys.
+(defun notmuch-headers-plist-to-alist (plist)
+  (cl-loop for (key value . rest) on plist by #'cddr
+          collect (cons (intern (substring (symbol-name key) 1)) value)))
+
+(defun notmuch-face-ensure-list-form (face)
+  "Return FACE in face list form.
+
+If FACE is already a face list, it will be returned as-is.  If
+FACE is a face name or face plist, it will be returned as a
+single element face list."
+  (if (and (listp face) (not (keywordp (car face))))
+      face
+    (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.
+
+This function combines FACE with any existing faces between START
+and END in OBJECT.  Attributes specified by FACE take precedence
+over existing attributes unless BELOW is non-nil.
+
+OBJECT may be a string, a buffer, or nil (which means the current
+buffer).  If object is a string, START and END are 0-based;
+otherwise they are buffer positions (integers or markers).  FACE
+must be a face name (a symbol or string), a property list of face
+attributes, or a list of these.  If START and/or END are omitted,
+they default to the beginning/end of OBJECT.  For convenience
+when applied to strings, this returns OBJECT."
+  ;; A face property can have three forms: a face name (a string or
+  ;; symbol), a property list, or a list of these two forms.  In the
+  ;; list case, the faces will be combined, with the earlier faces
+  ;; taking precedent.  Here we canonicalize everything to list form
+  ;; to make it easy to combine.
+  (let ((pos (cond (start start)
+                  ((stringp object) 0)
+                  (t 1)))
+       (end (cond (end end)
+                  ((stringp object) (length object))
+                  (t (1+ (buffer-size object)))))
+       (face-list (notmuch-face-ensure-list-form face)))
+    (while (< pos end)
+      (let* ((cur (get-text-property pos 'face object))
+            (cur-list (notmuch-face-ensure-list-form cur))
+            (new (cond ((null cur-list) face)
+                       (below (append cur-list face-list))
+                       (t (append face-list cur-list))))
+            (next (next-single-property-change pos 'face object end)))
+       (put-text-property pos next 'face new object)
+       (setq pos next))))
+  object)
+
+(defun notmuch-map-text-property (start end prop func &optional object)
+  "Transform text property PROP using FUNC.
+
+Applies FUNC to each distinct value of the text property PROP
+between START and END of OBJECT, setting PROP to the value
+returned by FUNC."
+  (while (< start end)
+    (let ((value (get-text-property start prop object))
+         (next (next-single-property-change start prop object end)))
+      (put-text-property start next prop (funcall func value) object)
+      (setq start next))))
+
+;;; Running Notmuch
+
+(defun notmuch-logged-error (msg &optional extra)
+  "Log MSG and EXTRA to *Notmuch errors* and signal MSG.
+
+This logs MSG and EXTRA to the *Notmuch errors* buffer and
+signals MSG as an error.  If EXTRA is non-nil, text referring the
+user to the *Notmuch errors* buffer will be appended to the
+signaled error.  This function does not return."
+  (with-current-buffer (get-buffer-create "*Notmuch errors*")
+    (goto-char (point-max))
+    (unless (bobp)
+      (newline))
+    (save-excursion
+      (insert "[" (current-time-string) "]\n" msg)
+      (unless (bolp)
+       (newline))
+      (when extra
+       (insert extra)
+       (unless (bolp)
+         (newline)))))
+  (error "%s%s" msg (if extra " (see *Notmuch errors* for more details)" "")))
+
+(defun notmuch-check-async-exit-status (proc msg &optional command err)
+  "If PROC exited abnormally, pop up an error buffer and signal an error.
+
+This is a wrapper around `notmuch-check-exit-status' for
+asynchronous process sentinels.  PROC and MSG must be the
+arguments passed to the sentinel.  COMMAND and ERR, if provided,
+are passed to `notmuch-check-exit-status'.  If COMMAND is not
+provided, it is taken from `process-command'."
+  (let ((exit-status
+        (cl-case (process-status proc)
+          ((exit) (process-exit-status proc))
+          ((signal) msg))))
+    (when exit-status
+      (notmuch-check-exit-status exit-status
+                                (or command (process-command proc))
+                                nil err))))
+
+(defun notmuch-check-exit-status (exit-status command &optional output err)
+  "If EXIT-STATUS is non-zero, pop up an error buffer and signal an error.
+
+If EXIT-STATUS is non-zero, pop up a notmuch error buffer
+describing the error and signal an Elisp error.  EXIT-STATUS must
+be a number indicating the exit status code of a process or a
+string describing the signal that terminated the process (such as
+returned by `call-process').  COMMAND must be a list giving the
+command and its arguments.  OUTPUT, if provided, is a string
+giving the output of command.  ERR, if provided, is the error
+output of command.  OUTPUT and ERR will be included in the error
+message."
+  (cond
+   ((eq exit-status 0) t)
+   ((eq exit-status 20)
+    (notmuch-logged-error "notmuch CLI version mismatch
+Emacs requested an older output format than supported by the notmuch CLI.
+You may need to restart Emacs or upgrade your notmuch Emacs package."))
+   ((eq exit-status 21)
+    (notmuch-logged-error "notmuch CLI version mismatch
+Emacs requested a newer output format than supported by the notmuch CLI.
+You may need to restart Emacs or upgrade your notmuch package."))
+   (t
+    (pcase-let*
+       ((`(,command . ,args) command)
+        (command (if (equal (file-name-nondirectory command)
+                            notmuch-command)
+                     notmuch-command
+                   command))
+        (command-string
+         (mapconcat (lambda (arg)
+                      (shell-quote-argument
+                       (cond ((stringp arg) arg)
+                             ((symbolp arg) (symbol-name arg))
+                             (t "*UNKNOWN ARGUMENT*"))))
+                    (cons command args)
+                    " "))
+        (extra
+         (concat "command: " command-string "\n"
+                 (if (integerp exit-status)
+                     (format "exit status: %s\n" exit-status)
+                   (format "exit signal: %s\n" exit-status))
+                 (and err    (concat "stderr:\n" err))
+                 (and output (concat "stdout:\n" output)))))
+      (if err
+         ;; We have an error message straight from the CLI.
+         (notmuch-logged-error
+          (replace-regexp-in-string "[ \n\r\t\f]*\\'" "" err) extra)
+       ;; We only have combined output from the CLI; don't inundate
+       ;; the user with it.  Mimic `process-lines'.
+       (notmuch-logged-error (format "%s exited with status %s"
+                                     command exit-status)
+                             extra))
+      ;; `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.
+
+This wraps `call-process'.  DESTINATION has the same meaning as
+for `call-process'.  ARGS is as described for
+`notmuch-call-notmuch-process'."
+  (let (stdin-string)
+    (while (keywordp (car args))
+      (cl-case (car args)
+       (:stdin-string (setq stdin-string (cadr args))
+                      (setq args (cddr args)))
+       (otherwise
+        (error "Unknown keyword argument: %s" (car args)))))
+    (if (null stdin-string)
+       (apply #'notmuch--call-process notmuch-command nil destination nil args)
+      (insert stdin-string)
+      (apply #'notmuch--call-process-region (point-min) (point-max)
+            notmuch-command t destination nil args))))
+
+(defun notmuch-call-notmuch-process (&rest args)
+  "Synchronously invoke `notmuch-command' with ARGS.
+
+The caller may provide keyword arguments before ARGS.  Currently
+supported keyword arguments are:
+
+  :stdin-string STRING - Write STRING to stdin
+
+If notmuch exits with a non-zero status, output from the process
+will appear in a buffer named \"*Notmuch errors*\" and an error
+will be signaled."
+  (with-temp-buffer
+    (let ((status (notmuch-call-notmuch--helper t args)))
+      (notmuch-check-exit-status status (cons notmuch-command args)
+                                (buffer-string)))))
+
+(defun notmuch-call-notmuch-sexp (&rest args)
+  "Invoke `notmuch-command' with ARGS and return the parsed S-exp output.
+
+This is equivalent to `notmuch-call-notmuch-process', but parses
+notmuch's output as an S-expression and returns the parsed value.
+Like `notmuch-call-notmuch-process', if notmuch exits with a
+non-zero status, this will report its output and signal an
+error."
+  (with-temp-buffer
+    (let ((err-file (make-temp-file "nmerr")))
+      (unwind-protect
+         (let ((status (notmuch-call-notmuch--helper (list t err-file) args))
+               (err (with-temp-buffer
+                      (insert-file-contents err-file)
+                      (unless (eobp)
+                        (buffer-string)))))
+           (notmuch-check-exit-status status (cons notmuch-command args)
+                                      (buffer-string) err)
+           (goto-char (point-min))
+           (read (current-buffer)))
+       (delete-file err-file)))))
+
+(defun notmuch-start-notmuch (name buffer sentinel &rest args)
+  "Start and return an asynchronous notmuch command.
+
+This starts and returns an asynchronous process running
+`notmuch-command' with ARGS.  The exit status is checked via
+`notmuch-check-async-exit-status'.  Output written to stderr is
+redirected and displayed when the process exits (even if the
+process exits successfully).  NAME and BUFFER are the same as in
+`start-process'.  SENTINEL is a process sentinel function to call
+when the process exits, or nil for none.  The caller must *not*
+invoke `set-process-sentinel' directly on the returned process,
+as that will interfere with the handling of stderr and the exit
+status."
+  (let* ((command (or (executable-find notmuch-command)
+                     (error "Command not found: %s" notmuch-command)))
+        (err-buffer (generate-new-buffer " *notmuch-stderr*"))
+        (proc (notmuch--make-process
+               :name name
+               :buffer buffer
+               :command (cons command args)
+               :connection-type 'pipe
+               :stderr err-buffer))
+        (err-proc (get-buffer-process err-buffer)))
+    (process-put proc 'err-buffer err-buffer)
+    (process-put proc 'sub-sentinel sentinel)
+    (set-process-sentinel proc #'notmuch-start-notmuch-sentinel)
+    (set-process-sentinel err-proc #'notmuch-start-notmuch-error-sentinel)
+    proc))
+
+(defun notmuch-start-notmuch-sentinel (proc event)
+  "Process sentinel function used by `notmuch-start-notmuch'."
+  (let* ((err-buffer (process-get proc 'err-buffer))
+        (err (and (buffer-live-p err-buffer)
+                  (not (zerop (buffer-size err-buffer)))
+                  (with-current-buffer err-buffer (buffer-string))))
+        (sub-sentinel (process-get proc 'sub-sentinel)))
+    (condition-case err
+       (progn
+         ;; Invoke the sub-sentinel, if any
+         (when sub-sentinel
+           (funcall sub-sentinel proc event))
+         ;; Check the exit status.  This will signal an error if the
+         ;; exit status is non-zero.  Don't do this if the process
+         ;; buffer is dead since that means Emacs killed the process
+         ;; and there's no point in telling the user that (but we
+         ;; still check for and report stderr output below).
+         (when (buffer-live-p (process-buffer proc))
+           (notmuch-check-async-exit-status proc event nil err))
+         ;; If that didn't signal an error, then any error output was
+         ;; really warning output.  Show warnings, if any.
+         (let ((warnings
+                (and err
+                     (with-current-buffer err-buffer
+                       (goto-char (point-min))
+                       (end-of-line)
+                       ;; Show first line; stuff remaining lines in the
+                       ;; errors buffer.
+                       (let ((l1 (buffer-substring (point-min) (point))))
+                         (skip-chars-forward "\n")
+                         (cons l1 (and (not (eobp))
+                                       (buffer-substring (point)
+                                                         (point-max)))))))))
+           (when warnings
+             (notmuch-logged-error (car warnings) (cdr warnings)))))
+      (error
+       ;; Emacs behaves strangely if an error escapes from a sentinel,
+       ;; so turn errors into messages.
+       (message "%s" (error-message-string err))))))
+
+(defun notmuch-start-notmuch-error-sentinel (proc _event)
+  (unless (process-live-p proc)
+    (let ((buffer (process-buffer proc)))
+      (when (buffer-live-p buffer)
+       (kill-buffer buffer)))))
+
+(defvar-local notmuch-show-process-crypto nil)
+
+(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 ()
+  "Return the bounds of the current interactive region.
+
+This returns (BEG END), where BEG and END are the bounds of the
+region if the region is active, or both `point' otherwise."
+  (if (region-active-p)
+      (list (region-beginning) (region-end))
+    (list (point) (point))))
+
+(define-obsolete-function-alias
+  'notmuch-search-interactive-region
+  '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)
+
+;;; notmuch-lib.el ends here
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>
diff --git a/emacs/notmuch-maildir-fcc.el b/emacs/notmuch-maildir-fcc.el
new file mode 100644 (file)
index 0000000..5102078
--- /dev/null
@@ -0,0 +1,362 @@
+;;; notmuch-maildir-fcc.el --- inserting using a fcc handler  -*- lexical-binding: t -*-
+
+;; Copyright © Jesse Rosenthal
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Jesse Rosenthal <jrosenthal@jhu.edu>
+
+;;; Code:
+
+(require 'seq)
+
+(require 'message)
+
+(require 'notmuch-lib)
+
+(defvar notmuch-maildir-fcc-count 0)
+
+;;; Options
+
+(defcustom notmuch-fcc-dirs "sent"
+  "Determines the Fcc Header which says where to save outgoing mail.
+
+Three types of values are permitted:
+
+- nil: no Fcc header is added,
+
+- a string: the value of `notmuch-fcc-dirs' is the Fcc header to
+  be used.
+
+- 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, 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
+folder is the folder (relative to the notmuch mailstore) to store
+the message in, and tag1 and tag2 are tag changes to apply to the
+stored message. This string is split using `split-string-and-unquote',
+so a folder name containing spaces can be specified by
+quoting each space with an immediately preceding backslash
+or surrounding the entire folder name in double quotes.
+
+If `notmuch-maildir-use-notmuch-insert' is nil then the Fcc
+header should be the directory where the message should be
+saved. A relative directory will be understood to specify a
+directory within the notmuch mail store, (as set by the
+database.path option in the notmuch configuration file).
+
+In all cases you will be prompted to create the folder or
+directory if it does not exist yet when sending a mail."
+
+  :type '(choice
+         (const :tag "No FCC header" nil)
+         (string :tag "A single folder")
+         (repeat :tag "A folder based on the From header"
+                 (cons regexp (choice (const :tag "No FCC header" nil)
+                                      (string :tag "Folder")))))
+  :require 'notmuch-fcc-initialization
+  :group 'notmuch-send)
+
+(defcustom notmuch-maildir-use-notmuch-insert t
+  "Should fcc use notmuch insert instead of simple fcc."
+  :type '(choice :tag "Fcc Method"
+                (const :tag "Use notmuch insert" t)
+                (const :tag "Use simple fcc" nil))
+  :group 'notmuch-send)
+
+;;; Functions which set up the fcc header in the message buffer.
+
+(defun notmuch-fcc-header-setup ()
+  "Add an Fcc header to the current message buffer.
+
+If the Fcc header is already set, then keep it as-is.
+Otherwise set it according to `notmuch-fcc-dirs'."
+  (let ((subdir
+        (cond
+         ((or (not notmuch-fcc-dirs)
+              (message-field-value "Fcc"))
+          ;; Nothing set or an existing header.
+          nil)
+         ((stringp notmuch-fcc-dirs)
+          notmuch-fcc-dirs)
+         ((and (listp notmuch-fcc-dirs)
+               (stringp (car notmuch-fcc-dirs)))
+          ;; Old style - no longer works.
+          (error "Invalid `notmuch-fcc-dirs' setting (old style)"))
+         ((listp notmuch-fcc-dirs)
+          (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
+      (if notmuch-maildir-use-notmuch-insert
+         (notmuch-maildir-add-notmuch-insert-style-fcc-header subdir)
+       (notmuch-maildir-add-file-style-fcc-header subdir)))))
+
+(defun notmuch-maildir-add-notmuch-insert-style-fcc-header (subdir)
+  ;; Notmuch insert does not accept absolute paths, so check the user
+  ;; really want this header inserted.
+  (when (or (not (= (elt subdir 0) ?/))
+           (y-or-n-p (format "Fcc header %s is an absolute path %s %s" subdir
+                             "and notmuch insert is requested."
+                             "Insert header anyway? ")))
+    (message-add-header (concat "Fcc: " subdir))))
+
+(defun notmuch-maildir-add-file-style-fcc-header (subdir)
+  (message-add-header
+   (concat "Fcc: "
+          (file-truename
+           ;; If the resulting directory is not an absolute path,
+           ;; prepend the standard notmuch database path.
+           (if (= (elt subdir 0) ?/)
+               subdir
+             (concat (notmuch-database-path) "/" subdir))))))
+
+;;; Functions for saving a message using either method.
+
+(defmacro with-temporary-notmuch-message-buffer (&rest body)
+  "Set-up a temporary copy of the current message-mode buffer."
+  `(let ((case-fold-search t)
+        (buf (current-buffer))
+        (mml-externalize-attachments message-fcc-externalize-attachments))
+     (with-current-buffer (get-buffer-create " *message temp*")
+       (message-clone-locals buf) ;; for message-encoded-mail-cache
+       (erase-buffer)
+       (insert-buffer-substring buf)
+       ,@body)))
+
+(defun notmuch-maildir-setup-message-for-saving ()
+  "Setup message for saving.
+
+This should be called on a temporary copy.
+This is taken from the function message-do-fcc."
+  (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))
+  (goto-char (point-min))
+  (when (re-search-forward
+        (concat "^" (regexp-quote mail-header-separator) "$")
+        nil t)
+    (replace-match "" t t )))
+
+(defun notmuch-maildir-message-do-fcc ()
+  "Process Fcc headers in the current buffer.
+
+This is a rearranged version of message mode's message-do-fcc."
+  (let (files file)
+    (save-excursion
+      (save-restriction
+       (message-narrow-to-headers)
+       (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)))
+        ;; Process FCC operations.
+        (mapc #'notmuch-fcc-handler files)
+        (kill-buffer (current-buffer)))))))
+
+(defun notmuch-fcc-handler (fcc-header)
+  "Store message with notmuch insert or normal (file) fcc.
+
+If `notmuch-maildir-use-notmuch-insert' is set then store the
+message using notmuch insert. Otherwise store the message using
+normal fcc."
+  (message "Doing Fcc...")
+  (if notmuch-maildir-use-notmuch-insert
+      (notmuch-maildir-fcc-with-notmuch-insert fcc-header)
+    (notmuch-maildir-fcc-file-fcc fcc-header))
+  (message "Doing Fcc...done"))
+
+;;; Functions for saving a message using notmuch insert.
+
+(defun notmuch-maildir-notmuch-insert-current-buffer (folder &optional create tags)
+  "Use notmuch insert to put the current buffer in the database.
+
+This inserts the current buffer as a message into the notmuch
+database in folder FOLDER. If CREATE is non-nil it will supply
+the --create-folder flag to create the folder if necessary. TAGS
+should be a list of tag changes to apply to the inserted message."
+  (apply 'notmuch-call-notmuch-process
+        :stdin-string (buffer-string) "insert"
+        (append (and create (list "--create-folder"))
+                (list (concat "--folder=" folder))
+                tags)))
+
+(defun notmuch-maildir-fcc-with-notmuch-insert (fcc-header &optional create)
+  "Store message with notmuch insert.
+
+The fcc-header should be of the form \"folder +tag1 -tag2\" where
+folder is the folder (relative to the notmuch mailstore) to store
+the message in, and tag1 and tag2 are tag changes to apply to the
+stored message. This string is split using `split-string-and-unquote',
+so a folder name containing spaces can be specified by
+quoting each space with an immediately preceding backslash
+or surrounding the entire folder name in double quotes.
+
+If CREATE is non-nil then create the folder if necessary."
+  (pcase-let ((`(,folder . ,tags)
+              (split-string-and-unquote fcc-header)))
+    (condition-case nil
+       (notmuch-maildir-notmuch-insert-current-buffer folder create tags)
+      ;; Since there are many reasons notmuch insert could fail, e.g.,
+      ;; locked database, non-existent folder (which could be due to a
+      ;; typo, or just the user want a new folder, let the user decide
+      ;; how to deal with it.
+      (error
+       (let ((response (read-char-choice "Insert failed: \
+\(r)etry, (c)reate folder, (i)gnore, or (e)dit the header? " '(?r ?c ?i ?e))))
+        (cl-case response
+          (?r (notmuch-maildir-fcc-with-notmuch-insert fcc-header))
+          (?c (notmuch-maildir-fcc-with-notmuch-insert fcc-header t))
+          (?i t)
+          (?e (notmuch-maildir-fcc-with-notmuch-insert
+               (read-from-minibuffer "Fcc header: " fcc-header)))))))))
+
+;;; Functions for saving a message using file fcc.
+
+(defun notmuch-maildir-fcc-host-fixer (hostname)
+  (replace-regexp-in-string "/\\|:"
+                           (lambda (s)
+                             (cond ((string-equal s "/") "\\057")
+                                   ((string-equal s ":") "\\072")
+                                   (t s)))
+                           hostname
+                           t
+                           t))
+
+(defun notmuch-maildir-fcc-make-uniq-maildir-id ()
+  (let* ((ftime (float-time))
+        (microseconds (mod (* 1000000 ftime) 1000000))
+        (hostname (notmuch-maildir-fcc-host-fixer (system-name))))
+    (cl-incf notmuch-maildir-fcc-count)
+    (format "%d.%d_%d_%d.%s"
+           ftime
+           (emacs-pid)
+           microseconds
+           notmuch-maildir-fcc-count
+           hostname)))
+
+(defun notmuch-maildir-fcc-dir-is-maildir-p (dir)
+  (and (file-exists-p (concat dir "/cur/"))
+       (file-exists-p (concat dir "/new/"))
+       (file-exists-p (concat dir "/tmp/"))))
+
+(defun notmuch-maildir-fcc-create-maildir (path)
+  (cond ((or (not (file-exists-p path)) (file-directory-p path))
+        (make-directory (concat path "/cur/") t)
+        (make-directory (concat path "/new/") t)
+        (make-directory (concat path "/tmp/") t))
+       ((file-regular-p path)
+        (error "%s is a file. Can't create maildir." path))
+       (t
+        (error "I don't know how to create a maildir here"))))
+
+(defun notmuch-maildir-fcc-save-buffer-to-tmp (destdir)
+  "Returns the msg id of the message written to the temp directory
+if successful, nil if not."
+  (let ((msg-id (notmuch-maildir-fcc-make-uniq-maildir-id)))
+    (while (file-exists-p (concat destdir "/tmp/" msg-id))
+      (setq msg-id (notmuch-maildir-fcc-make-uniq-maildir-id)))
+    (cond ((notmuch-maildir-fcc-dir-is-maildir-p destdir)
+          (write-file (concat destdir "/tmp/" msg-id))
+          msg-id)
+         (t
+          (error "Can't write to %s. Not a maildir." destdir)))))
+
+(defun notmuch-maildir-fcc-move-tmp-to-new (destdir msg-id)
+  (add-name-to-file
+   (concat destdir "/tmp/" msg-id)
+   (concat destdir "/new/" msg-id ":2,")))
+
+(defun notmuch-maildir-fcc-move-tmp-to-cur (destdir msg-id &optional mark-seen)
+  (add-name-to-file
+   (concat destdir "/tmp/" msg-id)
+   (concat destdir "/cur/" msg-id ":2," (and mark-seen "S"))))
+
+(defun notmuch-maildir-fcc-file-fcc (fcc-header)
+  "Write the message to the file specified by FCC-HEADER.
+
+If that fails, then offer the user a chance to correct the header
+or filesystem."
+  (if (notmuch-maildir-fcc-dir-is-maildir-p fcc-header)
+      (notmuch-maildir-fcc-write-buffer-to-maildir fcc-header t)
+    ;; The fcc-header is not a valid maildir see if the user wants to
+    ;; fix it in some way.
+    (let* ((prompt (format "Fcc %s is not a maildir: \
+\(r)etry, (c)reate folder, (i)gnore, or (e)dit the header? " fcc-header))
+          (response (read-char-choice prompt '(?r ?c ?i ?e))))
+      (cl-case response
+       (?r (notmuch-maildir-fcc-file-fcc fcc-header))
+       (?c (if (file-writable-p fcc-header)
+               (notmuch-maildir-fcc-create-maildir fcc-header)
+             (message "No permission to create %s." fcc-header)
+             (sit-for 2))
+           (notmuch-maildir-fcc-file-fcc fcc-header))
+       (?i t)
+       (?e (notmuch-maildir-fcc-file-fcc
+            (read-from-minibuffer "Fcc header: " fcc-header)))))))
+
+(defun notmuch-maildir-fcc-write-buffer-to-maildir (destdir &optional mark-seen)
+  "Write the current buffer to maildir destdir.
+
+If mark-seen is non-nil, then write it to \"cur/\", and mark it
+as read, otherwise write it to \"new/\". Return t if successful,
+and nil otherwise."
+  (let ((orig-buffer (buffer-name)))
+    (with-temp-buffer
+      (insert-buffer-substring orig-buffer)
+      (catch 'link-error
+       (let ((msg-id (notmuch-maildir-fcc-save-buffer-to-tmp destdir)))
+         (when msg-id
+           (condition-case nil
+               (if mark-seen
+                   (notmuch-maildir-fcc-move-tmp-to-cur destdir msg-id t)
+                 (notmuch-maildir-fcc-move-tmp-to-new destdir msg-id))
+             (file-already-exists
+              (throw 'link-error nil))))
+         (delete-file (concat destdir "/tmp/" msg-id))))
+      t)))
+
+;;; _
+
+(provide 'notmuch-maildir-fcc)
+
+;;; notmuch-maildir-fcc.el ends here
diff --git a/emacs/notmuch-message.el b/emacs/notmuch-message.el
new file mode 100644 (file)
index 0000000..0856a2e
--- /dev/null
@@ -0,0 +1,76 @@
+;;; notmuch-message.el --- message-mode functions specific to notmuch  -*- lexical-binding: t -*-
+;;
+;; Copyright © Jesse Rosenthal
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Jesse Rosenthal <jrosenthal@jhu.edu>
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'pcase)
+(require 'subr-x)
+
+(require 'message)
+(require 'notmuch-tag)
+
+(defcustom notmuch-message-replied-tags '("+replied")
+  "List of tag changes to apply to a message when it has been replied to.
+
+Tags starting with \"+\" (or not starting with either \"+\" or
+\"-\") in the list will be added, and tags starting with \"-\"
+will be removed from the message being replied to.
+
+For example, if you wanted to add a \"replied\" tag and remove
+the \"inbox\" and \"todo\" tags, you would set:
+    (\"+replied\" \"-inbox\" \"-todo\")"
+  :type '(repeat string)
+  :group 'notmuch-send)
+
+(defcustom notmuch-message-forwarded-tags '("+forwarded")
+  "List of tag changes to apply to a message when it has been forwarded.
+
+Tags starting with \"+\" (or not starting with either \"+\" or
+\"-\") in the list will be added, and tags starting with \"-\"
+will be removed from the message being forwarded.
+
+For example, if you wanted to add a \"forwarded\" tag and remove
+the \"inbox\" tag, you would set:
+    (\"+forwarded\" \"-inbox\")"
+  :type '(repeat string)
+  :group 'notmuch-send)
+
+(defvar-local notmuch-message-queued-tag-changes nil
+  "List of tag changes to be applied when sending a message.
+
+A list of queries and tag changes that are to be applied to them
+when the message that was composed in the current buffer is being
+send.  Each item in this list is a list of strings, where the
+first is a notmuch query and the rest are the tag changes to be
+applied to the matching messages.")
+
+(defun notmuch-message-apply-queued-tag-changes ()
+  ;; Apply the tag changes queued in the buffer-local variable
+  ;; notmuch-message-queued-tag-changes.
+  (pcase-dolist (`(,query . ,tags) notmuch-message-queued-tag-changes)
+    (notmuch-tag query tags)))
+
+(add-hook 'message-send-hook 'notmuch-message-apply-queued-tag-changes)
+
+(provide 'notmuch-message)
+
+;;; notmuch-message.el ends here
diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el
new file mode 100644 (file)
index 0000000..3679d7d
--- /dev/null
@@ -0,0 +1,651 @@
+;;; notmuch-mua.el --- emacs style mail-user-agent  -*- lexical-binding: t -*-
+;;
+;; Copyright © David Edmondson
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: David Edmondson <dme@dme.org>
+
+;;; Code:
+
+(eval-when-compile (require 'subr-x))
+
+(require 'message)
+(require 'gmm-utils)
+(require 'mm-view)
+(require 'format-spec)
+
+(require 'notmuch-lib)
+(require 'notmuch-address)
+(require 'notmuch-draft)
+(require 'notmuch-message)
+
+(declare-function notmuch-show-insert-body "notmuch-show" (msg body depth))
+(declare-function notmuch-fcc-header-setup "notmuch-maildir-fcc" ())
+(declare-function notmuch-maildir-message-do-fcc "notmuch-maildir-fcc" ())
+(declare-function notmuch-draft-postpone "notmuch-draft" ())
+(declare-function notmuch-draft-save "notmuch-draft" ())
+
+(defvar notmuch-show-indent-multipart)
+(defvar notmuch-show-insert-header-p-function)
+(defvar notmuch-show-max-text-part-size)
+(defvar notmuch-show-insert-text/plain-hook)
+
+;;; Options
+
+(defcustom notmuch-mua-send-hook nil
+  "Hook run before sending messages."
+  :type 'hook
+  :group 'notmuch-send
+  :group 'notmuch-hooks)
+
+(defcustom notmuch-mua-compose-in 'current-window
+  "Where to create the mail buffer used to compose a new message.
+Possible values are `current-window' (default), `new-window' and
+`new-frame'. If set to `current-window', the mail buffer will be
+displayed in the current window, so the old buffer will be
+restored when the mail buffer is killed. If set to `new-window'
+or `new-frame', the mail buffer will be displayed in a new
+window/frame that will be destroyed when the buffer is killed.
+You may want to customize `message-kill-buffer-on-exit'
+accordingly."
+  :group 'notmuch-send
+  :type '(choice (const :tag "Compose in the current window" current-window)
+                (const :tag "Compose mail in a new window"  new-window)
+                (const :tag "Compose mail in a new frame"   new-frame)))
+
+(defcustom notmuch-mua-user-agent-function nil
+  "Function used to generate a `User-Agent:' string.
+If this is `nil' then no `User-Agent:' will be generated."
+  :type '(choice (const :tag "No user agent string" nil)
+                (const :tag "Full" notmuch-mua-user-agent-full)
+                (const :tag "Notmuch" notmuch-mua-user-agent-notmuch)
+                (const :tag "Emacs" notmuch-mua-user-agent-emacs)
+                (function :tag "Custom user agent function"
+                          :value notmuch-mua-user-agent-full))
+  :group 'notmuch-send)
+
+(defcustom notmuch-mua-hidden-headers nil
+  "Headers that are added to the `message-mode' hidden headers list."
+  :type '(repeat string)
+  :group 'notmuch-send)
+
+(defcustom notmuch-identities nil
+  "Identities that can be used as the From: address when composing a new message.
+
+If this variable is left unset, then a list will be constructed from the
+name and addresses configured in the notmuch configuration file."
+  :type '(repeat string)
+  :group 'notmuch-send)
+
+(defcustom notmuch-always-prompt-for-sender nil
+  "Always prompt for the From: address when composing or forwarding a message.
+
+This is not taken into account when replying to a message, because in that case
+the From: header is already filled in by notmuch."
+  :type 'boolean
+  :group 'notmuch-send)
+
+(defgroup notmuch-reply nil
+  "Replying to messages in notmuch."
+  :group 'notmuch)
+
+(defcustom notmuch-mua-cite-function 'message-cite-original
+  "Function for citing an original message.
+
+Predefined functions include `message-cite-original' and
+`message-cite-original-without-signature'.  Note that these
+functions use `mail-citation-hook' if that is non-nil."
+  :type '(radio (function-item message-cite-original)
+               (function-item message-cite-original-without-signature)
+               (function-item sc-cite-original)
+               (function :tag "Other"))
+  :link '(custom-manual "(message)Insertion Variables")
+  :group 'notmuch-reply)
+
+(defcustom notmuch-mua-reply-insert-header-p-function
+  'notmuch-show-reply-insert-header-p-never
+  "Function to decide which parts get a header when replying.
+
+This function specifies which parts of a mime message with
+multiple parts get a header."
+  :type '(radio (const :tag "No part headers"
+                      notmuch-show-reply-insert-header-p-never)
+               (const :tag "All except multipart/* and hidden parts"
+                      notmuch-show-reply-insert-header-p-trimmed)
+               (const :tag "Only for included text parts"
+                      notmuch-show-reply-insert-header-p-minimal)
+               (const :tag "Exactly as in show view"
+                      notmuch-show-insert-header-p)
+               (function :tag "Other"))
+  :group 'notmuch-reply)
+
+(defcustom notmuch-mua-attachment-regexp
+  "\\b\\(attache\?ment\\|attached\\|attach\\|pi[èe]ce\s+jointe?\\)\\b"
+  "Message body text indicating that an attachment is expected.
+
+This is not used unless `notmuch-mua-attachment-check' is added
+to `notmuch-mua-send-hook'."
+  :type 'regexp
+  :group 'notmuch-send)
+
+;;; Various functions
+
+(defun notmuch-mua-attachment-check ()
+  "Signal an error an attachement is expected but missing.
+
+Signal an error if the message text indicates that an attachment
+is expected but no MML referencing an attachment is found.
+
+Typically this is added to `notmuch-mua-send-hook'."
+  (when (and
+        ;; When the message mentions attachment...
+        (save-excursion
+          (message-goto-body)
+          ;; Limit search from reaching other possible parts of the message
+          (let ((search-limit (search-forward "\n<#" nil t)))
+            (message-goto-body)
+            (cl-loop while (re-search-forward notmuch-mua-attachment-regexp
+                                              search-limit t)
+                     ;; For every instance of the "attachment" string
+                     ;; found, examine the text properties. If the text
+                     ;; has either a `face' or `syntax-table' property
+                     ;; then it is quoted text and should *not* cause the
+                     ;; user to be asked about a missing attachment.
+                     if (let ((props (text-properties-at (match-beginning 0))))
+                          (not (or (memq 'syntax-table props)
+                                   (memq 'face props))))
+                     return t
+                     finally return nil)))
+        ;; ...but doesn't have a part with a filename...
+        (save-excursion
+          (message-goto-body)
+          (not (re-search-forward "^<#part [^>]*filename=" nil t)))
+        ;; ...and that's not okay...
+        (not (y-or-n-p "Attachment mentioned, but no attachment - is that okay?")))
+    ;; ...signal an error.
+    (error "Missing attachment")))
+
+(defun notmuch-mua-get-switch-function ()
+  "Get a switch function according to `notmuch-mua-compose-in'."
+  (pcase notmuch-mua-compose-in
+    ('current-window 'switch-to-buffer)
+    ('new-window     'switch-to-buffer-other-window)
+    ('new-frame      'switch-to-buffer-other-frame)
+    (_ (error "Invalid value for `notmuch-mua-compose-in'"))))
+
+(defun notmuch-mua-maybe-set-window-dedicated ()
+  "Set the selected window as dedicated according to `notmuch-mua-compose-in'."
+  (when (or (eq notmuch-mua-compose-in 'new-frame)
+           (eq notmuch-mua-compose-in 'new-window))
+    (set-window-dedicated-p (selected-window) t)))
+
+(defun notmuch-mua-user-agent-full ()
+  "Generate a `User-Agent:' string suitable for notmuch."
+  (concat (notmuch-mua-user-agent-notmuch)
+         " "
+         (notmuch-mua-user-agent-emacs)))
+
+(defun notmuch-mua-user-agent-notmuch ()
+  "Generate a `User-Agent:' string suitable for notmuch."
+  (let ((notmuch-version (if (string= notmuch-emacs-version "unknown")
+                            (notmuch-cli-version)
+                          notmuch-emacs-version)))
+    (concat "Notmuch/" notmuch-version " (https://notmuchmail.org)")))
+
+(defun notmuch-mua-user-agent-emacs ()
+  "Generate a `User-Agent:' string suitable for notmuch."
+  (concat "Emacs/" emacs-version " (" system-configuration ")"))
+
+(defun notmuch-mua-add-more-hidden-headers ()
+  "Add some headers to the list that are hidden by default."
+  (mapc (lambda (header)
+         (unless (member header message-hidden-headers)
+           (push header message-hidden-headers)))
+       notmuch-mua-hidden-headers))
+
+(defun notmuch-mua-reply-crypto (parts)
+  "Add mml sign-encrypt flag if any part of original message is encrypted."
+  (cl-loop for part in parts
+          for type = (plist-get part :content-type)
+          if (notmuch-match-content-type type "multipart/encrypted")
+          do (mml-secure-message-sign-encrypt)
+          else if (notmuch-match-content-type type "multipart/*")
+          do (notmuch-mua-reply-crypto (plist-get part :content))))
+
+;; There is a bug in Emacs' message.el that results in a newline
+;; not being inserted after the References header, so the next header
+;; is concatenated to the end of it. This function fixes the problem,
+;; while guarding against the possibility that some current or future
+;; version of emacs has the bug fixed.
+(defun notmuch-mua-insert-references (original-func header references)
+  (funcall original-func header references)
+  (unless (bolp) (insert "\n")))
+
+;;; Mua reply
+
+(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
+       (setq args (append args '("--reply-to=all")))
+      (setq args (append args '("--reply-to=sender"))))
+    (setq args (append args (list query-string)))
+    ;; Get the reply object as SEXP, and parse it into an elisp object.
+    (setq reply (apply #'notmuch-call-notmuch-sexp args))
+    ;; Extract the original message to simplify the following code.
+    (setq original (plist-get reply :original))
+    ;; Extract the headers of both the reply and the original message.
+    (let* ((original-headers (plist-get original :headers))
+          (reply-headers (plist-get reply :reply-headers)))
+      ;; If sender is non-nil, set the From: header to its value.
+      (when sender
+       (plist-put reply-headers :From sender))
+      (let
+         ;; Overlay the composition window on that being used to read
+         ;; the original message.
+         ((same-window-regexps '("\\*mail .*")))
+       ;; We modify message-header-format-alist to get around
+       ;; a bug in message.el.  See the comment above on
+       ;; notmuch-mua-insert-references.
+       (let ((message-header-format-alist
+              (cl-loop for pair in message-header-format-alist
+                       if (eq (car pair) 'References)
+                       collect (cons 'References
+                                     (apply-partially
+                                      'notmuch-mua-insert-references
+                                      (cdr pair)))
+                       else
+                       collect pair)))
+         (notmuch-mua-mail (plist-get reply-headers :To)
+                           (notmuch-sanitize (plist-get reply-headers :Subject))
+                           (notmuch-headers-plist-to-alist reply-headers)
+                           nil (notmuch-mua-get-switch-function))))
+      ;; Create a buffer-local queue for tag changes triggered when
+      ;; sending the reply.
+      (when notmuch-message-replied-tags
+       (setq notmuch-message-queued-tag-changes
+             (list (cons query-string notmuch-message-replied-tags))))
+      ;; Insert the message body - but put it in front of the signature
+      ;; if one is present, and after any other content
+      ;; message*setup-hooks may have added to the message body already.
+      (save-restriction
+       (message-goto-body)
+       (narrow-to-region (point) (point-max))
+       (goto-char (point-max))
+       (if (re-search-backward message-signature-separator nil t)
+           (when message-signature-insert-empty-line
+             (forward-line -1))
+         (goto-char (point-max))))
+      (let ((from (plist-get original-headers :From))
+           (date (plist-get original-headers :Date))
+           (start (point)))
+       ;; notmuch-mua-cite-function constructs a citation line based
+       ;; on the From and Date headers of the original message, which
+       ;; are assumed to be in the buffer.
+       (insert "From: " from "\n")
+       (insert "Date: " date "\n\n")
+       (insert
+        (with-temp-buffer
+          (let
+              ;; Don't attempt to clean up messages, excerpt
+              ;; citations, etc. in the original message before
+              ;; quoting.
+              ((notmuch-show-insert-text/plain-hook nil)
+               ;; Don't omit long parts.
+               (notmuch-show-max-text-part-size 0)
+               ;; Insert headers for parts as appropriate for replying.
+               (notmuch-show-insert-header-p-function
+                notmuch-mua-reply-insert-header-p-function)
+               ;; Ensure that any encrypted parts are
+               ;; decrypted during the generation of the reply
+               ;; text.
+               (notmuch-show-process-crypto process-crypto)
+               ;; Don't indent multipart sub-parts.
+               (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))
+              (notmuch-show-insert-body original (plist-get original :body) 0)
+              (buffer-substring-no-properties (point-min) (point-max))))))
+       (set-mark (point))
+       (goto-char start)
+       ;; Quote the original message according to the user's configured style.
+       (funcall notmuch-mua-cite-function)))
+    ;; Crypto processing based crypto content of the original message
+    (when process-crypto
+      (notmuch-mua-reply-crypto (plist-get original :body))))
+  ;; Push mark right before signature, if any.
+  (message-goto-signature)
+  (unless (eobp)
+    (end-of-line -1))
+  (push-mark)
+  (message-goto-body)
+  (set-buffer-modified-p nil))
+
+;;; Mode and keymap
+
+(defvar notmuch-message-mode-map
+  (let ((map (make-sparse-keymap)))
+    (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)
+  "Keymap for `notmuch-message-mode'.")
+
+(define-derived-mode notmuch-message-mode message-mode "Message[Notmuch]"
+  "Notmuch message composition mode. Mostly like `message-mode'."
+  (notmuch-address-setup))
+
+(put 'notmuch-message-mode 'flyspell-mode-predicate 'mail-mode-flyspell-verify)
+
+;;; New messages
+
+(defun notmuch-mua-pop-to-buffer (name switch-function)
+  "Pop to buffer NAME, and warn if it already exists and is modified.
+Like `message-pop-to-buffer' but enable `notmuch-message-mode'
+instead of `message-mode' and SWITCH-FUNCTION is mandatory."
+  (let ((buffer (get-buffer name)))
+    (if (and buffer
+            (buffer-name buffer))
+       (let ((window (get-buffer-window buffer 0)))
+         (if window
+             ;; Raise the frame already displaying the message buffer.
+             (progn
+               (select-frame-set-input-focus (window-frame window))
+               (select-window window))
+           (funcall switch-function buffer)
+           (set-buffer buffer))
+         (when (buffer-modified-p)
+           (if (y-or-n-p "Message already being composed; erase? ")
+               (message nil)
+             (error "Message being composed"))))
+      (funcall switch-function name)
+      (set-buffer name))
+    (erase-buffer)
+    (notmuch-message-mode)))
+
+(defun notmuch-mua--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))))))
+
+(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.
+
+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))))
+  (notmuch-mua-pop-to-buffer (message-buffer-name "mail" to)
+                            (or switch-function
+                                (notmuch-mua-get-switch-function)))
+  (let ((headers
+        (append
+         ;; The following is copied from `message-mail'
+         `((To . ,(or to "")) (Subject . ,(or subject "")))
+         ;; C-h f compose-mail says that headers should be specified as
+         ;; (string . value); however all the rest of message expects
+         ;; headers to be symbols, not strings (eg message-header-format-alist).
+         ;; https://lists.gnu.org/archive/html/emacs-devel/2011-01/msg00337.html
+         ;; We need to convert any string input, eg from rmail-start-mail.
+         (dolist (h other-headers other-headers)
+           (when (stringp (car h))
+             (setcar h (intern (capitalize (car h))))))))
+       ;; 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)
+  (cond
+   ((and to subject) (message-goto-body))
+   (to (message-goto-subject))
+   (t (message-goto-to))))
+
+(defvar notmuch-mua-sender-history nil)
+
+(defun notmuch-mua-prompt-for-sender ()
+  "Prompt for a sender from the user's configured identities."
+  (if notmuch-identities
+      (completing-read "Send mail from: " notmuch-identities
+                      nil nil nil 'notmuch-mua-sender-history
+                      (car notmuch-identities))
+    (let* ((name (notmuch-user-name))
+          (addrs (cons (notmuch-user-primary-email)
+                       (notmuch-user-other-email)))
+          (address
+           (completing-read (concat "Sender address for " name ": ") addrs
+                            nil nil nil 'notmuch-mua-sender-history
+                            (car addrs))))
+      (message-make-from name address))))
+
+(put 'notmuch-mua-new-mail 'notmuch-prefix-doc "... and prompt for sender")
+(defun notmuch-mua-new-mail (&optional prompt-for-sender)
+  "Compose new mail.
+
+If PROMPT-FOR-SENDER is non-nil, the user will be prompted for
+the From: address first."
+  (interactive "P")
+  (let ((other-headers
+        (and (or prompt-for-sender notmuch-always-prompt-for-sender)
+             (list (cons 'From (notmuch-mua-prompt-for-sender))))))
+    (notmuch-mua-mail nil nil other-headers nil (notmuch-mua-get-switch-function))))
+
+(defun notmuch-mua-new-forward-messages (messages &optional prompt-for-sender)
+  "Compose a new message forwarding MESSAGES.
+
+If PROMPT-FOR-SENDER is non-nil, the user will be prompteed for
+the From: address."
+  (let* ((other-headers
+         (and (or prompt-for-sender notmuch-always-prompt-for-sender)
+              (list (cons 'From (notmuch-mua-prompt-for-sender)))))
+        ;; Comes from the first message and is applied later.
+        forward-subject
+        ;; List of accumulated message-references of forwarded messages.
+        forward-references
+        ;; List of corresponding message-query.
+        forward-queries)
+    ;; Generate the template for the outgoing message.
+    (notmuch-mua-mail nil "" other-headers nil (notmuch-mua-get-switch-function))
+    (save-excursion
+      ;; Insert all of the forwarded messages.
+      (mapc (lambda (id)
+             (let ((temp-buffer (get-buffer-create
+                                 (concat "*notmuch-fwd-raw-" id "*"))))
+               ;; Get the raw version of this message in the buffer.
+               (with-current-buffer temp-buffer
+                 (erase-buffer)
+                 (let ((coding-system-for-read 'no-conversion))
+                   (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
+                 ;; last (i.e. first) one.
+                 (setq forward-subject (message-make-forward-subject))
+                 (push (message-fetch-field "Message-ID") forward-references)
+                 (push id forward-queries))
+               ;; Make a copy ready to be forwarded in the
+               ;; composition buffer.
+               (message-forward-make-body temp-buffer)
+               ;; Kill the temporary buffer.
+               (kill-buffer temp-buffer)))
+           ;; `message-forward-make-body' always puts the message at
+           ;; the top, so do them in reverse order.
+           (reverse messages))
+      ;; Add in the appropriate subject.
+      (save-restriction
+       (message-narrow-to-headers)
+       (message-remove-header "Subject")
+       (message-add-header (concat "Subject: " forward-subject))
+       (message-remove-header "References")
+       (message-add-header (concat "References: "
+                                   (mapconcat 'identity forward-references " "))))
+      ;; Create a buffer-local queue for tag changes triggered when
+      ;; sending the message.
+      (when notmuch-message-forwarded-tags
+       (setq notmuch-message-queued-tag-changes
+             (cl-loop for id in forward-queries
+                      collect
+                      (cons id notmuch-message-forwarded-tags))))
+      ;; `message-forward-make-body' shows the User-agent header.  Hide
+      ;; it again.
+      (message-hide-headers)
+      (set-buffer-modified-p nil))))
+
+(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.  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
+  ;; message in the primary selection.
+  ;;
+  ;; This is not what the user wanted and is a privacy risk (accidental
+  ;; pasting of the quoted message). We can avoid some of the problems
+  ;; by let-binding select-active-regions to nil. This fixes if the
+  ;; primary selection was previously in a non-emacs window but not if
+  ;; it was in an emacs window. To avoid the problem in the latter case
+  ;; we deactivate mark.
+  (let ((sender (and prompt-for-sender
+                    (notmuch-mua-prompt-for-sender)))
+       (select-active-regions nil))
+    (notmuch-mua-reply query-string sender reply-all duplicate)
+    (deactivate-mark)))
+
+;;; Checks
+
+(defun notmuch-mua-check-no-misplaced-secure-tag ()
+  "Query user if there is a misplaced secure mml tag.
+
+Emacs message-send will (probably) ignore a secure mml tag unless
+it is at the start of the body. Returns t if there is no such
+tag, or the user confirms they mean it."
+  (save-excursion
+    (let ((body-start (progn (message-goto-body) (point))))
+      (goto-char (point-max))
+      (or
+       ;; We are always fine if there is no secure tag.
+       (not (search-backward "<#secure" nil t))
+       ;; There is a secure tag, so it must be at the start of the
+       ;; body, with no secure tag earlier (i.e., in the headers).
+       (and (= (point) body-start)
+           (not (search-backward "<#secure" nil t)))
+       ;; The user confirms they means it.
+       (yes-or-no-p "\
+There is a <#secure> tag not at the start of the body. It is
+likely that the message will be sent unsigned and unencrypted.
+Really send? ")))))
+
+(defun notmuch-mua-check-secure-tag-has-newline ()
+  "Query if the secure mml tag has a newline following it.
+
+Emacs message-send will (probably) ignore a correctly placed
+secure mml tag unless it is followed by a newline. Returns t if
+any secure tag is followed by a newline, or the user confirms
+they mean it."
+  (save-excursion
+    (message-goto-body)
+    (or
+     ;; There is no (correctly placed) secure tag.
+     (not (looking-at "<#secure"))
+     ;; The secure tag is followed by a newline.
+     (looking-at "<#secure[^\n>]*>\n")
+     ;; The user confirms they means it.
+     (yes-or-no-p "\
+The <#secure> tag at the start of the body is not followed by a
+newline. It is likely that the message will be sent unsigned and
+unencrypted.  Really send? "))))
+
+;;; Finishing commands
+
+(defun notmuch-mua-send-common (arg &optional exit)
+  (interactive "P")
+  (run-hooks 'notmuch-mua-send-hook)
+  (when (and (notmuch-mua-check-no-misplaced-secure-tag)
+            (notmuch-mua-check-secure-tag-has-newline))
+    (cl-letf (((symbol-function 'message-do-fcc)
+              #'notmuch-maildir-message-do-fcc))
+      (if exit
+         (message-send-and-exit arg)
+       (message-send arg)))))
+
+(defun notmuch-mua-send-and-exit (&optional arg)
+  (interactive "P")
+  (notmuch-mua-send-common arg t))
+
+(defun notmuch-mua-send (&optional arg)
+  (interactive "P")
+  (notmuch-mua-send-common arg))
+
+(defun notmuch-mua-kill-buffer ()
+  (interactive)
+  (message-kill-buffer))
+
+;;; _
+
+(define-mail-user-agent 'notmuch-user-agent
+  'notmuch-mua-mail
+  'notmuch-mua-send-and-exit
+  'notmuch-mua-kill-buffer
+  'notmuch-mua-send-hook)
+
+;; Add some more headers to the list that `message-mode' hides when
+;; composing a message.
+(notmuch-mua-add-more-hidden-headers)
+
+(provide 'notmuch-mua)
+
+;;; notmuch-mua.el ends here
diff --git a/emacs/notmuch-parser.el b/emacs/notmuch-parser.el
new file mode 100644 (file)
index 0000000..f04b07c
--- /dev/null
@@ -0,0 +1,194 @@
+;;; notmuch-parser.el --- streaming S-expression parser  -*- lexical-binding: t -*-
+;;
+;; Copyright © Austin Clements
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Austin Clements <aclements@csail.mit.edu>
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'pcase)
+(require 'subr-x)
+
+(defun notmuch-sexp-create-parser ()
+  "Return a new streaming S-expression parser.
+
+This parser is designed to incrementally read an S-expression
+whose structure is known to the caller.  Like a typical
+S-expression parsing interface, it provides a function to read a
+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.
+
+The parser always consumes input from point in the current
+buffer.  Hence, the caller is allowed to delete any data before
+point and may resynchronize after an error by moving point."
+  (vector 'notmuch-sexp-parser
+         0     ; List depth
+         nil   ; Partial parse position marker
+         nil)) ; Partial parse state
+
+(defmacro notmuch-sexp--depth (sp)         `(aref ,sp 1))
+(defmacro notmuch-sexp--partial-pos (sp)   `(aref ,sp 2))
+(defmacro notmuch-sexp--partial-state (sp) `(aref ,sp 3))
+
+(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
+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.
+Otherwise, this moves point to just past the end of the value and
+returns the value."
+  (skip-chars-forward " \n\r\t")
+  (cond ((eobp) 'retry)
+       ((= (char-after) ?\))
+        ;; We've reached the end of a list
+        (if (= (notmuch-sexp--depth sp) 0)
+            ;; .. but we weren't in a list.  Let read signal the
+            ;; error to be consistent with all other code paths.
+            (read (current-buffer))
+          ;; Go up a level and return an end token
+          (cl-decf (notmuch-sexp--depth sp))
+          (forward-char)
+          'end))
+       ((= (char-after) ?\()
+        ;; We're at the beginning of a list.  If we haven't started
+        ;; a partial parse yet, attempt to read the list in its
+        ;; entirety.  If this fails, or we've started a partial
+        ;; parse, extend the partial parse to figure out when we
+        ;; have a complete list.
+        (catch 'return
+          (unless (notmuch-sexp--partial-state sp)
+            (let ((start (point)))
+              (condition-case nil
+                  (throw 'return (read (current-buffer)))
+                (end-of-file (goto-char start)))))
+          ;; Extend the partial parse
+          (let (is-complete)
+            (save-excursion
+              (let* ((new-state (parse-partial-sexp
+                                 (or (notmuch-sexp--partial-pos sp) (point))
+                                 (point-max) 0 nil
+                                 (notmuch-sexp--partial-state sp)))
+                     ;; A complete value is available if we've
+                     ;; reached depth 0.
+                     (depth (car new-state)))
+                (cl-assert (>= depth 0))
+                (if (= depth 0)
+                    ;; Reset partial parse state
+                    (setf (notmuch-sexp--partial-state sp) nil
+                          (notmuch-sexp--partial-pos sp) nil
+                          is-complete t)
+                  ;; Update partial parse state
+                  (setf (notmuch-sexp--partial-state sp) new-state
+                        (notmuch-sexp--partial-pos sp) (point-marker)))))
+            (if is-complete
+                (read (current-buffer))
+              'retry))))
+       (t
+        ;; Attempt to read a non-compound value
+        (let ((start (point)))
+          (condition-case nil
+              (let ((val (read (current-buffer))))
+                ;; We got what looks like a complete read, but if
+                ;; we reached the end of the buffer in the process,
+                ;; we may not actually have all of the input we
+                ;; need (unless it's a string, which is delimited).
+                (if (or (stringp val) (not (eobp)))
+                    val
+                  ;; We can't be sure the input was complete
+                  (goto-char start)
+                  'retry))
+            (end-of-file
+             (goto-char start)
+             'retry))))))
+
+(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
+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
+elements inside the list.  If the input in buffer is not the
+beginning of a list, throw invalid-read-syntax."
+  (skip-chars-forward " \n\r\t")
+  (cond ((eobp) 'retry)
+       ((= (char-after) ?\()
+        (forward-char)
+        (cl-incf (notmuch-sexp--depth sp))
+        t)
+       (t
+        ;; Skip over the bad character like `read' does
+        (forward-char)
+        (signal 'invalid-read-syntax (list (string (char-before)))))))
+
+(defvar notmuch-sexp--parser nil
+  "The buffer-local notmuch-sexp-parser instance.
+
+Used by `notmuch-sexp-parse-partial-list'.")
+
+(defvar notmuch-sexp--state nil
+  "The buffer-local `notmuch-sexp-parse-partial-list' state.")
+
+(defun notmuch-sexp-parse-partial-list (result-function result-buffer)
+  "Incrementally parse an S-expression list from the current buffer.
+
+This function consumes an S-expression list from the current
+buffer, applying RESULT-FUNCTION in RESULT-BUFFER to each
+complete value in the list.  It operates incrementally and should
+be called whenever the input buffer has been extended with
+additional data.  The caller just needs to ensure it does not
+move point in the input buffer."
+  ;; Set up the initial state
+  (unless (local-variable-p 'notmuch-sexp--parser)
+    (setq-local notmuch-sexp--parser (notmuch-sexp-create-parser))
+    (setq-local notmuch-sexp--state 'begin))
+  (let (done)
+    (while (not done)
+      (cl-case notmuch-sexp--state
+       (begin
+        ;; Enter the list
+        (if (eq (notmuch-sexp-begin-list notmuch-sexp--parser) 'retry)
+            (setq done t)
+          (setq notmuch-sexp--state 'result)))
+       (result
+        ;; Parse a result
+        (let ((result (notmuch-sexp-read notmuch-sexp--parser)))
+          (cl-case result
+            (retry (setq done t))
+            (end   (setq notmuch-sexp--state 'end))
+            (t     (with-current-buffer result-buffer
+                     (funcall result-function result))))))
+       (end
+        ;; Skip over trailing whitespace.
+        (skip-chars-forward " \n\r\t")
+        ;; Any trailing data is unexpected.
+        (unless (eobp)
+          (error "Trailing garbage following expression"))
+        (setq done t)))))
+  ;; Clear out what we've parsed
+  (delete-region (point-min) (point)))
+
+(provide 'notmuch-parser)
+
+;;; notmuch-parser.el ends here
diff --git a/emacs/notmuch-pkg.el.tmpl b/emacs/notmuch-pkg.el.tmpl
new file mode 100644 (file)
index 0000000..85c631d
--- /dev/null
@@ -0,0 +1,6 @@
+;; %AG%
+(define-package
+  "notmuch"
+  %VERSION%
+  "Emacs based front-end (MUA) for notmuch"
+  '((emacs "25.1")))
diff --git a/emacs/notmuch-print.el b/emacs/notmuch-print.el
new file mode 100644 (file)
index 0000000..85fa1f2
--- /dev/null
@@ -0,0 +1,100 @@
+;;; notmuch-print.el --- printing messages from notmuch  -*- lexical-binding: t -*-
+;;
+;; Copyright © David Edmondson
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: David Edmondson <dme@dme.org>
+
+;;; Code:
+
+(require 'notmuch-lib)
+
+(declare-function notmuch-show-get-prop "notmuch-show" (prop &optional props))
+
+;;; Options
+
+(defcustom notmuch-print-mechanism 'notmuch-print-lpr
+  "How should printing be done?"
+  :group 'notmuch-show
+  :type '(choice
+         (function :tag "Use lpr" notmuch-print-lpr)
+         (function :tag "Use ps-print" notmuch-print-ps-print)
+         (function :tag "Use ps-print then evince" notmuch-print-ps-print/evince)
+         (function :tag "Use muttprint" notmuch-print-muttprint)
+         (function :tag "Use muttprint then evince" notmuch-print-muttprint/evince)
+         (function :tag "Using a custom function")))
+
+;;; Utility functions
+
+(defun notmuch-print-run-evince (file)
+  "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'.
+
+Optional OUTPUT allows passing a list of flags to muttprint."
+  (apply #'notmuch--call-process-region (point-min) (point-max)
+        ;; Reads from stdin.
+        "muttprint"
+        nil nil nil
+        ;; Show the tags.
+        "--printed-headers" "Date_To_From_CC_Newsgroups_*Subject*_/Tags/"
+        output))
+
+;;; User-visible functions
+
+(defun notmuch-print-lpr (_msg)
+  "Print a message buffer using lpr."
+  (lpr-buffer))
+
+(defun notmuch-print-ps-print (msg)
+  "Print a message buffer using the ps-print package."
+  (let ((subject (notmuch-prettify-subject
+                 (plist-get (notmuch-show-get-prop :headers msg) :Subject))))
+    (rename-buffer subject t)
+    (ps-print-buffer)))
+
+(defun notmuch-print-ps-print/evince (msg)
+  "Preview a message buffer using ps-print and evince."
+  (let ((ps-file (make-temp-file "notmuch" nil ".ps"))
+       (subject (notmuch-prettify-subject
+                 (plist-get (notmuch-show-get-prop :headers msg) :Subject))))
+    (rename-buffer subject t)
+    (ps-print-buffer ps-file)
+    (notmuch-print-run-evince ps-file)))
+
+(defun notmuch-print-muttprint (_msg)
+  "Print a message using muttprint."
+  (notmuch-print-run-muttprint))
+
+(defun notmuch-print-muttprint/evince (_msg)
+  "Preview a message buffer using muttprint and evince."
+  (let ((ps-file (make-temp-file "notmuch" nil ".ps")))
+    (notmuch-print-run-muttprint (list "--printer" (concat "TO_FILE:" ps-file)))
+    (notmuch-print-run-evince ps-file)))
+
+(defun notmuch-print-message (msg)
+  "Print a message using the user-selected mechanism."
+  (set-buffer-modified-p nil)
+  (funcall notmuch-print-mechanism msg))
+
+;;; _
+
+(provide 'notmuch-print)
+
+;;; notmuch-print.el ends here
diff --git a/emacs/notmuch-query.el b/emacs/notmuch-query.el
new file mode 100644 (file)
index 0000000..2a46144
--- /dev/null
@@ -0,0 +1,74 @@
+;;; notmuch-query.el --- provide an emacs api to query notmuch  -*- lexical-binding: t -*-
+;;
+;; Copyright © David Bremner
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: David Bremner <david@tethera.net>
+
+;;; Code:
+
+(require 'notmuch-lib)
+
+;;; Basic query function
+
+(define-obsolete-function-alias
+  'notmuch-query-get-threads
+  #'notmuch--run-show
+  "notmuch 0.37")
+
+;;; Mapping functions across collections of messages
+
+(defun notmuch-query-map-aux  (mapper function seq)
+  "Private function to do the actual mapping and flattening."
+  (cl-mapcan (lambda (tree)
+              (funcall mapper function tree))
+            seq))
+
+(defun notmuch-query-map-threads (fn threads)
+  "Apply function FN to every thread in THREADS.
+Flatten results to a list.  See the function
+`notmuch-query-get-threads' for more information."
+  (notmuch-query-map-aux 'notmuch-query-map-forest fn threads))
+
+(defun notmuch-query-map-forest (fn forest)
+  "Apply function FN to every message in FOREST.
+Flatten results to a list.  See the function
+`notmuch-query-get-threads' for more information."
+  (notmuch-query-map-aux 'notmuch-query-map-tree fn forest))
+
+(defun notmuch-query-map-tree (fn tree)
+  "Apply function FN to every message in TREE.
+Flatten results to a list.  See the function
+`notmuch--run-show' for more information."
+  (cons (funcall fn (car tree))
+       (notmuch-query-map-forest fn (cadr tree))))
+
+;;; Predefined queries
+
+(defun notmuch-query-get-message-ids (&rest search-terms)
+  "Return a list of message-ids of messages that match SEARCH-TERMS."
+  (notmuch-query-map-threads
+   (lambda (msg) (plist-get msg :id))
+   (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)
+
+;;; notmuch-query.el ends here
diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el
new file mode 100644 (file)
index 0000000..4cc5aa5
--- /dev/null
@@ -0,0 +1,2732 @@
+;;; notmuch-show.el --- displaying notmuch forests  -*- lexical-binding: t -*-
+;;
+;; Copyright © Carl Worth
+;; Copyright © David Edmondson
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Carl Worth <cworth@cworth.org>
+;;          David Edmondson <dme@dme.org>
+
+;;; Code:
+
+(require 'mm-view)
+(require 'message)
+(require 'mm-decode)
+(require 'mailcap)
+(require 'icalendar)
+(require 'goto-addr)
+
+(require 'notmuch-lib)
+(require 'notmuch-tag)
+(require 'notmuch-wash)
+(require 'notmuch-mua)
+(require 'notmuch-crypto)
+(require 'notmuch-print)
+(require 'notmuch-draft)
+
+(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")
+(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 parent-buffer))
+(declare-function notmuch-tree-get-message-properties "notmuch-tree" nil)
+(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
+
+(defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
+  "Headers that should be shown in a message, in this order.
+
+For an open message, all of these headers will be made visible
+according to `notmuch-message-headers-visible' or can be toggled
+with `notmuch-show-toggle-visibility-headers'. For a closed message,
+only the first header in the list will be visible."
+  :type '(repeat string)
+  :group 'notmuch-show)
+
+(defcustom notmuch-message-headers-visible t
+  "Should the headers be visible by default?
+
+If this value is non-nil, then all of the headers defined in
+`notmuch-message-headers' will be visible by default in the display
+of each message. Otherwise, these headers will be hidden and
+`notmuch-show-toggle-visibility-headers' can be used to make them
+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
+  :group 'notmuch-show)
+
+(defvar notmuch-show-markup-headers-hook '(notmuch-show-colour-headers)
+  "A list of functions called to decorate the headers listed in
+`notmuch-message-headers'.")
+
+(defcustom notmuch-show-hook '(notmuch-show-turn-on-visual-line-mode)
+  "Functions called after populating a `notmuch-show' buffer."
+  :type 'hook
+  :options '(notmuch-show-turn-on-visual-line-mode)
+  :group 'notmuch-show
+  :group 'notmuch-hooks)
+
+(defcustom notmuch-show-insert-text/plain-hook
+  '(notmuch-wash-wrap-long-lines
+    notmuch-wash-tidy-citations
+    notmuch-wash-elide-blank-lines
+    notmuch-wash-excerpt-citations)
+  "Functions used to improve the display of text/plain parts."
+  :type 'hook
+  :options '(notmuch-wash-convert-inline-patch-to-part
+            notmuch-wash-wrap-long-lines
+            notmuch-wash-tidy-citations
+            notmuch-wash-elide-blank-lines
+            notmuch-wash-excerpt-citations)
+  :group 'notmuch-show
+  :group 'notmuch-hooks)
+
+(defcustom notmuch-show-max-text-part-size 100000
+  "Maximum size of a text part to be shown by default in characters.
+
+Set to 0 to show the part regardless of size."
+  :type 'integer
+  :group 'notmuch-show)
+
+;; Mostly useful for debugging.
+(defcustom notmuch-show-all-multipart/alternative-parts nil
+  "Should all parts of multipart/alternative parts be shown?"
+  :type 'boolean
+  :group 'notmuch-show)
+
+(defcustom notmuch-show-indent-messages-width 1
+  "Width of message indentation in threads.
+
+Messages are shown indented according to their depth in a thread.
+This variable determines the width of this indentation measured
+in number of blanks.  Defaults to `1', choose `0' to disable
+indentation."
+  :type 'integer
+  :group 'notmuch-show)
+
+(defcustom notmuch-show-indent-multipart nil
+  "Should the sub-parts of a multipart/* part be indented?"
+  ;; dme: Not sure which is a good default.
+  :type 'boolean
+  :group 'notmuch-show)
+
+(defcustom notmuch-show-part-button-default-action 'notmuch-show-save-part
+  "Default part header button action (on ENTER or mouse click)."
+  :group 'notmuch-show
+  :type '(choice (const :tag "Save part"
+                       notmuch-show-save-part)
+                (const :tag "View part"
+                       notmuch-show-view-part)
+                (const :tag "View interactively"
+                       notmuch-show-interactively-view-part)))
+
+(defcustom notmuch-show-only-matching-messages nil
+  "Only matching messages are shown by default."
+  :type 'boolean
+  :group 'notmuch-show)
+
+;; By default, block all external images to prevent privacy leaks and
+;; potential attacks.
+(defcustom notmuch-show-text/html-blocked-images "."
+  "Remote images that have URLs matching this regexp will be blocked."
+  :type '(choice (const nil) regexp)
+  :group 'notmuch-show)
+
+;;; Variables
+
+(defvar-local notmuch-show-thread-id nil)
+
+(defvar-local notmuch-show-parent-buffer nil)
+
+(defvar-local notmuch-show-query-context nil)
+
+(defvar-local notmuch-show-process-crypto nil)
+
+(defvar-local notmuch-show-elide-non-matching-messages nil)
+
+(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.
+
+When set to nil (the default) stdout and stderr from attachment
+handlers is discarded. When set to t the stdout and stderr from
+each attachment handler is logged in buffers with names beginning
+\" *notmuch-part*\".")
+
+;;; Options
+
+(defcustom notmuch-show-stash-mlarchive-link-alist
+  '(("MARC" . "https://marc.info/?i=")
+    ("Mail Archive, The" . "https://mid.mail-archive.com/")
+    ("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/")
+    ;; ("opensubscriber" . "http://opensubscriber.com/")
+    )
+  "List of Mailing List Archives to use when stashing links.
+
+This list is used for generating a Mailing List Archive reference
+URI with the current message's Message-Id in
+`notmuch-show-stash-mlarchive-link'.
+
+If the cdr of the alist element is not a function, the cdr is
+expected to contain a URI that is concatenated with the current
+message's Message-Id to create a ML archive reference URI.
+
+If the cdr is a function, the function is called with the
+Message-Id as the argument, and the function is expected to
+return the ML archive reference URI."
+  :type '(alist :key-type (string :tag "Name")
+               :value-type (choice
+                            (string :tag "URL")
+                            (function :tag "Function returning the URL")))
+  :group 'notmuch-show)
+
+(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
+provided with an MLA argument nor `completing-read' input."
+  :type `(choice
+         ,@(mapcar
+            (lambda (mla)
+              (list 'const :tag (car mla) :value (car mla)))
+            notmuch-show-stash-mlarchive-link-alist))
+  :group 'notmuch-show)
+
+(defcustom notmuch-show-mark-read-tags '("-unread")
+  "List of tag changes to apply to a message when it is marked as read.
+
+Tags starting with \"+\" (or not starting with either \"+\" or
+\"-\") in the list will be added, and tags starting with \"-\"
+will be removed from the message being marked as read.
+
+For example, if you wanted to remove an \"unread\" tag and add a
+\"read\" tag (which would make little sense), you would set:
+    (\"-unread\" \"+read\")"
+  :type '(repeat string)
+  :group 'notmuch-show)
+
+(defcustom notmuch-show-mark-read-function #'notmuch-show-seen-current-message
+  "Function to control which messages are marked read.
+
+The function should take two arguments START and END which will
+be the start and end of the visible portion of the buffer and
+should mark the appropriate messages read by applying
+`notmuch-show-mark-read'. This function will be called after
+every user interaction with notmuch."
+  :type 'function
+  :group 'notmuch-show)
+
+(defcustom notmuch-show-imenu-indent nil
+  "Should Imenu display messages indented.
+
+By default, Imenu (see Info node `(emacs) Imenu') in a
+notmuch-show buffer displays all messages straight.  This is
+because the default Emacs frontend for Imenu makes it difficult
+to select an Imenu entry with spaces in front.  Other imenu
+frontends such as counsel-imenu does not have this limitation.
+In these cases, Imenu entries can be indented to reflect the
+position of the message in the thread."
+  :type 'boolean
+  :group 'notmuch-show)
+
+;;; Utilities
+
+(defmacro with-current-notmuch-show-message (&rest body)
+  "Evaluate body with current buffer set to the text of current message."
+  `(save-excursion
+     (let ((id (notmuch-show-get-message-id)))
+       (let ((buf (generate-new-buffer (concat "*notmuch-msg-" id "*"))))
+        (with-current-buffer buf
+          (let ((coding-system-for-read 'no-conversion))
+            (notmuch--call-process notmuch-command nil t nil "show" "--format=raw" id))
+          ,@body)
+        (kill-buffer buf)))))
+
+(defun notmuch-show-turn-on-visual-line-mode ()
+  "Enable Visual Line mode."
+  (visual-line-mode t))
+
+;;; Commands
+
+;; DEPRECATED in Notmuch 0.16 since we now have convenient part
+;; commands.  We'll keep the command around for a version or two in
+;; case people want to bind it themselves.
+(defun notmuch-show-view-all-mime-parts ()
+  "Use external viewers to view all attachments from the current message."
+  (interactive)
+  (with-current-notmuch-show-message
+   ;; We override the mm-inline-media-tests to indicate which message
+   ;; parts are already sufficiently handled by the original
+   ;; presentation of the message in notmuch-show mode. These parts
+   ;; will be inserted directly into the temporary buffer of
+   ;; with-current-notmuch-show-message and silently discarded.
+   ;;
+   ;; Any MIME part not explicitly mentioned here will be handled by an
+   ;; external viewer as configured in the various mailcap files.
+   (let ((mm-inline-media-tests
+         '(("text/.*" ignore identity)
+           ("application/pgp-signature" ignore identity)
+           ("multipart/alternative" ignore identity)
+           ("multipart/mixed" ignore identity)
+           ("multipart/related" ignore identity))))
+     (mm-display-parts (mm-dissect-buffer)))))
+
+(defun notmuch-show-save-attachments ()
+  "Save all attachments from the current message."
+  (interactive)
+  (with-current-notmuch-show-message
+   (let ((mm-handle (mm-dissect-buffer)))
+     (notmuch-save-attachments
+      mm-handle (> (notmuch-count-attachments mm-handle) 1))))
+  (message "Done"))
+
+(defun notmuch-show-with-message-as-text (fn)
+  "Apply FN to a text representation of the current message.
+
+FN is called with one argument, the message properties. It should
+operation on the contents of the current buffer."
+  ;; Remake the header to ensure that all information is available.
+  (let* ((to (notmuch-show-get-to))
+        (cc (notmuch-show-get-cc))
+        (from (notmuch-show-get-from))
+        (subject (notmuch-show-get-subject))
+        (date (notmuch-show-get-date))
+        (tags (notmuch-show-get-tags))
+        (depth (notmuch-show-get-depth))
+        (header (concat
+                 "Subject: " subject "\n"
+                 "To: " to "\n"
+                 (if (not (string-empty-p cc))
+                     (concat "Cc: " cc "\n")
+                   "")
+                 "From: " from "\n"
+                 "Date: " date "\n"
+                 (if tags
+                     (concat "Tags: "
+                             (mapconcat #'identity tags ", ") "\n")
+                   "")))
+        (all (buffer-substring (notmuch-show-message-top)
+                               (notmuch-show-message-bottom)))
+
+        (props (notmuch-show-get-message-properties))
+        (indenting notmuch-show-indent-content))
+    (with-temp-buffer
+      (insert all)
+      (when indenting
+       (indent-rigidly (point-min)
+                       (point-max)
+                       (- (* notmuch-show-indent-messages-width depth))))
+      ;; Remove the original header.
+      (goto-char (point-min))
+      (re-search-forward "^$" (point-max) nil)
+      (delete-region (point-min) (point))
+      (insert header)
+      (funcall fn props))))
+
+(defun notmuch-show-print-message ()
+  "Print the current message."
+  (interactive)
+  (notmuch-show-with-message-as-text 'notmuch-print-message))
+
+;;; Headers
+
+(defun notmuch-show-fontify-header ()
+  (let ((face (cond
+              ((looking-at "[Tt]o:")
+               'message-header-to)
+              ((looking-at "[Bb]?[Cc][Cc]:")
+               'message-header-cc)
+              ((looking-at "[Ss]ubject:")
+               'message-header-subject)
+              (t
+               'message-header-other))))
+    (overlay-put (make-overlay (point) (re-search-forward ":"))
+                'face 'message-header-name)
+    (overlay-put (make-overlay (point) (re-search-forward ".*$"))
+                'face face)))
+
+(defun notmuch-show-colour-headers ()
+  "Apply some colouring to the current headers."
+  (goto-char (point-min))
+  (while (looking-at "^[A-Za-z][-A-Za-z0-9]*:")
+    (notmuch-show-fontify-header)
+    (forward-line)))
+
+(defun notmuch-show-spaces-n (n)
+  "Return a string comprised of `n' spaces."
+  (make-string n ? ))
+
+(defun notmuch-show-update-tags (tags)
+  "Update the displayed tags of the current message."
+  (save-excursion
+    (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
+cell of (AUTHOR_EMAIL AUTHOR_NAME). Return (ADDRESS nil) if
+parsing fails."
+  (condition-case nil
+      (let (p-name p-address)
+       ;; It would be convenient to use `mail-header-parse-address',
+       ;; but that expects un-decoded mailbox parts, whereas our
+       ;; mailbox parts are already decoded (and hence may contain
+       ;; UTF-8). Given that notmuch should handle most of the awkward
+       ;; cases, some simple string deconstruction should be sufficient
+       ;; here.
+       (cond
+        ;; "User <user@dom.ain>" style.
+        ((string-match "\\(.*\\) <\\(.*\\)>" address)
+         (setq p-name (match-string 1 address))
+         (setq p-address (match-string 2 address)))
+
+        ;; "<user@dom.ain>" style.
+        ((string-match "<\\(.*\\)>" address)
+         (setq p-address (match-string 1 address)))
+        ;; Everything else.
+        (t
+         (setq p-address address)))
+       (when p-name
+         ;; Remove elements of the mailbox part that are not relevant for
+         ;; display, even if they are required during transport:
+         ;;
+         ;; Backslashes.
+         (setq p-name (replace-regexp-in-string "\\\\" "" p-name))
+         ;; Outer single and double quotes, which might be nested.
+         (cl-loop with start-of-loop
+                  do   (setq start-of-loop p-name)
+                  when (string-match "^\"\\(.*\\)\"$" p-name)
+                  do   (setq p-name (match-string 1 p-name))
+                  when (string-match "^'\\(.*\\)'$" p-name)
+                  do   (setq p-name (match-string 1 p-name))
+                  until (string= start-of-loop p-name)))
+       ;; If the address is 'foo@bar.com <foo@bar.com>' then show just
+       ;; 'foo@bar.com'.
+       (when (string= p-name p-address)
+         (setq p-name nil))
+       (cons p-address p-name))
+    (error (cons address nil))))
+
+(defun notmuch-show-clean-address (address)
+  "Try to clean a single email ADDRESS for display.
+Return unchanged ADDRESS if parsing fails."
+  (let* ((clean-address (notmuch-clean-address address))
+        (p-address (car clean-address))
+        (p-name (cdr clean-address)))
+    ;; If no name, return just the address.
+    (if (not p-name)
+       p-address
+      ;; Otherwise format the name and address together.
+      (concat p-name " <" p-address ">"))))
+
+(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))
+        (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
+      ;; invisible U+200E LEFT-TO-RIGHT MARK character which forces
+      ;; the header paragraph as left-to-right text.
+      (insert (propertize (string ?\x200e) 'invisible t)))
+    (insert (if notmuch-show-indent-content
+               (notmuch-show-spaces-n (* notmuch-show-indent-messages-width
+                                         depth))
+             "")
+           from
+           " ("
+           date
+           ") ("
+           (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)))
+
+(defun notmuch-show-insert-header (header header-value)
+  "Insert a single header."
+  (insert header ": " (notmuch-sanitize header-value) "\n"))
+
+(defun notmuch-show-insert-headers (headers)
+  "Insert the headers of the current message."
+  (let ((start (point)))
+    (mapc (lambda (header)
+           (let* ((header-symbol (intern (concat ":" header)))
+                  (header-value (plist-get headers header-symbol)))
+             (when (and header-value
+                        (not (string-equal "" header-value)))
+               (notmuch-show-insert-header header header-value))))
+         notmuch-message-headers)
+    (save-excursion
+      (save-restriction
+       (narrow-to-region start (point-max))
+       (run-hooks 'notmuch-show-markup-headers-hook)))))
+
+;;; Parts
+
+(define-button-type 'notmuch-show-part-button-type
+  'action 'notmuch-show-part-button-default
+  'follow-link t
+  'face 'message-mml
+  :supertype 'notmuch-button-type)
+
+(defun notmuch-show-insert-part-header (_nth content-type declared-type
+                                           &optional name comment)
+  (let ((base-label (concat (and name (concat name ": "))
+                           declared-type
+                           (and (not (string-equal declared-type content-type))
+                                (concat " (as " content-type ")"))
+                           comment)))
+    (prog1 (insert-button
+           (concat "[ " base-label " ]")
+           :base-label base-label
+           :type 'notmuch-show-part-button-type
+           :notmuch-part-hidden nil)
+      (insert "\n"))))
+
+(defun notmuch-show-toggle-part-invisibility (&optional button)
+  (interactive)
+  (let ((button (or button (button-at (point)))))
+    (when button
+      (let ((overlay (button-get button 'overlay))
+           (lazy-part (button-get button :notmuch-lazy-part)))
+       ;; We have a part to toggle if there is an overlay or if there
+       ;; is a lazy part.  If neither is present we cannot toggle the
+       ;; part so we just return nil.
+       (when (or overlay lazy-part)
+         (let* ((show (button-get button :notmuch-part-hidden))
+                (new-start (button-start button))
+                (button-label (button-get button :base-label))
+                (old-point (point))
+                (properties (text-properties-at (button-start button)))
+                (inhibit-read-only t))
+           ;; Toggle the button itself.
+           (button-put button :notmuch-part-hidden (not show))
+           (goto-char new-start)
+           (insert "[ " button-label (if show " ]" " (hidden) ]"))
+           (set-text-properties new-start (point) properties)
+           (let ((old-end (button-end button)))
+             (move-overlay button new-start (point))
+             (delete-region (point) old-end))
+           (goto-char (min old-point (1- (button-end button))))
+           ;; Return nil if there is a lazy-part, it is empty, and we are
+           ;; trying to show it.  In all other cases return t.
+           (if lazy-part
+               (when show
+                 (button-put button :notmuch-lazy-part nil)
+                 (notmuch-show-lazy-part lazy-part button))
+             (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
+
+(defvar notmuch-show--cids nil
+  "Alist from raw content ID to (MSG PART).")
+(make-variable-buffer-local 'notmuch-show--cids)
+
+(defun notmuch-show--register-cids (msg part)
+  "Register content-IDs in PART and all of PART's sub-parts."
+  (let ((content-id (plist-get part :content-id)))
+    (when content-id
+      ;; Note that content-IDs are globally unique, except when they
+      ;; aren't: RFC 2046 section 5.1.4 permits children of a
+      ;; multipart/alternative to have the same content-ID, in which
+      ;; case the MUA is supposed to pick the best one it can render.
+      ;; We simply add the content-ID to the beginning of our alist;
+      ;; so if this happens, we'll take the last (and "best")
+      ;; alternative (even if we can't render it).
+      (push (list content-id msg part) notmuch-show--cids)))
+  ;; Recurse on sub-parts
+  (when-let ((type (plist-get part :content-type)))
+    (pcase-let ((`(,type ,subtype)
+                (split-string (downcase type) "/")))
+      (cond ((equal type "multipart")
+            (mapc (apply-partially #'notmuch-show--register-cids msg)
+                  (plist-get part :content)))
+           ((and (equal type "message")
+                 (equal subtype "rfc822"))
+            (notmuch-show--register-cids
+             msg
+             (car (plist-get (car (plist-get part :content)) :body))))))))
+
+(defun notmuch-show--get-cid-content (cid)
+  "Return a list (CID-content content-type) or nil.
+
+This will only find parts from messages that have been inserted
+into the current buffer.  CID must be a raw content ID, without
+enclosing angle brackets, a cid: prefix, or URL encoding.  This
+will return nil if the CID is unknown or cannot be retrieved."
+  (when-let ((descriptor (cdr (assoc cid notmuch-show--cids))))
+    (pcase-let ((`(,msg ,part) descriptor))
+      ;; Request caching for this content, as some messages
+      ;; reference the same cid: part many times (hundreds!).
+      (list (notmuch-get-bodypart-binary
+            msg part notmuch-show-process-crypto 'cache)
+           (plist-get part :content-type)))))
+
+(defun notmuch-show-setup-w3m ()
+  "Instruct w3m how to retrieve content from a \"related\" part of a message."
+  (interactive)
+  (when (and (boundp 'w3m-cid-retrieve-function-alist)
+            (not (assq 'notmuch-show-mode w3m-cid-retrieve-function-alist)))
+    (push (cons 'notmuch-show-mode #'notmuch-show--cid-w3m-retrieve)
+         w3m-cid-retrieve-function-alist))
+  (setq mm-html-inhibit-images nil))
+
+(defvar w3m-current-buffer) ;; From `w3m.el'.
+(defun notmuch-show--cid-w3m-retrieve (url &rest _args)
+  ;; url includes the cid: prefix and is URL encoded (see RFC 2392).
+  (let* ((cid (url-unhex-string (substring url 4)))
+        (content-and-type
+         (with-current-buffer w3m-current-buffer
+           (notmuch-show--get-cid-content cid))))
+    (when content-and-type
+      (insert (car content-and-type))
+      (cadr content-and-type))))
+
+;; MIME part renderers
+
+(defun notmuch-show-multipart/*-to-list (part)
+  (mapcar (lambda (inner-part) (plist-get inner-part :content-type))
+         (plist-get part :content)))
+
+(defun notmuch-show-insert-part-multipart/alternative (msg part _content-type _nth depth _button)
+  (let ((chosen-type (car (notmuch-multipart/alternative-choose
+                          msg (notmuch-show-multipart/*-to-list part))))
+       (inner-parts (plist-get part :content))
+       (start (point)))
+    ;; This inserts all parts of the chosen type rather than just one,
+    ;; but it's not clear that this is the wrong thing to do - which
+    ;; should be chosen if there are more than one that match?
+    (mapc (lambda (inner-part)
+           (let* ((inner-type (plist-get inner-part :content-type))
+                  (hide (not (or notmuch-show-all-multipart/alternative-parts
+                                 (string= chosen-type inner-type)))))
+             (notmuch-show-insert-bodypart msg inner-part depth hide)))
+         inner-parts)
+
+    (when notmuch-show-indent-multipart
+      (indent-rigidly start (point) 1)))
+  t)
+
+(defun notmuch-show-insert-part-multipart/related (msg part _content-type _nth depth _button)
+  (let ((inner-parts (plist-get part :content))
+       (start (point)))
+    ;; Render the primary part.  FIXME: Support RFC 2387 Start header.
+    (notmuch-show-insert-bodypart msg (car inner-parts) depth)
+    ;; Add hidden buttons for the rest
+    (mapc (lambda (inner-part)
+           (notmuch-show-insert-bodypart msg inner-part depth t))
+         (cdr inner-parts))
+    (when notmuch-show-indent-multipart
+      (indent-rigidly start (point) 1)))
+  t)
+
+(defun notmuch-show-insert-part-multipart/signed (msg part _content-type _nth depth button)
+  (when button
+    (button-put button 'face 'notmuch-crypto-part-header))
+  ;; Insert a button detailing the signature status.
+  (notmuch-crypto-insert-sigstatus-button (car (plist-get part :sigstatus))
+                                         (notmuch-show-get-header :From msg))
+  (let ((inner-parts (plist-get part :content))
+       (start (point)))
+    ;; Show all of the parts.
+    (mapc (lambda (inner-part)
+           (notmuch-show-insert-bodypart msg inner-part depth))
+         inner-parts)
+    (when notmuch-show-indent-multipart
+      (indent-rigidly start (point) 1)))
+  t)
+
+(defun notmuch-show-insert-part-multipart/encrypted (msg part _content-type _nth depth button)
+  (when button
+    (button-put button 'face 'notmuch-crypto-part-header))
+  ;; Insert a button detailing the encryption status.
+  (notmuch-crypto-insert-encstatus-button (car (plist-get part :encstatus)))
+  ;; Insert a button detailing the signature status.
+  (notmuch-crypto-insert-sigstatus-button (car (plist-get part :sigstatus))
+                                         (notmuch-show-get-header :From msg))
+  (let ((inner-parts (plist-get part :content))
+       (start (point)))
+    ;; Show all of the parts.
+    (mapc (lambda (inner-part)
+           (notmuch-show-insert-bodypart msg inner-part depth))
+         inner-parts)
+    (when notmuch-show-indent-multipart
+      (indent-rigidly start (point) 1)))
+  t)
+
+(defun notmuch-show-insert-part-application/pgp-encrypted (_msg _part _content-type _nth _depth _button)
+  t)
+
+(defun notmuch-show-insert-part-multipart/* (msg part _content-type _nth depth _button)
+  (let ((inner-parts (plist-get part :content))
+       (start (point)))
+    ;; Show all of the parts.
+    (mapc (lambda (inner-part)
+           (notmuch-show-insert-bodypart msg inner-part depth))
+         inner-parts)
+    (when notmuch-show-indent-multipart
+      (indent-rigidly start (point) 1)))
+  t)
+
+(defun notmuch-show-insert-part-message/rfc822 (msg part _content-type _nth depth _button)
+  (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
+  ;; to the whole of the part including the part button if there is
+  ;; one.
+  (let ((start (if button
+                  (button-start button)
+                (point))))
+    (insert (notmuch-get-bodypart-text msg part notmuch-show-process-crypto))
+    (save-excursion
+      (save-restriction
+       (narrow-to-region start (point-max))
+       (run-hook-with-args 'notmuch-show-insert-text/plain-hook msg depth))))
+  t)
+
+(defun notmuch-show-insert-part-text/calendar (msg part _content-type _nth _depth _button)
+  (insert (with-temp-buffer
+           (insert (notmuch-get-bodypart-text msg part notmuch-show-process-crypto))
+           ;; notmuch-get-bodypart-text does no newline conversion.
+           ;; Replace CRLF with LF before icalendar can use it.
+           (goto-char (point-min))
+           (while (re-search-forward "\r\n" nil t)
+             (replace-match "\n" nil nil))
+           (let ((file (make-temp-file "notmuch-ical"))
+                 result)
+             (unwind-protect
+                 (progn
+                   (unless (icalendar-import-buffer file t)
+                     (error "Icalendar import error. %s"
+                            "See *icalendar-errors* for more information"))
+                   (set-buffer (get-file-buffer file))
+                   (setq result (buffer-substring (point-min) (point-max)))
+                   (set-buffer-modified-p nil)
+                   (kill-buffer (current-buffer)))
+               (delete-file file))
+             result)))
+  t)
+
+;; For backwards compatibility.
+(defun notmuch-show-insert-part-text/x-vcalendar (msg part _content-type _nth depth _button)
+  (notmuch-show-insert-part-text/calendar msg part nil nil depth nil))
+
+(when (version< emacs-version "25.3")
+  ;; https://bugs.gnu.org/28350
+  ;;
+  ;; For newer emacs, we fall back to notmuch-show-insert-part-*/*
+  ;; (see notmuch-show-handlers-for)
+  (defun notmuch-show-insert-part-text/enriched
+      (msg part content-type nth depth button)
+    ;; By requiring enriched below, we ensure that the function
+    ;; enriched-decode-display-prop is defined before it will be
+    ;; shadowed by the letf below. Otherwise the version in
+    ;; enriched.el may be loaded a bit later and used instead (for
+    ;; the first time).
+    (require 'enriched)
+    (cl-letf (((symbol-function 'enriched-decode-display-prop)
+              (lambda (start end &optional _param) (list start end))))
+      (notmuch-show-insert-part-*/* msg part content-type nth depth button))))
+
+(defun notmuch-show-get-mime-type-of-application/octet-stream (part)
+  ;; If we can deduce a MIME type from the filename of the attachment,
+  ;; we return that.
+  (and (plist-get part :filename)
+       (let ((extension (file-name-extension (plist-get part :filename))))
+        (and extension
+             (progn
+               (mailcap-parse-mimetypes)
+               (let ((mime-type (mailcap-extension-to-mime extension)))
+                 (and mime-type
+                      (not (string-equal mime-type "application/octet-stream"))
+                      mime-type)))))))
+
+(defun notmuch-show-insert-part-text/html (msg part content-type nth depth button)
+  (if (eq mm-text-html-renderer 'shr)
+      ;; It's easier to drive shr ourselves than to work around the
+      ;; goofy things `mm-shr' does (like irreversibly taking over
+      ;; content ID handling).
+      ;; FIXME: If we block an image, offer a button to load external
+      ;; images.
+      (let ((shr-blocked-images notmuch-show-text/html-blocked-images))
+       (notmuch-show--insert-part-text/html-shr msg part))
+    ;; Otherwise, let message-mode do the heavy lifting
+    ;;
+    ;; w3m sets up a keymap which "leaks" outside the invisible region
+    ;; and causes strange effects in notmuch. We set
+    ;; mm-inline-text-html-with-w3m-keymap to nil to tell w3m not to
+    ;; set a keymap (so the normal notmuch-show-mode-map remains).
+    (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)
+         (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
+
+(declare-function libxml-parse-html-region "xml.c")
+(declare-function shr-insert-document "shr")
+
+(defun notmuch-show--insert-part-text/html-shr (msg part)
+  ;; Make sure shr is loaded before we start let-binding its globals
+  (require 'shr)
+  (let ((dom (let ((process-crypto notmuch-show-process-crypto))
+              (with-temp-buffer
+                (insert (notmuch-get-bodypart-text msg part process-crypto))
+                (libxml-parse-html-region (point-min) (point-max)))))
+       (shr-content-function
+        (lambda (url)
+          ;; shr strips the "cid:" part of URL, but doesn't
+          ;; URL-decode it (see RFC 2392).
+          (let ((cid (url-unhex-string url)))
+            (car (notmuch-show--get-cid-content cid))))))
+    (shr-insert-document dom)
+    t))
+
+(defun notmuch-show-insert-part-*/* (msg part content-type _nth _depth _button)
+  ;; This handler _must_ succeed - it is the handler of last resort.
+  (notmuch-mm-display-part-inline msg part content-type notmuch-show-process-crypto)
+  t)
+
+;;; Functions for determining how to handle MIME parts.
+
+(defun notmuch-show-handlers-for (content-type)
+  "Return a list of content handlers for a part of type CONTENT-TYPE."
+  (let (result)
+    (mapc (lambda (func)
+           (when (functionp func)
+             (push func result)))
+         ;; Reverse order of prefrence.
+         (list (intern (concat "notmuch-show-insert-part-*/*"))
+               (intern (concat "notmuch-show-insert-part-"
+                               (car (split-string content-type "/"))
+                               "/*"))
+               (intern (concat "notmuch-show-insert-part-" content-type))))
+    result))
+
+;;; Parts
+
+(defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth button)
+  ;; Run the handlers until one of them succeeds.
+  (cl-loop for handler in (notmuch-show-handlers-for content-type)
+          until (condition-case err
+                    (funcall handler msg part content-type nth depth button)
+                  ;; Specifying `debug' here lets the debugger run if
+                  ;; `debug-on-error' is non-nil.
+                  ((debug error)
+                   (insert "!!! Bodypart handler `" (prin1-to-string handler)
+                           "' threw an error:\n"
+                           "!!! " (error-message-string err) "\n")
+                   nil))))
+
+(defun notmuch-show-create-part-overlays (button beg end)
+  "Add an overlay to the part between BEG and END."
+  ;; If there is no button (i.e., the part is text/plain and the first
+  ;; part) or if the part has no content then we don't make the part
+  ;; toggleable.
+  (when (and button (/= beg end))
+    (button-put button 'overlay (make-overlay beg end))
+    ;; Return true if we created an overlay.
+    t))
+
+(defun notmuch-show-record-part-information (part beg end)
+  "Store PART as a text property from BEG to END."
+  ;; Record part information.  Since we already inserted subparts,
+  ;; don't override existing :notmuch-part properties.
+  (notmuch-map-text-property beg end :notmuch-part
+                            (lambda (v) (or v part)))
+  ;; Make :notmuch-part front sticky and rear non-sticky so it stays
+  ;; applied to the beginning of each line when we indent the
+  ;; message.  Since we're operating on arbitrary renderer output,
+  ;; watch out for sticky specs of t, which means all properties are
+  ;; front-sticky/rear-nonsticky.
+  (notmuch-map-text-property beg end 'front-sticky
+                            (lambda (v)
+                              (if (listp v)
+                                  (cl-pushnew :notmuch-part v)
+                                v)))
+  (notmuch-map-text-property beg end 'rear-nonsticky
+                            (lambda (v)
+                              (if (listp v)
+                                  (cl-pushnew :notmuch-part v)
+                                v))))
+
+(defun notmuch-show-lazy-part (part-args button)
+  ;; Insert the lazy part after the button for the part. We would just
+  ;; move to the start of the new line following the button and insert
+  ;; the part but that point might have text properties (eg colours
+  ;; from a message header etc) so instead we start from the last
+  ;; character of the button by adding a newline and finish by
+  ;; removing the extra newline from the end of the part.
+  (save-excursion
+    (goto-char (button-end button))
+    (insert "\n")
+    (let* ((inhibit-read-only t)
+          ;; We need to use markers for the start and end of the part
+          ;; because the part insertion functions do not guarantee
+          ;; to leave point at the end of the part.
+          (part-beg (copy-marker (point) nil))
+          (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))
+          (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
+                       (* notmuch-show-indent-messages-width depth)))
+      (goto-char part-end)
+      (delete-char 1)
+      (notmuch-show-record-part-information (cadr part-args)
+                                           (button-start button)
+                                           part-end)
+      ;; Create the overlay. If the lazy-part turned out to be empty/not
+      ;; showable this returns nil.
+      (notmuch-show-create-part-overlays button part-beg part-end))))
+
+(defun notmuch-show-mime-type (part)
+  "Return the correct mime-type to use for PART."
+  (when-let ((content-type (plist-get part :content-type)))
+    (setq content-type (downcase content-type))
+    (or (and (string= content-type "application/octet-stream")
+            (notmuch-show-get-mime-type-of-application/octet-stream part))
+       (and (string= content-type "inline patch")
+            "text/x-diff")
+       content-type)))
+
+;; The following variable can be overridden by let bindings.
+(defvar notmuch-show-insert-header-p-function 'notmuch-show-insert-header-p
+  "Specify which function decides which part headers get inserted.
+
+The function should take two parameters, PART and HIDE, and
+should return non-NIL if a header button should be inserted for
+this part.")
+
+(defun notmuch-show-insert-header-p (part _hide)
+  ;; Show all part buttons except for the first part if it is text/plain.
+  (let ((mime-type (notmuch-show-mime-type part)))
+    (not (and (string= mime-type "text/plain")
+             (<= (plist-get part :id) 1)))))
+
+(defun notmuch-show-reply-insert-header-p-never (_part _hide)
+  nil)
+
+(defun notmuch-show-reply-insert-header-p-trimmed (part hide)
+  (let ((mime-type (notmuch-show-mime-type part)))
+    (and (not (notmuch-match-content-type mime-type "multipart/*"))
+        (not hide))))
+
+(defun notmuch-show-reply-insert-header-p-minimal (part hide)
+  (let ((mime-type (notmuch-show-mime-type part)))
+    (and (notmuch-match-content-type mime-type "text/*")
+        (not hide))))
+
+(defun notmuch-show-insert-bodypart (msg part depth &optional hide)
+  "Insert the body part PART at depth DEPTH in the current thread.
+
+HIDE determines whether to show or hide the part and the button
+as follows: If HIDE is nil, show the part and the button. If HIDE
+is t, hide the part initially and show the button."
+  (let* ((content-type (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 (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/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))
+        (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)
+    (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 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))
+    ;; Ensure that the part ends with a carriage return.
+    (unless (bolp)
+      (insert "\n"))
+    ;; We do not create the overlay for hidden (lazy) parts until
+    ;; they are inserted.
+    (if show-part
+       (notmuch-show-create-part-overlays button content-beg (point))
+      (save-excursion
+       (notmuch-show-toggle-part-invisibility button)))
+    (notmuch-show-record-part-information part beg (point))))
+
+(defun notmuch-show-insert-body (msg body depth)
+  "Insert the body BODY at depth DEPTH in the current thread."
+  ;; Register all content IDs for this message.  According to RFC
+  ;; 2392, content IDs are *global*, but it's okay if an MUA treats
+  ;; them as only global within a message.
+  (notmuch-show--register-cids msg (car body))
+  (mapc (lambda (part) (notmuch-show-insert-bodypart msg part depth)) body))
+
+(defun notmuch-show-make-symbol (type)
+  (make-symbol (concat "notmuch-show-" type)))
+
+(defun notmuch-show-strip-re (string)
+  (replace-regexp-in-string "^\\([Rr]e: *\\)+" "" string))
+
+(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))
+        ;; Indentation causes the buffer offset of the start/end
+        ;; points to move, so we must use markers.
+        message-start message-end
+        content-start content-end
+        headers-start headers-end
+        (bare-subject (notmuch-show-strip-re (plist-get headers :Subject))))
+    (setq message-start (point-marker))
+    (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
+    ;; to after the first header.
+    (notmuch-show-insert-headers headers)
+    (save-excursion
+      (goto-char content-start)
+      ;; If the subject of this message is the same as that of the
+      ;; previous message, don't display it when this message is
+      ;; collapsed.
+      (unless (string= notmuch-show-previous-subject bare-subject)
+       (forward-line 1))
+      (setq headers-start (point-marker)))
+    (setq headers-end (point-marker))
+    (setq notmuch-show-previous-subject bare-subject)
+    ;; A blank line between the headers and the body.
+    (insert "\n")
+    (notmuch-show-insert-body msg (plist-get msg :body)
+                             (if notmuch-show-indent-content depth 0))
+    ;; Ensure that the body ends with a newline.
+    (unless (bolp)
+      (insert "\n"))
+    (setq content-end (point-marker))
+    ;; Indent according to the depth in the thread.
+    (when notmuch-show-indent-content
+      (indent-rigidly content-start
+                     content-end
+                     (* notmuch-show-indent-messages-width depth)))
+    (setq message-end (point-max-marker))
+    ;; Save the extents of this message over the whole text of the
+    ;; message.
+    (put-text-property message-start message-end
+                      :notmuch-message-extent
+                      (cons message-start message-end))
+    ;; Create overlays used to control visibility
+    (plist-put msg :headers-overlay (make-overlay headers-start headers-end))
+    (plist-put msg :message-overlay (make-overlay headers-start content-end))
+    (plist-put msg :depth depth)
+    ;; Save the properties for this message. Currently this saves the
+    ;; entire message (augmented it with other stuff), which seems
+    ;; like overkill. We might save a reduced subset (for example, not
+    ;; the content).
+    (notmuch-show-set-message-properties msg)
+    ;; Set header visibility.
+    (notmuch-show-headers-visible msg notmuch-message-headers-visible)
+    ;; Message visibility depends on whether it matched the search
+    ;; criteria.
+    (notmuch-show-message-visible msg (and (plist-get msg :match)
+                                          (not (plist-get msg :excluded))))))
+
+;;; Toggle commands
+
+(defun notmuch-show-toggle-process-crypto ()
+  "Toggle the processing of cryptographic MIME parts."
+  (interactive)
+  (setq notmuch-show-process-crypto (not notmuch-show-process-crypto))
+  (message (if notmuch-show-process-crypto
+              "Processing cryptographic MIME parts."
+            "Not processing cryptographic MIME parts."))
+  (notmuch-show-refresh-view))
+
+(defun notmuch-show-toggle-elide-non-matching ()
+  "Toggle the display of non-matching messages."
+  (interactive)
+  (setq notmuch-show-elide-non-matching-messages
+       (not notmuch-show-elide-non-matching-messages))
+  (message (if notmuch-show-elide-non-matching-messages
+              "Showing matching messages only."
+            "Showing all messages."))
+  (notmuch-show-refresh-view))
+
+(defun notmuch-show-toggle-thread-indentation ()
+  "Toggle the indentation of threads."
+  (interactive)
+  (setq notmuch-show-indent-content (not notmuch-show-indent-content))
+  (message (if notmuch-show-indent-content
+              "Content is indented."
+            "Content is not indented."))
+  (notmuch-show-refresh-view))
+
+;;; Main insert functions
+
+(defun notmuch-show-insert-tree (tree depth)
+  "Insert the message tree TREE at depth DEPTH in the current thread."
+  (let ((msg (car tree))
+       (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))))
+
+(defun notmuch-show-insert-thread (thread depth)
+  "Insert the thread THREAD at depth DEPTH in the current forest."
+  (mapc (lambda (tree) (notmuch-show-insert-tree tree depth)) thread))
+
+(defun notmuch-show-insert-forest (forest)
+  "Insert the forest of threads FOREST."
+  (mapc (lambda (thread) (notmuch-show-insert-thread thread 0)) forest))
+
+;;; Link buttons
+
+(defvar notmuch-id-regexp
+  (concat
+   ;; Match the id: prefix only if it begins a word (to disallow, for
+   ;; example, matching cid:).
+   "\\<id:\\("
+   ;; If the term starts with a ", then parse Xapian's quoted boolean
+   ;; term syntax, which allows for anything as long as embedded
+   ;; double quotes escaped by doubling them.  We also disallow
+   ;; newlines (which Xapian allows) to prevent runaway terms.
+   "\"\\([^\"\n]\\|\"\"\\)*\""
+   ;; Otherwise, parse Xapian's unquoted syntax, which goes up to the
+   ;; next space or ).  We disallow [.,;] as the last character
+   ;; because these are probably part of the surrounding text, and not
+   ;; part of the id.  This doesn't match single character ids; meh.
+   "\\|[^\"[:space:])][^[:space:])]*[^])[:space:].,:;?!]"
+   "\\)")
+  "The regexp used to match id: links in messages.")
+
+(defvar notmuch-mid-regexp
+  ;; goto-address-url-regexp matched cid: links, which have the same
+  ;; grammar as the message ID part of a mid: link.  Construct the
+  ;; regexp using the same technique as goto-address-url-regexp.
+  (concat "\\<mid:\\(" thing-at-point-url-path-regexp "\\)")
+  "The regexp used to match mid: links in messages.
+
+See RFC 2392.")
+
+(defun notmuch-show-buttonise-links (start end)
+  "Buttonise URLs and mail addresses between START and END.
+
+This also turns id:\"<message id>\"-parts and mid: links into
+buttons for a corresponding notmuch search."
+  (goto-address-fontify-region start end)
+  (save-excursion
+    (let (links
+         (beg-line (progn (goto-char start) (line-beginning-position)))
+         (end-line (progn (goto-char end) (line-end-position))))
+      (goto-char beg-line)
+      (while (re-search-forward notmuch-id-regexp end-line t)
+       (push (list (match-beginning 0) (match-end 0)
+                   (match-string-no-properties 0)) links))
+      (goto-char beg-line)
+      (while (re-search-forward notmuch-mid-regexp end-line t)
+       (let* ((mid-cid (match-string-no-properties 1))
+              (mid (save-match-data
+                     (string-match "^[^/]*" mid-cid)
+                     (url-unhex-string (match-string 0 mid-cid)))))
+         (push (list (match-beginning 0) (match-end 0)
+                     (notmuch-id-to-query mid)) links)))
+      (pcase-dolist (`(,beg ,end ,link) links)
+       ;; Remove the overlay created by goto-address-mode
+       (remove-overlays beg end 'goto-address t)
+       (make-text-button beg end
+                         :type 'notmuch-button-type
+                         'action `(lambda (arg)
+                                    (notmuch-show ,link current-prefix-arg))
+                         'follow-link t
+                         'help-echo "Mouse-1, RET: search for this message"
+                         'face goto-address-mail-face)))))
+
+;;; Show command
+
+;;;###autoload
+(defun notmuch-show (thread-id &optional elide-toggle parent-buffer query-context buffer-name)
+  "Run \"notmuch show\" with the given thread ID and display results.
+
+ELIDE-TOGGLE, if non-nil, inverts the default elide behavior.
+
+The optional PARENT-BUFFER is the notmuch-search buffer from
+which this notmuch-show command was executed, (so that the
+next thread from that buffer can be show when done with this
+one).
+
+The optional QUERY-CONTEXT is a notmuch search term. Only
+messages from the thread matching this search term are shown if
+non-nil.
+
+The optional BUFFER-NAME provides the name of the buffer in
+which the message thread is shown. If it is nil (which occurs
+when the command is called interactively) the argument to the
+function is used.
+
+Returns the buffer containing the messages, or NIL if no messages
+matched."
+  (interactive "sNotmuch show: \nP")
+  (let ((buffer-name (generate-new-buffer-name
+                     (or buffer-name
+                         (concat "*notmuch-" thread-id "*"))))
+       (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)
+    (notmuch-show-mode)
+    ;; Set various buffer local variables to their appropriate initial
+    ;; state. Do this after enabling `notmuch-show-mode' so that they
+    ;; aren't wiped out.
+    (setq notmuch-show-thread-id thread-id)
+    (setq notmuch-show-parent-buffer parent-buffer)
+    (setq notmuch-show-query-context
+         (if (or (string= query-context "")
+                 (string= query-context "*"))
+             nil
+           query-context))
+    (setq notmuch-show-process-crypto notmuch-crypto-process-mime)
+    ;; If `elide-toggle', invert the default value.
+    (setq notmuch-show-elide-non-matching-messages
+         (if elide-toggle
+             (not notmuch-show-only-matching-messages)
+           notmuch-show-only-matching-messages))
+    (add-hook 'post-command-hook #'notmuch-show-command-hook nil t)
+    (jit-lock-register #'notmuch-show-buttonise-links)
+    (notmuch-tag-clear-cache)
+    (let ((inhibit-read-only t))
+      (if (notmuch-show--build-buffer)
+         ;; Messages were inserted into the buffer.
+         (current-buffer)
+       ;; No messages were inserted - presumably none matched the
+       ;; query.
+       (kill-buffer (current-buffer))
+       (ding)
+       (message "No messages matched the query!")
+       nil))))
+
+(defun notmuch-show--build-queries (thread context)
+  "Return a list of queries to try for this search.
+
+THREAD and CONTEXT are both strings, though CONTEXT may be nil.
+When CONTEXT is not nil, the first query is the conjunction of it
+and THREAD.  The next query is THREAD alone, and serves as a
+fallback if the prior matches no messages."
+  (let (queries)
+    (push (list thread) queries)
+    (when context
+      (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.
+
+Apply the previously saved STATE if supplied, otherwise show the
+first relevant message.
+
+If no messages match the query return NIL."
+  (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)
+        ;; Must be reset every time we are going to start inserting
+        ;; messages into the buffer.
+        (notmuch-show-previous-subject ""))
+    ;; Use results from the first query that returns some.
+    (while (and (not forest) queries)
+      (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)
+      ;; Store the original tags for each message so that we can
+      ;; display changes.
+      (notmuch-show-mapc
+       (lambda () (notmuch-show-set-prop :orig-tags (notmuch-show-get-tags))))
+      (setq header-line-format (notmuch-show--header-line-format))
+      (run-hooks 'notmuch-show-hook)
+      (if state
+         (notmuch-show-apply-state state)
+       ;; With no state to apply, just go to the first message.
+       (notmuch-show-goto-first-wanted-message)))
+    ;; Report back to the caller whether any messages matched.
+    forest))
+
+;;; Refresh command
+
+(defun notmuch-show-capture-state ()
+  "Capture the state of the current buffer.
+
+This includes:
+ - the list of open messages,
+ - the combination of current message id with/for each visible window."
+  (let* ((win-list (get-buffer-window-list (current-buffer) nil t))
+        (win-id-combo (mapcar (lambda (win)
+                                (with-selected-window win
+                                  (list win (notmuch-show-get-message-id))))
+                              win-list)))
+    (list win-id-combo (notmuch-show-get-message-ids-for-open-messages))))
+
+(defun notmuch-show-get-query ()
+  "Return the current query in this show buffer."
+  (if notmuch-show-query-context
+      (concat notmuch-show-thread-id
+             " and ("
+             notmuch-show-query-context
+             ")")
+    notmuch-show-thread-id))
+
+(defun notmuch-show-goto-message (msg-id)
+  "Go to message with msg-id."
+  (goto-char (point-min))
+  (unless (cl-loop if (string= msg-id (notmuch-show-get-message-id))
+                  return t
+                  until (not (notmuch-show-goto-message-next)))
+    (goto-char (point-min))
+    (message "Message-id not found."))
+  (notmuch-show-message-adjust))
+
+(defun notmuch-show-apply-state (state)
+  "Apply STATE to the current buffer.
+
+This includes:
+ - opening the messages previously opened,
+ - closing all other messages,
+ - moving to the correct current message in every displayed window."
+  (let ((win-msg-alist (car state))
+       (open (cadr state)))
+    ;; Open those that were open.
+    (goto-char (point-min))
+    (cl-loop do (notmuch-show-message-visible
+                (notmuch-show-get-message-properties)
+                (member (notmuch-show-get-message-id) open))
+            until (not (notmuch-show-goto-message-next)))
+    (dolist (win-msg-pair win-msg-alist)
+      (with-selected-window (car win-msg-pair)
+       ;; Go to the previously open message in this window
+       (notmuch-show-goto-message (cadr win-msg-pair))))))
+
+(defun notmuch-show-refresh-view (&optional reset-state)
+  "Refresh the current view.
+
+Refreshes the current view, observing changes in display
+preferences. If invoked with a prefix argument (or RESET-STATE is
+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
+    ;; to weird effects such as remaining images, so remove them
+    ;; manually.
+    (remove-overlays)
+    (erase-buffer)
+    (unless (notmuch-show--build-buffer state)
+      ;; No messages were inserted.
+      (kill-buffer (current-buffer))
+      (ding)
+      (message "Refreshing the buffer resulted in no messages!"))))
+
+;;; Keymaps
+
+(defvar notmuch-show-stash-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "c" 'notmuch-show-stash-cc)
+    (define-key map "d" 'notmuch-show-stash-date)
+    (define-key map "F" 'notmuch-show-stash-filename)
+    (define-key map "f" 'notmuch-show-stash-from)
+    (define-key map "i" 'notmuch-show-stash-message-id)
+    (define-key map "I" 'notmuch-show-stash-message-id-stripped)
+    (define-key map "s" 'notmuch-show-stash-subject)
+    (define-key map "T" 'notmuch-show-stash-tags)
+    (define-key map "t" 'notmuch-show-stash-to)
+    (define-key map "l" 'notmuch-show-stash-mlarchive-link)
+    (define-key map "L" 'notmuch-show-stash-mlarchive-link-and-go)
+    (define-key map "G" 'notmuch-show-stash-git-send-email)
+    (define-key map "?" 'notmuch-subkeymap-help)
+    map)
+  "Submap for stash commands.")
+(fset 'notmuch-show-stash-map notmuch-show-stash-map)
+
+(defvar notmuch-show-part-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "s" 'notmuch-show-save-part)
+    (define-key map "v" 'notmuch-show-view-part)
+    (define-key map "o" 'notmuch-show-interactively-view-part)
+    (define-key map "|" 'notmuch-show-pipe-part)
+    (define-key map "m" 'notmuch-show-choose-mime-of-part)
+    (define-key map "?" 'notmuch-subkeymap-help)
+    map)
+  "Submap for part commands.")
+(fset 'notmuch-show-part-map notmuch-show-part-map)
+
+(defvar notmuch-show-mode-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map notmuch-common-keymap)
+    (define-key map "Z" 'notmuch-tree-from-show-current-query)
+    (define-key map "U" 'notmuch-unthreaded-from-show-current-query)
+    (define-key map (kbd "<C-tab>") 'widget-backward)
+    (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
+    (define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
+    (define-key map (kbd "TAB") 'notmuch-show-next-button)
+    (define-key map "f" 'notmuch-show-forward-message)
+    (define-key map "F" 'notmuch-show-forward-open-messages)
+    (define-key map "b" 'notmuch-show-resend-message)
+    (define-key map "l" 'notmuch-show-filter-thread)
+    (define-key map "r" 'notmuch-show-reply-sender)
+    (define-key map "R" 'notmuch-show-reply)
+    (define-key map "|" 'notmuch-show-pipe-message)
+    (define-key map "w" 'notmuch-show-save-attachments)
+    (define-key map "V" 'notmuch-show-view-raw-message)
+    (define-key map "e" 'notmuch-show-resume-message)
+    (define-key map "c" 'notmuch-show-stash-map)
+    (define-key map "h" 'notmuch-show-toggle-visibility-headers)
+    (define-key map "k" 'notmuch-tag-jump)
+    (define-key map "*" 'notmuch-show-tag-all)
+    (define-key map "-" 'notmuch-show-remove-tag)
+    (define-key map "+" 'notmuch-show-add-tag)
+    (define-key map "X" 'notmuch-show-archive-thread-then-exit)
+    (define-key map "x" 'notmuch-show-archive-message-then-next-or-exit)
+    (define-key map "A" 'notmuch-show-archive-thread-then-next)
+    (define-key map "a" 'notmuch-show-archive-message-then-next-or-next-thread)
+    (define-key map "N" 'notmuch-show-next-message)
+    (define-key map "P" 'notmuch-show-previous-message)
+    (define-key map "n" 'notmuch-show-next-open-message)
+    (define-key map "p" 'notmuch-show-previous-open-message)
+    (define-key map (kbd "M-n") 'notmuch-show-next-thread-show)
+    (define-key map (kbd "M-p") 'notmuch-show-previous-thread-show)
+    (define-key map (kbd "DEL") 'notmuch-show-rewind)
+    (define-key map " " 'notmuch-show-advance-and-archive)
+    (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
+    (define-key map (kbd "RET") 'notmuch-show-toggle-message)
+    (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)
+    (define-key map "B" 'notmuch-show-browse-urls)
+    map)
+  "Keymap for \"notmuch show\" buffers.")
+
+;;; Mode
+
+(define-derived-mode notmuch-show-mode fundamental-mode "notmuch-show"
+  "Major mode for viewing a thread with notmuch.
+
+This buffer contains the results of the \"notmuch show\" command
+for displaying a single thread of email from your email archives.
+
+By default, various components of email messages, (citations,
+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, (for
+which \\[notmuch-show-next-button] and \\[notmuch-show-previous-button] are helpful).
+
+Reading the thread sequentially is well-supported by pressing
+\\[notmuch-show-advance-and-archive]. This will 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).
+
+Other commands are available to read or manipulate the thread
+more selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show-previous-message]' to advance to messages
+without removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread
+without scrolling through with \\[notmuch-show-advance-and-archive]).
+
+You can add or remove arbitrary tags from the current message with
+'\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
+
+All currently available key bindings:
+
+\\{notmuch-show-mode-map}"
+  (setq notmuch-buffer-refresh-function #'notmuch-show-refresh-view)
+  (setq buffer-read-only t)
+  (setq truncate-lines t)
+  (setq imenu-prev-index-position-function
+       #'notmuch-show-imenu-prev-index-position-function)
+  (setq imenu-extract-index-name-function
+       #'notmuch-show-imenu-extract-index-name-function))
+
+;;; Tree commands
+
+(defun notmuch-tree-from-show-current-query ()
+  "Call notmuch tree with the current query."
+  (interactive)
+  (notmuch-tree notmuch-show-thread-id
+               notmuch-show-query-context
+               (notmuch-show-get-message-id)))
+
+(defun notmuch-unthreaded-from-show-current-query ()
+  "Call notmuch unthreaded with the current query."
+  (interactive)
+  (notmuch-unthreaded notmuch-show-thread-id
+                     notmuch-show-query-context
+                     (notmuch-show-get-message-id)))
+
+;;; Movement related functions.
+
+(defun notmuch-show-move-to-message-top ()
+  (goto-char (notmuch-show-message-top)))
+
+(defun notmuch-show-move-to-message-bottom ()
+  (goto-char (notmuch-show-message-bottom)))
+
+;; There's some strangeness here where a text property applied to a
+;; region a->b is not found when point is at b. We walk backwards
+;; until finding the property.
+(defun notmuch-show-message-extent ()
+  "Return a cons cell containing the start and end buffer offset
+of the current message."
+  (let (r)
+    (save-excursion
+      (while (not (setq r (get-text-property (point) :notmuch-message-extent)))
+       (backward-char)))
+    r))
+
+(defun notmuch-show-message-top ()
+  (car (notmuch-show-message-extent)))
+
+(defun notmuch-show-message-bottom ()
+  (cdr (notmuch-show-message-extent)))
+
+(defun notmuch-show-goto-message-next ()
+  (let ((start (point)))
+    (notmuch-show-move-to-message-bottom)
+    (if (not (eobp))
+       t
+      (goto-char start)
+      nil)))
+
+(defun notmuch-show-goto-message-previous ()
+  (notmuch-show-move-to-message-top)
+  (if (bobp)
+      nil
+    (backward-char)
+    (notmuch-show-move-to-message-top)
+    t))
+
+(defun notmuch-show-mapc (function)
+  "Iterate through all messages in the current thread with
+`notmuch-show-goto-message-next' and call FUNCTION for side
+effects."
+  (save-excursion
+    (goto-char (point-min))
+    (cl-loop do (funcall function)
+            while (notmuch-show-goto-message-next))))
+
+;;; Functions relating to the visibility of messages and their components.
+
+(defun notmuch-show-message-visible (props visible-p)
+  (overlay-put (plist-get props :message-overlay) 'invisible (not visible-p))
+  (notmuch-show-set-prop :message-visible visible-p props))
+
+(defun notmuch-show-headers-visible (props visible-p)
+  (overlay-put (plist-get props :headers-overlay) 'invisible (not visible-p))
+  (notmuch-show-set-prop :headers-visible visible-p props))
+
+;;; Functions for setting and getting attributes of the current message.
+
+(defun notmuch-show-set-message-properties (props)
+  (save-excursion
+    (notmuch-show-move-to-message-top)
+    (put-text-property (point) (+ (point) 1)
+                      :notmuch-message-properties props)))
+
+(defun notmuch-show-get-message-properties ()
+  "Return the properties of the current message as a plist.
+
+Some useful entries are:
+:headers - Property list containing the headers :Date, :Subject, :From, etc.
+:body - Body of the message
+:tags - Tags for this message"
+  (save-excursion
+    (notmuch-show-move-to-message-top)
+    (get-text-property (point) :notmuch-message-properties)))
+
+(defun notmuch-show-get-part-properties ()
+  "Return the properties of the innermost part containing point.
+
+This is the part property list retrieved from the CLI.  Signals
+an error if there is no part containing point."
+  (or (get-text-property (point) :notmuch-part)
+      (error "No message part here")))
+
+(defun notmuch-show-set-prop (prop val &optional props)
+  (let ((inhibit-read-only t)
+       (props (or props
+                  (notmuch-show-get-message-properties))))
+    (plist-put props prop val)
+    (notmuch-show-set-message-properties props)))
+
+(defun notmuch-show-get-prop (prop &optional props)
+  "Get property PROP from current message in show or tree mode.
+
+It gets property PROP from PROPS or, if PROPS is nil, the current
+message in either tree or show. This means that several utility
+functions in notmuch-show can be used directly by notmuch-tree as
+they just need the correct message properties."
+  (plist-get (or props
+                (cond ((eq major-mode 'notmuch-show-mode)
+                       (notmuch-show-get-message-properties))
+                      ((eq major-mode 'notmuch-tree-mode)
+                       (notmuch-tree-get-message-properties))
+                      (t nil)))
+            prop))
+
+(defun notmuch-show-get-message-id (&optional bare)
+  "Return an id: query for the Message-Id of the current message.
+
+If optional argument BARE is non-nil, return
+the Message-Id without id: prefix and escaping."
+  (if bare
+      (notmuch-show-get-prop :id)
+    (notmuch-id-to-query (notmuch-show-get-prop :id))))
+
+(defun notmuch-show-get-messages-ids ()
+  "Return all id: queries of messages in the current thread."
+  (let ((message-ids))
+    (notmuch-show-mapc
+     (lambda () (push (notmuch-show-get-message-id) message-ids)))
+    message-ids))
+
+(defun notmuch-show-get-messages-ids-search ()
+  "Return a search string for all message ids of messages in the
+current thread."
+  (mapconcat 'identity (notmuch-show-get-messages-ids) " or "))
+
+;; dme: Would it make sense to use a macro for many of these?
+
+(defun notmuch-show-get-filename ()
+  "Return the filename of the current message."
+  (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."
+  (plist-get (notmuch-show-get-prop :headers props) header))
+
+(defun notmuch-show-get-cc ()
+  (notmuch-show-get-header :Cc))
+
+(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))
+
+(defun notmuch-show-get-from ()
+  (notmuch-show-get-header :From))
+
+(defun notmuch-show-get-subject ()
+  (notmuch-show-get-header :Subject))
+
+(defun notmuch-show-get-to ()
+  (notmuch-show-get-header :To))
+
+(defun notmuch-show-get-depth ()
+  (notmuch-show-get-prop :depth))
+
+(defun notmuch-show-set-tags (tags)
+  "Set the tags of the current message."
+  (notmuch-show-set-prop :tags tags)
+  (notmuch-show-update-tags tags))
+
+(defun notmuch-show-get-tags ()
+  "Return the tags of the current message."
+  (notmuch-show-get-prop :tags))
+
+(defun notmuch-show-message-visible-p ()
+  "Is the current message visible?"
+  (notmuch-show-get-prop :message-visible))
+
+(defun notmuch-show-headers-visible-p ()
+  "Are the headers of the current message visible?"
+  (notmuch-show-get-prop :headers-visible))
+
+(put 'notmuch-show-mark-read 'notmuch-prefix-doc
+     "Mark the current message as unread.")
+(defun notmuch-show-mark-read (&optional unread)
+  "Mark the current message as read.
+
+Mark the current message as read by applying the tag changes in
+`notmuch-show-mark-read-tags' to it (remove the \"unread\" tag by
+default). If a prefix argument is given, the message will be
+marked as unread, i.e. the tag changes in
+`notmuch-show-mark-read-tags' will be reversed."
+  (interactive "P")
+  (when notmuch-show-mark-read-tags
+    (apply 'notmuch-show-tag-message
+          (notmuch-tag-change-list notmuch-show-mark-read-tags unread))))
+
+(defun notmuch-show-seen-current-message (_start _end)
+  "Mark the current message read if it is open.
+
+We only mark it read once: if it is changed back then that is a
+user decision and we should not override it."
+  (when (and (notmuch-show-message-visible-p)
+            (not (notmuch-show-get-prop :seen)))
+    (notmuch-show-mark-read)
+    (notmuch-show-set-prop :seen t)))
+
+(defvar notmuch-show--seen-has-errored nil)
+(make-variable-buffer-local 'notmuch-show--seen-has-errored)
+
+(defun notmuch-show-command-hook ()
+  (when (eq major-mode 'notmuch-show-mode)
+    ;; We need to redisplay to get window-start and window-end correct.
+    (redisplay)
+    (save-excursion
+      (condition-case nil
+         (funcall notmuch-show-mark-read-function (window-start) (window-end))
+       ((debug error)
+        (unless notmuch-show--seen-has-errored
+          (setq notmuch-show--seen-has-errored t)
+          (setq header-line-format
+                (concat header-line-format
+                        (propertize
+                         "  [some mark read tag changes may have failed]"
+                         'face font-lock-warning-face)))))))))
+
+(defun notmuch-show-filter-thread (query)
+  "Filter or LIMIT the current thread based on a new query string.
+
+Reshows the current thread with matches defined by the new query-string."
+  (interactive (list (notmuch-read-query "Filter thread: ")))
+  (let ((msg-id (notmuch-show-get-message-id)))
+    (setq notmuch-show-query-context (if (string-empty-p query) nil query))
+    (notmuch-show-refresh-view t)
+    (notmuch-show-goto-message msg-id)))
+
+;;; Functions for getting attributes of several messages in the current thread.
+
+(defun notmuch-show-get-message-ids-for-open-messages ()
+  "Return a list of all id: queries for open messages in the current thread."
+  (save-excursion
+    (let (message-ids done)
+      (goto-char (point-min))
+      (while (not done)
+       (when (notmuch-show-message-visible-p)
+         (setq message-ids
+               (append message-ids (list (notmuch-show-get-message-id)))))
+       (setq done (not (notmuch-show-goto-message-next))))
+      message-ids)))
+
+;;; Commands typically bound to keys.
+
+(defun notmuch-show-advance ()
+  "Advance through thread.
+
+If the current message in the thread is not yet fully visible,
+scroll by a near screenful to read more of the message.
+
+Otherwise, (the end of the current message is already within the
+current window), advance to the next open message."
+  (interactive)
+  (let* ((end-of-this-message (notmuch-show-message-bottom))
+        (visible-end-of-this-message (1- end-of-this-message))
+        (ret nil))
+    (while (invisible-p visible-end-of-this-message)
+      (setq visible-end-of-this-message
+           (max (point-min)
+                (1- (previous-single-char-property-change
+                     visible-end-of-this-message 'invisible)))))
+    (cond
+     ;; Ideally we would test `end-of-this-message' against the result
+     ;; of `window-end', but that doesn't account for the fact that
+     ;; the end of the message might be hidden.
+     ((and visible-end-of-this-message
+          (> visible-end-of-this-message (window-end)))
+      ;; The bottom of this message is not visible - scroll.
+      (scroll-up nil))
+     ((not (= end-of-this-message (point-max)))
+      ;; This is not the last message - move to the next visible one.
+      (notmuch-show-next-open-message))
+     ((not (= (point) (point-max)))
+      ;; This is the last message, but the cursor is not at the end of
+      ;; the buffer. Move it there.
+      (goto-char (point-max)))
+     (t
+      ;; This is the last message - change the return value
+      (setq ret t)))
+    ret))
+
+(defun notmuch-show-advance-and-archive ()
+  "Advance through thread and archive.
+
+This command is intended to be one of the simplest ways to
+process a thread of email. It works exactly like
+notmuch-show-advance, in that it scrolls through messages in a
+show buffer, except that when it gets to the end of the buffer it
+archives the entire current thread, (apply changes in
+`notmuch-archive-tags'), kills the buffer, and displays the next
+thread from the search from which this thread was originally
+shown."
+  (interactive)
+  (when (notmuch-show-advance)
+    (notmuch-show-archive-thread-then-next)))
+
+(defun notmuch-show-rewind ()
+  "Backup through the thread (reverse scrolling compared to \
+\\[notmuch-show-advance-and-archive]).
+
+Specifically, if the beginning of the previous email is fewer
+than `window-height' lines from the current point, move to it
+just like `notmuch-show-previous-message'.
+
+Otherwise, just scroll down a screenful of the current message.
+
+This command does not modify any message tags, (it does not undo
+any effects from previous calls to
+`notmuch-show-advance-and-archive'."
+  (interactive)
+  (let ((start-of-message (notmuch-show-message-top))
+       (start-of-window (window-start)))
+    (cond
+     ;; Either this message is properly aligned with the start of the
+     ;; window or the start of this message is not visible on the
+     ;; screen - scroll.
+     ((or (= start-of-message start-of-window)
+         (< start-of-message start-of-window))
+      (scroll-down)
+      ;; If a small number of lines from the previous message are
+      ;; visible, realign so that the top of the current message is at
+      ;; the top of the screen.
+      (when (<= (count-screen-lines (window-start) start-of-message)
+               next-screen-context-lines)
+       (goto-char (notmuch-show-message-top))
+       (notmuch-show-message-adjust))
+      ;; Move to the top left of the window.
+      (goto-char (window-start)))
+     (t
+      ;; Move to the previous message.
+      (notmuch-show-previous-message)))))
+
+(put 'notmuch-show-reply 'notmuch-prefix-doc "... and prompt for sender")
+(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-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-show-get-prop :duplicate)))
+
+(put 'notmuch-show-forward-message 'notmuch-prefix-doc
+     "... and prompt for sender")
+(defun notmuch-show-forward-message (&optional prompt-for-sender)
+  "Forward the current message."
+  (interactive "P")
+  (notmuch-mua-new-forward-messages (list (notmuch-show-get-message-id))
+                                   prompt-for-sender))
+
+(put 'notmuch-show-forward-open-messages 'notmuch-prefix-doc
+     "... and prompt for sender")
+(defun notmuch-show-forward-open-messages (&optional prompt-for-sender)
+  "Forward the currently open messages."
+  (interactive "P")
+  (let ((open-messages (notmuch-show-get-message-ids-for-open-messages)))
+    (unless open-messages
+      (error "No open messages to forward."))
+    (notmuch-mua-new-forward-messages open-messages prompt-for-sender)))
+
+(defun notmuch-show-resend-message (addresses)
+  "Resend the current message."
+  (interactive (list (notmuch-address-from-minibuffer "Resend to: ")))
+  (when (y-or-n-p (concat "Confirm resend to " addresses " "))
+    (notmuch-show-view-raw-message)
+    (message-resend addresses)
+    (notmuch-bury-or-kill-this-buffer)))
+
+(defun notmuch-show-message-adjust ()
+  (recenter 0))
+
+(defun notmuch-show-next-message (&optional pop-at-end)
+  "Show the next message.
+
+If a prefix argument is given and this is the last message in the
+thread, navigate to the next thread in the parent search buffer."
+  (interactive "P")
+  (if (notmuch-show-goto-message-next)
+      (notmuch-show-message-adjust)
+    (if pop-at-end
+       (notmuch-show-next-thread)
+      (goto-char (point-max)))))
+
+(defun notmuch-show-previous-message ()
+  "Show the previous message or the start of the current message."
+  (interactive)
+  (if (= (point) (notmuch-show-message-top))
+      (notmuch-show-goto-message-previous)
+    (notmuch-show-move-to-message-top))
+  (notmuch-show-message-adjust))
+
+(defun notmuch-show-next-open-message (&optional pop-at-end)
+  "Show the next open message.
+
+If a prefix argument is given and this is the last open message
+in the thread, navigate to the next thread in the parent search
+buffer. Return t if there was a next open message in the thread
+to show, nil otherwise."
+  (interactive "P")
+  (let (r)
+    (while (and (setq r (notmuch-show-goto-message-next))
+               (not (notmuch-show-message-visible-p))))
+    (if r
+       (notmuch-show-message-adjust)
+      (if pop-at-end
+         (notmuch-show-next-thread)
+       (goto-char (point-max))))
+    r))
+
+(defun notmuch-show-next-matching-message ()
+  "Show the next matching message."
+  (interactive)
+  (let (r)
+    (while (and (setq r (notmuch-show-goto-message-next))
+               (not (notmuch-show-get-prop :match))))
+    (if r
+       (notmuch-show-message-adjust)
+      (goto-char (point-max)))))
+
+(defun notmuch-show-open-if-matched ()
+  "Open a message if it is matched (whether or not excluded)."
+  (let ((props (notmuch-show-get-message-properties)))
+    (notmuch-show-message-visible props (plist-get props :match))))
+
+(defun notmuch-show-goto-first-wanted-message ()
+  "Move to the first open message and mark it read."
+  (goto-char (point-min))
+  (unless (notmuch-show-message-visible-p)
+    (notmuch-show-next-open-message))
+  (when (eobp)
+    ;; There are no matched non-excluded messages so open all matched
+    ;; (necessarily excluded) messages and go to the first.
+    (notmuch-show-mapc 'notmuch-show-open-if-matched)
+    (force-window-update)
+    (goto-char (point-min))
+    (unless (notmuch-show-message-visible-p)
+      (notmuch-show-next-open-message))))
+
+(defun notmuch-show-previous-open-message ()
+  "Show the previous open message."
+  (interactive)
+  (while (and (if (= (point) (notmuch-show-message-top))
+                 (notmuch-show-goto-message-previous)
+               (notmuch-show-move-to-message-top))
+             (not (notmuch-show-message-visible-p))))
+  (notmuch-show-message-adjust))
+
+(defun notmuch-show-view-raw-message ()
+  "View the original source of the current message."
+  (interactive)
+  (let* ((id (notmuch-show-get-message-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))
+      (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)
+    (view-buffer buf 'kill-buffer-if-not-modified)))
+
+(defun notmuch-show-resume-message ()
+  "Resume EDITING the current draft message."
+  (interactive)
+  (notmuch-draft-resume (notmuch-show-get-message-id)))
+
+(put 'notmuch-show-pipe-message 'notmuch-doc
+     "Pipe the contents of the current message to a command.")
+(put 'notmuch-show-pipe-message 'notmuch-prefix-doc
+     "Pipe the thread as an mbox to a command.")
+(defun notmuch-show-pipe-message (entire-thread command)
+  "Pipe the contents of the current message (or thread) to COMMAND.
+
+COMMAND will be executed with the raw contents of the current
+email message as stdin. Anything printed by the command to stdout
+or stderr will appear in the *notmuch-pipe* buffer.
+
+If ENTIRE-THREAD is non-nil (or when invoked with a prefix
+argument), COMMAND will receive all open messages in the current
+thread (formatted as an mbox) rather than only the current
+message."
+  (interactive (let ((query-string (if current-prefix-arg
+                                      "Pipe all open messages to command: "
+                                    "Pipe message to command: ")))
+                (list current-prefix-arg (read-shell-command query-string))))
+  (let (shell-command)
+    (if entire-thread
+       (setq shell-command
+             (concat notmuch-command " show --format=mbox --exclude=false "
+                     (shell-quote-argument
+                      (mapconcat 'identity
+                                 (notmuch-show-get-message-ids-for-open-messages)
+                                 " OR "))
+                     " | " command))
+      (setq shell-command
+           (concat notmuch-command " show --format=raw "
+                   (shell-quote-argument (notmuch-show-get-message-id))
+                   " | " command)))
+    (let ((cwd default-directory)
+         (buf (get-buffer-create (concat "*notmuch-pipe*"))))
+      (with-current-buffer buf
+       (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.
+
+TAG-CHANGES is a list of tag operations for `notmuch-tag'."
+  (let* ((current-tags (notmuch-show-get-tags))
+        (new-tags (notmuch-update-tags current-tags tag-changes)))
+    (unless (equal current-tags new-tags)
+      (notmuch-tag (notmuch-show-get-message-id) tag-changes)
+      (notmuch-show-set-tags new-tags))))
+
+(defun notmuch-show-tag (tag-changes)
+  "Change tags for the current message.
+
+See `notmuch-tag' for information on the format of TAG-CHANGES."
+  (interactive (list (notmuch-read-tag-changes (notmuch-show-get-tags)
+                                              "Tag message")))
+  (notmuch-tag (notmuch-show-get-message-id) tag-changes)
+  (let* ((current-tags (notmuch-show-get-tags))
+        (new-tags (notmuch-update-tags current-tags tag-changes)))
+    (unless (equal current-tags new-tags)
+      (notmuch-show-set-tags new-tags))))
+
+(defun notmuch-show-tag-all (tag-changes)
+  "Change tags for all messages in the current show buffer.
+
+See `notmuch-tag' for information on the format of TAG-CHANGES."
+  (interactive
+   (list (let (tags)
+          (notmuch-show-mapc
+           (lambda () (setq tags (append (notmuch-show-get-tags) tags))))
+          (notmuch-read-tag-changes tags "Tag thread"))))
+  (notmuch-tag (notmuch-show-get-messages-ids-search) tag-changes)
+  (notmuch-show-mapc
+   (lambda ()
+     (let* ((current-tags (notmuch-show-get-tags))
+           (new-tags (notmuch-update-tags current-tags tag-changes)))
+       (unless (equal current-tags new-tags)
+        (notmuch-show-set-tags new-tags))))))
+
+(defun notmuch-show-add-tag (tag-changes)
+  "Change tags for the current message (defaulting to add).
+
+Same as `notmuch-show-tag' but sets initial input to '+'."
+  (interactive
+   (list (notmuch-read-tag-changes (notmuch-show-get-tags) "Tag message" "+")))
+  (notmuch-show-tag tag-changes))
+
+(defun notmuch-show-remove-tag (tag-changes)
+  "Change tags for the current message (defaulting to remove).
+
+Same as `notmuch-show-tag' but sets initial input to '-'."
+  (interactive
+   (list (notmuch-read-tag-changes (notmuch-show-get-tags) "Tag message" "-")))
+  (notmuch-show-tag tag-changes))
+
+(defun notmuch-show-toggle-visibility-headers ()
+  "Toggle the visibility of the current message headers."
+  (interactive)
+  (let ((props (notmuch-show-get-message-properties)))
+    (notmuch-show-headers-visible
+     props
+     (not (plist-get props :headers-visible))))
+  (force-window-update))
+
+(defun notmuch-show-toggle-message ()
+  "Toggle the visibility of the current message."
+  (interactive)
+  (let ((props (notmuch-show-get-message-properties)))
+    (notmuch-show-message-visible
+     props
+     (not (plist-get props :message-visible))))
+  (force-window-update))
+
+(put 'notmuch-show-open-or-close-all 'notmuch-doc "Show all messages.")
+(put 'notmuch-show-open-or-close-all 'notmuch-prefix-doc "Hide all messages.")
+(defun notmuch-show-open-or-close-all ()
+  "Set the visibility all of the messages in the current thread.
+
+By default make all of the messages visible. With a prefix
+argument, hide all of the messages."
+  (interactive)
+  (save-excursion
+    (goto-char (point-min))
+    (cl-loop do (notmuch-show-message-visible
+                (notmuch-show-get-message-properties)
+                (not current-prefix-arg))
+            until (not (notmuch-show-goto-message-next))))
+  (force-window-update))
+
+(defun notmuch-show-next-button ()
+  "Advance point to the next button in the buffer."
+  (interactive)
+  (forward-button 1))
+
+(defun notmuch-show-previous-button ()
+  "Move point back to the previous button in the buffer."
+  (interactive)
+  (backward-button 1))
+
+(defun notmuch-show-next-thread (&optional show previous)
+  "Move to the next item in the search results, if any.
+
+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.
+
+Return non-nil on success."
+  (interactive "P")
+  (let ((parent-buffer notmuch-show-parent-buffer))
+    (notmuch-bury-or-kill-this-buffer)
+    (when (buffer-live-p parent-buffer)
+      (switch-to-buffer parent-buffer)
+      (and (if previous
+              (notmuch-search-previous-thread)
+            (notmuch-search-next-thread))
+          show
+          (notmuch-search-show-thread)))))
+
+(defun notmuch-show-next-thread-show ()
+  "Show the next thread in the search results, if any."
+  (interactive)
+  (notmuch-show-next-thread t))
+
+(defun notmuch-show-previous-thread-show ()
+  "Show the previous thread in the search results, if any."
+  (interactive)
+  (notmuch-show-next-thread t t))
+
+(put 'notmuch-show-archive-thread 'notmuch-prefix-doc
+     "Un-archive each message in thread.")
+(defun notmuch-show-archive-thread (&optional unarchive)
+  "Archive each message in thread.
+
+Archive each message currently shown by applying the tag changes
+in `notmuch-archive-tags' to each. If a prefix argument is given,
+the messages will be \"unarchived\", i.e. the tag changes in
+`notmuch-archive-tags' will be reversed.
+
+Note: This command is safe from any race condition of new messages
+being delivered to the same thread. It does not archive the
+entire thread, but only the messages shown in the current
+buffer."
+  (interactive "P")
+  (when notmuch-archive-tags
+    (notmuch-show-tag-all
+     (notmuch-tag-change-list notmuch-archive-tags unarchive))))
+
+(defun notmuch-show-archive-thread-then-next ()
+  "Archive all messages in the current buffer, then show next thread from search."
+  (interactive)
+  (notmuch-show-archive-thread)
+  (notmuch-show-next-thread t))
+
+(defun notmuch-show-archive-thread-then-exit ()
+  "Archive all messages in the current buffer, then exit back to search results."
+  (interactive)
+  (notmuch-show-archive-thread)
+  (notmuch-show-next-thread))
+
+(put 'notmuch-show-archive-message 'notmuch-prefix-doc
+     "Un-archive the current message.")
+(defun notmuch-show-archive-message (&optional unarchive)
+  "Archive the current message.
+
+Archive the current message by applying the tag changes in
+`notmuch-archive-tags' to it. If a prefix argument is given, the
+message will be \"unarchived\", i.e. the tag changes in
+`notmuch-archive-tags' will be reversed."
+  (interactive "P")
+  (when notmuch-archive-tags
+    (apply 'notmuch-show-tag-message
+          (notmuch-tag-change-list notmuch-archive-tags unarchive))))
+
+(defun notmuch-show-archive-message-then-next-or-exit ()
+  "Archive current message, then show next open message in current thread.
+
+If at the last open message in the current thread, then exit back
+to search results."
+  (interactive)
+  (notmuch-show-archive-message)
+  (notmuch-show-next-open-message t))
+
+(defun notmuch-show-archive-message-then-next-or-next-thread ()
+  "Archive current message, then show next open message in current or next thread.
+
+If at the last open message in the current thread, then show next
+thread from search."
+  (interactive)
+  (notmuch-show-archive-message)
+  (unless (notmuch-show-next-open-message)
+    (notmuch-show-next-thread t)))
+
+(defun notmuch-show-stash-cc ()
+  "Copy CC field of current message to kill-ring."
+  (interactive)
+  (notmuch-common-do-stash (notmuch-show-get-cc)))
+
+(put 'notmuch-show-stash-date 'notmuch-prefix-doc
+     "Copy timestamp of current message to kill-ring.")
+(defun notmuch-show-stash-date (&optional stash-timestamp)
+  "Copy date of current message to kill-ring.
+
+If invoked with a prefix argument, copy timestamp of current
+message to kill-ring."
+  (interactive "P")
+  (if stash-timestamp
+      (notmuch-common-do-stash (format "%d" (notmuch-show-get-timestamp)))
+    (notmuch-common-do-stash (notmuch-show-get-date))))
+
+(defun notmuch-show-stash-filename ()
+  "Copy filename of current message to kill-ring."
+  (interactive)
+  (notmuch-common-do-stash (notmuch-show-get-filename)))
+
+(defun notmuch-show-stash-from ()
+  "Copy From address of current message to kill-ring."
+  (interactive)
+  (notmuch-common-do-stash (notmuch-show-get-from)))
+
+(put 'notmuch-show-stash-message-id 'notmuch-prefix-doc
+     "Copy thread: query matching current thread to kill-ring.")
+(defun notmuch-show-stash-message-id (&optional stash-thread-id)
+  "Copy id: query matching the current message to kill-ring.
+
+If invoked with a prefix argument (or STASH-THREAD-ID is
+non-nil), copy thread: query matching the current thread to
+kill-ring."
+  (interactive "P")
+  (if stash-thread-id
+      (notmuch-common-do-stash notmuch-show-thread-id)
+    (notmuch-common-do-stash (notmuch-show-get-message-id))))
+
+(defun notmuch-show-stash-message-id-stripped ()
+  "Copy message ID of current message (sans `id:' prefix) to kill-ring."
+  (interactive)
+  (notmuch-common-do-stash (notmuch-show-get-message-id t)))
+
+(defun notmuch-show-stash-subject ()
+  "Copy Subject field of current message to kill-ring."
+  (interactive)
+  (notmuch-common-do-stash (notmuch-show-get-subject)))
+
+(defun notmuch-show-stash-tags ()
+  "Copy tags of current message to kill-ring as a comma separated list."
+  (interactive)
+  (notmuch-common-do-stash (mapconcat 'identity (notmuch-show-get-tags) ",")))
+
+(defun notmuch-show-stash-to ()
+  "Copy To address of current message to kill-ring."
+  (interactive)
+  (notmuch-common-do-stash (notmuch-show-get-to)))
+
+(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.
+
+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
+                      (let ((completion-ignore-case t))
+                        (completing-read
+                         "Mailing List Archive: "
+                         notmuch-show-stash-mlarchive-link-alist
+                         nil t nil nil
+                         notmuch-show-stash-mlarchive-link-default)))
+                  notmuch-show-stash-mlarchive-link-alist))))
+    (notmuch-common-do-stash
+     (if (functionp url)
+        (funcall url (notmuch-show-get-message-id t))
+       (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.
+
+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')."
+  (interactive)
+  (notmuch-show-stash-mlarchive-link mla)
+  (browse-url (current-kill 0 t)))
+
+(defun notmuch-show-stash-git-helper (addresses prefix)
+  "Normalize all ADDRESSES while adding PREFIX.
+Escape, trim, quote and add PREFIX to each address in list
+of ADDRESSES, and return the result as a single string."
+  (mapconcat (lambda (x)
+              (concat prefix "\""
+                      ;; escape double-quotes
+                      (replace-regexp-in-string
+                       "\"" "\\\\\""
+                       ;; trim leading and trailing spaces
+                       (replace-regexp-in-string
+                        "\\(^ *\\| *$\\)" ""
+                        x)) "\""))
+            addresses " "))
+
+(put 'notmuch-show-stash-git-send-email 'notmuch-prefix-doc
+     "Copy From/To/Cc of current message to kill-ring.
+Use a form suitable for pasting to git send-email command line.")
+
+(defun notmuch-show-stash-git-send-email (&optional no-in-reply-to)
+  "Copy From/To/Cc/Message-Id of current message to kill-ring.
+Use a form suitable for pasting to git send-email command line.
+
+If invoked with a prefix argument (or NO-IN-REPLY-TO is non-nil),
+omit --in-reply-to=<Message-Id>."
+  (interactive "P")
+  (notmuch-common-do-stash
+   (mapconcat 'identity
+             (remove ""
+                     (list
+                      (notmuch-show-stash-git-helper
+                       (message-tokenize-header (notmuch-show-get-from)) "--to=")
+                      (notmuch-show-stash-git-helper
+                       (message-tokenize-header (notmuch-show-get-to)) "--to=")
+                      (notmuch-show-stash-git-helper
+                       (message-tokenize-header (notmuch-show-get-cc)) "--cc=")
+                      (unless no-in-reply-to
+                        (notmuch-show-stash-git-helper
+                         (list (notmuch-show-get-message-id t)) "--in-reply-to="))))
+             " ")))
+
+;;; Interactive part functions and their helpers
+
+(defun notmuch-show-generate-part-buffer (msg part)
+  "Return a temporary buffer containing the specified part's content."
+  (let ((buf (generate-new-buffer " *notmuch-part*"))
+       (process-crypto notmuch-show-process-crypto))
+    (with-current-buffer buf
+      ;; This is always used in the content of mm handles, which
+      ;; expect undecoded, binary part content.
+      (insert (notmuch-get-bodypart-binary msg part process-crypto)))
+    buf))
+
+(defun notmuch-show-current-part-handle (&optional mime-type)
+  "Return an mm-handle for the part containing point.
+
+This creates a temporary buffer for the part's content; the
+caller is responsible for killing this buffer as appropriate.  If
+MIME-TYPE is given then set the handle's mime-type to MIME-TYPE."
+  (let* ((msg (notmuch-show-get-message-properties))
+        (part (notmuch-show-get-part-properties))
+        (buf (notmuch-show-generate-part-buffer msg part))
+        (computed-type (or mime-type (plist-get part :computed-type)))
+        (filename (plist-get part :filename))
+        (disposition (and filename `(attachment (filename . ,filename)))))
+    (mm-make-handle buf (list computed-type) nil nil disposition)))
+
+(defun notmuch-show-apply-to-current-part-handle (fn &optional mime-type)
+  "Apply FN to an mm-handle for the part containing point.
+
+This ensures that the temporary buffer created for the mm-handle
+is destroyed when FN returns. If MIME-TYPE is given then force
+part to be treated as if it had that mime-type."
+  (let ((handle (notmuch-show-current-part-handle mime-type)))
+    ;; Emacs puts stdout/stderr into the calling buffer so we call
+    ;; it from a temp-buffer, unless notmuch-show-attachment-debug
+    ;; is non-nil, in which case we put it in " *notmuch-part*".
+    (unwind-protect
+       (if notmuch-show-attachment-debug
+           (with-current-buffer (generate-new-buffer " *notmuch-part*")
+             (funcall fn handle))
+         (with-temp-buffer
+           (funcall fn handle)))
+      (kill-buffer (mm-handle-buffer handle)))))
+
+(defun notmuch-show-part-button-default (&optional button)
+  (interactive)
+  (let ((button (or button (button-at (point)))))
+    ;; Try to toggle the part, if that fails then call the default
+    ;; action. The toggle fails if the part has no emacs renderable
+    ;; content.
+    (unless (notmuch-show-toggle-part-invisibility button)
+      (call-interactively notmuch-show-part-button-default-action))))
+
+(defun notmuch-show-save-part ()
+  "Save the MIME part containing point to a file."
+  (interactive)
+  (notmuch-show-apply-to-current-part-handle #'mm-save-part))
+
+(defun notmuch-show-view-part ()
+  "View the MIME part containing point in an external viewer."
+  (interactive)
+  ;; Set mm-inlined-types to nil to force an external viewer
+  (let ((mm-inlined-types nil))
+    (notmuch-show-apply-to-current-part-handle #'mm-display-part)))
+
+(defun notmuch-show-interactively-view-part ()
+  "View the MIME part containing point, prompting for a viewer."
+  (interactive)
+  (notmuch-show-apply-to-current-part-handle #'mm-interactively-view-part))
+
+(defun notmuch-show-pipe-part ()
+  "Pipe the MIME part containing point to an external command."
+  (interactive)
+  (notmuch-show-apply-to-current-part-handle #'mm-pipe-part))
+
+(defun notmuch-show--mm-display-part (handle)
+  "Use mm-display-part to display HANDLE in a new buffer.
+
+If the part is displayed in an external application then close
+the new buffer."
+  (let ((buf (get-buffer-create (generate-new-buffer-name
+                                (concat " *notmuch-internal-part*")))))
+    (pop-to-buffer-same-window buf)
+    (if (eq (mm-display-part handle) 'external)
+       (kill-buffer buf)
+      (goto-char (point-min))
+      (set-buffer-modified-p nil)
+      (view-buffer buf 'kill-buffer-if-not-modified))))
+
+(defun notmuch-show-choose-mime-of-part (mime-type)
+  "Choose the mime type to use for displaying part."
+  (interactive
+   (list (completing-read "Mime type to use (default text/plain): "
+                         (mailcap-mime-types) nil nil nil nil "text/plain")))
+  (notmuch-show-apply-to-current-part-handle #'notmuch-show--mm-display-part
+                                            mime-type))
+
+(defun notmuch-show-imenu-prev-index-position-function ()
+  "Move point to previous message in notmuch-show buffer.
+This function is used as a value for
+`imenu-prev-index-position-function'."
+  (if (bobp)
+      nil
+    (notmuch-show-previous-message)
+    t))
+
+(defun notmuch-show-imenu-extract-index-name-function ()
+  "Return imenu name for line at point.
+This function is used as a value for
+`imenu-extract-index-name-function'.  Point should be at the
+beginning of the line."
+  (back-to-indentation)
+  (buffer-substring-no-properties (if notmuch-show-imenu-indent
+                                     (line-beginning-position)
+                                   (point))
+                                 (line-end-position)))
+
+(defmacro notmuch-show--with-currently-shown-message (&rest body)
+  "Evaluate BODY with display restricted to the currently shown
+message."
+  `(save-excursion
+     (save-restriction
+       (let ((extent (notmuch-show-message-extent)))
+        (narrow-to-region (car extent) (cdr extent))
+        ,@body))))
+
+(defun notmuch-show--gather-urls ()
+  "Gather any URLs in the current message."
+  (notmuch-show--with-currently-shown-message
+   (let (urls)
+     (goto-char (point-min))
+     (while (re-search-forward goto-address-url-regexp (point-max) t)
+       (push (match-string-no-properties 0) urls))
+     (reverse urls))))
+
+(defun notmuch-show-browse-urls (&optional kill)
+  "Offer to browse any URLs in the current message.
+With a prefix argument, copy the URL to the kill ring rather than
+browsing."
+  (interactive "P")
+  (let ((urls (notmuch-show--gather-urls))
+       (prompt (if kill "Copy URL to kill ring: " "Browse URL: "))
+       (fn (if kill #'kill-new #'browse-url)))
+    (if urls
+       (funcall fn (completing-read prompt urls nil nil nil nil (car urls)))
+      (message "No URLs found."))))
+
+;;; _
+
+(provide 'notmuch-show)
+
+;;; notmuch-show.el ends here
diff --git a/emacs/notmuch-tag.el b/emacs/notmuch-tag.el
new file mode 100644 (file)
index 0000000..9597788
--- /dev/null
@@ -0,0 +1,587 @@
+;;; notmuch-tag.el --- tag messages within emacs  -*- lexical-binding: t -*-
+;;
+;; Copyright © Damien Cassou
+;; Copyright © Carl Worth
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Carl Worth <cworth@cworth.org>
+;;          Damien Cassou <damien.cassou@gmail.com>
+
+;;; Code:
+
+(require 'crm)
+
+(require 'notmuch-lib)
+
+(declare-function notmuch-search-tag "notmuch"
+                 (tag-changes &optional beg end only-matched))
+(declare-function notmuch-show-tag "notmuch-show" (tag-changes))
+(declare-function notmuch-tree-tag "notmuch-tree" (tag-changes))
+(declare-function notmuch-jump "notmuch-jump" (action-map prompt))
+
+;;; Keys
+
+(define-widget 'notmuch-tag-key-type 'list
+  "A single key tagging binding."
+  :format "%v"
+  :args '((list :inline t
+               :format "%v"
+               (key-sequence :tag "Key")
+               (radio :tag "Tag operations"
+                      (repeat :tag "Tag list"
+                              (string :format "%v" :tag "change"))
+                      (variable :tag "Tag variable"))
+               (string :tag "Name"))))
+
+(defcustom notmuch-tagging-keys
+  `((,(kbd "a") notmuch-archive-tags "Archive")
+    (,(kbd "u") notmuch-show-mark-read-tags "Mark read")
+    (,(kbd "f") ("+flagged") "Flag")
+    (,(kbd "s") ("+spam" "-inbox") "Mark as spam")
+    (,(kbd "d") ("+deleted" "-inbox") "Delete"))
+  "A list of keys and corresponding tagging operations.
+
+For each key (or key sequence) you can specify a sequence of
+tagging operations to apply, or a variable which contains a list
+of tagging operations such as `notmuch-archive-tags'. The final
+element is a name for this tagging operation. If the name is
+omitted or empty then the list of tag changes, or the variable
+name is used as the name.
+
+The key `notmuch-tag-jump-reverse-key' (k by default) should not
+be used (either as a key, or as the start of a key sequence) as
+it is already bound: it switches the menu to a menu of the
+reverse tagging operations. The reverse of a tagging operation is
+the same list of individual tag-ops but with `+tag' replaced by
+`-tag' and vice versa.
+
+If setting this variable outside of customize then it should be a
+list of triples (lists of three elements). Each triple should be
+of the form (key-binding tagging-operations name). KEY-BINDING
+can be a single character or a key sequence; TAGGING-OPERATIONS
+should either be a list of individual tag operations each of the
+form `+tag' or `-tag', or the variable name of a variable that is
+a list of tagging operations; NAME should be a name for the
+tagging operation, if omitted or empty than then name is taken
+from TAGGING-OPERATIONS."
+  :tag "List of tagging bindings"
+  :type '(repeat notmuch-tag-key-type)
+  :group 'notmuch-tag)
+
+;;; Faces and Formats
+
+(define-widget 'notmuch-tag-format-type 'lazy
+  "Customize widget for notmuch-tag-format and friends."
+  :type '(alist :key-type (regexp :tag "Tag")
+               :extra-offset -3
+               :value-type
+               (radio :format "%v"
+                      (const :tag "Hidden" nil)
+                      (set :tag "Modified"
+                           (string :tag "Display as")
+                           (list :tag "Face" :extra-offset -4
+                                 (const :format "" :inline t
+                                        (notmuch-apply-face tag))
+                                 (list :format "%v"
+                                       (const :format "" quote)
+                                       custom-face-edit))
+                           (list :format "%v" :extra-offset -4
+                                 (const :format "" :inline t
+                                        (notmuch-tag-format-image-data tag))
+                                 (choice :tag "Image"
+                                         (const :tag "Star"
+                                                (notmuch-tag-star-icon))
+                                         (const :tag "Empty star"
+                                                (notmuch-tag-star-empty-icon))
+                                         (const :tag "Tag"
+                                                (notmuch-tag-tag-icon))
+                                         (string :tag "Custom")))
+                           (sexp :tag "Custom")))))
+
+(defface notmuch-tag-unread
+  '((t :foreground "red"))
+  "Default face used for the unread tag.
+
+Used in the default value of `notmuch-tag-formats'."
+  :group 'notmuch-faces)
+
+(defface notmuch-tag-flagged
+  '((((class color)
+      (background dark))
+     (:foreground "LightBlue1"))
+    (((class color)
+      (background light))
+     (:foreground "blue")))
+  "Face used for the flagged tag.
+
+Used in the default value of `notmuch-tag-formats'."
+  :group 'notmuch-faces)
+
+(defcustom notmuch-tag-formats
+  '(("unread" (propertize tag 'face 'notmuch-tag-unread))
+    ("flagged" (propertize tag 'face 'notmuch-tag-flagged)
+     (notmuch-tag-format-image-data tag (notmuch-tag-star-icon))))
+  "Custom formats for individual tags.
+
+This is an association list of the form ((MATCH EXPR...)...),
+mapping tag name regexps to lists of formatting expressions.
+
+The first entry whose MATCH regexp-matches a tag is used to
+format that tag.  The regexp is implicitly anchored, so to match
+a literal tag name, just use that tag name (if it contains
+special regexp characters like \".\" or \"*\", these have to be
+escaped).
+
+The cdr of the matching entry gives a list of Elisp expressions
+that modify the tag.  If the list is empty, the tag is simply
+hidden.  Otherwise, each expression EXPR is evaluated in order:
+for the first expression, the variable `tag' is bound to the tag
+name; for each later expression, the variable `tag' is bound to
+the result of the previous expression.  In this way, each
+expression can build on the formatting performed by the previous
+expression.  The result of the last expression is displayed in
+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\"))
+
+See also `notmuch-tag-format-image', which can help replace tags
+with images."
+  :group 'notmuch-search
+  :group 'notmuch-show
+  :group 'notmuch-faces
+  :type 'notmuch-tag-format-type)
+
+(defface notmuch-tag-deleted
+  '((((class color) (supports :strike-through "red")) :strike-through "red")
+    (t :inverse-video t))
+  "Face used to display deleted tags.
+
+Used in the default value of `notmuch-tag-deleted-formats'."
+  :group 'notmuch-faces)
+
+(defcustom notmuch-tag-deleted-formats
+  '(("unread" (notmuch-apply-face bare-tag `notmuch-tag-deleted))
+    (".*" (notmuch-apply-face tag `notmuch-tag-deleted)))
+  "Custom formats for tags when deleted.
+
+For deleted tags the formats in `notmuch-tag-formats' are applied
+first and then these formats are applied on top; that is `tag'
+passed to the function is the tag with all these previous
+formattings applied. The formatted can access the original
+unformatted tag as `bare-tag'.
+
+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))
+
+See `notmuch-tag-formats' for full documentation."
+  :group 'notmuch-show
+  :group 'notmuch-faces
+  :type 'notmuch-tag-format-type)
+
+(defface notmuch-tag-added
+  '((t :underline "green"))
+  "Default face used for added tags.
+
+Used in the default value for `notmuch-tag-added-formats'."
+  :group 'notmuch-faces)
+
+(defcustom notmuch-tag-added-formats
+  '((".*" (notmuch-apply-face tag 'notmuch-tag-added)))
+  "Custom formats for tags when added.
+
+For added tags the formats in `notmuch-tag-formats' are applied
+first and then these formats are applied on top.
+
+To disable special formatting of added tags, set this variable to
+nil.
+
+See `notmuch-tag-formats' for full documentation."
+  :group 'notmuch-show
+  :group 'notmuch-faces
+  :type 'notmuch-tag-format-type)
+
+;;; Icons
+
+(defun notmuch-tag-format-image-data (tag data)
+  "Replace TAG with image DATA, if available.
+
+This function returns a propertized string that will display image
+DATA in place of TAG.This is designed for use in
+`notmuch-tag-formats'.
+
+DATA is the content of an SVG picture (e.g., as returned by
+`notmuch-tag-star-icon')."
+  (propertize tag 'display
+             `(image :type svg
+                     :data ,data
+                     :ascent center
+                     :mask heuristic)))
+
+(defun notmuch-tag-star-icon ()
+  "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\" 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\"
+       transform=\"matrix(0.2484147,-0.02623394,0.02623394,0.2484147,174.63605,255.37691)\"
+       style=\"fill:#ffff00;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\" />
+  </g>
+</svg>")
+
+(defun notmuch-tag-star-empty-icon ()
+  "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\" 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\"
+       transform=\"matrix(0.2484147,-0.02623394,0.02623394,0.2484147,174.63605,255.37691)\"
+       style=\"fill:#d6d6d1;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1\" />
+  </g>
+</svg>")
+
+(defun notmuch-tag-tag-icon ()
+  "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\" 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\"
+       style=\"fill:#ffff00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1\" />
+  </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)
+  "Cache of tag format lookup.  Internal to `notmuch-tag-format-tag'.")
+
+(defun notmuch-tag-clear-cache ()
+  "Clear the internal cache of tag formats."
+  (clrhash notmuch-tag--format-cache))
+
+(defun notmuch-tag--get-formats (tag alist)
+  "Find the first item whose car regexp-matches TAG."
+  (save-match-data
+    ;; Don't use assoc-default since there's no way to distinguish a
+    ;; missing key from a present key with a null cdr.
+    (cl-assoc tag alist
+             :test (lambda (tag key)
+                     (and (eq (string-match key tag) 0)
+                          (= (match-end 0) (length tag)))))))
+
+(defun notmuch-tag--do-format (bare-tag tag formats)
+  "Apply a tag-formats entry to TAG."
+  (cond ((null formats)                ;; - Tag not in `formats',
+        tag)                   ;;   the format is the tag itself.
+       ((null (cdr formats))   ;; - Tag was deliberately hidden,
+        nil)                   ;;   no format must be returned
+       (t
+        ;; Tag was found and has formats, we must apply all the
+        ;; formats.  TAG may be null so treat that as a special case.
+        (let ((return-tag (copy-sequence (or tag ""))))
+          (dolist (format (cdr formats))
+            (setq return-tag
+                  (eval format
+                        `((bare-tag . ,bare-tag)
+                          (tag . ,return-tag)))))
+          (if (and (null tag) (equal return-tag ""))
+              nil
+            return-tag)))))
+
+(defun notmuch-tag-format-tag (tags orig-tags tag)
+  "Format TAG according to `notmuch-tag-formats'.
+
+TAGS and ORIG-TAGS are lists of the current tags and the original
+tags; tags which have been deleted (i.e., are in ORIG-TAGS but
+are not in TAGS) are shown using formats from
+`notmuch-tag-deleted-formats'; tags which have been added (i.e.,
+are in TAGS but are not in ORIG-TAGS) are shown using formats
+from `notmuch-tag-added-formats' and tags which have not been
+changed (the normal case) are shown using formats from
+`notmuch-tag-formats'."
+  (let* ((tag-state (cond ((not (member tag tags)) 'deleted)
+                         ((not (member tag orig-tags)) 'added)))
+        (formatted-tag (gethash (cons tag tag-state)
+                                notmuch-tag--format-cache
+                                'missing)))
+    (when (eq formatted-tag 'missing)
+      (let ((base (notmuch-tag--get-formats tag notmuch-tag-formats))
+           (over (cl-case tag-state
+                   (deleted (notmuch-tag--get-formats
+                             tag notmuch-tag-deleted-formats))
+                   (added (notmuch-tag--get-formats
+                           tag notmuch-tag-added-formats))
+                   (otherwise nil))))
+       (setq formatted-tag (notmuch-tag--do-format tag tag base))
+       (setq formatted-tag (notmuch-tag--do-format tag formatted-tag over))
+       (puthash (cons tag tag-state) formatted-tag notmuch-tag--format-cache)))
+    formatted-tag))
+
+(defun notmuch-tag-format-tags (tags orig-tags &optional face)
+  "Return a string representing formatted TAGS."
+  (let ((face (or face 'notmuch-tag-face))
+       (all-tags (sort (delete-dups (append tags orig-tags nil)) #'string<)))
+    (notmuch-apply-face
+     (mapconcat #'identity
+               ;; nil indicated that the tag was deliberately hidden
+               (delq nil (mapcar (apply-partially #'notmuch-tag-format-tag
+                                                  tags orig-tags)
+                                 all-tags))
+               " ")
+     face
+     t)))
+
+;;; Hooks
+
+(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
+a list of strings of the form \"+TAG\" or \"-TAG\".
+'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)
+  :group 'notmuch-hooks)
+
+(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
+a list of strings of the form \"+TAG\" or \"-TAG\".
+'query' will be a string containing the search query that determines
+the messages that were tagged."
+  :type 'hook
+  :options '(notmuch-hl-line-mode)
+  :group 'notmuch-hooks)
+
+;;; User Input
+
+(defvar notmuch-select-tag-history nil
+  "Minibuffer history of `notmuch-select-tag-with-completion' function.")
+
+(defvar notmuch-read-tag-changes-history nil
+  "Minibuffer history of `notmuch-read-tag-changes' function.")
+
+(defun notmuch-tag-completions (&rest search-terms)
+  "Return a list of tags for messages matching SEARCH-TERMS.
+
+Return all tags if no search terms are given."
+  (unless search-terms
+    (setq search-terms (list "*")))
+  (split-string
+   (with-output-to-string
+     (with-current-buffer standard-output
+       (apply 'notmuch--call-process notmuch-command nil t
+             nil "search" "--output=tags" "--exclude=false" search-terms)))
+   "\n+" t))
+
+(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
+  (completing-read prompt
+                  (apply #'notmuch-tag-completions search-terms)
+                  nil nil nil 'notmuch-select-tag-history))
+
+(defun notmuch-read-tag-changes (current-tags &optional prompt initial-input)
+  "Prompt for tag changes in the minibuffer.
+
+CURRENT-TAGS is a list of tags that are present on the message
+or messages to be changed.  These are offered as tag removal
+completions.  CURRENT-TAGS may contain duplicates.  PROMPT, if
+non-nil, is the query string to present in the minibuffer.  It
+defaults to \"Tags\".  INITIAL-INPUT, if non-nil, will be the
+initial input in the minibuffer."
+  (let* ((all-tag-list (notmuch-tag-completions))
+        (add-tag-list (mapcar (apply-partially 'concat "+") all-tag-list))
+        (remove-tag-list (mapcar (apply-partially 'concat "-") current-tags))
+        (tag-list (append add-tag-list remove-tag-list))
+        (prompt (concat (or prompt "Tags") " (+add -drop): "))
+        (crm-separator " ")
+        ;; By default, space is bound to "complete word" function.
+        ;; Re-bind it to insert a space instead.  Note that <tab>
+        ;; still does the completion.
+        (crm-local-completion-map
+         (let ((map (make-sparse-keymap)))
+           (set-keymap-parent map crm-local-completion-map)
+           (define-key map " " 'self-insert-command)
+           map)))
+    (completing-read-multiple prompt tag-list
+                             nil nil initial-input
+                             'notmuch-read-tag-changes-history)))
+
+;;; Tagging
+
+(defun notmuch-update-tags (tags tag-changes)
+  "Return a copy of TAGS with additions and removals from TAG-CHANGES.
+
+TAG-CHANGES must be a list of tags names, each prefixed with
+either a \"+\" to indicate the tag should be added to TAGS if not
+present or a \"-\" to indicate that the tag should be removed
+from TAGS if present."
+  (let ((result-tags (copy-sequence tags)))
+    (dolist (tag-change tag-changes)
+      (let ((tag (and (not (string-empty-p tag-change))
+                     (substring tag-change 1))))
+       (cl-case (aref tag-change 0)
+         (?+ (unless (member tag result-tags)
+               (push tag result-tags)))
+         (?- (setq result-tags (delete tag result-tags)))
+         (otherwise
+          (error "Changed tag must be of the form `+this_tag' or `-that_tag'")))))
+    (sort result-tags 'string<)))
+
+(defconst notmuch-tag-argument-limit 1000
+  "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.
+
+NOTE: this variable is no longer used.")
+
+(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.  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\" ..)
+directly, so that hooks specified in notmuch-before-tag-hook and
+notmuch-after-tag-hook will be run."
+  ;; Perform some validation
+  (dolist (tag-change tag-changes)
+    (unless (string-match-p "^[-+]\\S-+$" tag-change)
+      (error "Tag must be of the form `+this_tag' or `-that_tag'")))
+  (unless query
+    (error "Nothing to tag!"))
+  (when tag-changes
+    (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.
+
+Add a \"+\" prefix to any tag in TAGS list that doesn't already
+begin with a \"+\" or a \"-\". If REVERSE is non-nil, replace all
+\"+\" prefixes with \"-\" and vice versa in the result."
+  (mapcar (lambda (str)
+           (let ((s (if (string-match "^[+-]" str) str (concat "+" str))))
+             (if reverse
+                 (concat (if (= (string-to-char s) ?-) "+" "-")
+                         (substring s 1))
+               s)))
+         tags))
+
+(defvar notmuch-tag-jump-reverse-key "k"
+  "The key in tag-jump to switch to the reverse tag changes.")
+
+(defun notmuch-tag-jump (reverse)
+  "Create a jump menu for tagging operations.
+
+Creates and displays a jump menu for the tagging operations
+specified in `notmuch-tagging-keys'. If REVERSE is set then it
+offers a menu of the reverses of the operations specified in
+`notmuch-tagging-keys'; i.e. each `+tag' is replaced by `-tag'
+and vice versa."
+  ;; In principle this function is simple, but it has to deal with
+  ;; lots of cases: different modes (search/show/tree), whether a name
+  ;; is specified, whether the tagging operations is a list of
+  ;; tag-ops, or a symbol that evaluates to such a list, and whether
+  ;; REVERSE is specified.
+  (interactive "P")
+  (let (action-map)
+    (pcase-dolist (`(,key ,tag ,name) notmuch-tagging-keys)
+      (let* ((tag-function (cl-case major-mode
+                            (notmuch-search-mode #'notmuch-search-tag)
+                            (notmuch-show-mode #'notmuch-show-tag)
+                            (notmuch-tree-mode #'notmuch-tree-tag)))
+            (tag (if (symbolp tag)
+                     (symbol-value tag)
+                   tag))
+            (tag-change (if reverse
+                            (notmuch-tag-change-list tag t)
+                          tag))
+            (name (or (and (not (string= name ""))
+                           name)
+                      (and (symbolp name)
+                           (symbol-name name))))
+            (name-string (if name
+                             (if reverse
+                                 (concat "Reverse " name)
+                               name)
+                           (mapconcat #'identity tag-change " "))))
+       (push (list key name-string
+                   (lambda () (funcall tag-function tag-change)))
+             action-map)))
+    (push (list notmuch-tag-jump-reverse-key
+               (if reverse
+                   "Forward tag changes "
+                 "Reverse tag changes")
+               (apply-partially 'notmuch-tag-jump (not reverse)))
+         action-map)
+    (setq action-map (nreverse action-map))
+    (notmuch-jump action-map "Tag: ")))
+
+;;; _
+
+(provide 'notmuch-tag)
+
+;;; notmuch-tag.el ends here
diff --git a/emacs/notmuch-tree.el b/emacs/notmuch-tree.el
new file mode 100644 (file)
index 0000000..b58fa6a
--- /dev/null
@@ -0,0 +1,1464 @@
+;;; notmuch-tree.el --- displaying notmuch forests  -*- lexical-binding: t -*-
+;;
+;; Copyright © Carl Worth
+;; Copyright © David Edmondson
+;; Copyright © Mark Walters
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: David Edmondson <dme@dme.org>
+;;          Mark Walters <markwalters1009@gmail.com>
+
+;;; Code:
+
+(require 'mail-parse)
+
+(require 'notmuch-lib)
+(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
+                            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" ())
+
+;; For `notmuch-tree-next-thread-from-search'.
+(declare-function notmuch-search-next-thread "notmuch" ())
+(declare-function notmuch-search-previous-thread "notmuch" ())
+(declare-function notmuch-tree-from-search-thread "notmuch" ())
+
+;; 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.")
+
+;;; Options
+
+(defgroup notmuch-tree nil
+  "Showing message and thread structure."
+  :group 'notmuch)
+
+(defcustom notmuch-tree-show-out nil
+  "View selected messages in new window rather than split-pane."
+  :type 'boolean
+  :group 'notmuch-tree)
+
+(defcustom notmuch-unthreaded-show-out t
+  "View selected messages in new window rather than split-pane."
+  :type 'boolean
+  :group 'notmuch-tree)
+
+(defun notmuch-tree-show-out ()
+  (if notmuch-tree-unthreaded
+      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 ")
+    ("tags" . "(%s)"))
+  "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
+  `(("date" . "%12s  ")
+    ("authors" . "%-20s")
+    ((("subject" . "%s")) ." %-54s ")
+    ("tags" . "(%s)"))
+  "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 ()
+  (if notmuch-tree-unthreaded
+      notmuch-unthreaded-result-format
+    notmuch-tree-result-format))
+
+;;; Faces
+;;;; Faces for messages that match the query
+
+(defface notmuch-tree-match-face
+  '((t :inherit default))
+  "Default face used in tree mode face for matching messages"
+  :group 'notmuch-tree
+  :group 'notmuch-faces)
+
+(defface notmuch-tree-match-date-face
+  nil
+  "Face used in tree mode for the date in messages matching the query."
+  :group 'notmuch-tree
+  :group 'notmuch-faces)
+
+(defface notmuch-tree-match-author-face
+  '((((class color)
+      (background dark))
+     (:foreground "OliveDrab1"))
+    (((class color)
+      (background light))
+     (:foreground "dark blue"))
+    (t
+     (:bold t)))
+  "Face used in tree mode for the author in messages matching the query."
+  :group 'notmuch-tree
+  :group 'notmuch-faces)
+
+(defface notmuch-tree-match-subject-face
+  nil
+  "Face used in tree mode for the subject in messages matching the query."
+  :group 'notmuch-tree
+  :group 'notmuch-faces)
+
+(defface notmuch-tree-match-tree-face
+  nil
+  "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-match-tag-face
+  '((((class color)
+      (background dark))
+     (:foreground "OliveDrab1"))
+    (((class color)
+      (background light))
+     (:foreground "navy blue" :bold t))
+    (t
+     (:bold t)))
+  "Face used in tree mode for tags in messages matching the query."
+  :group 'notmuch-tree
+  :group 'notmuch-faces)
+
+;;;; Faces for messages that do not match the query
+
+(defface notmuch-tree-no-match-face
+  '((t (:foreground "gray")))
+  "Default face used in tree mode face for non-matching messages."
+  :group 'notmuch-tree
+  :group 'notmuch-faces)
+
+(defface notmuch-tree-no-match-date-face
+  nil
+  "Face used in tree mode for non-matching dates."
+  :group 'notmuch-tree
+  :group 'notmuch-faces)
+
+(defface notmuch-tree-no-match-subject-face
+  nil
+  "Face used in tree mode for non-matching subjects."
+  :group 'notmuch-tree
+  :group 'notmuch-faces)
+
+(defface notmuch-tree-no-match-tree-face
+  nil
+  "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 non-matching authors."
+  :group 'notmuch-tree
+  :group 'notmuch-faces)
+
+(defface notmuch-tree-no-match-tag-face
+  nil
+  "Face used in tree mode face for non-matching tags."
+  :group 'notmuch-tree
+  :group 'notmuch-faces)
+
+;;; Variables
+
+(defvar-local notmuch-tree-previous-subject
+  "The subject of the most recent result shown during the async display.")
+
+(defvar-local notmuch-tree-basic-query nil
+  "A buffer local copy of argument query to the function notmuch-tree.")
+
+(defvar-local notmuch-tree-query-context nil
+  "A buffer local copy of argument query-context to the function notmuch-tree.")
+
+(defvar-local notmuch-tree-target-msg nil
+  "A buffer local copy of argument target to the function notmuch-tree.")
+
+(defvar-local notmuch-tree-open-target nil
+  "A buffer local copy of argument open-target to the function notmuch-tree.")
+
+(defvar-local notmuch-tree-parent-buffer nil)
+
+(defvar-local notmuch-tree-message-window nil
+  "The window of the message pane.
+
+It is set in both the tree buffer and the child show buffer. It
+is used to try and close the message pane when quitting tree view
+or the child show buffer.")
+(put 'notmuch-tree-message-window 'permanent-local t)
+
+(defvar-local notmuch-tree-message-buffer nil
+  "The buffer name of the show buffer in the message pane.
+
+This is used to try and make sure we don't close the message pane
+if the user has loaded a different buffer in that window.")
+(put 'notmuch-tree-message-buffer 'permanent-local t)
+
+;;; Tree wrapper commands
+
+(defmacro notmuch-tree--define-do-in-message-window (name cmd)
+  "Define NAME as a command that calls CMD interactively in the message window.
+If the message pane is closed then this command does nothing.
+Avoid using this macro in new code; it will be removed."
+  `(defun ,name ()
+     ,(concat "(In message window) " (documentation cmd t))
+     (interactive)
+     (when (window-live-p notmuch-tree-message-window)
+       (with-selected-window notmuch-tree-message-window
+        (call-interactively #',cmd)))))
+
+(notmuch-tree--define-do-in-message-window
+ notmuch-tree-previous-message-button
+ notmuch-show-previous-button)
+(notmuch-tree--define-do-in-message-window
+ notmuch-tree-next-message-button
+ notmuch-show-next-button)
+(notmuch-tree--define-do-in-message-window
+ notmuch-tree-toggle-message-process-crypto
+ notmuch-show-toggle-process-crypto)
+
+(defun notmuch-tree--message-process-crypto ()
+  "Return value of `notmuch-show-process-crypto' in the message window.
+If that window isn't alive, then return the current value.
+Avoid using this function in new code; it will be removed."
+  (if (window-live-p notmuch-tree-message-window)
+      (with-selected-window notmuch-tree-message-window
+       notmuch-show-process-crypto)
+    notmuch-show-process-crypto))
+
+(defmacro notmuch-tree--define-close-message-window-and (name cmd)
+  "Define NAME as a variant of CMD.
+
+NAME determines the value of `notmuch-show-process-crypto' in the
+message window, closes the window, and then call CMD interactively
+with that value let-bound.  If the message window does not exist,
+then NAME behaves like CMD."
+  `(defun ,name ()
+     ,(concat "(Close message pane and) " (documentation cmd t))
+     (interactive)
+     (let ((notmuch-show-process-crypto
+           (notmuch-tree--message-process-crypto)))
+       (notmuch-tree-close-message-window)
+       (call-interactively #',cmd))))
+
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-help
+ notmuch-help)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-new-mail
+ notmuch-mua-new-mail)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-jump-search
+ notmuch-jump-search)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-forward-message
+ notmuch-show-forward-message)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-reply-sender
+ notmuch-show-reply-sender)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-reply
+ notmuch-show-reply)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-view-raw-message
+ notmuch-show-view-raw-message)
+
+;;; Keymap
+
+(defvar notmuch-tree-mode-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map notmuch-common-keymap)
+    ;; 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 "o" 'notmuch-tree-toggle-order)
+    (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)
+
+    ;; these use notmuch-show functions directly
+    (define-key map "|" 'notmuch-show-pipe-message)
+    (define-key map "w" 'notmuch-show-save-attachments)
+    (define-key map "v" 'notmuch-show-view-all-mime-parts)
+    (define-key map "c" 'notmuch-show-stash-map)
+    (define-key map "b" 'notmuch-show-resend-message)
+
+    ;; these apply to the message pane
+    (define-key map (kbd "M-TAB")     'notmuch-tree-previous-message-button)
+    (define-key map (kbd "<backtab>") 'notmuch-tree-previous-message-button)
+    (define-key map (kbd "TAB")       'notmuch-tree-next-message-button)
+    (define-key map "$" 'notmuch-tree-toggle-message-process-crypto)
+
+    ;; bindings from show (or elsewhere) but we close the message pane first.
+    (define-key map "f" 'notmuch-tree-forward-message)
+    (define-key map "r" 'notmuch-tree-reply-sender)
+    (define-key map "R" 'notmuch-tree-reply)
+    (define-key map "V" 'notmuch-tree-view-raw-message)
+    (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)
+    (define-key map [mouse-1] 'notmuch-tree-show-message)
+    (define-key map "x" 'notmuch-tree-archive-message-then-next-or-exit)
+    (define-key map "X" 'notmuch-tree-archive-thread-then-exit)
+    (define-key map "A" 'notmuch-tree-archive-thread-then-next)
+    (define-key map "a" 'notmuch-tree-archive-message-then-next)
+    (define-key map "z" 'notmuch-tree-to-tree)
+    (define-key map "n" 'notmuch-tree-next-matching-message)
+    (define-key map "p" 'notmuch-tree-prev-matching-message)
+    (define-key map "N" 'notmuch-tree-next-message)
+    (define-key map "P" 'notmuch-tree-prev-message)
+    (define-key map (kbd "M-p") 'notmuch-tree-prev-thread)
+    (define-key map (kbd "M-n") 'notmuch-tree-next-thread)
+    (define-key map "k" 'notmuch-tag-jump)
+    (define-key map "-" 'notmuch-tree-remove-tag)
+    (define-key map "+" 'notmuch-tree-add-tag)
+    (define-key map "*" 'notmuch-tree-tag-thread)
+    (define-key map " " 'notmuch-tree-scroll-or-next)
+    (define-key map (kbd "DEL") 'notmuch-tree-scroll-message-window-back)
+    (define-key map "e" 'notmuch-tree-resume-message)
+    map)
+  "Keymap for \"notmuch tree\" buffers.")
+
+;;; Message properties
+
+(defun notmuch-tree-get-message-properties ()
+  "Return the properties of the current message as a plist.
+
+Some useful entries are:
+:headers - Property list containing the headers :Date, :Subject, :From, etc.
+:tags - Tags for this message."
+  (save-excursion
+    (beginning-of-line)
+    (get-text-property (point) :notmuch-message-properties)))
+
+(defun notmuch-tree-set-message-properties (props)
+  (save-excursion
+    (beginning-of-line)
+    (put-text-property (point)
+                      (+ (point) 1)
+                      :notmuch-message-properties props)))
+
+(defun notmuch-tree-set-prop (prop val &optional props)
+  (let ((inhibit-read-only t)
+       (props (or props
+                  (notmuch-tree-get-message-properties))))
+    (plist-put props prop val)
+    (notmuch-tree-set-message-properties props)))
+
+(defun notmuch-tree-get-prop (prop &optional props)
+  (plist-get (or props (notmuch-tree-get-message-properties))
+            prop))
+
+(defun notmuch-tree-set-tags (tags)
+  "Set the tags of the current message."
+  (notmuch-tree-set-prop :tags tags))
+
+(defun notmuch-tree-get-tags ()
+  "Return the tags of the current message."
+  (notmuch-tree-get-prop :tags))
+
+(defun notmuch-tree-get-message-id (&optional bare)
+  "Return the message id of the current message."
+  (let ((id (notmuch-tree-get-prop :id)))
+    (if id
+       (if bare
+           id
+         (notmuch-id-to-query id))
+      nil)))
+
+(defun notmuch-tree-get-match ()
+  "Return whether the current message is a match."
+  (notmuch-tree-get-prop :match))
+
+;;; Update display
+
+(defun notmuch-tree-refresh-result ()
+  "Redisplay the current message line.
+
+This redisplays the current line based on the messages
+properties (as they are now). This is used when tags are
+updated."
+  (let ((init-point (point))
+       (end (line-end-position))
+       (msg (notmuch-tree-get-message-properties))
+       (inhibit-read-only t))
+    (beginning-of-line)
+    ;; This is a little tricky: we override
+    ;; notmuch-tree-previous-subject to get the decision between
+    ;; ... and a subject right and it stops notmuch-tree-insert-msg
+    ;; from overwriting the buffer local copy of
+    ;; notmuch-tree-previous-subject if this is called while the
+    ;; buffer is displaying.
+    (let ((notmuch-tree-previous-subject
+          (notmuch-tree-get-prop :previous-subject)))
+      (delete-region (point) (1+ (line-end-position)))
+      (notmuch-tree-insert-msg msg))
+    (let ((new-end (line-end-position)))
+      (goto-char (if (= init-point end)
+                    new-end
+                  (min init-point (- new-end 1)))))))
+
+(defun notmuch-tree-tag-update-display (&optional tag-changes)
+  "Update display for TAG-CHANGES to current message.
+
+Updates the message in the message pane if appropriate, but does
+NOT change the database."
+  (let* ((current-tags (notmuch-tree-get-tags))
+        (new-tags (notmuch-update-tags current-tags tag-changes))
+        (tree-msg-id (notmuch-tree-get-message-id)))
+    (unless (equal current-tags new-tags)
+      (notmuch-tree-set-tags new-tags)
+      (notmuch-tree-refresh-result)
+      (when (window-live-p notmuch-tree-message-window)
+       (with-selected-window notmuch-tree-message-window
+         (when (string= tree-msg-id (notmuch-show-get-message-id))
+           (notmuch-show-update-tags new-tags)))))))
+
+;;; Commands (and some helper functions used by them)
+
+(defun notmuch-tree-tag (tag-changes)
+  "Change tags for the current message."
+  (interactive
+   (list (notmuch-read-tag-changes (notmuch-tree-get-tags) "Tag message")))
+  (notmuch-tag (notmuch-tree-get-message-id) tag-changes)
+  (notmuch-tree-tag-update-display tag-changes))
+
+(defun notmuch-tree-add-tag (tag-changes)
+  "Same as `notmuch-tree-tag' but sets initial input to '+'."
+  (interactive
+   (list (notmuch-read-tag-changes (notmuch-tree-get-tags) "Tag message" "+")))
+  (notmuch-tree-tag tag-changes))
+
+(defun notmuch-tree-remove-tag (tag-changes)
+  "Same as `notmuch-tree-tag' but sets initial input to '-'."
+  (interactive
+   (list (notmuch-read-tag-changes (notmuch-tree-get-tags) "Tag message" "-")))
+  (notmuch-tree-tag tag-changes))
+
+(defun notmuch-tree-resume-message ()
+  "Resume EDITING the current draft message."
+  (interactive)
+  (notmuch-tree-close-message-window)
+  (let ((id (notmuch-tree-get-message-id)))
+    (if id
+       (notmuch-draft-resume id)
+      (message "No message to resume!"))))
+
+;; The next two functions close the message window before calling
+;; notmuch-search or notmuch-tree but they do so after the user has
+;; entered the query (in case the user was basing the query on
+;; something in the message window).
+
+(defun notmuch-tree-to-search ()
+  "Run \"notmuch search\" with the given `query' and display results."
+  (interactive)
+  (let ((query (notmuch-read-query "Notmuch search: ")))
+    (notmuch-tree-close-message-window)
+    (notmuch-search query)))
+
+(defun notmuch-tree-to-tree ()
+  "Run a query and display results in tree view."
+  (interactive)
+  (let ((query (notmuch-read-query "Notmuch tree view search: ")))
+    (notmuch-tree-close-message-window)
+    (notmuch-tree query)))
+
+(defun notmuch-tree-archive-thread-then-next ()
+  "Archive all messages in the current buffer, then show next thread from search."
+  (interactive)
+  (notmuch-tree-archive-thread)
+  (notmuch-tree-next-thread))
+
+(defun notmuch-unthreaded-from-tree-current-query ()
+  "Switch from tree view to unthreaded view."
+  (interactive)
+  (unless notmuch-tree-unthreaded
+    (notmuch-tree-refresh-view 'unthreaded)))
+
+(defun notmuch-tree-from-unthreaded-current-query ()
+  "Switch from unthreaded view to tree view."
+  (interactive)
+  (when notmuch-tree-unthreaded
+    (notmuch-tree-refresh-view 'tree)))
+
+(defun notmuch-search-from-tree-current-query ()
+  "Call notmuch search with the current query."
+  (interactive)
+  (notmuch-tree-close-message-window)
+  (notmuch-search (notmuch-tree-get-query)))
+
+(defun notmuch-tree-message-window-kill-hook ()
+  "Close the message pane when exiting the show buffer."
+  (let ((buffer (current-buffer)))
+    (when (and (window-live-p notmuch-tree-message-window)
+              (eq (window-buffer notmuch-tree-message-window) buffer))
+      ;; We could check whether this is the only window in its frame,
+      ;; but simply ignoring the error that is thrown otherwise is
+      ;; what we had to do for Emacs 24 and we stick to that because
+      ;; it is still the simplest approach.
+      (ignore-errors
+       (delete-window notmuch-tree-message-window)))))
+
+(defun notmuch-tree-command-hook ()
+  (when (eq major-mode 'notmuch-tree-mode)
+    ;; We just run the notmuch-show-command-hook on the message pane.
+    (when (buffer-live-p notmuch-tree-message-buffer)
+      (with-current-buffer notmuch-tree-message-buffer
+       (notmuch-show-command-hook)))))
+
+(defun notmuch-tree-show-message-in ()
+  "Show the current message (in split-pane)."
+  (interactive)
+  (let ((id (notmuch-tree-get-message-id))
+       (inhibit-read-only t)
+       buffer)
+    (when id
+      ;; We close and reopen the window to kill off un-needed buffers
+      ;; this might cause flickering but seems ok.
+      (notmuch-tree-close-message-window)
+      (setq notmuch-tree-message-window
+           (split-window-vertically (/ (window-height) 4)))
+      (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-single-message t)
+             ;; Ensure that `pop-to-buffer-same-window' uses the
+             ;; window we want it to use.
+             (display-buffer-overriding-action
+                '((display-buffer-same-window)
+                  (inhibit-same-window . nil))))
+         (setq buffer (notmuch-show id))))
+      ;; We need the `let' as notmuch-tree-message-window is buffer local.
+      (let ((window notmuch-tree-message-window))
+       (with-current-buffer buffer
+         (setq notmuch-tree-message-window window)
+         (add-hook 'kill-buffer-hook 'notmuch-tree-message-window-kill-hook)))
+      (when notmuch-show-mark-read-tags
+       (notmuch-tree-tag-update-display notmuch-show-mark-read-tags))
+      (setq notmuch-tree-message-buffer buffer))))
+
+(defun notmuch-tree-show-message-out ()
+  "Show the current message (in whole window)."
+  (interactive)
+  (let ((id (notmuch-tree-get-message-id))
+       (inhibit-read-only t))
+    (when id
+      ;; We close the window to kill off un-needed buffers.
+      (notmuch-tree-close-message-window)
+      ;; 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.
+
+Shows in split pane or whole window according to value of
+`notmuch-tree-show-out'. A prefix argument reverses the choice."
+  (interactive "P")
+  (if (or (and (notmuch-tree-show-out) (not arg))
+         (and (not (notmuch-tree-show-out)) arg))
+      (notmuch-tree-show-message-out)
+    (notmuch-tree-show-message-in)))
+
+(defun notmuch-tree-scroll-message-window ()
+  "Scroll the message window (if it exists)."
+  (interactive)
+  (when (window-live-p notmuch-tree-message-window)
+    (with-selected-window notmuch-tree-message-window
+      (if (pos-visible-in-window-p (point-max))
+         t
+       (scroll-up)))))
+
+(defun notmuch-tree-scroll-message-window-back ()
+  "Scroll the message window back (if it exists)."
+  (interactive)
+  (when (window-live-p notmuch-tree-message-window)
+    (with-selected-window notmuch-tree-message-window
+      (if (pos-visible-in-window-p (point-min))
+         t
+       (scroll-down)))))
+
+(defun notmuch-tree-scroll-or-next ()
+  "Scroll the message window.
+If it at end go to next message."
+  (interactive)
+  (when (notmuch-tree-scroll-message-window)
+    (notmuch-tree-next-matching-message)))
+
+(defun notmuch-tree-quit (&optional kill-both)
+  "Close the split view or exit tree."
+  (interactive "P")
+  (when (or (not (notmuch-tree-close-message-window)) kill-both)
+    (kill-buffer (current-buffer))))
+
+(defun notmuch-tree-close-message-window ()
+  "Close the message-window. Return t if close succeeds."
+  (interactive)
+  (when (and (window-live-p notmuch-tree-message-window)
+            (eq (window-buffer notmuch-tree-message-window)
+                notmuch-tree-message-buffer))
+    (delete-window notmuch-tree-message-window)
+    (unless (get-buffer-window-list notmuch-tree-message-buffer)
+      (kill-buffer notmuch-tree-message-buffer))
+    t))
+
+(defun notmuch-tree-archive-message (&optional unarchive)
+  "Archive the current message.
+
+Archive the current message by applying the tag changes in
+`notmuch-archive-tags' to it. If a prefix argument is given, the
+message will be \"unarchived\", i.e. the tag changes in
+`notmuch-archive-tags' will be reversed."
+  (interactive "P")
+  (when notmuch-archive-tags
+    (notmuch-tree-tag
+     (notmuch-tag-change-list notmuch-archive-tags unarchive))))
+
+(defun notmuch-tree-archive-message-then-next (&optional unarchive)
+  "Archive the current message and move to next matching message."
+  (interactive "P")
+  (notmuch-tree-archive-message unarchive)
+  (notmuch-tree-next-matching-message))
+
+(defun notmuch-tree-archive-thread-then-exit ()
+  "Archive all messages in the current buffer, then exit notmuch-tree."
+  (interactive)
+  (notmuch-tree-archive-thread)
+  (notmuch-tree-quit t))
+
+(defun notmuch-tree-archive-message-then-next-or-exit ()
+  "Archive current message, then show next open message in current thread.
+
+If at the last open message in the current thread, then exit back
+to search results."
+  (interactive)
+  (notmuch-tree-archive-message)
+  (notmuch-tree-next-matching-message t))
+
+(defun notmuch-tree-next-message ()
+  "Move to next message."
+  (interactive)
+  (forward-line)
+  (when (window-live-p notmuch-tree-message-window)
+    (notmuch-tree-show-message-in)))
+
+(defun notmuch-tree-prev-message ()
+  "Move to previous message."
+  (interactive)
+  (forward-line -1)
+  (when (window-live-p notmuch-tree-message-window)
+    (notmuch-tree-show-message-in)))
+
+(defun notmuch-tree-goto-matching-message (&optional prev)
+  "Move to the next or previous matching message.
+
+Returns t if there was a next matching message in the thread to show,
+nil otherwise."
+  (let ((dir (if prev -1 nil))
+       (eobfn (if prev #'bobp #'eobp)))
+    (while (and (not (funcall eobfn))
+               (not (notmuch-tree-get-match)))
+      (forward-line dir))
+    (not (funcall eobfn))))
+
+(defun notmuch-tree-matching-message (&optional prev pop-at-end)
+  "Move to the next or previous matching message."
+  (interactive "P")
+  (forward-line (if prev -1 nil))
+  (if (and (not (notmuch-tree-goto-matching-message prev)) pop-at-end)
+      (notmuch-tree-quit pop-at-end)
+    (when (window-live-p notmuch-tree-message-window)
+      (notmuch-tree-show-message-in))))
+
+(defun notmuch-tree-prev-matching-message (&optional pop-at-end)
+  "Move to previous matching message."
+  (interactive "P")
+  (notmuch-tree-matching-message t pop-at-end))
+
+(defun notmuch-tree-next-matching-message (&optional pop-at-end)
+  "Move to next matching message."
+  (interactive "P")
+  (notmuch-tree-matching-message nil pop-at-end))
+
+(defun notmuch-tree-refresh-view (&optional view)
+  "Refresh view."
+  (interactive)
+  (when (get-buffer-process (current-buffer))
+    (error "notmuch tree process already running for current buffer"))
+  (let ((inhibit-read-only t)
+       (basic-query notmuch-tree-basic-query)
+       (unthreaded (cond ((eq view 'unthreaded) t)
+                         ((eq view 'tree) nil)
+                         (t notmuch-tree-unthreaded)))
+       (query-context notmuch-tree-query-context)
+       (target (notmuch-tree-get-message-id)))
+    (erase-buffer)
+    (notmuch-tree-worker basic-query
+                        query-context
+                        target
+                        nil
+                        unthreaded
+                        notmuch-search-oldest-first)))
+
+(defun notmuch-tree-thread-top ()
+  (when (notmuch-tree-get-message-properties)
+    (while (not (or (notmuch-tree-get-prop :first) (eobp)))
+      (forward-line -1))))
+
+(defun notmuch-tree-prev-thread-in-tree ()
+  "Move to the previous thread in the current tree"
+  (interactive)
+  (forward-line -1)
+  (notmuch-tree-thread-top)
+  (not (bobp)))
+
+(defun notmuch-tree-next-thread-in-tree ()
+  "Get the next thread in the current tree. Returns t if a thread was
+found or nil if not."
+  (interactive)
+  (forward-line 1)
+  (while (not (or (notmuch-tree-get-prop :first) (eobp)))
+    (forward-line 1))
+  (not (eobp)))
+
+(defun notmuch-tree-next-thread-from-search (&optional previous)
+  "Move to the next thread in the parent search results, if any.
+
+If PREVIOUS is non-nil, move to the previous item in the
+search results instead."
+  (interactive "P")
+  (let ((parent-buffer notmuch-tree-parent-buffer))
+    (notmuch-tree-quit t)
+    (when (buffer-live-p parent-buffer)
+      (switch-to-buffer parent-buffer)
+      (if previous
+         (notmuch-search-previous-thread)
+       (notmuch-search-next-thread))
+      (notmuch-tree-from-search-thread))))
+
+(defun notmuch-tree-next-thread (&optional previous)
+  "Move to the next thread in the current tree or parent search results.
+
+If PREVIOUS is non-nil, move to the previous thread in the tree or
+search results instead."
+  (interactive)
+  (unless (if previous (notmuch-tree-prev-thread-in-tree)
+           (notmuch-tree-next-thread-in-tree))
+    (notmuch-tree-next-thread-from-search previous)))
+
+(defun notmuch-tree-prev-thread ()
+  "Move to the previous thread in the current tree or parent search results."
+  (interactive)
+  (notmuch-tree-next-thread t))
+
+(defun notmuch-tree-thread-mapcar (function)
+  "Call FUNCTION for each message in the current thread.
+FUNCTION is called for side effects only."
+  (save-excursion
+    (notmuch-tree-thread-top)
+    (cl-loop collect (funcall function)
+            do (forward-line)
+            while (and (notmuch-tree-get-message-properties)
+                       (not (notmuch-tree-get-prop :first))))))
+
+(defun notmuch-tree-get-messages-ids-thread-search ()
+  "Return a search string for all message ids of messages in the current thread."
+  (mapconcat 'identity
+            (notmuch-tree-thread-mapcar 'notmuch-tree-get-message-id)
+            " or "))
+
+(defun notmuch-tree-tag-thread (tag-changes)
+  "Tag all messages in the current thread."
+  (interactive
+   (let ((tags (apply #'append (notmuch-tree-thread-mapcar
+                               (lambda () (notmuch-tree-get-tags))))))
+     (list (notmuch-read-tag-changes tags "Tag thread"))))
+  (when (notmuch-tree-get-message-properties)
+    (notmuch-tag (notmuch-tree-get-messages-ids-thread-search) tag-changes)
+    (notmuch-tree-thread-mapcar
+     (lambda () (notmuch-tree-tag-update-display tag-changes)))))
+
+(defun notmuch-tree-archive-thread (&optional unarchive)
+  "Archive each message in thread.
+
+Archive each message currently shown by applying the tag changes
+in `notmuch-archive-tags' to each. If a prefix argument is given,
+the messages will be \"unarchived\", i.e. the tag changes in
+`notmuch-archive-tags' will be reversed.
+
+Note: This command is safe from any race condition of new messages
+being delivered to the same thread. It does not archive the
+entire thread, but only the messages shown in the current
+buffer."
+  (interactive "P")
+  (when notmuch-archive-tags
+    (notmuch-tree-tag-thread
+     (notmuch-tag-change-list notmuch-archive-tags unarchive))))
+
+;;; Functions for displaying the tree buffer itself
+
+(defun notmuch-tree-clean-address (address)
+  "Try to clean a single email ADDRESS for display. Return
+AUTHOR_NAME if present, otherwise return AUTHOR_EMAIL. Return
+unchanged ADDRESS if parsing fails."
+  (let* ((clean-address (notmuch-clean-address address))
+        (p-address (car clean-address))
+        (p-name (cdr clean-address)))
+
+    ;; If we have a name return that otherwise return the address.
+    (or p-name p-address)))
+
+(defun notmuch-tree-format-field (field format-string msg)
+  "Format a FIELD of MSG according to FORMAT-STRING and return string."
+  (let* ((headers (plist-get msg :headers))
+        (match (plist-get msg :match)))
+    (cond
+     ((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
+                   'notmuch-tree-no-match-date-face)))
+       (propertize (format format-string (plist-get msg :date_relative))
+                   'face face)))
+
+     ((string-equal field "tree")
+      (let ((tree-status (plist-get msg :tree-status))
+           (face (if match
+                     'notmuch-tree-match-tree-face
+                   'notmuch-tree-no-match-tree-face)))
+
+       (propertize (format format-string
+                           (mapconcat #'identity (reverse tree-status) ""))
+                   'face face)))
+
+     ((string-equal field "subject")
+      (let ((bare-subject (notmuch-show-strip-re (plist-get headers :Subject)))
+           (previous-subject notmuch-tree-previous-subject)
+           (face (if match
+                     'notmuch-tree-match-subject-face
+                   'notmuch-tree-no-match-subject-face)))
+
+       (setq notmuch-tree-previous-subject bare-subject)
+       (propertize (format format-string
+                           (if (string= previous-subject bare-subject)
+                               " ..."
+                             bare-subject))
+                   'face face)))
+
+     ((string-equal field "authors")
+      (let ((author (notmuch-tree-clean-address (plist-get headers :From)))
+           (len (length (format format-string "")))
+           (face (if match
+                     'notmuch-tree-match-author-face
+                   'notmuch-tree-no-match-author-face)))
+       (when (> (length author) len)
+         (setq author (substring author 0 len)))
+       (propertize (format format-string author) 'face face)))
+
+     ((string-equal field "tags")
+      (let ((tags (plist-get msg :tags))
+           (orig-tags (plist-get msg :orig-tags))
+           (face (if match
+                     'notmuch-tree-match-tag-face
+                   'notmuch-tree-no-match-tag-face)))
+       (format format-string (notmuch-tag-format-tags tags orig-tags face)))))))
+
+(defun notmuch-tree-format-field-list (field-list msg)
+  "Format fields of MSG according to FIELD-LIST and return string."
+  (let ((face (if (plist-get msg :match)
+                 'notmuch-tree-match-face
+               'notmuch-tree-no-match-face))
+       (result-string))
+    (dolist (spec field-list result-string)
+      (let ((field-string (notmuch-tree-format-field (car spec) (cdr spec) msg)))
+       (setq result-string (concat result-string field-string))))
+    (notmuch-apply-face result-string face t)))
+
+(defun notmuch-tree-insert-msg (msg)
+  "Insert the message MSG according to notmuch-tree-result-format."
+  ;; We need to save the previous subject as it will get overwritten
+  ;; by the insert-field calls.
+  (let ((previous-subject notmuch-tree-previous-subject))
+    (insert (notmuch-tree-format-field-list (notmuch-tree-result-format) msg))
+    (notmuch-tree-set-message-properties msg)
+    (notmuch-tree-set-prop :previous-subject previous-subject)
+    (insert "\n")))
+
+(defun notmuch-tree-goto-and-insert-msg (msg)
+  "Insert msg at the end of the buffer. Move point to msg if it is the target."
+  (save-excursion
+    (goto-char (point-max))
+    (notmuch-tree-insert-msg msg))
+  (let ((msg-id (notmuch-id-to-query (plist-get msg :id)))
+       (target notmuch-tree-target-msg))
+    (when (or (and (not target) (plist-get msg :match))
+             (string= msg-id target))
+      (setq notmuch-tree-target-msg "found")
+      (goto-char (point-max))
+      (forward-line -1)
+      (when notmuch-tree-open-target
+       (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.
+
+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))
+       ;; outline level, computed from the message's depth and
+       ;; whether or not it's the first message in the tree.
+       (level (1+ (if (and (eq 0 depth) (not first)) 1 depth))))
+    (cond
+     ((and (< 0 depth) (not last))
+      (push (alist-get 'vertical-tee  notmuch-tree-thread-symbols) tree-status))
+     ((and (< 0 depth) last)
+      (push (alist-get 'bottom notmuch-tree-thread-symbols) tree-status))
+     ((and (eq 0 depth) first last)
+      (push (alist-get 'prefix notmuch-tree-thread-symbols) tree-status))
+     ((and (eq 0 depth) first (not last))
+      (push (alist-get 'top-tee notmuch-tree-thread-symbols) tree-status))
+     ((and (eq 0 depth) (not first) last)
+      (push (alist-get 'bottom notmuch-tree-thread-symbols) tree-status))
+     ((and (eq 0 depth) (not first) (not last))
+      (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 (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."
+  (let ((n (length thread)))
+    (cl-loop for tree in thread
+            for count from 1 to n
+            do (notmuch-tree-insert-tree tree depth tree-status
+                                         (eq count 1)
+                                         (eq count n)))))
+
+(defun notmuch-tree-insert-forest-thread (forest-thread)
+  "Insert a single complete thread."
+  ;; 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.
+
+This function inserts a collection of several complete threads as
+passed to it by notmuch-tree-process-filter."
+  (mapc 'notmuch-tree-insert-forest-thread forest))
+
+(define-derived-mode notmuch-tree-mode fundamental-mode "notmuch-tree"
+  "Major mode displaying messages (as opposed to threads) of a notmuch search.
+
+This buffer contains the results of a \"notmuch tree\" of your
+email archives. Each line in the buffer represents a single
+message giving the relative date, the author, subject, and any
+tags.
+
+Pressing \\[notmuch-tree-show-message] on any line displays that message.
+
+Complete list of currently available key bindings:
+
+\\{notmuch-tree-mode-map}"
+  (setq notmuch-buffer-refresh-function #'notmuch-tree-refresh-view)
+  (hl-line-mode 1)
+  (setq buffer-read-only 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."
+  (let ((buffer (process-buffer proc))
+       (status (process-status proc))
+       (exit-status (process-exit-status proc)))
+    (when (memq status '(exit signal))
+      (kill-buffer (process-get proc 'parse-buf))
+      (when (buffer-live-p buffer)
+       (with-current-buffer buffer
+         (save-excursion
+           (let ((inhibit-read-only t))
+             (goto-char (point-max))
+             (when (eq status 'signal)
+               (insert "Incomplete search results (tree view process was killed).\n"))
+             (when (eq status 'exit)
+               (insert "End of search results.")
+               (unless (= exit-status 0)
+                 (insert (format " (process returned %d)" exit-status)))
+               (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."
+  (let ((results-buf (process-buffer proc))
+       (parse-buf (process-get proc 'parse-buf))
+       (inhibit-read-only t))
+    (if (not (buffer-live-p results-buf))
+       (delete-process proc)
+      (with-current-buffer parse-buf
+       ;; Insert new data
+       (save-excursion
+         (goto-char (point-max))
+         (insert string))
+       (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 oldest-first)
+  "Insert the tree view of the search in the current buffer.
+
+This is is a helper function for notmuch-tree. The arguments are
+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-tree-unthreaded unthreaded)
+  (setq notmuch-tree-basic-query basic-query)
+  (setq notmuch-tree-query-context (if (or (string= query-context "")
+                                          (string= query-context "*"))
+                                      nil
+                                    query-context))
+  (setq notmuch-tree-target-msg target)
+  (setq notmuch-tree-open-target open-target)
+  ;; Set the default value for `notmuch-show-process-crypto' in this
+  ;; buffer. Although we don't use this some of the functions we call
+  ;; (such as reply) do. It is a buffer local variable so setting it
+  ;; will not affect genuine show buffers.
+  (setq notmuch-show-process-crypto notmuch-crypto-process-mime)
+  (erase-buffer)
+  (goto-char (point-min))
+  (let* ((search-args (concat basic-query
+                             (and query-context
+                                  (concat " and (" query-context ")"))))
+        (sort-arg (if oldest-first "--sort=oldest-first" "--sort=newest-first"))
+        (message-arg (if unthreaded "--unthreaded" "--entire-thread")))
+    (when (equal (car (notmuch--process-lines notmuch-command "count" search-args)) "0")
+      (setq search-args basic-query))
+    (notmuch-tag-clear-cache)
+    (let ((proc (notmuch-start-notmuch
+                "notmuch-tree" (current-buffer) #'notmuch-tree-process-sentinel
+                "show" "--body=false" "--format=sexp" "--format-version=5"
+                sort-arg message-arg search-args))
+         ;; Use a scratch buffer to accumulate partial output.
+         ;; This buffer will be killed by the sentinel, which
+         ;; should be called no matter how the process dies.
+         (parse-buf (generate-new-buffer " *notmuch tree parse*")))
+      (process-put proc 'parse-buf parse-buf)
+      (set-process-filter proc 'notmuch-tree-process-filter)
+      (set-process-query-on-exit-flag proc nil))))
+
+(defun notmuch-tree-get-query ()
+  "Return the current query in this tree buffer."
+  (if notmuch-tree-query-context
+      (concat notmuch-tree-basic-query
+             " and ("
+             notmuch-tree-query-context
+             ")")
+    notmuch-tree-basic-query))
+
+(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 (&optional query query-context target buffer-name
+                              open-target unthreaded parent-buffer oldest-first)
+  "Display threads matching QUERY in tree view.
+
+The arguments are:
+  QUERY: the main query. This can be any query but in many cases will be
+      a single thread. If nil this is read interactively from the minibuffer.
+  QUERY-CONTEXT: is an additional term for the query. The query used
+      is QUERY and QUERY-CONTEXT unless that does not match any messages
+      in which case we fall back to just QUERY.
+  TARGET: A message ID (with the id: prefix) that will be made
+      current if it appears in the tree view results.
+  BUFFER-NAME: the name of the buffer to display the tree view. If
+      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)
+  (unless query
+    (setq query (notmuch-read-query (concat "Notmuch "
+                                           (if unthreaded "unthreaded " "tree ")
+                                           "view search: "))))
+  (let* ((name
+         (or buffer-name
+             (notmuch-search-buffer-title query
+                                          (if unthreaded "unthreaded" "tree"))))
+        (buffer (get-buffer-create (generate-new-buffer-name name)))
+       (inhibit-read-only t))
+    (pop-to-buffer-same-window buffer))
+  ;; Don't track undo information for this buffer
+  (setq buffer-undo-list t)
+  (notmuch-tree-worker query query-context target open-target unthreaded oldest-first)
+  (setq notmuch-tree-parent-buffer parent-buffer)
+  (setq truncate-lines t))
+
+(defun notmuch-unthreaded (&optional query query-context target buffer-name
+                                    open-target)
+  "Display threads matching QUERY in unthreaded view.
+
+See function NOTMUCH-TREE for documentation of the arguments"
+  (interactive)
+  (notmuch-tree query query-context target buffer-name open-target t))
+
+(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)))
+
+(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)
+  (if (null notmuch-tree-outline-visibility)
+      (notmuch-tree-prev-thread)
+    (notmuch-tree-outline--next t t nil)))
+
+;;;; Mode definition
+(defvar notmuch-tree-outline-mode-lighter nil
+  "The lighter mark for notmuch-tree-outline mode.
+Usually empty since outline-minor-mode's lighter will be active.")
+
+(define-minor-mode notmuch-tree-outline-mode
+  "Minor mode allowing message trees to be folded as outlines.
+
+When this mode is set, each thread and subthread in the results
+list is treated as a foldable section, with its first message as
+its header.
+
+The mode just makes available in the tree buffer all the
+keybindings in `outline-minor-mode', and binds the following
+additional keys:
+
+\\{notmuch-tree-outline-mode-map}
+
+The customizable variable `notmuch-tree-outline-visibility'
+controls how navigation in the buffer is affected by this mode:
+
+  - If it is set to nil, `notmuch-tree-outline-previous',
+    `notmuch-tree-outline-next', and their thread counterparts
+    behave just as the corresponding notmuch-tree navigation keys
+    when this mode is not enabled.
+
+  - If, on the other hand, `notmuch-tree-outline-visibility' is
+    set to a non-nil value, these commands hiding the outlines of
+    the trees you are not reading as you move to new messages.
+
+To enable notmuch-tree-outline-mode by default in all
+notmuch-tree buffers, just set
+`notmuch-tree-outline-mode-enabled' to t."
+  :lighter notmuch-tree-outline-mode-lighter
+  :keymap `((,(kbd "TAB") . outline-cycle)
+           (,(kbd "M-TAB") . outline-cycle-buffer)
+           ("n" . notmuch-tree-outline-next)
+           ("p" . notmuch-tree-outline-previous)
+           (,(kbd "M-n") . notmuch-tree-outline-next-thread)
+           (,(kbd "M-p") . notmuch-tree-outline-previous-thread))
+  (outline-minor-mode notmuch-tree-outline-mode)
+  (unless (derived-mode-p 'notmuch-tree-mode)
+    (user-error "notmuch-tree-outline-mode is only meaningful for notmuch trees!"))
+  (if notmuch-tree-outline-mode
+      (progn (setq-local outline-regexp "^[^\n]+")
+            (setq-local outline-level #'notmuch-tree-outline--level)
+            (notmuch-tree-outline--set-visibility))
+    (setq-local outline-regexp (default-value 'outline-regexp))
+    (setq-local        outline-level (default-value 'outline-level))))
+
+;;; _
+
+(provide 'notmuch-tree)
+
+;;; notmuch-tree.el ends here
diff --git a/emacs/notmuch-version.el.tmpl b/emacs/notmuch-version.el.tmpl
new file mode 100644 (file)
index 0000000..9730829
--- /dev/null
@@ -0,0 +1,27 @@
+;;; notmuch-version.el --- version of notmuch  -*- emacs-lisp -*-
+;;
+;; %AG%
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(defconst notmuch-emacs-version %VERSION%
+  "Version of Notmuch Emacs MUA.")
+
+(provide 'notmuch-version)
+
+;;; notmuch-version.el ends here
diff --git a/emacs/notmuch-wash.el b/emacs/notmuch-wash.el
new file mode 100644 (file)
index 0000000..653ecc2
--- /dev/null
@@ -0,0 +1,418 @@
+;;; notmuch-wash.el --- cleaning up message bodies  -*- lexical-binding: t -*-
+;;
+;; Copyright © Carl Worth
+;; Copyright © David Edmondson
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Carl Worth <cworth@cworth.org>
+;;          David Edmondson <dme@dme.org>
+
+;;; Code:
+
+(require 'coolj)
+(require 'diff-mode)
+(require 'notmuch-lib)
+
+(declare-function notmuch-show-insert-bodypart "notmuch-show"
+                 (msg part depth &optional hide))
+(defvar notmuch-show-indent-messages-width)
+
+;;; Options
+
+(defgroup notmuch-wash nil
+  "Cleaning up messages for display."
+  :group 'notmuch)
+
+(defcustom notmuch-wash-signature-regexp "^\\(-- ?\\|_+\\)$"
+  "Pattern to match a line that separates content from signature."
+  :type 'regexp
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-citation-regexp "\\(^[[:space:]]*>.*\n\\)+"
+  "Pattern to match citation lines."
+  :type 'regexp
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-original-regexp "^\\(--+\s?[oO]riginal [mM]essage\s?--+\\)$"
+  "Pattern to match a line that separates original message from
+reply in top-posted message."
+  :type 'regexp
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-signature-hidden-format
+  "[ %d-line signature. Click/Enter to show. ]"
+  "String used to construct button text for hidden signatures.
+Can use up to one integer format parameter, i.e. %d."
+  :type 'string
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-signature-visible-format
+  "[ %d-line signature. Click/Enter to hide. ]"
+  "String used to construct button text for visible signatures.
+Can use up to one integer format parameter, i.e. %d."
+  :type 'string
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-citation-hidden-format
+  "[ %d more citation lines. Click/Enter to show. ]"
+  "String used to construct button text for hidden citations.
+Can use up to one integer format parameter, i.e. %d."
+  :type 'string
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-citation-visible-format
+  "[ %d more citation lines. Click/Enter to hide. ]"
+  "String used to construct button text for visible citations.
+Can use up to one integer format parameter, i.e. %d."
+  :type 'string
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-original-hidden-format
+  "[ %d-line hidden original message. Click/Enter to show. ]"
+  "String used to construct button text for hidden citations.
+Can use up to one integer format parameter, i.e. %d."
+  :type 'string
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-button-original-visible-format
+  "[ %d-line original message. Click/Enter to hide. ]"
+  "String used to construct button text for visible citations.
+Can use up to one integer format parameter, i.e. %d."
+  :type 'string
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-signature-lines-max 12
+  "Maximum length of signature that will be hidden by default."
+  :type 'integer
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-citation-lines-prefix 3
+  "Always show at least this many lines from the start of a citation.
+
+If there is one more line than the sum of
+`notmuch-wash-citation-lines-prefix' and
+`notmuch-wash-citation-lines-suffix', show that, otherwise
+collapse the remaining lines into a button."
+  :type 'integer
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-citation-lines-suffix 3
+  "Always show at least this many lines from the end of a citation.
+
+If there is one more line than the sum of
+`notmuch-wash-citation-lines-prefix' and
+`notmuch-wash-citation-lines-suffix', show that, otherwise
+collapse the remaining lines into a button."
+  :type 'integer
+  :group 'notmuch-wash)
+
+(defcustom notmuch-wash-wrap-lines-length nil
+  "Wrap line after at most this many characters.
+
+If this is nil, lines in messages will be wrapped to fit in the
+current window. If this is a number, lines will be wrapped after
+this many characters (ignoring indentation due to thread depth)
+or at the window width (whichever one is lower)."
+  :type '(choice (const :tag "window width" nil)
+                (integer :tag "number of characters"))
+  :group 'notmuch-wash)
+
+;;; Faces
+
+(defface notmuch-wash-toggle-button
+  '((t (:inherit font-lock-comment-face)))
+  "Face used for buttons toggling the visibility of washed away
+message parts."
+  :group 'notmuch-wash
+  :group 'notmuch-faces)
+
+(defface notmuch-wash-cited-text
+  '((t (:inherit message-cited-text)))
+  "Face used for cited text."
+  :group 'notmuch-wash
+  :group 'notmuch-faces)
+
+;;; Buttons
+
+(defun notmuch-wash-toggle-invisible-action (cite-button)
+  ;; Toggle overlay visibility
+  (let ((overlay (button-get cite-button 'overlay)))
+    (overlay-put overlay 'invisible (not (overlay-get overlay 'invisible))))
+  ;; Update button text
+  (let* ((new-start (button-start cite-button))
+        (overlay (button-get cite-button 'overlay))
+        (button-label (notmuch-wash-button-label overlay))
+        (old-point (point))
+        (properties (text-properties-at (point)))
+        (inhibit-read-only t))
+    (goto-char new-start)
+    (insert button-label)
+    (set-text-properties new-start (point) properties)
+    (let ((old-end (button-end cite-button)))
+      (move-overlay cite-button new-start (point))
+      (delete-region (point) old-end))
+    (goto-char (min old-point (1- (button-end cite-button))))))
+
+(define-button-type 'notmuch-wash-button-invisibility-toggle-type
+  'action 'notmuch-wash-toggle-invisible-action
+  'follow-link t
+  'face 'notmuch-wash-toggle-button
+  :supertype 'notmuch-button-type)
+
+(define-button-type 'notmuch-wash-button-citation-toggle-type
+  'help-echo "mouse-1, RET: Show citation"
+  :supertype 'notmuch-wash-button-invisibility-toggle-type)
+
+(define-button-type 'notmuch-wash-button-signature-toggle-type
+  'help-echo "mouse-1, RET: Show signature"
+  :supertype 'notmuch-wash-button-invisibility-toggle-type)
+
+(define-button-type 'notmuch-wash-button-original-toggle-type
+  'help-echo "mouse-1, RET: Show original message"
+  :supertype 'notmuch-wash-button-invisibility-toggle-type)
+
+(defun notmuch-wash-region-isearch-show (overlay)
+  (notmuch-wash-toggle-invisible-action
+   (overlay-get overlay 'notmuch-wash-button)))
+
+(defun notmuch-wash-button-label (overlay)
+  (let* ((type (overlay-get overlay 'type))
+        (invis-spec (overlay-get overlay 'invisible))
+        (state (if (invisible-p invis-spec) "hidden" "visible"))
+        (label-format (symbol-value
+                       (intern-soft
+                        (format "notmuch-wash-button-%s-%s-format"
+                                type state))))
+        (lines-count (count-lines (overlay-start overlay)
+                                  (overlay-end overlay))))
+    (format label-format lines-count)))
+
+(defun notmuch-wash-region-to-button (beg end type &optional prefix)
+  "Auxiliary function to do the actual making of overlays and buttons.
+
+BEG and END are buffer locations. TYPE should a string, either
+\"citation\" or \"signature\". Optional PREFIX is some arbitrary
+text to insert before the button, probably for indentation.  Note
+that PREFIX should not include a newline."
+  ;; This uses some slightly tricky conversions between strings and
+  ;; symbols because of the way the button code works. Note that
+  ;; replacing intern-soft with make-symbol will cause this to fail,
+  ;; since the newly created symbol has no plist.
+  (let ((overlay (make-overlay beg end))
+       (button-type (intern-soft (concat "notmuch-wash-button-"
+                                         type "-toggle-type"))))
+    (overlay-put overlay 'invisible t)
+    (overlay-put overlay 'isearch-open-invisible #'notmuch-wash-region-isearch-show)
+    (overlay-put overlay 'type type)
+    (goto-char (1+ end))
+    (save-excursion
+      (goto-char beg)
+      (when prefix
+       (insert-before-markers prefix))
+      (let ((button-beg (point)))
+       (insert-before-markers (notmuch-wash-button-label overlay) "\n")
+       (let ((button (make-button button-beg (1- (point))
+                                  'overlay overlay
+                                  :type button-type)))
+         (overlay-put overlay 'notmuch-wash-button button))))))
+
+;;; Hook functions
+
+(defun notmuch-wash-excerpt-citations (_msg _depth)
+  "Excerpt citations and up to one signature."
+  (goto-char (point-min))
+  (beginning-of-line)
+  (when (and (< (point) (point-max))
+            (re-search-forward notmuch-wash-original-regexp nil t))
+    (notmuch-wash-region-to-button (match-beginning 0)
+                                  (point-max)
+                                  "original"))
+  (while (and (< (point) (point-max))
+             (re-search-forward notmuch-wash-citation-regexp nil t))
+    (let* ((cite-start (match-beginning 0))
+          (cite-end (match-end 0))
+          (cite-lines (count-lines cite-start cite-end)))
+      (overlay-put (make-overlay cite-start cite-end)
+                  'face 'notmuch-wash-cited-text)
+      (when (> cite-lines (+ notmuch-wash-citation-lines-prefix
+                            notmuch-wash-citation-lines-suffix
+                            1))
+       (goto-char cite-start)
+       (forward-line notmuch-wash-citation-lines-prefix)
+       (let ((hidden-start (point-marker)))
+         (goto-char cite-end)
+         (forward-line (- notmuch-wash-citation-lines-suffix))
+         (notmuch-wash-region-to-button
+          hidden-start (point-marker)
+          "citation")))))
+  (when (and (not (eobp))
+            (re-search-forward notmuch-wash-signature-regexp nil t))
+    (let ((sig-start (match-beginning 0)))
+      (when (<= (count-lines sig-start (point-max))
+               notmuch-wash-signature-lines-max)
+       (let ((sig-start-marker (make-marker))
+             (sig-end-marker (make-marker)))
+         (set-marker sig-start-marker sig-start)
+         (set-marker sig-end-marker (point-max))
+         (overlay-put (make-overlay sig-start-marker sig-end-marker)
+                      'face 'message-cited-text)
+         (notmuch-wash-region-to-button
+          sig-start-marker sig-end-marker
+          "signature"))))))
+
+(defun notmuch-wash-elide-blank-lines (_msg _depth)
+  "Elide leading, trailing and successive blank lines."
+  ;; Algorithm derived from `article-strip-multiple-blank-lines' in
+  ;; `gnus-art.el'.
+  ;; Make all blank lines empty.
+  (goto-char (point-min))
+  (while (re-search-forward "^[[:space:]\t]+$" nil t)
+    (replace-match "" nil t))
+  ;; Replace multiple empty lines with a single empty line.
+  (goto-char (point-min))
+  (while (re-search-forward "^\n\\(\n+\\)" nil t)
+    (delete-region (match-beginning 1) (match-end 1)))
+  ;; Remove a leading blank line.
+  (goto-char (point-min))
+  (when (looking-at "\n")
+    (delete-region (match-beginning 0) (match-end 0)))
+  ;; Remove a trailing blank line.
+  (goto-char (point-max))
+  (when (looking-at "\n")
+    (delete-region (match-beginning 0) (match-end 0))))
+
+(defun notmuch-wash-tidy-citations (_msg _depth)
+  "Improve the display of cited regions of a message.
+
+Perform several transformations on the message body:
+
+- Remove lines of repeated citation leaders with no other
+  content,
+- Remove citation leaders standing alone before a block of cited
+  text,
+- Remove citation trailers standing alone after a block of cited
+  text."
+  ;; Remove lines of repeated citation leaders with no other content.
+  (goto-char (point-min))
+  (while (re-search-forward "\\(^>[> ]*\n\\)\\{2,\\}" nil t)
+    (replace-match "\\1"))
+  ;; Remove citation leaders standing alone before a block of cited text.
+  (goto-char (point-min))
+  (while (re-search-forward "\\(\n\\|^[^>].*\\)\n\\(^>[> ]*\n\\)" nil t)
+    (replace-match "\\1\n"))
+  ;; Remove citation trailers standing alone after a block of cited text.
+  (goto-char (point-min))
+  (while (re-search-forward "\\(^>[> ]*\n\\)\\(^$\\|^[^>].*\\)" nil t)
+    (replace-match "\\2")))
+
+(defun notmuch-wash-wrap-long-lines (_msg depth)
+  "Wrap long lines in the message.
+
+If `notmuch-wash-wrap-lines-length' is a number, this will wrap
+the message lines to the minimum of the width of the window or
+its value. Otherwise, this function will wrap long lines in the
+message at the window width. When doing so, citation leaders in
+the wrapped text are maintained."
+  (let* ((coolj-wrap-follows-window-size nil)
+        (indent (* depth notmuch-show-indent-messages-width))
+        (limit (if (numberp notmuch-wash-wrap-lines-length)
+                   (min (+ notmuch-wash-wrap-lines-length indent)
+                        (window-width))
+                 (window-width)))
+        (fill-column (- limit
+                        indent
+                        ;; 2 to avoid poor interaction with
+                        ;; `word-wrap'.
+                        2)))
+    (coolj-wrap-region (point-min) (point-max))))
+
+;;;; Convert Inline Patches
+
+(defun notmuch-wash-subject-to-filename (subject &optional maxlen)
+  "Convert a mail SUBJECT into a filename.
+
+The resulting filename is similar to the names generated by \"git
+format-patch\", without the leading patch sequence number
+\"0001-\" and \".patch\" extension. Any leading \"[PREFIX]\"
+style strings are removed prior to conversion.
+
+Optional argument MAXLEN is the maximum length of the resulting
+filename, before trimming any trailing . and - characters."
+  (let* ((s (replace-regexp-in-string "^ *\\(\\[[^]]*\\] *\\)*" "" subject))
+        (s (replace-regexp-in-string "[^A-Za-z0-9._]+" "-" s))
+        (s (replace-regexp-in-string "\\.+" "." s))
+        (s (if maxlen (substring s 0 (min (length s) maxlen)) s))
+        (s (replace-regexp-in-string "[.-]*$" "" s)))
+    s))
+
+(defun notmuch-wash-subject-to-patch-sequence-number (subject)
+  "Convert a patch mail SUBJECT into a patch sequence number.
+
+Return the patch sequence number N from the last \"[PATCH N/M]\"
+style prefix in SUBJECT, or nil if such a prefix can't be found."
+  (and (string-match
+       "^ *\\(\\[[^]]*\\] *\\)*\\[[^]]*?\\([0-9]+\\)/[0-9]+[^]]*\\].*"
+       subject)
+       (string-to-number (substring subject (match-beginning 2) (match-end 2)))))
+
+(defun notmuch-wash-subject-to-patch-filename (subject)
+  "Convert a patch mail SUBJECT into a filename.
+
+The resulting filename is similar to the names generated by \"git
+format-patch\". If the patch mail was generated and sent using
+\"git format-patch/send-email\", this should re-create the
+original filename the sender had."
+  (format "%04d-%s.patch"
+         (or (notmuch-wash-subject-to-patch-sequence-number subject) 1)
+         (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.
+
+Given that this function guesses whether a buffer includes a
+patch and then guesses the extent of the patch, there is scope
+for error."
+  (goto-char (point-min))
+  (when (re-search-forward diff-file-header-re nil t)
+    (beginning-of-line -1)
+    (let ((patch-start (point))
+         (patch-end (point-max))
+         part)
+      (goto-char patch-start)
+      (when (or
+            ;; Patch ends with signature.
+            (re-search-forward notmuch-wash-signature-regexp nil t)
+            ;; Patch ends with bugtraq comment.
+            (re-search-forward "^\\*\\*\\* " nil t))
+       (setq patch-end (match-beginning 0)))
+      (save-restriction
+       (narrow-to-region patch-start patch-end)
+       (setq part (plist-put part :content-type "inline patch"))
+       (setq part (plist-put part :content (buffer-string)))
+       (setq part (plist-put part :id -1))
+       (setq part (plist-put part :filename
+                             (notmuch-wash-subject-to-patch-filename
+                              (plist-get
+                               (plist-get msg :headers) :Subject))))
+       (delete-region (point-min) (point-max))
+       (notmuch-show-insert-bodypart nil part depth)))))
+
+;;; _
+
+(provide 'notmuch-wash)
+
+;;; notmuch-wash.el ends here
diff --git a/emacs/notmuch.el b/emacs/notmuch.el
new file mode 100644 (file)
index 0000000..6eef4af
--- /dev/null
@@ -0,0 +1,1239 @@
+;;; notmuch.el --- run notmuch within emacs  -*- lexical-binding: t -*-
+;;
+;; Copyright © Carl Worth
+;;
+;; This file is part of Notmuch.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Carl Worth <cworth@cworth.org>
+;; Homepage: https://notmuchmail.org
+
+;;; Commentary:
+
+;; This is an emacs-based interface to the notmuch mail system.
+;;
+;; You will first need to have the notmuch program installed and have a
+;; notmuch database built in order to use this. See
+;; https://notmuchmail.org for details.
+;;
+;; To install this software, copy it to a directory that is on the
+;; `load-path' variable within emacs (a good candidate is
+;; /usr/local/share/emacs/site-lisp). If you are viewing this from the
+;; notmuch source distribution then you can simply run:
+;;
+;;     sudo make install-emacs
+;;
+;; to install it.
+;;
+;; Then, to actually run it, add:
+;;
+;;     (autoload 'notmuch "notmuch" "Notmuch mail" t)
+;;
+;; to your ~/.emacs file, and then run "M-x notmuch" from within emacs,
+;; or run:
+;;
+;;     emacs -f notmuch
+;;
+;; Have fun, and let us know if you have any comment, questions, or
+;; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
+;; required, but is available from https://notmuchmail.org).
+;;
+;; Note for MELPA users (and others tracking the development version
+;; of notmuch-emacs):
+;;
+;; This emacs package needs a fairly closely matched version of the
+;; notmuch program. If you use the MELPA version of notmuch.el (as
+;; opposed to MELPA stable), you should be prepared to track the
+;; master development branch (i.e. build from git) for the notmuch
+;; program as well. Upgrading notmuch-emacs too far beyond the notmuch
+;; program can CAUSE YOUR EMAIL TO STOP WORKING.
+;;
+;; TL;DR: notmuch-emacs from MELPA and notmuch from distro packages is
+;; NOT SUPPORTED.
+
+;;; Code:
+
+(require 'mm-view)
+(require 'message)
+
+(require 'hl-line)
+
+(require 'notmuch-lib)
+(require 'notmuch-tag)
+(require 'notmuch-show)
+(require 'notmuch-tree)
+(require 'notmuch-mua)
+(require 'notmuch-hello)
+(require 'notmuch-maildir-fcc)
+(require 'notmuch-message)
+(require 'notmuch-parser)
+
+;;; Options
+
+(defcustom notmuch-search-result-format
+  `(("date" . "%12s ")
+    ("count" . "%-7s ")
+    ("authors" . "%-20s ")
+    ("subject" . "%s ")
+    ("tags" . "(%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
+         (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
+;; convention used in e.g. emacs and gnus. The value, `notmuch-config[.el[c]]'
+;; is consistent with notmuch cli configuration file `~/.notmuch-config'.
+(defcustom notmuch-init-file (locate-user-emacs-file "notmuch-config")
+  "Your Notmuch Emacs-Lisp configuration file name.
+If a file with one of the suffixes defined by `get-load-suffixes' exists,
+it will be read instead.
+This file is read once when notmuch is loaded; the notmuch hooks added
+there will be called at other points of notmuch execution."
+  :type 'file
+  :group 'notmuch)
+
+(defcustom notmuch-search-hook '(notmuch-hl-line-mode)
+  "List of functions to call when notmuch displays the search results."
+  :type 'hook
+  :options '(notmuch-hl-line-mode)
+  :group 'notmuch-search
+  :group 'notmuch-hooks)
+
+;;; Mime Utilities
+
+(defun notmuch-foreach-mime-part (function mm-handle)
+  (cond ((stringp (car mm-handle))
+        (dolist (part (cdr mm-handle))
+          (notmuch-foreach-mime-part function part)))
+       ((bufferp (car mm-handle))
+        (funcall function mm-handle))
+       (t (dolist (part mm-handle)
+            (notmuch-foreach-mime-part function part)))))
+
+(defun notmuch-count-attachments (mm-handle)
+  (let ((count 0))
+    (notmuch-foreach-mime-part
+     (lambda (p)
+       (let ((disposition (mm-handle-disposition p)))
+        (and (listp disposition)
+             (or (equal (car disposition) "attachment")
+                 (and (equal (car disposition) "inline")
+                      (assq 'filename disposition)))
+             (cl-incf count))))
+     mm-handle)
+    count))
+
+(defun notmuch-save-attachments (mm-handle &optional queryp)
+  (notmuch-foreach-mime-part
+   (lambda (p)
+     (let ((disposition (mm-handle-disposition p)))
+       (and (listp disposition)
+           (or (equal (car disposition) "attachment")
+               (and (equal (car disposition) "inline")
+                    (assq 'filename disposition)))
+           (or (not queryp)
+               (y-or-n-p
+                (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
+           (mm-save-part p))))
+   mm-handle))
+
+;;; Keymap
+
+(defvar notmuch-search-mode-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map notmuch-common-keymap)
+    (define-key map "x" 'notmuch-bury-or-kill-this-buffer)
+    (define-key map (kbd "DEL") 'notmuch-search-scroll-down)
+    (define-key map "b" 'notmuch-search-scroll-down)
+    (define-key map " " 'notmuch-search-scroll-up)
+    (define-key map "<" 'notmuch-search-first-thread)
+    (define-key map ">" 'notmuch-search-last-thread)
+    (define-key map "p" 'notmuch-search-previous-thread)
+    (define-key map "n" 'notmuch-search-next-thread)
+    (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 "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)
+    (define-key map "a" 'notmuch-search-archive-thread)
+    (define-key map "-" 'notmuch-search-remove-tag)
+    (define-key map "+" 'notmuch-search-add-tag)
+    (define-key map (kbd "RET") 'notmuch-search-show-thread)
+    (define-key map (kbd "M-RET") 'notmuch-tree-from-search-thread)
+    (define-key map "Z" 'notmuch-tree-from-search-current-query)
+    (define-key map "U" 'notmuch-unthreaded-from-search-current-query)
+    map)
+  "Keymap for \"notmuch search\" buffers.")
+
+;;; Internal Variables
+
+(defvar notmuch-query-history nil
+  "Variable to store minibuffer history for notmuch queries.")
+
+(defvar-local notmuch-search-query-string nil)
+(defvar-local notmuch-search-target-thread nil)
+(defvar-local notmuch-search-target-line nil)
+
+;;; Stashing
+
+(defvar notmuch-search-stash-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "i" 'notmuch-search-stash-thread-id)
+    (define-key map "q" 'notmuch-stash-query)
+    (define-key map "?" 'notmuch-subkeymap-help)
+    map)
+  "Submap for stash commands.")
+(fset 'notmuch-search-stash-map notmuch-search-stash-map)
+
+(defun notmuch-search-stash-thread-id ()
+  "Copy thread ID of current thread to kill-ring."
+  (interactive)
+  (notmuch-common-do-stash (notmuch-search-find-thread-id)))
+
+(defun notmuch-stash-query ()
+  "Copy current query to kill-ring."
+  (interactive)
+  (notmuch-common-do-stash notmuch-search-query-string))
+
+;;; Movement
+
+(defun notmuch-search-scroll-up ()
+  "Move forward through search results by one window's worth."
+  (interactive)
+  (condition-case nil
+      (scroll-up nil)
+    ((end-of-buffer) (notmuch-search-last-thread))))
+
+(defun notmuch-search-scroll-down ()
+  "Move backward through the search results by one window's worth."
+  (interactive)
+  ;; I don't know why scroll-down doesn't signal beginning-of-buffer
+  ;; the way that scroll-up signals end-of-buffer, but c'est la vie.
+  ;;
+  ;; So instead of trapping a signal we instead check whether the
+  ;; window begins on the first line of the buffer and if so, move
+  ;; directly to that position. (We have to count lines since the
+  ;; window-start position is not the same as point-min due to the
+  ;; invisible thread-ID characters on the first line.
+  (if (equal (count-lines (point-min) (window-start)) 0)
+      (goto-char (point-min))
+    (scroll-down nil)))
+
+(defun notmuch-search-next-thread ()
+  "Select the next thread in the search results."
+  (interactive)
+  (when (notmuch-search-get-result)
+    (goto-char (notmuch-search-result-end))))
+
+(defun notmuch-search-previous-thread ()
+  "Select the previous thread in the search results."
+  (interactive)
+  (if (notmuch-search-get-result)
+      (unless (bobp)
+       (goto-char (notmuch-search-result-beginning (- (point) 1))))
+    ;; We must be past the end; jump to the last result
+    (notmuch-search-last-thread)))
+
+(defun notmuch-search-last-thread ()
+  "Select the last thread in the search results."
+  (interactive)
+  (goto-char (point-max))
+  (forward-line -2)
+  (let ((beg (notmuch-search-result-beginning)))
+    (when beg
+      (goto-char beg))))
+
+(defun notmuch-search-first-thread ()
+  "Select the first thread in the search results."
+  (interactive)
+  (goto-char (point-min)))
+
+;;; Faces
+
+(defface notmuch-message-summary-face
+  `((((class color) (background light))
+     ,@(and (>= emacs-major-version 27) '(:extend t))
+     :background "#f0f0f0")
+    (((class color) (background dark))
+     ,@(and (>= emacs-major-version 27) '(:extend t))
+     :background "#303030"))
+  "Face for the single-line message summary in notmuch-show-mode."
+  :group 'notmuch-show
+  :group 'notmuch-faces)
+
+(defface notmuch-search-date
+  '((t :inherit default))
+  "Face used in search mode for dates."
+  :group 'notmuch-search
+  :group 'notmuch-faces)
+
+(defface notmuch-search-count
+  '((t :inherit default))
+  "Face used in search mode for the count matching the query."
+  :group 'notmuch-search
+  :group 'notmuch-faces)
+
+(defface notmuch-search-subject
+  '((t :inherit default))
+  "Face used in search mode for subjects."
+  :group 'notmuch-search
+  :group 'notmuch-faces)
+
+(defface notmuch-search-matching-authors
+  '((t :inherit default))
+  "Face used in search mode for authors matching the query."
+  :group 'notmuch-search
+  :group 'notmuch-faces)
+
+(defface notmuch-search-non-matching-authors
+  '((((class color)
+      (background dark))
+     (:foreground "grey30"))
+    (((class color)
+      (background light))
+     (:foreground "grey60"))
+    (t
+     (:italic t)))
+  "Face used in search mode for authors not matching the query."
+  :group 'notmuch-search
+  :group 'notmuch-faces)
+
+(defface notmuch-tag-face
+  '((((class color)
+      (background dark))
+     (:foreground "OliveDrab1"))
+    (((class color)
+      (background light))
+     (:foreground "navy blue" :bold t))
+    (t
+     (:bold t)))
+  "Face used in search mode face for tags."
+  :group 'notmuch-search
+  :group 'notmuch-faces)
+
+(defface notmuch-search-flagged-face
+  '((((class color)
+      (background dark))
+     (:foreground "LightBlue1"))
+    (((class color)
+      (background light))
+     (:foreground "blue")))
+  "Face used in search mode face for flagged threads.
+
+This face is the default value for the \"flagged\" tag in
+`notmuch-search-line-faces'."
+  :group 'notmuch-search
+  :group 'notmuch-faces)
+
+(defface notmuch-search-unread-face
+  '((t
+     (:weight bold)))
+  "Face used in search mode for unread threads.
+
+This face is the default value for the \"unread\" tag in
+`notmuch-search-line-faces'."
+  :group 'notmuch-search
+  :group 'notmuch-faces)
+
+;;; Mode
+
+(define-derived-mode notmuch-search-mode fundamental-mode "notmuch-search"
+  "Major mode displaying results of a notmuch search.
+
+This buffer contains the results of a \"notmuch search\" of your
+email archives. Each line in the buffer represents a single
+thread giving a summary of the thread (a relative date, the
+number of matched messages and total messages in the thread,
+participants in the thread, a representative subject line, and
+any tags).
+
+Pressing \\[notmuch-search-show-thread] on any line displays that
+thread. The '\\[notmuch-search-add-tag]' and
+'\\[notmuch-search-remove-tag]' keys can be used to add or remove
+tags from a thread. The '\\[notmuch-search-archive-thread]' key
+is a convenience for archiving a thread (applying changes in
+`notmuch-archive-tags'). The '\\[notmuch-search-tag-all]' key can
+be used to add and/or remove tags from all messages (as opposed
+to threads) that match the current query.  Use with caution, as
+this will also tag matching messages that arrived *after*
+constructing the buffer.
+
+Other useful commands are '\\[notmuch-search-filter]' for
+filtering the current search based on an additional query string,
+'\\[notmuch-search-filter-by-tag]' for filtering to include only
+messages with a given tag, and '\\[notmuch-search]' to execute a
+new, global search.
+
+Complete list of currently available key bindings:
+
+\\{notmuch-search-mode-map}"
+  (setq notmuch-buffer-refresh-function #'notmuch-search-refresh-view)
+  (setq-local scroll-preserve-screen-position t)
+  (add-to-invisibility-spec (cons 'ellipsis t))
+  (setq truncate-lines t)
+  (setq buffer-read-only t)
+  (setq imenu-prev-index-position-function
+       #'notmuch-search-imenu-prev-index-position-function)
+  (setq imenu-extract-index-name-function
+       #'notmuch-search-imenu-extract-index-name-function))
+
+;;; Search Results
+
+(defun notmuch-search-get-result (&optional pos)
+  "Return the result object for the thread at POS (or point).
+
+If there is no thread at POS (or point), returns nil."
+  (get-text-property (or pos (point)) 'notmuch-search-result))
+
+(defun notmuch-search-result-beginning (&optional pos)
+  "Return the point at the beginning of the thread at POS (or point).
+
+If there is no thread at POS (or point), returns nil."
+  (and (notmuch-search-get-result pos)
+       ;; We pass 1+point because previous-single-property-change starts
+       ;; searching one before the position we give it.
+       (previous-single-property-change (1+ (or pos (point)))
+                                       'notmuch-search-result nil
+                                       (point-min))))
+
+(defun notmuch-search-result-end (&optional pos)
+  "Return the point at the end of the thread at POS (or point).
+
+The returned point will be just after the newline character that
+ends the result line.  If there is no thread at POS (or point),
+returns nil."
+  (and (notmuch-search-get-result pos)
+       (next-single-property-change (or pos (point))
+                                   'notmuch-search-result nil
+                                   (point-max))))
+
+(defun notmuch-search-foreach-result (beg end fn)
+  "Invoke FN for each result between BEG and END.
+
+FN should take one argument.  It will be applied to the character
+position of the beginning of each result that overlaps the region
+between points BEG and END.  As a special case, if (= BEG END),
+FN will be applied to the result containing point BEG."
+  (let ((pos (notmuch-search-result-beginning beg))
+       ;; End must be a marker in case fn changes the
+       ;; text.
+       (end (copy-marker end))
+       ;; Make sure we examine at least one result, even if
+       ;; (= beg end).
+       (first t))
+    ;; We have to be careful if the region extends beyond the results.
+    ;; In this case, pos could be null or there could be no result at
+    ;; pos.
+    (while (and pos (or (< pos end) first))
+      (when (notmuch-search-get-result pos)
+       (funcall fn pos))
+      (setq pos (notmuch-search-result-end pos))
+      (setq first nil))))
+;; Unindent the function argument of notmuch-search-foreach-result so
+;; the indentation of callers doesn't get out of hand.
+(put 'notmuch-search-foreach-result 'lisp-indent-function 2)
+
+(defun notmuch-search-properties-in-region (property beg end)
+  (let (output)
+    (notmuch-search-foreach-result beg end
+      (lambda (pos)
+       (push (plist-get (notmuch-search-get-result pos) property) output)))
+    output))
+
+(defun notmuch-search-find-thread-id (&optional bare)
+  "Return the thread for the current thread.
+
+If BARE is set then do not prefix with \"thread:\"."
+  (let ((thread (plist-get (notmuch-search-get-result) :thread)))
+    (when thread
+      (concat (and (not bare) "thread:") thread))))
+
+(defun notmuch-search-find-stable-query ()
+  "Return the stable queries for the current thread.
+
+Return a list (MATCHED-QUERY UNMATCHED-QUERY) for the
+matched and unmatched messages in the current thread."
+  (plist-get (notmuch-search-get-result) :query))
+
+(defun notmuch-search-find-stable-query-region (beg end &optional only-matched)
+  "Return the stable query for the current region.
+
+If ONLY-MATCHED is non-nil, include only matched messages.  If it
+is nil, include both matched and unmatched messages. If there are
+no messages in the region then return nil."
+  (let ((query-list nil) (all (not only-matched)))
+    (dolist (queries (notmuch-search-properties-in-region :query beg end))
+      (when (car queries)
+       (push (car queries) query-list))
+      (when (and all (cadr queries))
+       (push (cadr queries) query-list)))
+    (and query-list
+        (concat "(" (mapconcat 'identity query-list ") or (") ")"))))
+
+(defun notmuch-search-find-authors ()
+  "Return the authors for the current thread."
+  (plist-get (notmuch-search-get-result) :authors))
+
+(defun notmuch-search-find-authors-region (beg end)
+  "Return a list of authors for the current region."
+  (notmuch-search-properties-in-region :authors beg end))
+
+(defun notmuch-search-find-subject ()
+  "Return the subject for the current thread."
+  (plist-get (notmuch-search-get-result) :subject))
+
+(defun notmuch-search-find-subject-region (beg end)
+  "Return a list of authors for the current region."
+  (notmuch-search-properties-in-region :subject beg end))
+
+(defun notmuch-search-show-thread (&optional elide-toggle)
+  "Display the currently selected thread.
+
+With a prefix argument, invert the default value of
+`notmuch-show-only-matching-messages' when displaying the
+thread.
+
+Return non-nil on success."
+  (interactive "P")
+  (let ((thread-id (notmuch-search-find-thread-id)))
+    (if thread-id
+       (notmuch-show thread-id
+                     elide-toggle
+                     (current-buffer)
+                     notmuch-search-query-string
+                     ;; Name the buffer based on the subject.
+                     (format "*%s*" (truncate-string-to-width
+                                     (notmuch-search-find-subject)
+                                     30 nil nil t)))
+      (message "End of search results.")
+      nil)))
+
+(defun notmuch-tree-from-search-current-query ()
+  "Tree view of current query."
+  (interactive)
+  (notmuch-tree notmuch-search-query-string))
+
+(defun notmuch-unthreaded-from-search-current-query ()
+  "Unthreaded view of current query."
+  (interactive)
+  (notmuch-unthreaded notmuch-search-query-string))
+
+(defun notmuch-tree-from-search-thread ()
+  "Show the selected thread with notmuch-tree."
+  (interactive)
+  (notmuch-tree (notmuch-search-find-thread-id)
+               notmuch-search-query-string
+               nil
+               (notmuch-prettify-subject (notmuch-search-find-subject))
+               t nil (current-buffer)))
+
+(defun notmuch-search-reply-to-thread (&optional prompt-for-sender)
+  "Begin composing a reply-all to the entire current thread in a new buffer."
+  (interactive "P")
+  (notmuch-mua-new-reply (notmuch-search-find-thread-id)
+                        prompt-for-sender t))
+
+(defun notmuch-search-reply-to-thread-sender (&optional prompt-for-sender)
+  "Begin composing a reply to the entire current thread in a new buffer."
+  (interactive "P")
+  (notmuch-mua-new-reply (notmuch-search-find-thread-id)
+                        prompt-for-sender nil))
+
+;;; Tags
+
+(defun notmuch-search-set-tags (tags &optional pos)
+  (notmuch-search-update-result
+   (plist-put (notmuch-search-get-result pos) :tags tags)
+   pos))
+
+(defun notmuch-search-get-tags (&optional pos)
+  (plist-get (notmuch-search-get-result pos) :tags))
+
+(defun notmuch-search-get-tags-region (beg end)
+  (let (output)
+    (notmuch-search-foreach-result beg end
+      (lambda (pos)
+       (setq output (append output (notmuch-search-get-tags pos)))))
+    (delete-dups output)))
+
+(defun notmuch-search-interactive-tag-changes (&optional initial-input)
+  "Prompt for tag changes for the current thread or region.
+
+Return (TAG-CHANGES REGION-BEGIN REGION-END)."
+  (pcase-let ((`(,beg ,end) (notmuch-interactive-region)))
+    (list (notmuch-read-tag-changes (notmuch-search-get-tags-region beg end)
+                                   (if (= beg end) "Tag thread" "Tag region")
+                                   initial-input)
+         beg end)))
+
+(defun notmuch-search-tag (tag-changes &optional beg end only-matched)
+  "Change tags for the currently selected thread or region.
+
+See `notmuch-tag' for information on the format of TAG-CHANGES.
+When called interactively, this uses the region if the region is
+active.  When called directly, BEG and END provide the region.
+If these are nil or not provided, then, if the region is active
+this applied to all threads meeting the region, and if the region
+is inactive this applies to the thread at point.
+
+If ONLY-MATCHED is non-nil, only tag matched messages."
+  (interactive (notmuch-search-interactive-tag-changes))
+  (unless (and beg end)
+    (setq beg (car (notmuch-interactive-region)))
+    (setq end (cadr (notmuch-interactive-region))))
+  (let ((search-string (notmuch-search-find-stable-query-region
+                       beg end only-matched)))
+    (notmuch-tag search-string tag-changes)
+    (notmuch-search-foreach-result beg end
+      (lambda (pos)
+       (notmuch-search-set-tags
+        (notmuch-update-tags (notmuch-search-get-tags pos) tag-changes)
+        pos)))))
+
+(defun notmuch-search-add-tag (tag-changes &optional beg end)
+  "Change tags for the current thread or region (defaulting to add).
+
+Same as `notmuch-search-tag' but sets initial input to '+'."
+  (interactive (notmuch-search-interactive-tag-changes "+"))
+  (notmuch-search-tag tag-changes beg end))
+
+(defun notmuch-search-remove-tag (tag-changes &optional beg end)
+  "Change tags for the current thread or region (defaulting to remove).
+
+Same as `notmuch-search-tag' but sets initial input to '-'."
+  (interactive (notmuch-search-interactive-tag-changes "-"))
+  (notmuch-search-tag tag-changes beg end))
+
+(put 'notmuch-search-archive-thread 'notmuch-prefix-doc
+     "Un-archive the currently selected thread.")
+(defun notmuch-search-archive-thread (&optional unarchive beg end)
+  "Archive the currently selected thread or region.
+
+Archive each message in the currently selected thread by applying
+the tag changes in `notmuch-archive-tags' to each (remove the
+\"inbox\" tag by default). If a prefix argument is given, the
+messages will be \"unarchived\" (i.e. the tag changes in
+`notmuch-archive-tags' will be reversed).
+
+This function advances the next thread when finished."
+  (interactive (cons current-prefix-arg (notmuch-interactive-region)))
+  (when notmuch-archive-tags
+    (notmuch-search-tag
+     (notmuch-tag-change-list notmuch-archive-tags unarchive) beg end))
+  (when (eq beg end)
+    (notmuch-search-next-thread)))
+
+;;; Search Results
+
+(defun notmuch-search-update-result (result &optional pos)
+  "Replace the result object of the thread at POS (or point) by
+RESULT and redraw it.
+
+This will keep point in a reasonable location.  However, if there
+are enclosing save-excursions and the saved point is in the
+result being updated, the point will be restored to the beginning
+of the result."
+  (let ((start (notmuch-search-result-beginning pos))
+       (end (notmuch-search-result-end pos))
+       (init-point (point))
+       (inhibit-read-only t))
+    ;; Delete the current thread
+    (delete-region start end)
+    ;; Insert the updated thread
+    (notmuch-search-show-result result start)
+    ;; If point was inside the old result, make an educated guess
+    ;; about where to place it now.  Unfortunately, this won't work
+    ;; with save-excursion (or any other markers that would be nice to
+    ;; preserve, such as the window start), but there's nothing we can
+    ;; do about that without a way to retrieve markers in a region.
+    (when (and (>= init-point start) (<= init-point end))
+      (let* ((new-end (notmuch-search-result-end start))
+            (new-point (if (= init-point end)
+                           new-end
+                         (min init-point (- new-end 1)))))
+       (goto-char new-point)))))
+
+(defun notmuch-search-process-sentinel (proc _msg)
+  "Add a message to let user know when \"notmuch search\" exits."
+  (let ((buffer (process-buffer proc))
+       (status (process-status proc))
+       (exit-status (process-exit-status proc))
+       (never-found-target-thread nil))
+    (when (memq status '(exit signal))
+      (catch 'return
+       (kill-buffer (process-get proc 'parse-buf))
+       (when (buffer-live-p buffer)
+         (with-current-buffer buffer
+           (save-excursion
+             (let ((inhibit-read-only t)
+                   (atbob (bobp)))
+               (goto-char (point-max))
+               (when (eq status 'signal)
+                 (insert "Incomplete search results (search process was killed).\n"))
+               (when (eq status 'exit)
+                 (insert "End of search results.\n")
+                 ;; For version mismatch, there's no point in
+                 ;; showing the search buffer
+                 (when (or (= exit-status 20) (= exit-status 21))
+                   (kill-buffer)
+                   (throw 'return nil))
+                 (when (and atbob
+                            (not (string= notmuch-search-target-thread "found")))
+                   (setq never-found-target-thread t)))))
+           (when (and never-found-target-thread
+                      notmuch-search-target-line)
+             (goto-char (point-min))
+             (forward-line (1- notmuch-search-target-line)))))))))
+
+(define-widget 'notmuch--custom-face-edit 'lazy
+  "Custom face edit with a tag Edit Face"
+  ;; I could not persuage custom-face-edit to respect the :tag
+  ;; property so create a widget specially
+  :tag "Manually specify face"
+  :type 'custom-face-edit)
+
+(defcustom notmuch-search-line-faces
+  '(("unread" . notmuch-search-unread-face)
+    ("flagged" . notmuch-search-flagged-face))
+  "Alist of tags to faces for line highlighting in notmuch-search.
+Each element looks like (TAG . FACE).
+A thread with TAG will have FACE applied.
+
+Here is an example of how to color search results based on tags.
+ (the following text would be placed in your ~/.emacs file):
+
+ (setq notmuch-search-line-faces \\='((\"unread\" . (:foreground \"green\"))
+                                  (\"deleted\" . (:foreground \"red\"
+                                                 :background \"blue\"))))
+
+The FACE must be a face name (a symbol or string), a property
+list of face attributes, or a list of these.  The faces for
+matching tags are merged, with earlier attributes overriding
+later. A message having both \"deleted\" and \"unread\" tags with
+the above settings would have a green foreground and blue
+background."
+  :type '(alist :key-type (string)
+               :value-type (radio (face :tag "Face name")
+                                  (notmuch--custom-face-edit)))
+  :group 'notmuch-search
+  :group 'notmuch-faces)
+
+(defun notmuch-search-color-line (start end line-tag-list)
+  "Colorize lines in `notmuch-show' based on tags."
+  ;; Reverse the list so earlier entries take precedence
+  (dolist (elem (reverse notmuch-search-line-faces))
+    (let ((tag (car elem))
+         (face (cdr elem)))
+      (when (member tag line-tag-list)
+       (notmuch-apply-face nil face nil start end)))))
+
+(defun notmuch-search-author-propertize (authors)
+  "Split `authors' into matching and non-matching authors and
+propertize appropriately. If no boundary between authors and
+non-authors is found, assume that all of the authors match."
+  (if (string-match "\\(.*\\)|\\(.*\\)" authors)
+      (concat (propertize (concat (match-string 1 authors) ",")
+                         'face 'notmuch-search-matching-authors)
+             (propertize (match-string 2 authors)
+                         'face 'notmuch-search-non-matching-authors))
+    (propertize authors 'face 'notmuch-search-matching-authors)))
+
+(defun notmuch-search-insert-authors (format-string authors)
+  ;; Save the match data to avoid interfering with
+  ;; `notmuch-search-process-filter'.
+  (save-match-data
+    (let* ((formatted-authors (format format-string authors))
+          (formatted-sample (format format-string ""))
+          (visible-string formatted-authors)
+          (invisible-string "")
+          (padding ""))
+      ;; Truncate the author string to fit the specification.
+      (when (> (length formatted-authors)
+              (length formatted-sample))
+       (let ((visible-length (- (length formatted-sample)
+                                (length "... "))))
+         ;; Truncate the visible string according to the width of
+         ;; the display string.
+         (setq visible-string (substring formatted-authors 0 visible-length))
+         (setq invisible-string (substring formatted-authors visible-length))
+         ;; If possible, truncate the visible string at a natural
+         ;; break (comma or pipe), as incremental search doesn't
+         ;; match across the visible/invisible border.
+         (when (string-match "\\(.*\\)\\([,|] \\)\\([^,|]*\\)" visible-string)
+           ;; Second clause is destructive on `visible-string', so
+           ;; order is important.
+           (setq invisible-string (concat (match-string 3 visible-string)
+                                          invisible-string))
+           (setq visible-string (concat (match-string 1 visible-string)
+                                        (match-string 2 visible-string))))
+         ;; `visible-string' may be shorter than the space allowed
+         ;; by `format-string'. If so we must insert some padding
+         ;; after `invisible-string'.
+         (setq padding (make-string (- (length formatted-sample)
+                                       (length visible-string)
+                                       (length "..."))
+                                    ? ))))
+      ;; Use different faces to show matching and non-matching authors.
+      (if (string-match "\\(.*\\)|\\(.*\\)" visible-string)
+         ;; The visible string contains both matching and
+         ;; non-matching authors.
+         (progn
+           (setq visible-string (notmuch-search-author-propertize visible-string))
+           ;; The invisible string must contain only non-matching
+           ;; authors, as the visible-string contains both.
+           (setq invisible-string (propertize invisible-string
+                                              'face 'notmuch-search-non-matching-authors)))
+       ;; The visible string contains only matching authors.
+       (setq visible-string (propertize visible-string
+                                        'face 'notmuch-search-matching-authors))
+       ;; The invisible string may contain both matching and
+       ;; non-matching authors.
+       (setq invisible-string (notmuch-search-author-propertize invisible-string)))
+      ;; If there is any invisible text, add it as a tooltip to the
+      ;; visible text.
+      (unless (string-empty-p invisible-string)
+       (setq visible-string
+             (propertize visible-string
+                         'help-echo (concat "..." invisible-string))))
+      ;; Insert the visible and, if present, invisible author strings.
+      (insert visible-string)
+      (unless (string-empty-p invisible-string)
+       (let ((start (point))
+             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)
+  (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."
+  ;; Ignore excluded matches
+  (unless (= (plist-get result :matched) 0)
+    (save-excursion
+      (goto-char pos)
+      (dolist (spec notmuch-search-result-format)
+       (notmuch-search-insert-field (car spec) (cdr spec) result))
+      (insert "\n")
+      (notmuch-search-color-line pos (point) (plist-get result :tags))
+      (put-text-property pos (point) 'notmuch-search-result result))))
+
+(defun notmuch-search-append-result (result)
+  "Insert RESULT at the end of the buffer.
+
+This is only called when a result is first inserted so it also
+sets the :orig-tag property."
+  (let ((new-result (plist-put result :orig-tags (plist-get result :tags)))
+       (pos (point-max)))
+    (notmuch-search-show-result new-result pos)
+    (when (string= (plist-get result :thread) notmuch-search-target-thread)
+      (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))
+       (parse-buf (process-get proc 'parse-buf))
+       (inhibit-read-only t))
+    (when (buffer-live-p results-buf)
+      (with-current-buffer parse-buf
+       ;; Insert new data
+       (save-excursion
+         (goto-char (point-max))
+         (insert string))
+       (notmuch-sexp-parse-partial-list 'notmuch-search-append-result
+                                        results-buf))
+      (with-current-buffer results-buf
+       (notmuch--search-hook-wrapper)))))
+
+;;; Commands (and some helper functions used by them)
+
+(defun notmuch-search-tag-all (tag-changes)
+  "Add/remove tags from all messages in current search buffer.
+
+See `notmuch-tag' for information on the format of TAG-CHANGES."
+  (interactive
+   (list (notmuch-read-tag-changes
+         (notmuch-search-get-tags-region (point-min) (point-max)) "Tag all")))
+  (notmuch-search-tag tag-changes (point-min) (point-max) t))
+
+(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
+               (longest-length 0))
+           (cl-loop for tuple in notmuch-saved-searches
+                    if (let ((quoted-query
+                              (regexp-quote
+                               (notmuch-saved-search-get tuple :query))))
+                         (and (string-match (concat "^" quoted-query) query)
+                              (> (length (match-string 0 query))
+                                 longest-length)))
+                    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)
+          (notmuch-search-format-buffer-name saved-search-name
+                                             saved-search-type
+                                             t))
+         (saved-search
+          (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.
+
+PROMPT is the string to prompt with."
+  (let* ((all-tags
+         (mapcar (lambda (tag) (notmuch-escape-boolean-term tag))
+                 (notmuch--process-lines notmuch-command "search" "--output=tags" "*")))
+        (completions
+         (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:"
+                       "subject:" "attachment:")
+                 (mapcar (lambda (tag) (concat "tag:" tag)) all-tags)
+                 (mapcar (lambda (tag) (concat "is:" tag)) all-tags)
+                 (mapcar (lambda (mimetype) (concat "mimetype:" mimetype))
+                         (mailcap-mime-types))))
+        (keymap (copy-keymap minibuffer-local-map))
+        (current-query (cl-case major-mode
+                         (notmuch-search-mode (notmuch-search-get-query))
+                         (notmuch-show-mode (notmuch-show-get-query))
+                         (notmuch-tree-mode (notmuch-tree-get-query))))
+        (minibuffer-completion-table
+         (completion-table-dynamic
+          (lambda (string)
+            ;; Generate a list of possible completions for the current input.
+            (cond
+             ;; This ugly regexp is used to get the last word of the input
+             ;; possibly preceded by a '('.
+             ((string-match "\\(^\\|.* (?\\)\\([^ ]*\\)$" string)
+              (mapcar (lambda (compl)
+                        (concat (match-string-no-properties 1 string) compl))
+                      (all-completions (match-string-no-properties 2 string)
+                                       completions)))
+             (t (list string)))))))
+    ;; This was simpler than convincing completing-read to accept spaces:
+    (define-key keymap (kbd "TAB") 'minibuffer-complete)
+    (let ((history-delete-duplicates t))
+      (read-from-minibuffer prompt nil keymap nil
+                           'notmuch-search-history current-query nil))))
+
+(defun notmuch-search-get-query ()
+  "Return the current query in this search buffer."
+  notmuch-search-query-string)
+
+(put 'notmuch-search 'notmuch-doc "Search for messages.")
+;;;###autoload
+(defun notmuch-search (&optional query oldest-first 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
+  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
+               appear in the search results.
+  NO-DISPLAY: Do not try to foreground the search results buffer. If it is
+              already foregrounded i.e. displayed in a window, this has no
+              effect, meaning the buffer will remain visible.
+
+When called interactively, this will prompt for a query and use
+the configured default sort order."
+  (interactive
+   (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)))
+
+  (let* ((query (or query (notmuch-read-query "Notmuch search: ")))
+        (buffer (get-buffer-create (notmuch-search-buffer-title query))))
+    (if no-display
+       (set-buffer buffer)
+      (pop-to-buffer-same-window buffer))
+    (notmuch-search-mode)
+    ;; Don't track undo information for this buffer
+    (setq buffer-undo-list t)
+    (setq notmuch-search-query-string query)
+    (setq notmuch-search-oldest-first oldest-first)
+    (setq notmuch-search-target-thread target-thread)
+    (setq notmuch-search-target-line target-line)
+    (notmuch-tag-clear-cache)
+    (when (get-buffer-process buffer)
+      (error "notmuch search process already running for query `%s'" query))
+    (let ((inhibit-read-only t))
+      (erase-buffer)
+      (goto-char (point-min))
+      (save-excursion
+       (let ((proc (notmuch-start-notmuch
+                    "notmuch-search" buffer #'notmuch-search-process-sentinel
+                    "search" "--format=sexp" "--format-version=5"
+                    (if oldest-first
+                        "--sort=oldest-first"
+                      "--sort=newest-first")
+                    query)))
+         ;; Use a scratch buffer to accumulate partial output.
+         ;; This buffer will be killed by the sentinel, which
+         ;; should be called no matter how the process dies.
+         (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))))))
+
+(defun notmuch-search-refresh-view ()
+  "Refresh the current view.
+
+Erases the current buffer and runs a new search with the same
+query string as the current search. If the current thread is in
+the new search results, then point will be placed on the same
+thread. Otherwise, point will be moved to attempt to be in the
+same relative position within the new buffer."
+  (interactive)
+  (notmuch-search notmuch-search-query-string
+                 notmuch-search-oldest-first
+                 (notmuch-search-find-thread-id 'bare)
+                 (line-number-at-pos)
+                 t)
+  (goto-char (point-min)))
+
+(defun notmuch-search-toggle-order ()
+  "Toggle the current search order.
+
+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-search-refresh-view))
+
+(defun notmuch-group-disjunctive-query-string (query-string)
+  "Group query if it contains a complex expression.
+Enclose QUERY-STRING in parentheses if contains \"OR\" operators."
+  (if (string-match-p "\\<[oO][rR]\\>" query-string)
+      (concat "( " query-string " )")
+    query-string))
+
+(defun notmuch-search-filter (query)
+  "Filter or LIMIT the current search results based on an additional query string.
+
+Runs a new 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 ((grouped-query (notmuch-group-disjunctive-query-string query))
+       (grouped-original-query (notmuch-group-disjunctive-query-string
+                                notmuch-search-query-string)))
+    (notmuch-search (if (string= grouped-original-query "*")
+                       grouped-query
+                     (concat grouped-original-query " and " grouped-query))
+                   notmuch-search-oldest-first)))
+
+(defun notmuch-search-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-search-query-string)))
+  (notmuch-search (concat notmuch-search-query-string " and tag:" tag)
+                 notmuch-search-oldest-first))
+
+(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)))
+
+(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 ()
+  "Run notmuch and display saved searches, known tags, etc."
+  (interactive)
+  (notmuch-hello))
+
+(defun notmuch-interesting-buffer (b)
+  "Whether the current buffer's major-mode is a notmuch mode."
+  (with-current-buffer b
+    (memq major-mode '(notmuch-show-mode
+                      notmuch-search-mode
+                      notmuch-tree-mode
+                      notmuch-hello-mode
+                      notmuch-message-mode))))
+
+;;;###autoload
+(defun notmuch-cycle-notmuch-buffers ()
+  "Cycle through any existing notmuch buffers (search, show or hello).
+
+If the current buffer is the only notmuch buffer, bury it.
+If no notmuch buffers exist, run `notmuch'."
+  (interactive)
+  (let (start first)
+    ;; If the current buffer is a notmuch buffer, remember it and then
+    ;; bury it.
+    (when (notmuch-interesting-buffer (current-buffer))
+      (setq start (current-buffer))
+      (bury-buffer))
+
+    ;; Find the first notmuch buffer.
+    (setq first (cl-loop for buffer in (buffer-list)
+                        if (notmuch-interesting-buffer buffer)
+                        return buffer))
+
+    (if first
+       ;; If the first one we found is any other than the starting
+       ;; buffer, switch to it.
+       (unless (eq first start)
+         (pop-to-buffer-same-window first))
+      (notmuch))))
+
+;;; Integrations
+;;;; Hl-line Support
+
+(defun notmuch-hl-line-mode ()
+  (prog1 (hl-line-mode)
+    (when hl-line-overlay
+      (overlay-put hl-line-overlay 'priority 1))))
+
+;;;; Imenu Support
+
+(defun notmuch-search-imenu-prev-index-position-function ()
+  "Move point to previous message in notmuch-search buffer.
+Used as`imenu-prev-index-position-function' in notmuch buffers."
+  (notmuch-search-previous-thread))
+
+(defun notmuch-search-imenu-extract-index-name-function ()
+  "Return imenu name for line at point.
+Used as `imenu-extract-index-name-function' in notmuch buffers.
+Point should be at the beginning of the line."
+  (format "%s (%s)"
+         (notmuch-search-find-subject)
+         (notmuch-search-find-authors)))
+
+;;; _
+
+(provide 'notmuch)
+
+;; After provide to avoid loops if notmuch was require'd via notmuch-init-file.
+(when init-file-user ; don't load init file if the -q option was used.
+  (load notmuch-init-file t t nil t))
+
+;;; notmuch.el ends here
diff --git a/emacs/rstdoc.el b/emacs/rstdoc.el
new file mode 100644 (file)
index 0000000..5b8a9d0
--- /dev/null
@@ -0,0 +1,90 @@
+;;; rstdoc.el --- help generate documentation from docstrings  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2018 David Bremner
+
+;; Author: David Bremner <david@tethera.net>
+;; Created: 26 May 2018
+;; Keywords: emacs lisp, documentation
+;; Homepage: https://notmuchmail.org
+
+;; This file is not part of GNU Emacs.
+
+;; rstdoc.el 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.
+;;
+;; rstdoc.el 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 rstdoc.el.  If not, see <https://www.gnu.org/licenses/>.
+;;
+
+;;; Commentary:
+
+;; Rstdoc provides a facility to extract all of the docstrings defined in
+;; an elisp source file. Usage:
+;;
+;; emacs -Q --batch -L . -l rstdoc -f rstdoc-batch-extract foo.el foo.rsti
+
+;;; Code:
+
+(defun rstdoc-batch-extract ()
+  "Extract docstrings to and from the files on the command line."
+  (apply #'rstdoc-extract command-line-args-left))
+
+(defun rstdoc-extract (in-file out-file)
+  "Write docstrings from IN-FILE to OUT-FILE."
+  (load-file in-file)
+  (let* ((definitions (cdr (assoc (expand-file-name in-file) load-history)))
+        (text-quoting-style 'grave)
+        (doc-hash (make-hash-table :test 'eq)))
+    (mapc
+     (lambda (elt)
+       (let ((pair
+             (pcase elt
+               (`(defun . ,name) (cons name (documentation name)))
+               (`(,_ . ,_)  nil)
+               (sym (cons sym (get sym 'variable-documentation))))))
+        (when (and pair (cdr pair))
+          (puthash (car pair) (cdr pair) doc-hash))))
+     definitions)
+    (with-temp-buffer
+      (maphash
+       (lambda (key val)
+        (rstdoc--insert-docstring key val))
+       doc-hash)
+      (write-region (point-min) (point-max) out-file))))
+
+(defun rstdoc--insert-docstring (symbol docstring)
+  (insert (format "\n.. |docstring::%s| replace::\n" symbol))
+  (insert (replace-regexp-in-string "^" "    "
+                                   (rstdoc--rst-quote-string docstring)))
+  (insert "\n"))
+
+(defvar rst--escape-alist
+  '( ("\\\\='" . "\001")
+     ("`\\([^\n`']*\\)[`']" . "\002\\1\002") ;; good enough for now...
+     ("`" . "\\\\`")
+     ("\001" . "'")
+     ("\002" . "`")
+     ("[*]" . "\\\\*")
+     ("^[[:space:]]*$" . "|br|")
+     ("^[[:space:]]" . "|indent| "))
+    "list of (regex . replacement) pairs")
+
+(defun rstdoc--rst-quote-string (str)
+  (with-temp-buffer
+    (insert str)
+    (dolist (pair rst--escape-alist)
+      (goto-char (point-min))
+      (while (re-search-forward (car pair) nil t)
+       (replace-match (cdr pair))))
+    (buffer-substring (point-min) (point-max))))
+
+(provide 'rstdoc)
+
+;;; rstdoc.el ends here
diff --git a/emacs/rstdoc.rsti b/emacs/rstdoc.rsti
new file mode 100644 (file)
index 0000000..a449b58
--- /dev/null
@@ -0,0 +1,21 @@
+.. -*- rst -*-
+
+.. |br| replace:: |br-texinfo| |br-html|
+       
+.. |br-texinfo| raw:: texinfo
+
+   @* @*
+
+.. |br-html| raw:: html
+
+   <br /><br />
+
+.. |indent| replace:: |indent-texinfo| |indent-html|
+
+.. |indent-texinfo| raw:: texinfo
+
+   @* @ @ @ @
+
+.. |indent-html| raw:: html
+
+   <br />&nbsp;&nbsp;&nbsp;&nbsp;
diff --git a/gmime-filter-reply.c b/gmime-filter-reply.c
new file mode 100644 (file)
index 0000000..35349cc
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * Copyright © 2009 Keith Packard <keithp@keithp.com>
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+#include <stdbool.h>
+
+#include "gmime-filter-reply.h"
+#include "notmuch-client.h"
+
+/**
+ * SECTION: gmime-filter-reply
+ * @title: GMimeFilterReply
+ * @short_description: Add/remove reply markers
+ *
+ * A #GMimeFilter for adding or removing reply markers
+ **/
+
+
+static void g_mime_filter_reply_class_init (GMimeFilterReplyClass *klass, void *class_data);
+static void g_mime_filter_reply_init (GMimeFilterReply *filter, GMimeFilterReplyClass *klass);
+static void g_mime_filter_reply_finalize (GObject *object);
+
+static GMimeFilter *filter_copy (GMimeFilter *filter);
+static void filter_filter (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                          char **out, size_t *outlen, size_t *outprespace);
+static void filter_complete (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                            char **out, size_t *outlen, size_t *outprespace);
+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)
+{
+    return type;
+}
+
+
+static void
+g_mime_filter_reply_class_init (GMimeFilterReplyClass *klass, unused (void *class_data))
+{
+    GObjectClass *object_class = G_OBJECT_CLASS (klass);
+    GMimeFilterClass *filter_class = GMIME_FILTER_CLASS (klass);
+
+    object_class->finalize = g_mime_filter_reply_finalize;
+
+    filter_class->copy = filter_copy;
+    filter_class->filter = filter_filter;
+    filter_class->complete = filter_complete;
+    filter_class->reset = filter_reset;
+}
+
+static void
+g_mime_filter_reply_init (GMimeFilterReply *filter, GMimeFilterReplyClass *klass)
+{
+    (void) klass;
+    filter->saw_nl = true;
+    filter->saw_angle = false;
+}
+
+static void
+g_mime_filter_reply_finalize (GObject *object)
+{
+    G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+
+static GMimeFilter *
+filter_copy (GMimeFilter *filter)
+{
+    GMimeFilterReply *reply = (GMimeFilterReply *) filter;
+
+    return g_mime_filter_reply_new (reply->encode);
+}
+
+static void
+filter_filter (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
+              char **outbuf, size_t *outlen, size_t *outprespace)
+{
+    GMimeFilterReply *reply = (GMimeFilterReply *) filter;
+    const char *inptr = inbuf;
+    const char *inend = inbuf + inlen;
+    char *outptr;
+
+    (void) prespace;
+    if (reply->encode) {
+       g_mime_filter_set_size (filter, 3 * inlen, false);
+
+       outptr = filter->outbuf;
+       while (inptr < inend) {
+           if (reply->saw_nl) {
+               *outptr++ = '>';
+               *outptr++ = ' ';
+               reply->saw_nl = false;
+           }
+           if (*inptr == '\n')
+               reply->saw_nl = true;
+           else
+               reply->saw_nl = false;
+           if (*inptr != '\r')
+               *outptr++ = *inptr;
+           inptr++;
+       }
+    } else {
+       g_mime_filter_set_size (filter, inlen + 1, false);
+
+       outptr = filter->outbuf;
+       while (inptr < inend) {
+           if (reply->saw_nl) {
+               if (*inptr == '>')
+                   reply->saw_angle = true;
+               else
+                   *outptr++ = *inptr;
+               reply->saw_nl = false;
+           } else if (reply->saw_angle) {
+               if (*inptr == ' ')
+                   ;
+               else
+                   *outptr++ = *inptr;
+               reply->saw_angle = false;
+           } else if (*inptr != '\r') {
+               if (*inptr == '\n')
+                   reply->saw_nl = true;
+               *outptr++ = *inptr;
+           }
+
+           inptr++;
+       }
+    }
+
+    *outlen = outptr - filter->outbuf;
+    *outprespace = filter->outpre;
+    *outbuf = filter->outbuf;
+}
+
+static void
+filter_complete (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
+                char **outbuf, size_t *outlen, size_t *outprespace)
+{
+    if (inbuf && inlen)
+       filter_filter (filter, inbuf, inlen, prespace, outbuf, outlen, outprespace);
+}
+
+static void
+filter_reset (GMimeFilter *filter)
+{
+    GMimeFilterReply *reply = (GMimeFilterReply *) filter;
+
+    reply->saw_nl = true;
+    reply->saw_angle = false;
+}
+
+
+/**
+ * g_mime_filter_reply_new:
+ * @encode: %true if the filter should encode or %false otherwise
+ * @dots: encode/decode dots (as for SMTP)
+ *
+ * Creates a new #GMimeFilterReply filter.
+ *
+ * If @encode is %true, then all lines will be prefixed by "> ",
+ * otherwise any lines starting with "> " will have that removed
+ *
+ * Returns: a new #GMimeFilterReply filter.
+ **/
+GMimeFilter *
+g_mime_filter_reply_new (gboolean encode)
+{
+    GMimeFilterReply *new_reply;
+
+    new_reply = (GMimeFilterReply *) g_object_new (GMIME_TYPE_FILTER_REPLY, NULL);
+    new_reply->encode = encode;
+
+    return (GMimeFilter *) new_reply;
+}
+
diff --git a/gmime-filter-reply.h b/gmime-filter-reply.h
new file mode 100644 (file)
index 0000000..988fe2d
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright © 2009 Keith Packard <keithp@keithp.com>
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+#ifndef _GMIME_FILTER_REPLY_H_
+#define _GMIME_FILTER_REPLY_H_
+
+#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 ())
+#define GMIME_FILTER_REPLY(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+                                                                       GMIME_TYPE_FILTER_REPLY, \
+                                                                       GMimeFilterReply))
+#define GMIME_FILTER_REPLY_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GMIME_TYPE_FILTER_REPLY, \
+                                                                    GMimeFilterReplyClass))
+#define GMIME_IS_FILTER_REPLY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+                                                                       GMIME_TYPE_FILTER_REPLY))
+#define GMIME_IS_FILTER_REPLY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+                                                                    GMIME_TYPE_FILTER_REPLY))
+#define GMIME_FILTER_REPLY_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GMIME_TYPE_FILTER_REPLY, \
+                                                                      GMimeFilterReplyClass))
+
+typedef struct _GMimeFilterReply GMimeFilterReply;
+typedef struct _GMimeFilterReplyClass GMimeFilterReplyClass;
+
+/**
+ * GMimeFilterReply:
+ * @parent_object: parent #GMimeFilter
+ * @encode: encoding vs decoding reply markers
+ * @saw_nl: previous char was a \n
+ * @saw_angle: previous char was a >
+ *
+ * A filter to insert/remove reply markers (lines beginning with >)
+ **/
+struct _GMimeFilterReply {
+    GMimeFilter parent_object;
+
+    gboolean encode;
+    gboolean saw_nl;
+    gboolean saw_angle;
+};
+
+struct _GMimeFilterReplyClass {
+    GMimeFilterClass parent_class;
+
+};
+
+
+GType g_mime_filter_reply_get_type (void);
+
+GMimeFilter *g_mime_filter_reply_new (gboolean encode);
+
+G_END_DECLS
+
+
+#endif /* _GMIME_FILTER_REPLY_H_ */
diff --git a/hooks.c b/hooks.c
new file mode 100644 (file)
index 0000000..0cf72e7
--- /dev/null
+++ b/hooks.c
@@ -0,0 +1,104 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2011 Jani Nikula
+ *
+ * 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: Jani Nikula <jani@nikula.org>
+ */
+
+#include "notmuch-client.h"
+#include <sys/wait.h>
+
+int
+notmuch_run_hook (notmuch_database_t *notmuch, const char *hook)
+{
+    char *hook_path;
+    const char *config_path;
+    int status = 0;
+    pid_t pid;
+
+    hook_path = talloc_asprintf (notmuch, "%s/%s",
+                                notmuch_config_get (notmuch, NOTMUCH_CONFIG_HOOK_DIR),
+                                hook);
+    if (hook_path == NULL) {
+       fprintf (stderr, "Out of memory\n");
+       return 1;
+    }
+
+    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
+        * notmuch dir. Dangling symbolic links also result in ENOENT, but
+        * we'll ignore that too for simplicity. */
+       if (errno != ENOENT) {
+           fprintf (stderr, "Error: %s hook access failed: %s\n", hook,
+                    strerror (errno));
+           status = 1;
+       }
+       goto DONE;
+    }
+
+    /* Flush any buffered output before forking. */
+    fflush (stdout);
+
+    pid = fork ();
+    if (pid == -1) {
+       fprintf (stderr, "Error: %s hook fork failed: %s\n", hook,
+                strerror (errno));
+       status = 1;
+       goto DONE;
+    } else if (pid == 0) {
+       execl (hook_path, hook_path, NULL);
+       /* Same as above for ENOENT, but unlikely now. Indicate all other errors
+        * to parent through non-zero exit status. */
+       if (errno != ENOENT) {
+           fprintf (stderr, "Error: %s hook execution failed: %s\n", hook,
+                    strerror (errno));
+           status = 1;
+       }
+       exit (status);
+    }
+
+    if (waitpid (pid, &status, 0) == -1) {
+       fprintf (stderr, "Error: %s hook wait failed: %s\n", hook,
+                strerror (errno));
+       status = 1;
+       goto DONE;
+    }
+
+    if (! WIFEXITED (status) || WEXITSTATUS (status)) {
+       if (WIFEXITED (status)) {
+           fprintf (stderr, "Error: %s hook failed with status %d\n",
+                    hook, WEXITSTATUS (status));
+       } else if (WIFSIGNALED (status)) {
+           fprintf (stderr, "Error: %s hook terminated with signal %d\n",
+                    hook, WTERMSIG (status));
+       }
+       status = 1;
+    }
+
+  DONE:
+    talloc_free (hook_path);
+
+    return status;
+}
diff --git a/lib/Makefile b/lib/Makefile
new file mode 100644 (file)
index 0000000..de492a7
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/lib/Makefile.local b/lib/Makefile.local
new file mode 100644 (file)
index 0000000..4e76630
--- /dev/null
@@ -0,0 +1,99 @@
+# -*- makefile-gmake -*-
+
+dir := lib
+
+# The (often-reused) $dir works fine within targets/prerequisites,
+# but cannot be used reliably within commands, so copy its value to a
+# variable that is not reused.
+lib := $(dir)
+
+ifeq ($(PLATFORM),MACOSX)
+LIBRARY_SUFFIX = dylib
+# On OS X, library version numbers go before suffix.
+LINKER_NAME = libnotmuch.$(LIBRARY_SUFFIX)
+SONAME = libnotmuch.$(LIBNOTMUCH_VERSION_MAJOR).$(LIBRARY_SUFFIX)
+LIBNAME = libnotmuch.$(LIBNOTMUCH_VERSION_MAJOR).$(LIBNOTMUCH_VERSION_MINOR).$(LIBNOTMUCH_VERSION_RELEASE).$(LIBRARY_SUFFIX)
+LIBRARY_LINK_FLAG = -dynamiclib -install_name $(libdir)/$(SONAME) -compatibility_version $(LIBNOTMUCH_VERSION_MAJOR).$(LIBNOTMUCH_VERSION_MINOR) -current_version $(LIBNOTMUCH_VERSION_MAJOR).$(LIBNOTMUCH_VERSION_MINOR).$(LIBNOTMUCH_VERSION_RELEASE)
+else
+LIBRARY_SUFFIX = so
+LINKER_NAME = libnotmuch.$(LIBRARY_SUFFIX)
+SONAME = $(LINKER_NAME).$(LIBNOTMUCH_VERSION_MAJOR)
+LIBNAME = $(SONAME).$(LIBNOTMUCH_VERSION_MINOR).$(LIBNOTMUCH_VERSION_RELEASE)
+LIBRARY_LINK_FLAG = -shared -Wl,--version-script=$(srcdir)/$(lib)/notmuch.sym,-soname=$(SONAME) $(NO_UNDEFINED_LDFLAGS)
+ifeq ($(PLATFORM),OPENBSD)
+LIBRARY_LINK_FLAG += -lc
+endif
+ifeq ($(LIBDIR_IN_LDCONFIG),1)
+ifeq ($(DESTDIR),)
+LIBRARY_INSTALL_POST_COMMAND=ldconfig
+endif
+endif
+endif
+
+extra_cflags += -I$(srcdir)/$(dir) -fPIC -fvisibility=hidden
+extra_cxxflags += -fvisibility-inlines-hidden
+
+libnotmuch_c_srcs =            \
+       $(notmuch_compat_srcs)  \
+       $(dir)/filenames.c      \
+       $(dir)/string-list.c    \
+       $(dir)/message-file.c   \
+       $(dir)/message-id.c     \
+       $(dir)/messages.c       \
+       $(dir)/sha1.c           \
+       $(dir)/built-with.c     \
+       $(dir)/string-map.c     \
+       $(dir)/indexopts.c      \
+       $(dir)/tags.c
+
+libnotmuch_cxx_srcs =          \
+       $(dir)/database.cc      \
+       $(dir)/parse-time-vrp.cc        \
+       $(dir)/directory.cc     \
+       $(dir)/index.cc         \
+       $(dir)/message.cc       \
+       $(dir)/add-message.cc   \
+       $(dir)/message-property.cc \
+       $(dir)/query.cc         \
+       $(dir)/query-fp.cc      \
+       $(dir)/config.cc        \
+       $(dir)/regexp-fields.cc \
+       $(dir)/thread.cc \
+       $(dir)/thread-fp.cc     \
+       $(dir)/features.cc      \
+       $(dir)/prefix.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)
+
+$(dir)/libnotmuch.a: $(libnotmuch_modules)
+       $(call quiet,AR) rcs $@ $^
+
+$(dir)/$(LIBNAME): $(libnotmuch_modules) util/libnotmuch_util.a parse-time-string/libparse-time-string.a
+       $(call quiet,CXX $(CXXFLAGS)) $(libnotmuch_modules) $(FINAL_LIBNOTMUCH_LDFLAGS) $(LIBRARY_LINK_FLAG) -o $@ util/libnotmuch_util.a parse-time-string/libparse-time-string.a
+
+$(dir)/$(SONAME): $(dir)/$(LIBNAME)
+       ln -sf $(LIBNAME) $@
+
+$(dir)/$(LINKER_NAME): $(dir)/$(SONAME)
+       ln -sf $(LIBNAME) $@
+
+install: install-$(dir)
+
+install-$(dir): $(dir)/$(LIBNAME)
+       mkdir -p "$(DESTDIR)$(libdir)/"
+       install -m0644 "$(lib)/$(LIBNAME)" "$(DESTDIR)$(libdir)/"
+       ln -sf $(LIBNAME) "$(DESTDIR)$(libdir)/$(SONAME)"
+       ln -sf $(LIBNAME) "$(DESTDIR)$(libdir)/$(LINKER_NAME)"
+       mkdir -p "$(DESTDIR)$(includedir)"
+       install -m0644 "$(srcdir)/$(lib)/notmuch.h" "$(DESTDIR)$(includedir)/"
+       $(LIBRARY_INSTALL_POST_COMMAND)
+
+SRCS  := $(SRCS) $(libnotmuch_c_srcs) $(libnotmuch_cxx_srcs)
+CLEAN += $(libnotmuch_modules) $(dir)/$(SONAME) $(dir)/$(LINKER_NAME)
+CLEAN += $(dir)/$(LIBNAME) $(dir)/libnotmuch.a
+CLEAN += $(dir)/notmuch.h.gch
diff --git a/lib/add-message.cc b/lib/add-message.cc
new file mode 100644 (file)
index 0000000..b16748f
--- /dev/null
@@ -0,0 +1,605 @@
+#include "database-private.h"
+
+/* Parse a References header value, putting a (talloc'ed under 'ctx')
+ * copy of each referenced message-id into 'hash'.
+ *
+ * We explicitly avoid including any reference identical to
+ * 'message_id' in the result (to avoid mass confusion when a single
+ * message references itself cyclically---and yes, mail messages are
+ * not infrequent in the wild that do this---don't ask me why).
+ *
+ * Return the last reference parsed, if it is not equal to message_id.
+ */
+static char *
+parse_references (void *ctx,
+                 const char *message_id,
+                 GHashTable *hash,
+                 const char *refs)
+{
+    char *ref, *last_ref = NULL;
+
+    if (refs == NULL || *refs == '\0')
+       return NULL;
+
+    while (*refs) {
+       ref = _notmuch_message_id_parse (ctx, refs, &refs);
+
+       if (ref && strcmp (ref, message_id)) {
+           g_hash_table_add (hash, ref);
+           last_ref = ref;
+       }
+    }
+
+    /* The return value of this function is used to add a parent
+     * reference to the database.  We should avoid making a message
+     * its own parent, thus the above check.
+     */
+    return talloc_strdup (ctx, last_ref);
+}
+
+static const char *
+_notmuch_database_generate_thread_id (notmuch_database_t *notmuch)
+{
+
+    notmuch->last_thread_id++;
+
+    sprintf (notmuch->thread_id_str, "%016" PRIx64, notmuch->last_thread_id);
+
+    notmuch->writable_xapian_db->set_metadata ("last_thread_id", notmuch->thread_id_str);
+
+    return notmuch->thread_id_str;
+}
+
+static char *
+_get_metadata_thread_id_key (void *ctx, const char *message_id)
+{
+    if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
+       message_id = _notmuch_message_id_compressed (ctx, message_id);
+
+    return talloc_asprintf (ctx, NOTMUCH_METADATA_THREAD_ID_PREFIX "%s",
+                           message_id);
+}
+
+
+static notmuch_status_t
+_resolve_message_id_to_thread_id_old (notmuch_database_t *notmuch,
+                                     void *ctx,
+                                     const char *message_id,
+                                     const char **thread_id_ret);
+
+
+/* Find the thread ID to which the message with 'message_id' belongs.
+ *
+ * Note: 'thread_id_ret' must not be NULL!
+ * On success '*thread_id_ret' is set to a newly talloced string belonging to
+ * 'ctx'.
+ *
+ * Note: If there is no message in the database with the given
+ * 'message_id' then a new thread_id will be allocated for this
+ * message ID and stored in the database metadata so that the
+ * thread ID can be looked up if the message is added to the database
+ * later.
+ */
+static notmuch_status_t
+_resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
+                                 void *ctx,
+                                 const char *message_id,
+                                 const char **thread_id_ret)
+{
+    notmuch_private_status_t status;
+    notmuch_message_t *message;
+
+    if (! (notmuch->features & NOTMUCH_FEATURE_GHOSTS))
+       return _resolve_message_id_to_thread_id_old (notmuch, ctx, message_id,
+                                                    thread_id_ret);
+
+    /* Look for this message (regular or ghost) */
+    message = _notmuch_message_create_for_message_id (
+       notmuch, message_id, &status);
+    if (status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
+       /* Message exists */
+       *thread_id_ret = talloc_steal (
+           ctx, notmuch_message_get_thread_id (message));
+    } else if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+       /* Message did not exist.  Give it a fresh thread ID and
+        * populate this message as a ghost message. */
+       *thread_id_ret = talloc_strdup (
+           ctx, _notmuch_database_generate_thread_id (notmuch));
+       if (! *thread_id_ret) {
+           status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
+       } else {
+           status = _notmuch_message_initialize_ghost (message, *thread_id_ret);
+           if (status == 0)
+               /* Commit the new ghost message */
+               _notmuch_message_sync (message);
+       }
+    } else {
+       /* Create failed. Fall through. */
+    }
+
+    notmuch_message_destroy (message);
+
+    return COERCE_STATUS (status, "Error creating ghost message");
+}
+
+/* Pre-ghost messages _resolve_message_id_to_thread_id */
+static notmuch_status_t
+_resolve_message_id_to_thread_id_old (notmuch_database_t *notmuch,
+                                     void *ctx,
+                                     const char *message_id,
+                                     const char **thread_id_ret)
+{
+    notmuch_status_t status;
+    notmuch_message_t *message;
+    std::string thread_id_string;
+    char *metadata_key;
+    Xapian::WritableDatabase *db;
+
+    status = notmuch_database_find_message (notmuch, message_id, &message);
+
+    if (status)
+       return status;
+
+    if (message) {
+       *thread_id_ret = talloc_steal (ctx,
+                                      notmuch_message_get_thread_id (message));
+
+       notmuch_message_destroy (message);
+
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    /* Message has not been seen yet.
+     *
+     * We may have seen a reference to it already, in which case, we
+     * can return the thread ID stored in the metadata. Otherwise, we
+     * generate a new thread ID and store it there.
+     */
+    db = notmuch->writable_xapian_db;
+    metadata_key = _get_metadata_thread_id_key (ctx, message_id);
+    thread_id_string = notmuch->xapian_db->get_metadata (metadata_key);
+
+    if (thread_id_string.empty ()) {
+       *thread_id_ret = talloc_strdup (ctx,
+                                       _notmuch_database_generate_thread_id (notmuch));
+       db->set_metadata (metadata_key, *thread_id_ret);
+    } else {
+       *thread_id_ret = talloc_strdup (ctx, thread_id_string.c_str ());
+    }
+
+    talloc_free (metadata_key);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_merge_threads (notmuch_database_t *notmuch,
+               const char *winner_thread_id,
+               const char *loser_thread_id)
+{
+    Xapian::PostingIterator loser, loser_end;
+    notmuch_message_t *message = NULL;
+    notmuch_private_status_t private_status;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+    _notmuch_database_find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
+
+    for (; loser != loser_end; loser++) {
+       message = _notmuch_message_create (notmuch, notmuch,
+                                          *loser, &private_status);
+       if (message == NULL) {
+           ret = COERCE_STATUS (private_status,
+                                "Cannot find document for doc_id from query");
+           goto DONE;
+       }
+
+       _notmuch_message_remove_term (message, "thread", loser_thread_id);
+       _notmuch_message_add_term (message, "thread", winner_thread_id);
+       _notmuch_message_sync (message);
+
+       notmuch_message_destroy (message);
+       message = NULL;
+    }
+
+  DONE:
+    if (message)
+       notmuch_message_destroy (message);
+
+    return ret;
+}
+
+static void
+_my_talloc_free_for_g_hash (void *ptr)
+{
+    talloc_free (ptr);
+}
+
+notmuch_status_t
+_notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
+                                          notmuch_message_t *message,
+                                          notmuch_message_file_t *message_file,
+                                          const char **thread_id)
+{
+    GHashTable *parents = NULL;
+    const char *refs, *in_reply_to, *in_reply_to_message_id, *strict_message_id = NULL;
+    const char *last_ref_message_id, *this_message_id;
+    GList *l, *keys = NULL;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+    parents = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                    _my_talloc_free_for_g_hash, NULL);
+    this_message_id = notmuch_message_get_message_id (message);
+
+    refs = _notmuch_message_file_get_header (message_file, "references");
+    last_ref_message_id = parse_references (message,
+                                           this_message_id,
+                                           parents, refs);
+
+    in_reply_to = _notmuch_message_file_get_header (message_file, "in-reply-to");
+    if (in_reply_to)
+       strict_message_id = _notmuch_message_id_parse_strict (message,
+                                                             in_reply_to);
+
+    in_reply_to_message_id = parse_references (message,
+                                              this_message_id,
+                                              parents, in_reply_to);
+
+    /* For the parent of this message, use
+     * 1) the In-Reply-To header, if it looks sane, otherwise
+     * 2) the last message ID of the References header, if available.
+     * 3) Otherwise, fall back to the first message ID in
+     * the In-Reply-To header.
+     */
+
+    if (strict_message_id) {
+       _notmuch_message_add_term (message, "replyto", strict_message_id);
+    } else if (last_ref_message_id) {
+       _notmuch_message_add_term (message, "replyto",
+                                  last_ref_message_id);
+    } else if (in_reply_to_message_id) {
+       _notmuch_message_add_term (message, "replyto",
+                                  in_reply_to_message_id);
+    }
+
+    keys = g_hash_table_get_keys (parents);
+    for (l = keys; l; l = l->next) {
+       char *parent_message_id;
+       const char *parent_thread_id = NULL;
+
+       parent_message_id = (char *) l->data;
+
+       _notmuch_message_add_term (message, "reference",
+                                  parent_message_id);
+
+       ret = _resolve_message_id_to_thread_id (notmuch,
+                                               message,
+                                               parent_message_id,
+                                               &parent_thread_id);
+       if (ret)
+           goto DONE;
+
+       if (*thread_id == NULL) {
+           *thread_id = talloc_strdup (message, parent_thread_id);
+           _notmuch_message_add_term (message, "thread", *thread_id);
+       } else if (strcmp (*thread_id, parent_thread_id)) {
+           ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
+           if (ret)
+               goto DONE;
+       }
+    }
+
+  DONE:
+    if (keys)
+       g_list_free (keys);
+    if (parents)
+       g_hash_table_unref (parents);
+
+    return ret;
+}
+
+static notmuch_status_t
+_notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
+                                           notmuch_message_t *message,
+                                           const char **thread_id)
+{
+    const char *message_id = notmuch_message_get_message_id (message);
+    Xapian::PostingIterator child, children_end;
+    notmuch_message_t *child_message = NULL;
+    const char *child_thread_id;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    notmuch_private_status_t private_status;
+
+    _notmuch_database_find_doc_ids (notmuch, "reference", message_id, &child, &children_end);
+
+    for (; child != children_end; child++) {
+
+       child_message = _notmuch_message_create (message, notmuch,
+                                                *child, &private_status);
+       if (child_message == NULL) {
+           ret = COERCE_STATUS (private_status,
+                                "Cannot find document for doc_id from query");
+           goto DONE;
+       }
+
+       child_thread_id = notmuch_message_get_thread_id (child_message);
+       if (*thread_id == NULL) {
+           *thread_id = talloc_strdup (message, child_thread_id);
+           _notmuch_message_add_term (message, "thread", *thread_id);
+       } else if (strcmp (*thread_id, child_thread_id)) {
+           _notmuch_message_remove_term (child_message, "reference",
+                                         message_id);
+           _notmuch_message_sync (child_message);
+           ret = _merge_threads (notmuch, *thread_id, child_thread_id);
+           if (ret)
+               goto DONE;
+       }
+
+       notmuch_message_destroy (child_message);
+       child_message = NULL;
+    }
+
+  DONE:
+    if (child_message)
+       notmuch_message_destroy (child_message);
+
+    return ret;
+}
+
+/* Fetch and clear the stored thread_id for message, or NULL if none. */
+static char *
+_consume_metadata_thread_id (void *ctx, notmuch_database_t *notmuch,
+                            notmuch_message_t *message)
+{
+    const char *message_id;
+    std::string stored_id;
+    char *metadata_key;
+
+    message_id = notmuch_message_get_message_id (message);
+    metadata_key = _get_metadata_thread_id_key (ctx, message_id);
+
+    /* Check if we have already seen related messages to this one.
+     * If we have then use the thread_id that we stored at that time.
+     */
+    stored_id = notmuch->xapian_db->get_metadata (metadata_key);
+    if (stored_id.empty ()) {
+       return NULL;
+    } else {
+       /* Clear the metadata for this message ID. We don't need it
+        * anymore. */
+       notmuch->writable_xapian_db->set_metadata (metadata_key, "");
+
+       return talloc_strdup (ctx, stored_id.c_str ());
+    }
+}
+
+/* Given a blank or ghost 'message' and its corresponding
+ * 'message_file' link it to existing threads in the database.
+ *
+ * First, if is_ghost, this retrieves the thread ID already stored in
+ * the message (which will be the case if a message was previously
+ * added that referenced this one).  If the message is blank
+ * (!is_ghost), it doesn't have a thread ID yet (we'll generate one
+ * later in this function).  If the database does not support ghost
+ * messages, this checks for a thread ID stored in database metadata
+ * for this message ID.
+ *
+ * Second, we look at 'message_file' and its link-relevant headers
+ * (References and In-Reply-To) for message IDs.
+ *
+ * Finally, we look in the database for existing message that
+ * reference 'message'.
+ *
+ * In all cases, we assign to the current message the first thread ID
+ * found. We will also merge any existing, distinct threads where this
+ * message belongs to both, (which is not uncommon when messages are
+ * processed out of order).
+ *
+ * Finally, if no thread ID has been found through referenced messages, we
+ * call _notmuch_message_generate_thread_id to generate a new thread
+ * ID. This should only happen for new, top-level messages, (no
+ * References or In-Reply-To header in this message, and no previously
+ * added message refers to this message).
+ */
+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_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 (! 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 {
+       thread_id = _consume_metadata_thread_id (local, notmuch, message);
+       if (thread_id)
+           _notmuch_message_add_term (message, "thread", thread_id);
+    }
+
+    status = _notmuch_database_link_message_to_parents (notmuch, message,
+                                                       message_file,
+                                                       &thread_id);
+    if (status)
+       goto DONE;
+
+    if (! (notmuch->features & NOTMUCH_FEATURE_GHOSTS)) {
+       /* In general, it shouldn't be necessary to link children,
+        * since the earlier indexing of those children will have
+        * stored a thread ID for the missing parent.  However, prior
+        * to ghost messages, these stored thread IDs were NOT
+        * rewritten during thread merging (and there was no
+        * performant way to do so), so if indexed children were
+        * pulled into a different thread ID by a merge, it was
+        * necessary to pull them *back* into the stored thread ID of
+        * the parent.  With ghost messages, we just rewrite the
+        * stored thread IDs during merging, so this workaround isn't
+        * necessary. */
+       status = _notmuch_database_link_message_to_children (notmuch, message,
+                                                            &thread_id);
+       if (status)
+           goto DONE;
+    }
+
+    /* If not part of any existing thread, generate a new thread ID. */
+    if (thread_id == NULL) {
+       thread_id = _notmuch_database_generate_thread_id (notmuch);
+
+       _notmuch_message_add_term (message, "thread", thread_id);
+    }
+
+  DONE:
+    talloc_free (local);
+
+    return status;
+}
+
+notmuch_status_t
+notmuch_database_index_file (notmuch_database_t *notmuch,
+                            const char *filename,
+                            notmuch_indexopts_t *indexopts,
+                            notmuch_message_t **message_ret)
+{
+    notmuch_message_file_t *message_file;
+    notmuch_message_t *message = NULL;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS, ret2;
+    notmuch_private_status_t private_status;
+    notmuch_bool_t is_ghost = false, is_new = false;
+    notmuch_indexopts_t *def_indexopts = NULL;
+
+    const char *date;
+    const char *from, *to, *subject;
+    char *message_id = NULL;
+
+    if (message_ret)
+       *message_ret = NULL;
+
+    ret = _notmuch_database_ensure_writable (notmuch);
+    if (ret)
+       return ret;
+
+    message_file = _notmuch_message_file_open (notmuch, filename);
+    if (message_file == NULL)
+       return NOTMUCH_STATUS_FILE_ERROR;
+
+    /* Adding a message may change many documents.  Do this all
+     * atomically. */
+    ret = notmuch_database_begin_atomic (notmuch);
+    if (ret)
+       goto DONE;
+
+    ret = _notmuch_message_file_get_headers (message_file,
+                                            &from, &subject, &to, &date,
+                                            &message_id);
+    if (ret)
+       goto DONE;
+
+    try {
+       /* Now that we have a message ID, we get a message object,
+        * (which may or may not reference an existing document in the
+        * database). */
+
+       message = _notmuch_message_create_for_message_id (notmuch,
+                                                         message_id,
+                                                         &private_status);
+
+       talloc_free (message_id);
+
+       /* We cannot call notmuch_message_get_flag for a new message */
+       switch (private_status) {
+       case NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
+           is_ghost = false;
+           is_new = true;
+           break;
+       case NOTMUCH_PRIVATE_STATUS_SUCCESS:
+           ret = notmuch_message_get_flag_st (message, NOTMUCH_MESSAGE_FLAG_GHOST, &is_ghost);
+           if (ret)
+               goto DONE;
+           is_new = false;
+           break;
+       default:
+           ret = COERCE_STATUS (private_status,
+                                "Unexpected status value from _notmuch_message_create_for_message_id");
+           goto DONE;
+       }
+
+       ret = _notmuch_message_add_filename (message, filename);
+       if (ret)
+           goto DONE;
+
+       if (is_new || is_ghost) {
+           _notmuch_message_add_term (message, "type", "mail");
+           if (is_ghost)
+               /* Convert ghost message to a regular message */
+               _notmuch_message_remove_term (message, "type", "ghost");
+       }
+
+       ret = _notmuch_database_link_message (notmuch, message,
+                                             message_file, is_ghost, is_new);
+       if (ret)
+           goto DONE;
+
+       if (is_new || is_ghost)
+           _notmuch_message_set_header_values (message, date, from, subject);
+
+       if (! indexopts) {
+           def_indexopts = notmuch_database_get_default_indexopts (notmuch);
+           indexopts = def_indexopts;
+       }
+
+       ret = _notmuch_message_index_file (message, indexopts, message_file);
+       if (ret)
+           goto DONE;
+
+       if (! is_new && ! is_ghost)
+           ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+
+       _notmuch_message_sync (message);
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch, "A Xapian exception occurred adding message: %s.\n",
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
+       ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+       goto DONE;
+    }
+
+  DONE:
+    if (def_indexopts)
+       notmuch_indexopts_destroy (def_indexopts);
+
+    if (message) {
+       if ((ret == NOTMUCH_STATUS_SUCCESS ||
+            ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) && message_ret)
+           *message_ret = message;
+       else
+           notmuch_message_destroy (message);
+    }
+
+    if (message_file)
+       _notmuch_message_file_close (message_file);
+
+    ret2 = notmuch_database_end_atomic (notmuch);
+    if ((ret == NOTMUCH_STATUS_SUCCESS ||
+        ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) &&
+       ret2 != NOTMUCH_STATUS_SUCCESS)
+       ret = ret2;
+
+    return ret;
+}
+
+notmuch_status_t
+notmuch_database_add_message (notmuch_database_t *notmuch,
+                             const char *filename,
+                             notmuch_message_t **message_ret)
+{
+    return notmuch_database_index_file (notmuch, filename,
+                                       NULL,
+                                       message_ret);
+
+}
diff --git a/lib/built-with.c b/lib/built-with.c
new file mode 100644 (file)
index 0000000..275e72b
--- /dev/null
@@ -0,0 +1,40 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2016 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 "notmuch.h"
+#include "notmuch-private.h"
+
+notmuch_bool_t
+notmuch_built_with (const char *name)
+{
+    if (STRNCMP_LITERAL (name, "compact") == 0) {
+       return true;
+    } else if (STRNCMP_LITERAL (name, "field_processor") == 0) {
+       return true;
+    } else if (STRNCMP_LITERAL (name, "retry_lock") == 0) {
+       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;
+    }
+}
diff --git a/lib/config.cc b/lib/config.cc
new file mode 100644 (file)
index 0000000..6cd15fa
--- /dev/null
@@ -0,0 +1,726 @@
+/* config.cc - API for database metadata
+ *
+ * Copyright © 2016 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 "notmuch.h"
+#include "notmuch-private.h"
+#include "database-private.h"
+
+#include <pwd.h>
+#include <netdb.h>
+
+static const std::string CONFIG_PREFIX = "C";
+
+struct _notmuch_config_list {
+    notmuch_database_t *notmuch;
+    Xapian::TermIterator iterator;
+    char *current_key;
+    char *current_val;
+};
+
+struct _notmuch_config_values {
+    const char *iterator;
+    size_t tok_len;
+    const char *string;
+    void *children; /* talloc_context */
+};
+
+struct _notmuch_config_pairs {
+    notmuch_string_map_iterator_t *iter;
+};
+
+static const char *_notmuch_config_key_to_string (notmuch_config_key_t key);
+static char *_expand_path (void *ctx, const char *key, const char *val);
+
+static int
+_notmuch_config_list_destroy (notmuch_config_list_t *list)
+{
+    /* invoke destructor w/o deallocating memory */
+    list->iterator.~TermIterator();
+    return 0;
+}
+
+notmuch_status_t
+notmuch_database_set_config (notmuch_database_t *notmuch,
+                            const char *key,
+                            const char *value)
+{
+    notmuch_status_t status;
+
+    status = _notmuch_database_ensure_writable (notmuch);
+    if (status)
+       return status;
+
+    if (! notmuch->config) {
+       if ((status = _notmuch_config_load_from_database (notmuch)))
+           return status;
+    }
+
+    try {
+       notmuch->writable_xapian_db->set_metadata (CONFIG_PREFIX + key, value);
+    } catch (const Xapian::Error &error) {
+       status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+       notmuch->exception_reported = true;
+       _notmuch_database_log (notmuch, "Error: A Xapian exception occurred setting metadata: %s\n",
+                              error.get_msg ().c_str ());
+    }
+
+    if (status)
+       return status;
+
+    _notmuch_string_map_set (notmuch->config, key, value);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_metadata_value (notmuch_database_t *notmuch,
+                const char *key,
+                std::string &value)
+{
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+
+    try {
+       value = notmuch->xapian_db->get_metadata (CONFIG_PREFIX + key);
+    } catch (const Xapian::Error &error) {
+       status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+       notmuch->exception_reported = true;
+       _notmuch_database_log (notmuch, "Error: A Xapian exception occurred getting metadata: %s\n",
+                              error.get_msg ().c_str ());
+    }
+    return status;
+}
+
+notmuch_status_t
+notmuch_database_get_config (notmuch_database_t *notmuch,
+                            const char *key,
+                            char **value)
+{
+    const char *stored_val;
+    notmuch_status_t status;
+
+    if (! notmuch->config) {
+       if ((status = _notmuch_config_load_from_database (notmuch)))
+           return status;
+    }
+
+    if (! value)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    stored_val = _notmuch_string_map_get (notmuch->config, key);
+    if (! stored_val) {
+       /* XXX in principle this API should be fixed so empty string
+        * is distinguished from not found */
+       *value = strdup ("");
+    } else {
+       *value = strdup (stored_val);
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_database_get_config_list (notmuch_database_t *notmuch,
+                                 const char *prefix,
+                                 notmuch_config_list_t **out)
+{
+    notmuch_config_list_t *list = NULL;
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+
+    list = talloc (notmuch, notmuch_config_list_t);
+    if (! list) {
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    list->notmuch = notmuch;
+    list->current_key = NULL;
+    list->current_val = NULL;
+
+    try {
+
+       new(&(list->iterator)) Xapian::TermIterator (notmuch->xapian_db->metadata_keys_begin
+                                                        (CONFIG_PREFIX + (prefix ? prefix : "")));
+       talloc_set_destructor (list, _notmuch_config_list_destroy);
+
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch,
+                              "A Xapian exception occurred getting metadata iterator: %s.\n",
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
+       status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    *out = list;
+
+  DONE:
+    if (status) {
+       if (list) {
+           talloc_free (list);
+           if (status != NOTMUCH_STATUS_XAPIAN_EXCEPTION)
+               _notmuch_config_list_destroy (list);
+       }
+    } else {
+       talloc_set_destructor (list, _notmuch_config_list_destroy);
+    }
+
+    return status;
+}
+
+notmuch_bool_t
+notmuch_config_list_valid (notmuch_config_list_t *metadata)
+{
+    if (metadata->iterator == metadata->notmuch->xapian_db->metadata_keys_end ())
+       return false;
+
+    return true;
+}
+
+static inline char *
+_key_from_iterator (notmuch_config_list_t *list)
+{
+    return talloc_strdup (list, (*list->iterator).c_str () + CONFIG_PREFIX.length ());
+}
+
+const char *
+notmuch_config_list_key (notmuch_config_list_t *list)
+{
+    if (list->current_key)
+       talloc_free (list->current_key);
+
+    list->current_key = _key_from_iterator (list);
+
+    return list->current_key;
+}
+
+const char *
+notmuch_config_list_value (notmuch_config_list_t *list)
+{
+    std::string strval;
+    notmuch_status_t status;
+    char *key = _key_from_iterator (list);
+
+    /* TODO: better error reporting?? */
+    status = _metadata_value (list->notmuch, key, strval);
+    if (status)
+       return NULL;
+
+    if (list->current_val)
+       talloc_free (list->current_val);
+
+    list->current_val = talloc_strdup (list, strval.c_str ());
+    talloc_free (key);
+    return list->current_val;
+}
+
+void
+notmuch_config_list_move_to_next (notmuch_config_list_t *list)
+{
+    list->iterator++;
+}
+
+void
+notmuch_config_list_destroy (notmuch_config_list_t *list)
+{
+    talloc_free (list);
+}
+
+notmuch_status_t
+_notmuch_config_load_from_database (notmuch_database_t *notmuch)
+{
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+    notmuch_config_list_t *list;
+
+    if (notmuch->config == NULL)
+       notmuch->config = _notmuch_string_map_create (notmuch);
+
+    if (unlikely (notmuch->config == NULL))
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+    status = notmuch_database_get_config_list (notmuch, "", &list);
+    if (status)
+       return status;
+
+    for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+       const char *key = notmuch_config_list_key (list);
+       char *normalized_val = 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;
+}
+
+notmuch_config_values_t *
+notmuch_config_get_values (notmuch_database_t *notmuch, notmuch_config_key_t key)
+{
+    const char *key_str = _notmuch_config_key_to_string (key);
+
+    if (! key_str)
+       return NULL;
+
+    return notmuch_config_get_values_string (notmuch, key_str);
+}
+
+notmuch_config_values_t *
+notmuch_config_get_values_string (notmuch_database_t *notmuch, const char *key_str)
+{
+    notmuch_config_values_t *values = NULL;
+    bool ok = false;
+
+    values = talloc (notmuch, notmuch_config_values_t);
+    if (unlikely (! values))
+       goto DONE;
+
+    values->children = talloc_new (values);
+
+    values->string = _notmuch_string_map_get (notmuch->config, key_str);
+    if (! values->string)
+       goto DONE;
+
+    values->iterator = strsplit_len (values->string, ';', &(values->tok_len));
+    ok = true;
+
+  DONE:
+    if (! ok) {
+       if (values)
+           talloc_free (values);
+       return NULL;
+    }
+    return values;
+}
+
+notmuch_bool_t
+notmuch_config_values_valid (notmuch_config_values_t *values)
+{
+    if (! values)
+       return false;
+
+    return (values->iterator != NULL);
+}
+
+const char *
+notmuch_config_values_get (notmuch_config_values_t *values)
+{
+    return talloc_strndup (values->children, values->iterator, values->tok_len);
+}
+
+void
+notmuch_config_values_start (notmuch_config_values_t *values)
+{
+    if (values == NULL)
+       return;
+    if (values->children) {
+       talloc_free (values->children);
+    }
+
+    values->children = talloc_new (values);
+
+    values->iterator = strsplit_len (values->string, ';', &(values->tok_len));
+}
+
+void
+notmuch_config_values_move_to_next (notmuch_config_values_t *values)
+{
+    values->iterator += values->tok_len;
+    values->iterator = strsplit_len (values->iterator, ';', &(values->tok_len));
+}
+
+void
+notmuch_config_values_destroy (notmuch_config_values_t *values)
+{
+    talloc_free (values);
+}
+
+notmuch_config_pairs_t *
+notmuch_config_get_pairs (notmuch_database_t *notmuch,
+                         const char *prefix)
+{
+    notmuch_config_pairs_t *pairs = talloc (notmuch, notmuch_config_pairs_t);
+
+    pairs->iter = _notmuch_string_map_iterator_create (notmuch->config, prefix, false);
+    return pairs;
+}
+
+notmuch_bool_t
+notmuch_config_pairs_valid (notmuch_config_pairs_t *pairs)
+{
+    return _notmuch_string_map_iterator_valid (pairs->iter);
+}
+
+void
+notmuch_config_pairs_move_to_next (notmuch_config_pairs_t *pairs)
+{
+    _notmuch_string_map_iterator_move_to_next (pairs->iter);
+}
+
+const char *
+notmuch_config_pairs_key (notmuch_config_pairs_t *pairs)
+{
+    return _notmuch_string_map_iterator_key (pairs->iter);
+}
+
+const char *
+notmuch_config_pairs_value (notmuch_config_pairs_t *pairs)
+{
+    return _notmuch_string_map_iterator_value (pairs->iter);
+}
+
+void
+notmuch_config_pairs_destroy (notmuch_config_pairs_t *pairs)
+{
+    _notmuch_string_map_iterator_destroy (pairs->iter);
+    talloc_free (pairs);
+}
+
+static char *
+_expand_path (void *ctx, const char *key, const char *val)
+{
+    char *expanded_val;
+
+    if ((strcmp (key, "database.path") == 0 ||
+        strcmp (key, "database.mail_root") == 0 ||
+        strcmp (key, "database.hook_dir") == 0 ||
+        strcmp (key, "database.backup_path") == 0 ) &&
+       val[0] != '/')
+       expanded_val = talloc_asprintf (ctx, "%s/%s", getenv ("HOME"), val);
+    else
+       expanded_val = talloc_strdup (ctx, val);
+
+    return expanded_val;
+}
+
+notmuch_status_t
+_notmuch_config_load_from_file (notmuch_database_t *notmuch,
+                               GKeyFile *file,
+                               char **status_string)
+{
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+    gchar **groups = NULL, **keys, *val;
+
+    if (notmuch->config == NULL)
+       notmuch->config = _notmuch_string_map_create (notmuch);
+
+    if (unlikely (notmuch->config == NULL)) {
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    groups = g_key_file_get_groups (file, NULL);
+    for (gchar **grp = groups; *grp; grp++) {
+       keys = g_key_file_get_keys (file, *grp, NULL, NULL);
+       for (gchar **keys_p = keys; *keys_p; keys_p++) {
+           char *absolute_key = talloc_asprintf (notmuch, "%s.%s", *grp,  *keys_p);
+           char *normalized_val;
+           GError *gerr = NULL;
+
+           /* If we opened from a given path, do not overwrite it */
+           if (strcmp (absolute_key, "database.path") == 0 &&
+               (notmuch->params & NOTMUCH_PARAM_DATABASE) &&
+               notmuch->xapian_db)
+               continue;
+
+           val = g_key_file_get_string (file, *grp, *keys_p, &gerr);
+           if (gerr) {
+               if (status_string)
+                   IGNORE_RESULT (asprintf (status_string,
+                                            "GLib: %s\n",
+                                            gerr->message));
+               g_error_free (gerr);
+           }
+           if (! val) {
+               status = NOTMUCH_STATUS_FILE_ERROR;
+               goto DONE;
+           }
+
+           normalized_val = _expand_path (notmuch, absolute_key, val);
+           _notmuch_string_map_set (notmuch->config, absolute_key, normalized_val);
+           g_free (val);
+           talloc_free (absolute_key);
+           talloc_free (normalized_val);
+           if (status)
+               goto DONE;
+       }
+       g_strfreev (keys);
+    }
+
+  DONE:
+    if (groups)
+       g_strfreev (groups);
+
+    return status;
+}
+
+notmuch_status_t
+notmuch_config_get_bool (notmuch_database_t *notmuch, notmuch_config_key_t key, notmuch_bool_t *val)
+{
+    const char *key_string, *val_string;
+
+    key_string = _notmuch_config_key_to_string (key);
+    if (! key_string) {
+       return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
+    }
+
+    val_string = _notmuch_string_map_get (notmuch->config, key_string);
+    if (! val_string) {
+       *val = FALSE;
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    if (strcase_equal (val_string, "false") || strcase_equal (val_string, "no"))
+       *val = FALSE;
+    else if (strcase_equal (val_string, "true") || strcase_equal (val_string, "yes"))
+       *val = TRUE;
+    else
+       return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static const char *
+_get_name_from_passwd_file (void *ctx)
+{
+    long pw_buf_size;
+    char *pw_buf;
+    struct passwd passwd, *ignored;
+    const char *name;
+    int e;
+
+    pw_buf_size = sysconf (_SC_GETPW_R_SIZE_MAX);
+    if (pw_buf_size == -1) pw_buf_size = 64;
+    pw_buf = (char *) talloc_size (ctx, pw_buf_size);
+
+    while ((e = getpwuid_r (getuid (), &passwd, pw_buf,
+                           pw_buf_size, &ignored)) == ERANGE) {
+       pw_buf_size = pw_buf_size * 2;
+       pw_buf = (char *) talloc_zero_size (ctx, pw_buf_size);
+    }
+
+    if (e == 0) {
+       char *comma = strchr (passwd.pw_gecos, ',');
+       if (comma)
+           name = talloc_strndup (ctx, passwd.pw_gecos,
+                                  comma - passwd.pw_gecos);
+       else
+           name = talloc_strdup (ctx, passwd.pw_gecos);
+    } else {
+       name = talloc_strdup (ctx, "");
+    }
+
+    talloc_free (pw_buf);
+
+    return name;
+}
+
+static char *
+_get_username_from_passwd_file (void *ctx)
+{
+    long pw_buf_size;
+    char *pw_buf;
+    struct passwd passwd, *ignored;
+    char *name;
+    int e;
+
+    pw_buf_size = sysconf (_SC_GETPW_R_SIZE_MAX);
+    if (pw_buf_size == -1) pw_buf_size = 64;
+    pw_buf = (char *) talloc_zero_size (ctx, pw_buf_size);
+
+    while ((e = getpwuid_r (getuid (), &passwd, pw_buf,
+                           pw_buf_size, &ignored)) == ERANGE) {
+       pw_buf_size = pw_buf_size * 2;
+       pw_buf = (char *) talloc_zero_size (ctx, pw_buf_size);
+    }
+
+    if (e == 0)
+       name = talloc_strdup (ctx, passwd.pw_name);
+    else
+       name = talloc_strdup (ctx, "");
+
+    talloc_free (pw_buf);
+
+    return name;
+}
+
+static const char *
+_get_email_from_passwd_file (void *ctx)
+{
+    char *email;
+
+    char *username = _get_username_from_passwd_file (ctx);
+
+    email = talloc_asprintf (ctx, "%s@localhost", username);
+
+    talloc_free (username);
+    return email;
+}
+
+static const char *
+_notmuch_config_key_to_string (notmuch_config_key_t key)
+{
+    switch (key) {
+    case NOTMUCH_CONFIG_DATABASE_PATH:
+       return "database.path";
+    case NOTMUCH_CONFIG_MAIL_ROOT:
+       return "database.mail_root";
+    case NOTMUCH_CONFIG_HOOK_DIR:
+       return "database.hook_dir";
+    case NOTMUCH_CONFIG_BACKUP_DIR:
+       return "database.backup_dir";
+    case NOTMUCH_CONFIG_EXCLUDE_TAGS:
+       return "search.exclude_tags";
+    case NOTMUCH_CONFIG_NEW_TAGS:
+       return "new.tags";
+    case NOTMUCH_CONFIG_NEW_IGNORE:
+       return "new.ignore";
+    case NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS:
+       return "maildir.synchronize_flags";
+    case NOTMUCH_CONFIG_PRIMARY_EMAIL:
+       return "user.primary_email";
+    case NOTMUCH_CONFIG_OTHER_EMAIL:
+       return "user.other_email";
+    case NOTMUCH_CONFIG_USER_NAME:
+       return "user.name";
+    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;
+    }
+}
+
+static const char *
+_notmuch_config_default (notmuch_database_t *notmuch, notmuch_config_key_t key)
+{
+    char *path;
+    const char *name, *email;
+
+    switch (key) {
+    case NOTMUCH_CONFIG_DATABASE_PATH:
+       path = getenv ("MAILDIR");
+       if (path)
+           path = talloc_strdup (notmuch, path);
+       else
+           path = talloc_asprintf (notmuch, "%s/mail",
+                                   getenv ("HOME"));
+       return path;
+    case NOTMUCH_CONFIG_MAIL_ROOT:
+       /* by default, mail root is the same as database path */
+       return notmuch_database_get_path (notmuch);
+    case NOTMUCH_CONFIG_EXCLUDE_TAGS:
+       return "";
+    case NOTMUCH_CONFIG_NEW_TAGS:
+       return "unread;inbox";
+    case NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS:
+       return "true";
+    case NOTMUCH_CONFIG_USER_NAME:
+       name = getenv ("NAME");
+       if (name)
+           name = talloc_strdup (notmuch, name);
+       else
+           name = _get_name_from_passwd_file (notmuch);
+       return name;
+    case NOTMUCH_CONFIG_PRIMARY_EMAIL:
+       email = getenv ("EMAIL");
+       if (email)
+           email = talloc_strdup (notmuch, email);
+       else
+           email = _get_email_from_passwd_file (notmuch);
+       return email;
+    case NOTMUCH_CONFIG_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:
+       return NULL;
+    default:
+    case NOTMUCH_CONFIG_LAST:
+       INTERNAL_ERROR ("illegal key enum %d", key);
+    }
+}
+
+notmuch_status_t
+_notmuch_config_load_defaults (notmuch_database_t *notmuch)
+{
+    notmuch_config_key_t key;
+    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;
+        key = notmuch_config_key_t (key + 1)) {
+       const char *val = notmuch_config_get (notmuch, key);
+       const char *key_string = _notmuch_config_key_to_string (key);
+
+       val = _notmuch_string_map_get (notmuch->config, key_string);
+       if (! val) {
+           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 status;
+}
+
+const char *
+notmuch_config_get (notmuch_database_t *notmuch, notmuch_config_key_t key)
+{
+
+    return _notmuch_string_map_get (notmuch->config, _notmuch_config_key_to_string (key));
+}
+
+const char *
+notmuch_config_path (notmuch_database_t *notmuch)
+{
+    return notmuch->config_path;
+}
+
+notmuch_status_t
+notmuch_config_set (notmuch_database_t *notmuch, notmuch_config_key_t key, const char *val)
+{
+
+    return notmuch_database_set_config (notmuch, _notmuch_config_key_to_string (key), val);
+}
+
+void
+_notmuch_config_cache (notmuch_database_t *notmuch, notmuch_config_key_t key, const char *val)
+{
+    if (notmuch->config == NULL)
+       notmuch->config = _notmuch_string_map_create (notmuch);
+
+    _notmuch_string_map_set (notmuch->config, _notmuch_config_key_to_string (key), val);
+}
diff --git a/lib/database-private.h b/lib/database-private.h
new file mode 100644 (file)
index 0000000..61232f1
--- /dev/null
@@ -0,0 +1,395 @@
+/* database-private.h - For peeking into the internals of notmuch_database_t
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#ifndef NOTMUCH_DATABASE_PRIVATE_H
+#define NOTMUCH_DATABASE_PRIVATE_H
+
+/* According to WG14/N1124, a C++ implementation won't provide us a
+ * macro like PRIx64 (which gives a printf format string for
+ * formatting a uint64_t as hexadecimal) unless we define
+ * __STDC_FORMAT_MACROS before including inttypes.h. That's annoying,
+ * but there it is.
+ */
+#define __STDC_FORMAT_MACROS
+#include <inttypes.h>
+
+#include "notmuch-private.h"
+
+#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
+
+#ifdef SILENCE_XAPIAN_DEPRECATION_WARNINGS
+#define XAPIAN_DEPRECATED(D) D
+#endif
+
+#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.
+ *
+ * A database stores the set of features that it "uses" (implicitly
+ * before database version 3 and explicitly as of version 3).
+ *
+ * A given library version will "recognize" a particular set of
+ * features; if a database uses a feature that the library does not
+ * recognize, the library will refuse to open it.  It is assumed the
+ * set of recognized features grows monotonically over time.  A
+ * library version will "implement" some subset of the recognized
+ * features: some operations may require that the database use (or not
+ * use) some feature, while other operations may support both
+ * databases that use and that don't use some feature.
+ *
+ * On disk, the database stores string names for these features (see
+ * the feature_names array).  These enum bit values are never
+ * persisted to disk and may change freely.
+ */
+enum _notmuch_features {
+    /* If set, file names are stored in "file-direntry" terms.  If
+     * unset, file names are stored in document data.
+     *
+     * Introduced: version 1. */
+    NOTMUCH_FEATURE_FILE_TERMS                 = 1 << 0,
+
+    /* If set, directory timestamps are stored in documents with
+     * XDIRECTORY terms and relative paths.  If unset, directory
+     * timestamps are stored in documents with XTIMESTAMP terms and
+     * absolute paths.
+     *
+     * Introduced: version 1. */
+    NOTMUCH_FEATURE_DIRECTORY_DOCS             = 1 << 1,
+
+    /* If set, the from, subject, and message-id headers are stored in
+     * message document values.  If unset, message documents *may*
+     * have these values, but if the value is empty, it must be
+     * retrieved from the message file.
+     *
+     * Introduced: optional in version 1, required as of version 3.
+     */
+    NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES     = 1 << 2,
+
+    /* If set, folder terms are boolean and path terms exist.  If
+     * unset, folder terms are probabilistic and stemmed and path
+     * terms do not exist.
+     *
+     * Introduced: version 2. */
+    NOTMUCH_FEATURE_BOOL_FOLDER                        = 1 << 3,
+
+    /* If set, missing messages are stored in ghost mail documents.
+     * If unset, thread IDs of ghost messages are stored as database
+     * metadata instead of in ghost documents.
+     *
+     * Introduced: version 3. */
+    NOTMUCH_FEATURE_GHOSTS                     = 1 << 4,
+
+
+    /* If set, then the database was created after the introduction of
+     * indexed mime types. If unset, then the database may contain a
+     * mixture of messages with indexed and non-indexed mime types.
+     *
+     * Introduced: version 3. */
+    NOTMUCH_FEATURE_INDEXED_MIMETYPES          = 1 << 5,
+
+    /* If set, messages store the revision number of the last
+     * modification in NOTMUCH_VALUE_LAST_MOD.
+     *
+     * Introduced: version 3. */
+    NOTMUCH_FEATURE_LAST_MOD                   = 1 << 6,
+
+    /* If set, unprefixed terms are stored only for the message body,
+     * not for headers.
+     *
+     * Introduced: version 3. */
+    NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY         = 1 << 7,
+};
+
+/* In C++, a named enum is its own type, so define bitwise operators
+ * on _notmuch_features. */
+inline _notmuch_features
+operator| (_notmuch_features a, _notmuch_features b)
+{
+    return static_cast<_notmuch_features>(
+       static_cast<unsigned>(a) | static_cast<unsigned>(b));
+}
+
+inline _notmuch_features
+operator& (_notmuch_features a, _notmuch_features b)
+{
+    return static_cast<_notmuch_features>(
+       static_cast<unsigned>(a) & static_cast<unsigned>(b));
+}
+
+inline _notmuch_features
+operator~ (_notmuch_features a)
+{
+    return static_cast<_notmuch_features>(~static_cast<unsigned>(a));
+}
+
+inline _notmuch_features&
+operator|= (_notmuch_features &a, _notmuch_features b)
+{
+    a = a | b;
+    return a;
+}
+
+inline _notmuch_features&
+operator&= (_notmuch_features &a, _notmuch_features b)
+{
+    a = a & b;
+    return a;
+}
+
+/*
+ * Configuration options for xapian database fields */
+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;
+
+/*
+ * define bitwise operators to hide casts */
+inline notmuch_field_flag_t
+operator| (notmuch_field_flag_t a, notmuch_field_flag_t b)
+{
+    return static_cast<notmuch_field_flag_t>(
+       static_cast<unsigned>(a) | static_cast<unsigned>(b));
+}
+
+inline notmuch_field_flag_t
+operator& (notmuch_field_flag_t a, notmuch_field_flag_t b)
+{
+    return static_cast<notmuch_field_flag_t>(
+       static_cast<unsigned>(a) & static_cast<unsigned>(b));
+}
+
+#define NOTMUCH_QUERY_PARSER_FLAGS (Xapian::QueryParser::FLAG_BOOLEAN | \
+                                   Xapian::QueryParser::FLAG_PHRASE | \
+                                   Xapian::QueryParser::FLAG_LOVEHATE | \
+                                   Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE | \
+                                   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;
+
+    /* Path to actual database */
+    const char *xapian_path;
+
+    /* Path to config loaded, if any */
+    const char *config_path;
+
+    int atomic_nesting;
+    /* true if changes have been made in this atomic section */
+    bool atomic_dirty;
+    Xapian::Database *xapian_db;
+    Xapian::WritableDatabase *writable_xapian_db;
+    bool open;
+    /* Bit mask of features used by this database.  This is a
+     * bitwise-OR of NOTMUCH_FEATURE_* values (above). */
+    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;
+
+    /* Highest committed revision number.  Modifications are recorded
+     * under a higher revision number, which can be generated with
+     * notmuch_database_new_revision. */
+    unsigned long revision;
+    const char *uuid;
+
+    /* Keep track of the number of times the database has been re-opened
+     * (or other global invalidations of notmuch's caching)
+     */
+    unsigned long view;
+    Xapian::QueryParser *query_parser;
+    Xapian::Stem *stemmer;
+    Xapian::TermGenerator *term_gen;
+    Xapian::RangeProcessor *value_range_processor;
+    Xapian::RangeProcessor *date_range_processor;
+    Xapian::RangeProcessor *last_mod_range_processor;
+
+    /* XXX it's slightly gross to use two parallel string->string maps
+     * here, but at least they are small */
+    notmuch_string_map_t *user_prefix;
+    notmuch_string_map_t *user_header;
+
+    /* Cached and possibly overridden configuration */
+    notmuch_string_map_t *config;
+
+    /* 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
+ * version number, so hard-code them for earlier versions. */
+#define NOTMUCH_FEATURES_V0 ((enum _notmuch_features) 0)
+#define NOTMUCH_FEATURES_V1 (NOTMUCH_FEATURES_V0 | NOTMUCH_FEATURE_FILE_TERMS | \
+                            NOTMUCH_FEATURE_DIRECTORY_DOCS)
+#define NOTMUCH_FEATURES_V2 (NOTMUCH_FEATURES_V1 | NOTMUCH_FEATURE_BOOL_FOLDER)
+
+/* Current database features.  If any of these are missing from a
+ * database, request an upgrade.
+ * NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES and
+ * NOTMUCH_FEATURE_INDEXED_MIMETYPES are not included because upgrade
+ * doesn't currently introduce the features (though brand new databases
+ * will have it). */
+#define NOTMUCH_FEATURES_CURRENT \
+    (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_DIRECTORY_DOCS | \
+     NOTMUCH_FEATURE_BOOL_FOLDER | NOTMUCH_FEATURE_GHOSTS | \
+     NOTMUCH_FEATURE_LAST_MOD)
+
+/* Return the list of terms from the given iterator matching a prefix.
+ * The prefix will be stripped from the strings in the returned list.
+ * The list will be allocated using ctx as the talloc context.
+ *
+ * The function returns NULL on failure.
+ */
+notmuch_string_list_t *
+_notmuch_database_get_terms_with_prefix (void *ctx, Xapian::TermIterator &i,
+                                        Xapian::TermIterator &end,
+                                        const char *prefix);
+
+void
+_notmuch_database_find_doc_ids (notmuch_database_t *notmuch,
+                               const char *prefix_name,
+                               const char *value,
+                               Xapian::PostingIterator *begin,
+                               Xapian::PostingIterator *end);
+
+#define NOTMUCH_DATABASE_VERSION 3
+
+/* features.cc */
+
+_notmuch_features
+_notmuch_database_parse_features (const void *ctx, const char *features, unsigned int version,
+                                 char mode, char **incompat_out);
+
+char *
+_notmuch_database_print_features (const void *ctx, unsigned int features);
+
+/* prefix.cc */
+notmuch_status_t
+_notmuch_database_setup_standard_query_fields (notmuch_database_t *notmuch);
+
+notmuch_status_t
+_notmuch_database_setup_user_query_fields (notmuch_database_t *notmuch);
+
+#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
diff --git a/lib/database.cc b/lib/database.cc
new file mode 100644 (file)
index 0000000..737a3f3
--- /dev/null
@@ -0,0 +1,1589 @@
+/* database.cc - The database interfaces of the notmuch mail library
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "database-private.h"
+#include "string-util.h"
+
+#include <iostream>
+
+#include <sys/time.h>
+#include <sys/stat.h>
+#include <signal.h>
+#include <ftw.h>
+
+#include <glib.h>               /* g_free, GPtrArray, GHashTable */
+#include <glib-object.h>        /* g_type_init */
+
+#include <gmime/gmime.h>        /* g_mime_init */
+
+using namespace std;
+
+typedef struct {
+    const char *name;
+    const char *prefix;
+    notmuch_field_flag_t flags;
+} prefix_t;
+
+#define NOTMUCH_DATABASE_VERSION 3
+
+#define STRINGIFY(s) _SUB_STRINGIFY (s)
+#define _SUB_STRINGIFY(s) #s
+
+#define LOG_XAPIAN_EXCEPTION(message, error) _log_xapian_exception (__location__, message, error)
+
+static void
+_log_xapian_exception (const char *where, notmuch_database_t *notmuch,  const Xapian::Error error)
+{
+    _notmuch_database_log (notmuch,
+                          "A Xapian exception occurred at %s: %s\n",
+                          where,
+                          error.get_msg ().c_str ());
+    notmuch->exception_reported = true;
+}
+
+notmuch_database_mode_t
+_notmuch_database_mode (notmuch_database_t *notmuch)
+{
+    if (notmuch->writable_xapian_db)
+       return NOTMUCH_DATABASE_MODE_READ_WRITE;
+    else
+       return NOTMUCH_DATABASE_MODE_READ_ONLY;
+}
+
+/* Here's the current schema for our database (for NOTMUCH_DATABASE_VERSION):
+ *
+ * We currently have three different types of documents (mail, ghost,
+ * and directory) and also some metadata.
+ *
+ * There are two kinds of prefixes used in notmuch. There are the
+ * human friendly 'prefix names' like "thread:", which are also used
+ * in the query parser, and the actual prefix terms in the database
+ * (e.g. "G"). The correspondence is maintained in the file scope data
+ * structure 'prefix_table'.
+ *
+ * Mail document
+ * -------------
+ * A mail document is associated with a particular email message. It
+ * is stored in one or more files on disk and is uniquely identified
+ * by its "id" field (which is generally the message ID). It is
+ * indexed with the following prefixed terms which the database uses
+ * to construct threads, etc.:
+ *
+ *    Single terms of given prefix:
+ *
+ *     type:   mail
+ *
+ *     id:     Unique ID of mail. This is from the Message-ID header
+ *             if present and not too long (see NOTMUCH_MESSAGE_ID_MAX).
+ *             If it's present and too long, then we use
+ *             "notmuch-sha1-<sha1_sum_of_message_id>".
+ *              If this header is not present, we use
+ *             "notmuch-sha1-<sha1_sum_of_entire_file>".
+ *
+ *     thread: The ID of the thread to which the mail belongs
+ *
+ *     replyto: The ID from the In-Reply-To header of the mail (if any).
+ *
+ *    Multiple terms of given prefix:
+ *
+ *     reference: All message IDs from In-Reply-To and References
+ *                headers in the message.
+ *
+ *     tag:       Any tags associated with this message by the user.
+ *
+ *     file-direntry:  A colon-separated pair of values
+ *                     (INTEGER:STRING), where INTEGER is the
+ *                     document ID of a directory document, and
+ *                     STRING is the name of a file within that
+ *                     directory for this mail message.
+ *
+ *      property:       Has a property with key=value
+ *                 FIXME: if no = is present, should match on any value
+ *
+ *    A mail document also has four values:
+ *
+ *     TIMESTAMP:      The time_t value corresponding to the message's
+ *                     Date header.
+ *
+ *     MESSAGE_ID:     The unique ID of the mail mess (see "id" above)
+ *
+ *     FROM:           The value of the "From" header
+ *
+ *     SUBJECT:        The value of the "Subject" header
+ *
+ *     LAST_MOD:       The revision number as of the last tag or
+ *                     filename change.
+ *
+ * The prefixed terms described above are also searchable without an
+ * explicit field name, but as of notmuch 0.29 this is due to
+ * query-parser setup, not extra terms in the database.  In addition,
+ * terms from the content of the message are added without a prefix
+ * for use by the user in searching. Note that the prefix name "body"
+ * is used to refer to the empty prefix string in the database.
+ *
+ * The path of the containing folder is added with the "folder" prefix
+ * (see _notmuch_message_add_folder_terms).  Sub-paths of the the path
+ * of the mail message are added with the "path" prefix.
+ *
+ * The data portion of a mail document is empty.
+ *
+ * Ghost mail document [if NOTMUCH_FEATURE_GHOSTS]
+ * -----------------------------------------------
+ * A ghost mail document is like a mail document, but where we don't
+ * have the message content.  These are used to track thread reference
+ * information for messages we haven't received.
+ *
+ * A ghost mail document has type: ghost; id and thread fields that
+ * are identical to the mail document fields; and a MESSAGE_ID value.
+ *
+ * Directory document
+ * ------------------
+ * A directory document is used by a client of the notmuch library to
+ * maintain data necessary to allow for efficient polling of mail
+ * directories.
+ *
+ * All directory documents contain one term:
+ *
+ *     directory:      The directory path (relative to the database path)
+ *                     Or the SHA1 sum of the directory path (if the
+ *                     path itself is too long to fit in a Xapian
+ *                     term).
+ *
+ * And all directory documents for directories other than top-level
+ * directories also contain the following term:
+ *
+ *     directory-direntry: A colon-separated pair of values
+ *                         (INTEGER:STRING), where INTEGER is the
+ *                         document ID of the parent directory
+ *                         document, and STRING is the name of this
+ *                         directory within that parent.
+ *
+ * All directory documents have a single value:
+ *
+ *     TIMESTAMP:      The mtime of the directory (at last scan)
+ *
+ * The data portion of a directory document contains the path of the
+ * directory (relative to the database path).
+ *
+ * Database metadata
+ * -----------------
+ * Xapian allows us to store arbitrary name-value pairs as
+ * "metadata". We currently use the following metadata names with the
+ * given meanings:
+ *
+ *     version         The database schema version, (which is distinct
+ *                     from both the notmuch package version (see
+ *                     notmuch --version) and the libnotmuch library
+ *                     version. The version is stored as an base-10
+ *                     ASCII integer. The initial database version
+ *                     was 1, (though a schema existed before that
+ *                     were no "version" database value existed at
+ *                     all). Successive versions are allocated as
+ *                     changes are made to the database (such as by
+ *                     indexing new fields).
+ *
+ *     features        The set of features supported by this
+ *                     database. This consists of a set of
+ *                     '\n'-separated lines, where each is a feature
+ *                     name, a '\t', and compatibility flags.  If the
+ *                     compatibility flags contain 'w', then the
+ *                     opener must support this feature to safely
+ *                     write this database.  If the compatibility
+ *                     flags contain 'r', then the opener must
+ *                     support this feature to read this database.
+ *                     Introduced in database version 3.
+ *
+ *     last_thread_id  The last thread ID generated. This is stored
+ *                     as a 16-byte hexadecimal ASCII representation
+ *                     of a 64-bit unsigned integer. The first ID
+ *                     generated is 1 and the value will be
+ *                     incremented for each thread ID.
+ *
+ *     C*              metadata keys starting with C indicate
+ *                     configuration data. It can be managed with the
+ *                     n_database_*config* API.  There is a convention
+ *                     of hierarchical keys separated by '.' (e.g.
+ *                     query.notmuch stores the value for the named
+ *                     query 'notmuch'), but it is not enforced by the
+ *                     API.
+ *
+ * Obsolete metadata
+ * -----------------
+ *
+ * If ! NOTMUCH_FEATURE_GHOSTS, there are no ghost mail documents.
+ * Instead, the database has the following additional database
+ * metadata:
+ *
+ *     thread_id_*     A pre-allocated thread ID for a particular
+ *                     message. This is actually an arbitrarily large
+ *                     family of metadata name. Any particular name is
+ *                     formed by concatenating "thread_id_" with a message
+ *                     ID (or the SHA1 sum of a message ID if it is very
+ *                     long---see description of 'id' in the mail
+ *                     document). The value stored is a thread ID.
+ *
+ *                     These thread ID metadata values are stored
+ *                     whenever a message references a parent message
+ *                     that does not yet exist in the database. A
+ *                     thread ID will be allocated and stored, and if
+ *                     the message is later added, the stored thread
+ *                     ID will be used (and the metadata value will
+ *                     be cleared).
+ *
+ *                     Even before a message is added, it's
+ *                     pre-allocated thread ID is useful so that all
+ *                     descendant messages that reference this common
+ *                     parent can be recognized as belonging to the
+ *                     same thread.
+ */
+
+
+notmuch_string_map_iterator_t *
+_notmuch_database_user_headers (notmuch_database_t *notmuch)
+{
+    return _notmuch_string_map_iterator_create (notmuch->user_header, "", false);
+}
+
+const char *
+notmuch_status_to_string (notmuch_status_t status)
+{
+    switch (status) {
+    case NOTMUCH_STATUS_SUCCESS:
+       return "No error occurred";
+    case NOTMUCH_STATUS_OUT_OF_MEMORY:
+       return "Out of memory";
+    case NOTMUCH_STATUS_READ_ONLY_DATABASE:
+       return "Attempt to write to a read-only database";
+    case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
+       return "A Xapian exception occurred";
+    case NOTMUCH_STATUS_FILE_ERROR:
+       return "Something went wrong trying to read or write a file";
+    case NOTMUCH_STATUS_FILE_NOT_EMAIL:
+       return "File is not an email";
+    case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+       return "Message ID is identical to a message in database";
+    case NOTMUCH_STATUS_NULL_POINTER:
+       return "Erroneous NULL pointer";
+    case NOTMUCH_STATUS_TAG_TOO_LONG:
+       return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
+    case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
+       return "Unbalanced number of calls to notmuch_message_freeze/thaw";
+    case NOTMUCH_STATUS_UNBALANCED_ATOMIC:
+       return "Unbalanced number of calls to notmuch_database_begin_atomic/end_atomic";
+    case NOTMUCH_STATUS_UNSUPPORTED_OPERATION:
+       return "Unsupported operation";
+    case NOTMUCH_STATUS_UPGRADE_REQUIRED:
+       return "Operation requires a database upgrade";
+    case NOTMUCH_STATUS_PATH_ERROR:
+       return "Path supplied is illegal for this function";
+    case NOTMUCH_STATUS_IGNORED:
+       return "Argument was ignored";
+    case NOTMUCH_STATUS_ILLEGAL_ARGUMENT:
+       return "Illegal argument for function";
+    case NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL:
+       return "Crypto protocol missing, malformed, or unintelligible";
+    case NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION:
+       return "Crypto engine initialization failure";
+    case NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL:
+       return "Unknown crypto protocol";
+    case NOTMUCH_STATUS_NO_CONFIG:
+       return "No configuration file found";
+    case NOTMUCH_STATUS_NO_DATABASE:
+       return "No database found";
+    case NOTMUCH_STATUS_DATABASE_EXISTS:
+       return "Database exists, not recreated";
+    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";
+    }
+}
+
+void
+_notmuch_database_log (notmuch_database_t *notmuch,
+                      const char *format,
+                      ...)
+{
+    va_list va_args;
+
+    va_start (va_args, format);
+
+    if (notmuch->status_string)
+       talloc_free (notmuch->status_string);
+
+    notmuch->status_string = talloc_vasprintf (notmuch, format, va_args);
+    va_end (va_args);
+}
+
+void
+_notmuch_database_log_append (notmuch_database_t *notmuch,
+                             const char *format,
+                             ...)
+{
+    va_list va_args;
+
+    va_start (va_args, format);
+
+    if (notmuch->status_string)
+       notmuch->status_string = talloc_vasprintf_append (notmuch->status_string, format, va_args);
+    else
+       notmuch->status_string = talloc_vasprintf (notmuch, format, va_args);
+
+    va_end (va_args);
+}
+
+static void
+find_doc_ids_for_term (notmuch_database_t *notmuch,
+                      const char *term,
+                      Xapian::PostingIterator *begin,
+                      Xapian::PostingIterator *end)
+{
+    *begin = notmuch->xapian_db->postlist_begin (term);
+
+    *end = notmuch->xapian_db->postlist_end (term);
+}
+
+void
+_notmuch_database_find_doc_ids (notmuch_database_t *notmuch,
+                               const char *prefix_name,
+                               const char *value,
+                               Xapian::PostingIterator *begin,
+                               Xapian::PostingIterator *end)
+{
+    char *term;
+
+    term = talloc_asprintf (notmuch, "%s%s",
+                           _find_prefix (prefix_name), value);
+
+    find_doc_ids_for_term (notmuch, term, begin, end);
+
+    talloc_free (term);
+}
+
+notmuch_private_status_t
+_notmuch_database_find_unique_doc_id (notmuch_database_t *notmuch,
+                                     const char *prefix_name,
+                                     const char *value,
+                                     unsigned int *doc_id)
+{
+    Xapian::PostingIterator i, end;
+
+    _notmuch_database_find_doc_ids (notmuch, prefix_name, value, &i, &end);
+
+    if (i == end) {
+       *doc_id = 0;
+       return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
+    }
+
+    *doc_id = *i;
+
+#if DEBUG_DATABASE_SANITY
+    i++;
+
+    if (i != end)
+       INTERNAL_ERROR ("Term %s:%s is not unique as expected.\n",
+                       prefix_name, value);
+#endif
+
+    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+static Xapian::Document
+find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
+{
+    return notmuch->xapian_db->get_document (doc_id);
+}
+
+/* Generate a compressed version of 'message_id' of the form:
+ *
+ *     notmuch-sha1-<sha1_sum_of_message_id>
+ */
+char *
+_notmuch_message_id_compressed (void *ctx, const char *message_id)
+{
+    char *sha1, *compressed;
+
+    sha1 = _notmuch_sha1_of_string (message_id);
+
+    compressed = talloc_asprintf (ctx, "notmuch-sha1-%s", sha1);
+    free (sha1);
+
+    return compressed;
+}
+
+notmuch_status_t
+notmuch_database_find_message (notmuch_database_t *notmuch,
+                              const char *message_id,
+                              notmuch_message_t **message_ret)
+{
+    notmuch_private_status_t status;
+    unsigned int doc_id;
+
+    if (message_ret == NULL)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
+       message_id = _notmuch_message_id_compressed (notmuch, message_id);
+
+    try {
+       status = _notmuch_database_find_unique_doc_id (notmuch, "id",
+                                                      message_id, &doc_id);
+
+       if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
+           *message_ret = NULL;
+       else {
+           *message_ret = _notmuch_message_create (notmuch, notmuch, doc_id,
+                                                   NULL);
+           if (*message_ret == NULL)
+               return NOTMUCH_STATUS_OUT_OF_MEMORY;
+       }
+
+       return NOTMUCH_STATUS_SUCCESS;
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch, "A Xapian exception occurred finding message: %s.\n",
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
+       *message_ret = NULL;
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+}
+
+notmuch_status_t
+_notmuch_database_ensure_writable (notmuch_database_t *notmuch)
+{
+    if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY) {
+       _notmuch_database_log (notmuch, "Cannot write to a read-only database.\n");
+       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;
+}
+
+/* Allocate a revision number for the next change. */
+unsigned long
+_notmuch_database_new_revision (notmuch_database_t *notmuch)
+{
+    unsigned long new_revision = notmuch->revision + 1;
+
+    /* If we're in an atomic section, hold off on updating the
+     * committed revision number until we commit the atomic section.
+     */
+    if (notmuch->atomic_nesting)
+       notmuch->atomic_dirty = true;
+    else
+       notmuch->revision = new_revision;
+
+    return new_revision;
+}
+
+notmuch_status_t
+notmuch_database_close (notmuch_database_t *notmuch)
+{
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+
+    /* Many Xapian objects (and thus notmuch objects) hold references to
+     * the database, so merely deleting the database may not suffice to
+     * close it.  Thus, we explicitly close it here. */
+    if (notmuch->open) {
+       try {
+           /* Close the database.  This implicitly flushes
+            * 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;
+           if (! notmuch->exception_reported) {
+               _notmuch_database_log (notmuch,
+                                      "Error: A Xapian exception occurred closing database: %s\n",
+                                      error.get_msg ().c_str ());
+           }
+       }
+    }
+    notmuch->open = false;
+    return status;
+}
+
+static int
+unlink_cb (const char *path,
+          unused (const struct stat *sb),
+          unused (int type),
+          unused (struct FTW *ftw))
+{
+    return remove (path);
+}
+
+static int
+rmtree (const char *path)
+{
+    return nftw (path, unlink_cb, 64, FTW_DEPTH | FTW_PHYS);
+}
+
+class NotmuchCompactor : public Xapian::Compactor
+{
+    notmuch_compact_status_cb_t status_cb;
+    void *status_closure;
+
+public:
+    NotmuchCompactor(notmuch_compact_status_cb_t cb, void *closure) :
+       status_cb (cb), status_closure (closure)
+    {
+    }
+
+    virtual void
+    set_status (const std::string &table, const std::string &status)
+    {
+       char *msg;
+
+       if (status_cb == NULL)
+           return;
+
+       if (status.length () == 0)
+           msg = talloc_asprintf (NULL, "compacting table %s", table.c_str ());
+       else
+           msg = talloc_asprintf (NULL, "     %s", status.c_str ());
+
+       if (msg == NULL) {
+           return;
+       }
+
+       status_cb (msg, status_closure);
+       talloc_free (msg);
+    }
+};
+
+/* Compacts the given database, optionally saving the original database
+ * in backup_path. Additionally, a callback function can be provided to
+ * give the user feedback on the progress of the (likely long-lived)
+ * compaction process.
+ *
+ * The backup path must point to a directory on the same volume as the
+ * original database. Passing a NULL backup_path will result in the
+ * uncompacted database being deleted after compaction has finished.
+ * Note that the database write lock will be held during the
+ * compaction process to protect data integrity.
+ */
+notmuch_status_t
+notmuch_database_compact (const char *path,
+                         const char *backup_path,
+                         notmuch_compact_status_cb_t status_cb,
+                         void *closure)
+{
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    notmuch_database_t *notmuch = NULL;
+    char *message = NULL;
+
+    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;
+    }
+
+    _notmuch_config_cache (notmuch, NOTMUCH_CONFIG_DATABASE_PATH, path);
+
+    return notmuch_database_compact_db (notmuch,
+                                       backup_path,
+                                       status_cb,
+                                       closure);
+}
+
+notmuch_status_t
+notmuch_database_compact_db (notmuch_database_t *notmuch,
+                            const char *backup_path,
+                            notmuch_compact_status_cb_t status_cb,
+                            void *closure)
+{
+    void *local;
+    const char *xapian_path, *compact_xapian_path;
+    const char *path;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    struct stat statbuf;
+    bool keep_backup;
+    char *message;
+
+    ret = _notmuch_database_ensure_writable (notmuch);
+    if (ret)
+       return ret;
+
+    path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH);
+    if (! path)
+       return NOTMUCH_STATUS_PATH_ERROR;
+
+    local = talloc_new (NULL);
+    if (! local)
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+    ret = _notmuch_choose_xapian_path (local, path, &xapian_path, &message);
+    if (ret)
+       goto DONE;
+
+    if (! (compact_xapian_path = talloc_asprintf (local, "%s.compact", xapian_path))) {
+       ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    if (backup_path == NULL) {
+       if (! (backup_path = talloc_asprintf (local, "%s.old", xapian_path))) {
+           ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+           goto DONE;
+       }
+       keep_backup = false;
+    } else {
+       keep_backup = true;
+    }
+
+    if (stat (backup_path, &statbuf) != -1) {
+       _notmuch_database_log (notmuch, "Path already exists: %s\n", backup_path);
+       ret = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
+    }
+    if (errno != ENOENT) {
+       _notmuch_database_log (notmuch, "Unknown error while stat()ing path: %s\n",
+                              strerror (errno));
+       ret = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
+    }
+
+    /* Unconditionally attempt to remove old work-in-progress database (if
+     * any). This is "protected" by database lock. If this fails due to write
+     * errors (etc), the following code will fail and provide error message.
+     */
+    (void) rmtree (compact_xapian_path);
+
+    try {
+       NotmuchCompactor compactor (status_cb, closure);
+       notmuch->xapian_db->compact (compact_xapian_path, Xapian::DBCOMPACT_NO_RENUMBER, 0,
+                                    compactor);
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch, "Error while compacting: %s\n", error.get_msg ().c_str ());
+       ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+       goto DONE;
+    }
+
+    if (rename (xapian_path, backup_path)) {
+       _notmuch_database_log (notmuch, "Error moving %s to %s: %s\n",
+                              xapian_path, backup_path, strerror (errno));
+       ret = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
+    }
+
+    if (rename (compact_xapian_path, xapian_path)) {
+       _notmuch_database_log (notmuch, "Error moving %s to %s: %s\n",
+                              compact_xapian_path, xapian_path, strerror (errno));
+       ret = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
+    }
+
+    if (! keep_backup) {
+       if (rmtree (backup_path)) {
+           _notmuch_database_log (notmuch, "Error removing old database %s: %s\n",
+                                  backup_path, strerror (errno));
+           ret = NOTMUCH_STATUS_FILE_ERROR;
+           goto DONE;
+       }
+    }
+
+  DONE:
+    if (notmuch) {
+       notmuch_status_t ret2;
+
+       const char *str = notmuch_database_status_string (notmuch);
+       if (status_cb && str)
+           status_cb (str, closure);
+
+       ret2 = notmuch_database_destroy (notmuch);
+
+       /* don't clobber previous error status */
+       if (ret == NOTMUCH_STATUS_SUCCESS && ret2 != NOTMUCH_STATUS_SUCCESS)
+           ret = ret2;
+    }
+
+    talloc_free (local);
+
+    return ret;
+}
+
+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);
+
+    delete notmuch->term_gen;
+    notmuch->term_gen = NULL;
+    delete notmuch->query_parser;
+    notmuch->query_parser = NULL;
+    delete notmuch->xapian_db;
+    notmuch->xapian_db = NULL;
+    delete notmuch->value_range_processor;
+    notmuch->value_range_processor = NULL;
+    delete notmuch->date_range_processor;
+    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);
+
+    return status;
+}
+
+const char *
+notmuch_database_get_path (notmuch_database_t *notmuch)
+{
+    return notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH);
+}
+
+unsigned int
+notmuch_database_get_version (notmuch_database_t *notmuch)
+{
+    unsigned int version;
+    string version_string;
+    const char *str;
+    char *end;
+
+    try {
+       version_string = notmuch->xapian_db->get_metadata ("version");
+    } catch (const Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (notmuch, error);
+       return 0;
+    }
+
+    if (version_string.empty ())
+       return 0;
+
+    str = version_string.c_str ();
+    if (str == NULL || *str == '\0')
+       return 0;
+
+    version = strtoul (str, &end, 10);
+    if (*end != '\0')
+       INTERNAL_ERROR ("Malformed database version: %s", str);
+
+    return version;
+}
+
+notmuch_bool_t
+notmuch_database_needs_upgrade (notmuch_database_t *notmuch)
+{
+    unsigned int version;
+
+    if (_notmuch_database_mode (notmuch) != NOTMUCH_DATABASE_MODE_READ_WRITE)
+       return FALSE;
+
+    if (NOTMUCH_FEATURES_CURRENT & ~notmuch->features)
+       return TRUE;
+
+    version = notmuch_database_get_version (notmuch);
+
+    return (version > 0 && version < NOTMUCH_DATABASE_VERSION);
+}
+
+static volatile sig_atomic_t do_progress_notify = 0;
+
+static void
+handle_sigalrm (unused (int signal))
+{
+    do_progress_notify = 1;
+}
+
+/* Upgrade the current database.
+ *
+ * After opening a database in read-write mode, the client should
+ * check if an upgrade is needed (notmuch_database_needs_upgrade) and
+ * if so, upgrade with this function before making any modifications.
+ *
+ * The optional progress_notify callback can be used by the caller to
+ * provide progress indication to the user. If non-NULL it will be
+ * called periodically with 'count' as the number of messages upgraded
+ * so far and 'total' the overall number of messages that will be
+ * converted.
+ */
+notmuch_status_t
+notmuch_database_upgrade (notmuch_database_t *notmuch,
+                         void (*progress_notify)(void *closure,
+                                                 double progress),
+                         void *closure)
+{
+    void *local = talloc_new (NULL);
+    Xapian::TermIterator t, t_end;
+    Xapian::WritableDatabase *db;
+    struct sigaction action;
+    struct itimerval timerval;
+    bool timer_is_active = false;
+    enum _notmuch_features target_features, new_features;
+    notmuch_status_t status;
+    notmuch_private_status_t private_status;
+    notmuch_query_t *query = NULL;
+    unsigned int count = 0, total = 0;
+
+    if (_notmuch_database_mode (notmuch) != NOTMUCH_DATABASE_MODE_READ_WRITE)
+       return NOTMUCH_STATUS_READ_ONLY_DATABASE;
+
+    db = notmuch->writable_xapian_db;
+
+    target_features = notmuch->features | NOTMUCH_FEATURES_CURRENT;
+    new_features = NOTMUCH_FEATURES_CURRENT & ~notmuch->features;
+
+    if (! notmuch_database_needs_upgrade (notmuch))
+       return NOTMUCH_STATUS_SUCCESS;
+
+    if (progress_notify) {
+       /* Set up our handler for SIGALRM */
+       memset (&action, 0, sizeof (struct sigaction));
+       action.sa_handler = handle_sigalrm;
+       sigemptyset (&action.sa_mask);
+       action.sa_flags = SA_RESTART;
+       sigaction (SIGALRM, &action, NULL);
+
+       /* Then start a timer to send SIGALRM once per second. */
+       timerval.it_interval.tv_sec = 1;
+       timerval.it_interval.tv_usec = 0;
+       timerval.it_value.tv_sec = 1;
+       timerval.it_value.tv_usec = 0;
+       setitimer (ITIMER_REAL, &timerval, NULL);
+
+       timer_is_active = true;
+    }
+
+    /* Figure out how much total work we need to do. */
+    if (new_features &
+       (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER |
+        NOTMUCH_FEATURE_LAST_MOD)) {
+       query = notmuch_query_create (notmuch, "");
+       unsigned msg_count;
+
+       status = notmuch_query_count_messages (query, &msg_count);
+       if (status)
+           goto DONE;
+
+       total += msg_count;
+       notmuch_query_destroy (query);
+       query = NULL;
+    }
+    if (new_features & NOTMUCH_FEATURE_DIRECTORY_DOCS) {
+       t_end = db->allterms_end ("XTIMESTAMP");
+       for (t = db->allterms_begin ("XTIMESTAMP"); t != t_end; t++)
+           ++total;
+    }
+    if (new_features & NOTMUCH_FEATURE_GHOSTS) {
+       /* The ghost message upgrade converts all thread_id_*
+        * metadata values into ghost message documents. */
+       t_end = db->metadata_keys_end ("thread_id_");
+       for (t = db->metadata_keys_begin ("thread_id_"); t != t_end; ++t)
+           ++total;
+    }
+
+    /* Perform the upgrade in a transaction. */
+    db->begin_transaction (true);
+
+    /* Set the target features so we write out changes in the desired
+     * format. */
+    notmuch->features = target_features;
+
+    /* Perform per-message upgrades. */
+    if (new_features &
+       (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER |
+        NOTMUCH_FEATURE_LAST_MOD)) {
+       notmuch_messages_t *messages;
+       notmuch_message_t *message;
+       char *filename;
+
+       query = notmuch_query_create (notmuch, "");
+
+       status = notmuch_query_search_messages (query, &messages);
+       if (status)
+           goto DONE;
+       for (;
+            notmuch_messages_valid (messages);
+            notmuch_messages_move_to_next (messages)) {
+           if (do_progress_notify) {
+               progress_notify (closure, (double) count / total);
+               do_progress_notify = 0;
+           }
+
+           message = notmuch_messages_get (messages);
+
+           /* Before version 1, each message document had its
+            * filename in the data field. Copy that into the new
+            * format by calling notmuch_message_add_filename.
+            */
+           if (new_features & NOTMUCH_FEATURE_FILE_TERMS) {
+               filename = _notmuch_message_talloc_copy_data (message);
+               if (filename && *filename != '\0') {
+                   _notmuch_message_add_filename (message, filename);
+                   _notmuch_message_clear_data (message);
+               }
+               talloc_free (filename);
+           }
+
+           /* Prior to version 2, the "folder:" prefix was
+            * probabilistic and stemmed. Change it to the current
+            * boolean prefix. Add "path:" prefixes while at it.
+            */
+           if (new_features & NOTMUCH_FEATURE_BOOL_FOLDER)
+               _notmuch_message_upgrade_folder (message);
+
+           /* Prior to NOTMUCH_FEATURE_LAST_MOD, messages did not
+            * track modification revisions.  Give all messages the
+            * next available revision; since we just started tracking
+            * revisions for this database, that will be 1.
+            */
+           if (new_features & NOTMUCH_FEATURE_LAST_MOD)
+               _notmuch_message_upgrade_last_mod (message);
+
+           _notmuch_message_sync (message);
+
+           notmuch_message_destroy (message);
+
+           count++;
+       }
+
+       notmuch_query_destroy (query);
+       query = NULL;
+    }
+
+    /* Perform per-directory upgrades. */
+
+    /* Before version 1 we stored directory timestamps in
+     * XTIMESTAMP documents instead of the current XDIRECTORY
+     * documents. So copy those as well. */
+    if (new_features & NOTMUCH_FEATURE_DIRECTORY_DOCS) {
+       t_end = notmuch->xapian_db->allterms_end ("XTIMESTAMP");
+
+       for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
+            t != t_end;
+            t++) {
+           Xapian::PostingIterator p, p_end;
+           std::string term = *t;
+
+           p_end = notmuch->xapian_db->postlist_end (term);
+
+           for (p = notmuch->xapian_db->postlist_begin (term);
+                p != p_end;
+                p++) {
+               Xapian::Document document;
+               time_t mtime;
+               notmuch_directory_t *directory;
+
+               if (do_progress_notify) {
+                   progress_notify (closure, (double) count / total);
+                   do_progress_notify = 0;
+               }
+
+               document = find_document_for_doc_id (notmuch, *p);
+               mtime = Xapian::sortable_unserialise (
+                   document.get_value (NOTMUCH_VALUE_TIMESTAMP));
+
+               directory = _notmuch_directory_find_or_create (notmuch, term.c_str () + 10,
+                                                              NOTMUCH_FIND_CREATE, &status);
+               notmuch_directory_set_mtime (directory, mtime);
+               notmuch_directory_destroy (directory);
+
+               db->delete_document (*p);
+           }
+
+           ++count;
+       }
+    }
+
+    /* Perform metadata upgrades. */
+
+    /* Prior to NOTMUCH_FEATURE_GHOSTS, thread IDs for missing
+     * messages were stored as database metadata. Change these to
+     * ghost messages.
+     */
+    if (new_features & NOTMUCH_FEATURE_GHOSTS) {
+       notmuch_message_t *message;
+       std::string message_id, thread_id;
+
+       t_end = db->metadata_keys_end (NOTMUCH_METADATA_THREAD_ID_PREFIX);
+       for (t = db->metadata_keys_begin (NOTMUCH_METADATA_THREAD_ID_PREFIX);
+            t != t_end; ++t) {
+           if (do_progress_notify) {
+               progress_notify (closure, (double) count / total);
+               do_progress_notify = 0;
+           }
+
+           message_id = (*t).substr (
+               strlen (NOTMUCH_METADATA_THREAD_ID_PREFIX));
+           thread_id = db->get_metadata (*t);
+
+           /* Create ghost message */
+           message = _notmuch_message_create_for_message_id (
+               notmuch, message_id.c_str (), &private_status);
+           if (private_status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
+               /* Document already exists; ignore the stored thread ID */
+           } else if (private_status ==
+                      NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+               private_status = _notmuch_message_initialize_ghost (
+                   message, thread_id.c_str ());
+               if (! private_status)
+                   _notmuch_message_sync (message);
+           }
+
+           if (private_status) {
+               _notmuch_database_log (notmuch,
+                                      "Upgrade failed while creating ghost messages.\n");
+               status = COERCE_STATUS (private_status,
+                                       "Unexpected status from _notmuch_message_initialize_ghost");
+               goto DONE;
+           }
+
+           /* Clear saved metadata thread ID */
+           db->set_metadata (*t, "");
+
+           ++count;
+       }
+    }
+
+    status = NOTMUCH_STATUS_SUCCESS;
+    db->set_metadata ("features", _notmuch_database_print_features (local, notmuch->features));
+    db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));
+
+  DONE:
+    if (status == NOTMUCH_STATUS_SUCCESS)
+       db->commit_transaction ();
+    else
+       db->cancel_transaction ();
+
+    if (timer_is_active) {
+       /* Now stop the timer. */
+       timerval.it_interval.tv_sec = 0;
+       timerval.it_interval.tv_usec = 0;
+       timerval.it_value.tv_sec = 0;
+       timerval.it_value.tv_usec = 0;
+       setitimer (ITIMER_REAL, &timerval, NULL);
+
+       /* And disable the signal handler. */
+       action.sa_handler = SIG_IGN;
+       sigaction (SIGALRM, &action, NULL);
+    }
+
+    if (query)
+       notmuch_query_destroy (query);
+
+    talloc_free (local);
+    return status;
+}
+
+notmuch_status_t
+notmuch_database_begin_atomic (notmuch_database_t *notmuch)
+{
+    if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY ||
+       notmuch->atomic_nesting > 0)
+       goto DONE;
+
+    if (notmuch_database_needs_upgrade (notmuch))
+       return NOTMUCH_STATUS_UPGRADE_REQUIRED;
+
+    try {
+       notmuch->writable_xapian_db->begin_transaction (false);
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch, "A Xapian exception occurred beginning transaction: %s.\n",
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+  DONE:
+    notmuch->atomic_nesting++;
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_database_end_atomic (notmuch_database_t *notmuch)
+{
+    Xapian::WritableDatabase *db;
+
+    if (notmuch->atomic_nesting == 0)
+       return NOTMUCH_STATUS_UNBALANCED_ATOMIC;
+
+    if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY ||
+       notmuch->atomic_nesting > 1)
+       goto DONE;
+
+    db = notmuch->writable_xapian_db;
+    try {
+       db->commit_transaction ();
+       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 ((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 ());
+       notmuch->exception_reported = true;
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    if (notmuch->atomic_dirty) {
+       ++notmuch->revision;
+       notmuch->atomic_dirty = false;
+    }
+
+  DONE:
+    notmuch->atomic_nesting--;
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+unsigned long
+notmuch_database_get_revision (notmuch_database_t *notmuch,
+                              const char **uuid)
+{
+    if (uuid)
+       *uuid = notmuch->uuid;
+    return notmuch->revision;
+}
+
+/* We allow the user to use arbitrarily long paths for directories. But
+ * we have a term-length limit. So if we exceed that, we'll use the
+ * SHA-1 of the path for the database term.
+ *
+ * Note: This function may return the original value of 'path'. If it
+ * does not, then the caller is responsible to free() the returned
+ * value.
+ */
+const char *
+_notmuch_database_get_directory_db_path (const char *path)
+{
+    int term_len = strlen (_find_prefix ("directory")) + strlen (path);
+
+    if (term_len > NOTMUCH_TERM_MAX)
+       return _notmuch_sha1_of_string (path);
+    else
+       return path;
+}
+
+/* Given a path, split it into two parts: the directory part is all
+ * components except for the last, and the basename is that last
+ * component. Getting the return-value for either part is optional
+ * (the caller can pass NULL).
+ *
+ * The original 'path' can represent either a regular file or a
+ * directory---the splitting will be carried out in the same way in
+ * either case. Trailing slashes on 'path' will be ignored, and any
+ * cases of multiple '/' characters appearing in series will be
+ * treated as a single '/'.
+ *
+ * Allocation (if any) will have 'ctx' as the talloc owner. But
+ * pointers will be returned within the original path string whenever
+ * possible.
+ *
+ * Note: If 'path' is non-empty and contains no non-trailing slash,
+ * (that is, consists of a filename with no parent directory), then
+ * the directory returned will be an empty string. However, if 'path'
+ * is an empty string, then both directory and basename will be
+ * returned as NULL.
+ */
+notmuch_status_t
+_notmuch_database_split_path (void *ctx,
+                             const char *path,
+                             const char **directory,
+                             const char **basename)
+{
+    const char *slash;
+
+    if (path == NULL || *path == '\0') {
+       if (directory)
+           *directory = NULL;
+       if (basename)
+           *basename = NULL;
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    /* Find the last slash (not counting a trailing slash), if any. */
+
+    slash = path + strlen (path) - 1;
+
+    /* First, skip trailing slashes. */
+    while (slash != path && *slash == '/')
+       --slash;
+
+    /* Then, find a slash. */
+    while (slash != path && *slash != '/') {
+       if (basename)
+           *basename = slash;
+
+       --slash;
+    }
+
+    /* Finally, skip multiple slashes. */
+    while (slash != path && *(slash - 1) == '/')
+       --slash;
+
+    if (slash == path) {
+       if (directory)
+           *directory = talloc_strdup (ctx, "");
+       if (basename)
+           *basename = path;
+    } else {
+       if (directory)
+           *directory = talloc_strndup (ctx, path, slash - path);
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Find the document ID of the specified directory.
+ *
+ * If (flags & NOTMUCH_FIND_CREATE), a new directory document will be
+ * created if one does not exist for 'path'.  Otherwise, if the
+ * directory document does not exist, this sets *directory_id to
+ * ((unsigned int)-1) and returns NOTMUCH_STATUS_SUCCESS.
+ */
+notmuch_status_t
+_notmuch_database_find_directory_id (notmuch_database_t *notmuch,
+                                    const char *path,
+                                    notmuch_find_flags_t flags,
+                                    unsigned int *directory_id)
+{
+    notmuch_directory_t *directory;
+    notmuch_status_t status;
+
+    if (path == NULL) {
+       *directory_id = 0;
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    directory = _notmuch_directory_find_or_create (notmuch, path, flags, &status);
+    if (status || ! directory) {
+       *directory_id = -1;
+       return status;
+    }
+
+    *directory_id = _notmuch_directory_get_document_id (directory);
+
+    notmuch_directory_destroy (directory);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+const char *
+_notmuch_database_get_directory_path (void *ctx,
+                                     notmuch_database_t *notmuch,
+                                     unsigned int doc_id)
+{
+    Xapian::Document document;
+
+    document = find_document_for_doc_id (notmuch, doc_id);
+
+    return talloc_strdup (ctx, document.get_data ().c_str ());
+}
+
+/* Given a legal 'filename' for the database, (either relative to
+ * database path or absolute with initial components identical to
+ * database path), return a new string (with 'ctx' as the talloc
+ * owner) suitable for use as a direntry term value.
+ *
+ * If (flags & NOTMUCH_FIND_CREATE), the necessary directory documents
+ * will be created in the database as needed.  Otherwise, if the
+ * necessary directory documents do not exist, this sets
+ * *direntry to NULL and returns NOTMUCH_STATUS_SUCCESS.
+ */
+notmuch_status_t
+_notmuch_database_filename_to_direntry (void *ctx,
+                                       notmuch_database_t *notmuch,
+                                       const char *filename,
+                                       notmuch_find_flags_t flags,
+                                       char **direntry)
+{
+    const char *relative, *directory, *basename;
+    Xapian::docid directory_id;
+    notmuch_status_t status;
+
+    relative = _notmuch_database_relative_path (notmuch, filename);
+
+    status = _notmuch_database_split_path (ctx, relative,
+                                          &directory, &basename);
+    if (status)
+       return status;
+
+    status = _notmuch_database_find_directory_id (notmuch, directory, flags,
+                                                 &directory_id);
+    if (status || directory_id == (unsigned int) -1) {
+       *direntry = NULL;
+       return status;
+    }
+
+    *direntry = talloc_asprintf (ctx, "%u:%s", directory_id, basename);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Given a legal 'path' for the database, return the relative path.
+ *
+ * The return value will be a pointer to the original path contents,
+ * and will be either the original string (if 'path' was relative) or
+ * a portion of the string (if path was absolute and begins with the
+ * database path).
+ */
+const char *
+_notmuch_database_relative_path (notmuch_database_t *notmuch,
+                                const char *path)
+{
+    const char *db_path, *relative;
+    unsigned int db_path_len;
+
+    db_path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT);
+    db_path_len = strlen (db_path);
+
+    relative = path;
+
+    if (*relative == '/') {
+       while (*relative == '/' && *(relative + 1) == '/')
+           relative++;
+
+       if (strncmp (relative, db_path, db_path_len) == 0) {
+           relative += db_path_len;
+           while (*relative == '/')
+               relative++;
+       }
+    }
+
+    return relative;
+}
+
+notmuch_status_t
+notmuch_database_get_directory (notmuch_database_t *notmuch,
+                               const char *path,
+                               notmuch_directory_t **directory)
+{
+    notmuch_status_t status;
+
+    if (directory == NULL)
+       return NOTMUCH_STATUS_NULL_POINTER;
+    *directory = NULL;
+
+    try {
+       *directory = _notmuch_directory_find_or_create (notmuch, path,
+                                                       NOTMUCH_FIND_LOOKUP, &status);
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch, "A Xapian exception occurred getting directory: %s.\n",
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
+       status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+    return status;
+}
+
+/* Allocate a document ID that satisfies the following criteria:
+ *
+ * 1. The ID does not exist for any document in the Xapian database
+ *
+ * 2. The ID was not previously returned from this function
+ *
+ * 3. The ID is the smallest integer satisfying (1) and (2)
+ *
+ * This function will trigger an internal error if these constraints
+ * cannot all be satisfied, (that is, the pool of available document
+ * IDs has been exhausted).
+ */
+unsigned int
+_notmuch_database_generate_doc_id (notmuch_database_t *notmuch)
+{
+    assert (notmuch->last_doc_id >= notmuch->xapian_db->get_lastdocid ());
+
+    notmuch->last_doc_id++;
+
+    if (notmuch->last_doc_id == 0)
+       INTERNAL_ERROR ("Xapian document IDs are exhausted.\n");
+
+    return notmuch->last_doc_id;
+}
+
+notmuch_status_t
+notmuch_database_remove_message (notmuch_database_t *notmuch,
+                                const char *filename)
+{
+    notmuch_status_t status;
+    notmuch_message_t *message;
+
+    status = notmuch_database_find_message_by_filename (notmuch, filename,
+                                                       &message);
+
+    if (status == NOTMUCH_STATUS_SUCCESS && message) {
+       if (notmuch_message_count_files (message) > 1) {
+           status = _notmuch_message_remove_filename (message, filename);
+       }
+       if (status == NOTMUCH_STATUS_SUCCESS)
+           status = _notmuch_message_delete (message);
+       else if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
+           _notmuch_message_sync (message);
+
+       notmuch_message_destroy (message);
+    }
+
+    return status;
+}
+
+notmuch_status_t
+notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
+                                          const char *filename,
+                                          notmuch_message_t **message_ret)
+{
+    void *local;
+    const char *prefix = _find_prefix ("file-direntry");
+    char *direntry, *term;
+    Xapian::PostingIterator i, end;
+    notmuch_status_t status;
+
+    if (message_ret == NULL)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    if (! (notmuch->features & NOTMUCH_FEATURE_FILE_TERMS))
+       return NOTMUCH_STATUS_UPGRADE_REQUIRED;
+
+    /* return NULL on any failure */
+    *message_ret = NULL;
+
+    local = talloc_new (notmuch);
+
+    try {
+       status = _notmuch_database_filename_to_direntry (
+           local, notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
+       if (status || ! direntry)
+           goto DONE;
+
+       term = talloc_asprintf (local, "%s%s", prefix, direntry);
+
+       find_doc_ids_for_term (notmuch, term, &i, &end);
+
+       if (i != end) {
+           notmuch_private_status_t private_status;
+
+           *message_ret = _notmuch_message_create (notmuch, notmuch, *i,
+                                                   &private_status);
+           if (*message_ret == NULL)
+               status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       }
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch,
+                              "Error: A Xapian exception occurred finding message by filename: %s\n",
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
+       status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+  DONE:
+    talloc_free (local);
+
+    if (status && *message_ret) {
+       notmuch_message_destroy (*message_ret);
+       *message_ret = NULL;
+    }
+    return status;
+}
+
+notmuch_string_list_t *
+_notmuch_database_get_terms_with_prefix (void *ctx, Xapian::TermIterator &i,
+                                        Xapian::TermIterator &end,
+                                        const char *prefix)
+{
+    int prefix_len = strlen (prefix);
+    notmuch_string_list_t *list;
+
+    list = _notmuch_string_list_create (ctx);
+    if (unlikely (list == NULL))
+       return NULL;
+
+    for (i.skip_to (prefix); i != end; i++) {
+       /* Terminate loop at first term without desired prefix. */
+       if (strncmp ((*i).c_str (), prefix, prefix_len))
+           break;
+
+       _notmuch_string_list_append (list, (*i).c_str () + prefix_len);
+    }
+
+    return list;
+}
+
+notmuch_tags_t *
+notmuch_database_get_all_tags (notmuch_database_t *db)
+{
+    Xapian::TermIterator i, end;
+    notmuch_string_list_t *tags;
+
+    try {
+       i = db->xapian_db->allterms_begin ();
+       end = db->xapian_db->allterms_end ();
+       tags = _notmuch_database_get_terms_with_prefix (db, i, end,
+                                                       _find_prefix ("tag"));
+       _notmuch_string_list_sort (tags);
+       return _notmuch_tags_create (db, tags);
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (db, "A Xapian exception occurred getting tags: %s.\n",
+                              error.get_msg ().c_str ());
+       db->exception_reported = true;
+       return NULL;
+    }
+}
+
+const char *
+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;
+}
diff --git a/lib/directory.cc b/lib/directory.cc
new file mode 100644 (file)
index 0000000..5cf64d7
--- /dev/null
@@ -0,0 +1,335 @@
+/* directory.cc - Results of directory-based searches from a notmuch database
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+#include "database-private.h"
+
+/* Create an iterator to iterate over the basenames of files (or
+ * directories) that all share a common parent directory.
+ */
+static notmuch_filenames_t *
+_create_filenames_for_terms_with_prefix (void *ctx,
+                                        notmuch_database_t *notmuch,
+                                        const char *prefix)
+{
+    notmuch_string_list_t *filename_list;
+    Xapian::TermIterator i, end;
+
+    i = notmuch->xapian_db->allterms_begin ();
+    end = notmuch->xapian_db->allterms_end ();
+    filename_list = _notmuch_database_get_terms_with_prefix (ctx, i, end,
+                                                            prefix);
+    if (unlikely (filename_list == NULL))
+       return NULL;
+
+    return _notmuch_filenames_create (ctx, filename_list);
+}
+
+struct _notmuch_directory {
+    notmuch_database_t *notmuch;
+    Xapian::docid document_id;
+    Xapian::Document doc;
+    time_t mtime;
+};
+
+#define LOG_XAPIAN_EXCEPTION(directory, error) _log_xapian_exception (__location__, directory, error)
+
+static void
+_log_xapian_exception (const char *where, notmuch_directory_t *dir,  const Xapian::Error error)
+{
+    notmuch_database_t *notmuch = dir->notmuch;
+
+    _notmuch_database_log (notmuch,
+                          "A Xapian exception occurred at %s: %s\n",
+                          where,
+                          error.get_msg ().c_str ());
+    notmuch->exception_reported = true;
+}
+
+
+/* We end up having to call the destructor explicitly because we had
+ * to use "placement new" in order to initialize C++ objects within a
+ * block that we allocated with talloc. So C++ is making talloc
+ * slightly less simple to use, (we wouldn't need
+ * talloc_set_destructor at all otherwise).
+ */
+static int
+_notmuch_directory_destructor (notmuch_directory_t *directory)
+{
+    directory->doc.~Document ();
+
+    return 0;
+}
+
+static notmuch_private_status_t
+find_directory_document (notmuch_database_t *notmuch,
+                        const char *db_path,
+                        Xapian::Document *document)
+{
+    notmuch_private_status_t status;
+    Xapian::docid doc_id;
+
+    status = _notmuch_database_find_unique_doc_id (notmuch, "directory",
+                                                  db_path, &doc_id);
+    if (status) {
+       *document = Xapian::Document ();
+       return status;
+    }
+
+    *document = notmuch->xapian_db->get_document (doc_id);
+    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+/* Find or create a directory document.
+ *
+ * 'path' should be a path relative to the path of 'database', or else
+ * should be an absolute path with initial components that match the
+ * path of 'database'.
+ *
+ * If (flags & NOTMUCH_FIND_CREATE), then the directory document will
+ * be created if it does not exist.  Otherwise, if the directory
+ * document does not exist, *status_ret is set to
+ * NOTMUCH_STATUS_SUCCESS and this returns NULL.
+ */
+notmuch_directory_t *
+_notmuch_directory_find_or_create (notmuch_database_t *notmuch,
+                                  const char *path,
+                                  notmuch_find_flags_t flags,
+                                  notmuch_status_t *status_ret)
+{
+    notmuch_directory_t *directory;
+    notmuch_private_status_t private_status;
+    const char *db_path;
+    bool create = (flags & NOTMUCH_FIND_CREATE);
+
+    if (! (notmuch->features & NOTMUCH_FEATURE_DIRECTORY_DOCS)) {
+       *status_ret = NOTMUCH_STATUS_UPGRADE_REQUIRED;
+       return NULL;
+    }
+
+    *status_ret = NOTMUCH_STATUS_SUCCESS;
+
+    path = _notmuch_database_relative_path (notmuch, path);
+
+    if (create && _notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY)
+       INTERNAL_ERROR ("Failure to ensure database is writable");
+
+    directory = talloc (notmuch, notmuch_directory_t);
+    if (unlikely (directory == NULL)) {
+       *status_ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       return NULL;
+    }
+
+    directory->notmuch = notmuch;
+
+    /* "placement new"---not actually allocating memory */
+    new (&directory->doc) Xapian::Document;
+
+    talloc_set_destructor (directory, _notmuch_directory_destructor);
+
+    db_path = _notmuch_database_get_directory_db_path (path);
+
+    try {
+       Xapian::TermIterator i, end;
+
+       private_status = find_directory_document (notmuch, db_path,
+                                                 &directory->doc);
+       directory->document_id = directory->doc.get_docid ();
+
+       if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+           if (! create) {
+               notmuch_directory_destroy (directory);
+               directory = NULL;
+               *status_ret = NOTMUCH_STATUS_SUCCESS;
+               goto DONE;
+           }
+
+           void *local = talloc_new (directory);
+           const char *parent, *basename;
+           Xapian::docid parent_id;
+           char *term = talloc_asprintf (local, "%s%s",
+                                         _find_prefix ("directory"), db_path);
+           directory->doc.add_term (term, 0);
+
+           directory->doc.set_data (path);
+
+           _notmuch_database_split_path (local, path, &parent, &basename);
+
+           *status_ret = _notmuch_database_find_directory_id (
+               notmuch, parent, NOTMUCH_FIND_CREATE, &parent_id);
+           if (*status_ret) {
+               notmuch_directory_destroy (directory);
+               directory = NULL;
+               goto DONE;
+           }
+
+           if (basename) {
+               term = talloc_asprintf (local, "%s%u:%s",
+                                       _find_prefix ("directory-direntry"),
+                                       parent_id, basename);
+               directory->doc.add_term (term, 0);
+           }
+
+           directory->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
+                                     Xapian::sortable_serialise (0));
+
+           directory->document_id = _notmuch_database_generate_doc_id (notmuch);
+           directory->notmuch->
+               writable_xapian_db
+               -> replace_document (directory->document_id, directory->doc);
+           talloc_free (local);
+       }
+
+       directory->mtime = Xapian::sortable_unserialise (
+           directory->doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch,
+                              "A Xapian exception occurred finding/creating a directory: %s.\n",
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
+       notmuch_directory_destroy (directory);
+       directory = NULL;
+       *status_ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+  DONE:
+    if (db_path != path)
+       free ((char *) db_path);
+
+    return directory;
+}
+
+unsigned int
+_notmuch_directory_get_document_id (notmuch_directory_t *directory)
+{
+    return directory->document_id;
+}
+
+notmuch_status_t
+notmuch_directory_set_mtime (notmuch_directory_t *directory,
+                            time_t mtime)
+{
+    notmuch_database_t *notmuch = directory->notmuch;
+    notmuch_status_t status;
+
+    status = _notmuch_database_ensure_writable (notmuch);
+    if (status)
+       return status;
+
+    try {
+       directory->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
+                                 Xapian::sortable_serialise (mtime));
+
+       directory->notmuch
+           ->writable_xapian_db->replace_document (directory->document_id, directory->doc);
+
+       directory->mtime = mtime;
+
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch,
+                              "A Xapian exception occurred setting directory mtime: %s.\n",
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+time_t
+notmuch_directory_get_mtime (notmuch_directory_t *directory)
+{
+    return directory->mtime;
+}
+
+notmuch_filenames_t *
+notmuch_directory_get_child_files (notmuch_directory_t *directory)
+{
+    char *term;
+    notmuch_filenames_t *child_files = NULL;
+
+    term = talloc_asprintf (directory, "%s%u:",
+                           _find_prefix ("file-direntry"),
+                           directory->document_id);
+
+    try {
+       child_files = _create_filenames_for_terms_with_prefix (directory,
+                                                              directory->notmuch,
+                                                              term);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (directory, error);
+    }
+
+    talloc_free (term);
+
+    return child_files;
+}
+
+notmuch_filenames_t *
+notmuch_directory_get_child_directories (notmuch_directory_t *directory)
+{
+    char *term;
+    notmuch_filenames_t *child_directories = NULL;
+
+    term = talloc_asprintf (directory, "%s%u:",
+                           _find_prefix ("directory-direntry"),
+                           directory->document_id);
+
+    try {
+       child_directories = _create_filenames_for_terms_with_prefix (directory,
+                                                                    directory->notmuch, term);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (directory, error);
+    }
+
+    talloc_free (term);
+
+    return child_directories;
+}
+
+notmuch_status_t
+notmuch_directory_delete (notmuch_directory_t *directory)
+{
+    notmuch_status_t status;
+
+    status = _notmuch_database_ensure_writable (directory->notmuch);
+    if (status)
+       return status;
+
+    try {
+       directory->notmuch->
+           writable_xapian_db->delete_document (directory->document_id);
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (directory->notmuch,
+                              "A Xapian exception occurred deleting directory entry: %s.\n",
+                              error.get_msg ().c_str ());
+       directory->notmuch->exception_reported = true;
+       status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+    notmuch_directory_destroy (directory);
+
+    return status;
+}
+
+void
+notmuch_directory_destroy (notmuch_directory_t *directory)
+{
+    talloc_free (directory);
+}
diff --git a/lib/features.cc b/lib/features.cc
new file mode 100644 (file)
index 0000000..cf0196c
--- /dev/null
@@ -0,0 +1,114 @@
+#include "database-private.h"
+
+static const struct {
+    /* NOTMUCH_FEATURE_* value. */
+    _notmuch_features value;
+    /* Feature name as it appears in the database.  This name should
+     * be appropriate for displaying to the user if an older version
+     * of notmuch doesn't support this feature. */
+    const char *name;
+    /* Compatibility flags when this feature is declared. */
+    const char *flags;
+} feature_names[] = {
+    { NOTMUCH_FEATURE_FILE_TERMS,
+      "multiple paths per message", "rw" },
+    { NOTMUCH_FEATURE_DIRECTORY_DOCS,
+      "relative directory paths", "rw" },
+    /* Header values are not required for reading a database because a
+     * reader can just refer to the message file. */
+    { NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES,
+      "from/subject/message-ID in database", "w" },
+    { NOTMUCH_FEATURE_BOOL_FOLDER,
+      "exact folder:/path: search", "rw" },
+    { NOTMUCH_FEATURE_GHOSTS,
+      "mail documents for missing messages", "w" },
+    /* Knowledge of the index mime-types are not required for reading
+     * a database because a reader will just be unable to query
+     * them. */
+    { NOTMUCH_FEATURE_INDEXED_MIMETYPES,
+      "indexed MIME types", "w" },
+    { NOTMUCH_FEATURE_LAST_MOD,
+      "modification tracking", "w" },
+    /* Existing databases will work fine for all queries not involving
+     * 'body:' */
+    { NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY,
+      "index body and headers separately", "w" },
+};
+
+char *
+_notmuch_database_print_features (const void *ctx, unsigned int features)
+{
+    unsigned int i;
+    char *res = talloc_strdup (ctx, "");
+
+    for (i = 0; i < ARRAY_SIZE (feature_names); ++i)
+       if (features & feature_names[i].value)
+           res = talloc_asprintf_append_buffer (
+               res, "%s\t%s\n", feature_names[i].name, feature_names[i].flags);
+
+    return res;
+}
+
+
+/* Parse a database features string from the given database version.
+ * Returns the feature bit set.
+ *
+ * For version < 3, this ignores the features string and returns a
+ * hard-coded set of features.
+ *
+ * If there are unrecognized features that are required to open the
+ * database in mode (which should be 'r' or 'w'), return a
+ * comma-separated list of unrecognized but required features in
+ * *incompat_out suitable for presenting to the user.  *incompat_out
+ * will be allocated from ctx.
+ */
+_notmuch_features
+_notmuch_database_parse_features (const void *ctx, const char *features, unsigned int version,
+                                 char mode, char **incompat_out)
+{
+    _notmuch_features res = static_cast<_notmuch_features>(0);
+    unsigned int namelen, i;
+    size_t llen = 0;
+    const char *flags;
+
+    /* Prior to database version 3, features were implied by the
+     * version number. */
+    if (version == 0)
+       return NOTMUCH_FEATURES_V0;
+    else if (version == 1)
+       return NOTMUCH_FEATURES_V1;
+    else if (version == 2)
+       return NOTMUCH_FEATURES_V2;
+
+    /* Parse the features string */
+    while ((features = strtok_len_c (features + llen, "\n", &llen)) != NULL) {
+       flags = strchr (features, '\t');
+       if (! flags || flags > features + llen)
+           continue;
+       namelen = flags - features;
+
+       for (i = 0; i < ARRAY_SIZE (feature_names); ++i) {
+           if (strlen (feature_names[i].name) == namelen &&
+               strncmp (feature_names[i].name, features, namelen) == 0) {
+               res |= feature_names[i].value;
+               break;
+           }
+       }
+
+       if (i == ARRAY_SIZE (feature_names) && incompat_out) {
+           /* Unrecognized feature */
+           const char *have = strchr (flags, mode);
+           if (have && have < features + llen) {
+               /* This feature is required to access this database in
+                * 'mode', but we don't understand it. */
+               if (! *incompat_out)
+                   *incompat_out = talloc_strdup (ctx, "");
+               *incompat_out = talloc_asprintf_append_buffer (
+                   *incompat_out, "%s%.*s", **incompat_out ? ", " : "",
+                   namelen, features);
+           }
+       }
+    }
+
+    return res;
+}
diff --git a/lib/filenames.c b/lib/filenames.c
new file mode 100644 (file)
index 0000000..37d631d
--- /dev/null
@@ -0,0 +1,76 @@
+/* filenames.c - Iterator for a list of filenames
+ *
+ * Copyright © 2010 Intel Corporation
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+
+struct _notmuch_filenames {
+    notmuch_string_node_t *iterator;
+};
+
+/* The notmuch_filenames_t iterates over a notmuch_string_list_t of
+ * file names */
+notmuch_filenames_t *
+_notmuch_filenames_create (const void *ctx,
+                          notmuch_string_list_t *list)
+{
+    notmuch_filenames_t *filenames;
+
+    filenames = talloc (ctx, notmuch_filenames_t);
+    if (unlikely (filenames == NULL))
+       return NULL;
+
+    filenames->iterator = list->head;
+    (void) talloc_reference (filenames, list);
+
+    return filenames;
+}
+
+notmuch_bool_t
+notmuch_filenames_valid (notmuch_filenames_t *filenames)
+{
+    if (filenames == NULL)
+       return false;
+
+    return (filenames->iterator != NULL);
+}
+
+const char *
+notmuch_filenames_get (notmuch_filenames_t *filenames)
+{
+    if ((filenames == NULL) || (filenames->iterator == NULL))
+       return NULL;
+
+    return filenames->iterator->string;
+}
+
+void
+notmuch_filenames_move_to_next (notmuch_filenames_t *filenames)
+{
+    if ((filenames == NULL) || (filenames->iterator == NULL))
+       return;
+
+    filenames->iterator = filenames->iterator->next;
+}
+
+void
+notmuch_filenames_destroy (notmuch_filenames_t *filenames)
+{
+    talloc_free (filenames);
+}
diff --git a/lib/index.cc b/lib/index.cc
new file mode 100644 (file)
index 0000000..629dcb2
--- /dev/null
@@ -0,0 +1,770 @@
+/*
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+
+#include <gmime/gmime.h>
+#include <gmime/gmime-filter.h>
+
+#include <xapian.h>
+
+
+typedef struct {
+    int state;
+    int a;
+    int b;
+    int next_if_match;
+    int next_if_not_match;
+} scanner_state_t;
+
+/* Simple, linear state-transition diagram for the uuencode filter.
+ *
+ * If the character being processed is within the range of [a, b]
+ * for the current state then we transition next_if_match
+ * state. If not, we transition to the next_if_not_match state.
+ *
+ * The final two states are special in that they are the states in
+ * which we discard data. */
+static const int first_uuencode_skipping_state = 11;
+static const scanner_state_t uuencode_states[] = {
+    { 0,  'b',  'b',  1,  0 },
+    { 1,  'e',  'e',  2,  0 },
+    { 2,  'g',  'g',  3,  0 },
+    { 3,  'i',  'i',  4,  0 },
+    { 4,  'n',  'n',  5,  0 },
+    { 5,  ' ',  ' ',  6,  0 },
+    { 6,  '0',  '7',  7,  0 },
+    { 7,  '0',  '7',  8,  0 },
+    { 8,  '0',  '7',  9,  0 },
+    { 9,  ' ',  ' ',  10, 0 },
+    { 10, '\n', '\n', 11, 10 },
+    { 11, 'M',  'M',  12, 0 },
+    { 12, ' ',  '`',  12, 11 }
+};
+
+/* The following table is intended to implement this DFA (in 'dot'
+ * format). Note that 2 and 3 are "hidden" states used to step through
+ * the possible out edges of state 1.
+ *
+ * digraph html_filter {
+ *     0 -> 1  [label="<"];
+ *     0 -> 0;
+ *     1 -> 4 [label="'"];
+ *     1 -> 5 [label="\""];
+ *     1 -> 0 [label=">"];
+ *     1 -> 1;
+ *     4 -> 1 [label="'"];
+ *     4 -> 4;
+ *     5 -> 1 [label="\""];
+ *     5 -> 5;
+ * }
+ */
+static const int first_html_skipping_state = 1;
+static const scanner_state_t html_states[] = {
+    { 0,  '<',  '<',  1,  0 },
+    { 1,  '\'', '\'', 4,  2 },  /* scanning for quote or > */
+    { 1,  '"',  '"',  5,  3 },
+    { 1,  '>',  '>',  0,  1 },
+    { 4,  '\'', '\'', 1,  4 },  /* inside single quotes */
+    { 5,  '"', '"',   1,  5 },  /* inside double quotes */
+};
+
+/* Oh, how I wish that gobject didn't require so much noisy boilerplate!
+ * (Though I have at least eliminated some of the stock set...) */
+typedef struct _NotmuchFilterDiscardNonTerm NotmuchFilterDiscardNonTerm;
+typedef struct _NotmuchFilterDiscardNonTermClass NotmuchFilterDiscardNonTermClass;
+
+/**
+ * NotmuchFilterDiscardNonTerm:
+ *
+ * @parent_object: parent #GMimeFilter
+ * @encode: encoding vs decoding
+ * @state: State of the parser
+ *
+ * A filter to discard uuencoded portions of an email.
+ *
+ * A uuencoded portion is identified as beginning with a line
+ * matching:
+ *
+ *     begin [0-7][0-7][0-7] .*
+ *
+ * After that detection, and beginning with the following line,
+ * characters will be discarded as long as the first character of each
+ * line begins with M and subsequent characters on the line are within
+ * the range of ASCII characters from ' ' to '`'.
+ *
+ * This is not a perfect UUencode filter. It's possible to have a
+ * message that will legitimately match that pattern, (so that some
+ * legitimate content is discarded). And for most UUencoded files, the
+ * final line of encoded data (the line not starting with M) will be
+ * indexed.
+ **/
+struct _NotmuchFilterDiscardNonTerm {
+    GMimeFilter parent_object;
+    GMimeContentType *content_type;
+    int state;
+    int first_skipping_state;
+    const scanner_state_t *states;
+};
+
+struct _NotmuchFilterDiscardNonTermClass {
+    GMimeFilterClass parent_class;
+};
+
+static GMimeFilter *notmuch_filter_discard_non_term_new (GMimeContentType *content);
+
+static void notmuch_filter_discard_non_term_finalize (GObject *object);
+
+static GMimeFilter *filter_copy (GMimeFilter *filter);
+static void filter_filter (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                          char **out, size_t *outlen, size_t *outprespace);
+static void filter_complete (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                            char **out, size_t *outlen, size_t *outprespace);
+static void filter_reset (GMimeFilter *filter);
+
+
+static GMimeFilterClass *parent_class = NULL;
+
+static void
+notmuch_filter_discard_non_term_class_init (NotmuchFilterDiscardNonTermClass *klass,
+                                           unused (void *class_data))
+{
+    GObjectClass *object_class = G_OBJECT_CLASS (klass);
+    GMimeFilterClass *filter_class = GMIME_FILTER_CLASS (klass);
+
+    object_class->finalize = notmuch_filter_discard_non_term_finalize;
+
+    filter_class->copy = filter_copy;
+    filter_class->filter = filter_filter;
+    filter_class->complete = filter_complete;
+    filter_class->reset = filter_reset;
+}
+
+static void
+notmuch_filter_discard_non_term_finalize (GObject *object)
+{
+    G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static GMimeFilter *
+filter_copy (GMimeFilter *gmime_filter)
+{
+    NotmuchFilterDiscardNonTerm *filter = (NotmuchFilterDiscardNonTerm *) gmime_filter;
+
+    return notmuch_filter_discard_non_term_new (filter->content_type);
+}
+
+static void
+filter_filter (GMimeFilter *gmime_filter, char *inbuf, size_t inlen, size_t prespace,
+              char **outbuf, size_t *outlen, size_t *outprespace)
+{
+    NotmuchFilterDiscardNonTerm *filter = (NotmuchFilterDiscardNonTerm *) gmime_filter;
+    const scanner_state_t *states = filter->states;
+    const char *inptr = inbuf;
+    const char *inend = inbuf + inlen;
+    char *outptr;
+
+    (void) prespace;
+
+    int next;
+
+    g_mime_filter_set_size (gmime_filter, inlen, false);
+    outptr = gmime_filter->outbuf;
+
+    next = filter->state;
+    while (inptr < inend) {
+       /* Each state is defined by a contiguous set of rows of the
+        * state table marked by a common value for '.state'. The
+        * state numbers must be equal to the index of the first row
+        * in a given state; thus the loop condition here looks for a
+        * jump to a first row of a state, which is a real transition
+        * in the underlying DFA.
+        */
+       do {
+           if (*inptr >= states[next].a && *inptr <= states[next].b) {
+               next = states[next].next_if_match;
+           } else {
+               next = states[next].next_if_not_match;
+           }
+
+       } while (next != states[next].state);
+
+       if (filter->state < filter->first_skipping_state)
+           *outptr++ = *inptr;
+
+       filter->state = next;
+       inptr++;
+    }
+
+    *outlen = outptr - gmime_filter->outbuf;
+    *outprespace = gmime_filter->outpre;
+    *outbuf = gmime_filter->outbuf;
+}
+
+static void
+filter_complete (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
+                char **outbuf, size_t *outlen, size_t *outprespace)
+{
+    if (inbuf && inlen)
+       filter_filter (filter, inbuf, inlen, prespace, outbuf, outlen, outprespace);
+}
+
+static void
+filter_reset (GMimeFilter *gmime_filter)
+{
+    NotmuchFilterDiscardNonTerm *filter = (NotmuchFilterDiscardNonTerm *) gmime_filter;
+
+    filter->state = 0;
+}
+
+/**
+ * notmuch_filter_discard_non_term_new:
+ *
+ * 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)
+{
+    NotmuchFilterDiscardNonTerm *filter;
+
+    filter = (NotmuchFilterDiscardNonTerm *) g_object_new (type, NULL);
+    filter->content_type = content_type;
+    filter->state = 0;
+    if (g_mime_content_type_is_type (content_type, "text", "html")) {
+       filter->states = html_states;
+       filter->first_skipping_state = first_html_skipping_state;
+    } else {
+       filter->states = uuencode_states;
+       filter->first_skipping_state = first_uuencode_skipping_state;
+    }
+
+    return (GMimeFilter *) filter;
+}
+
+/* We're finally down to a single (NAME + address) email "mailbox". */
+static void
+_index_address_mailbox (notmuch_message_t *message,
+                       const char *prefix_name,
+                       InternetAddress *address)
+{
+    InternetAddressMailbox *mailbox = INTERNET_ADDRESS_MAILBOX (address);
+    const char *name, *addr, *combined;
+    void *local = talloc_new (message);
+
+    name = internet_address_get_name (address);
+    addr = internet_address_mailbox_get_addr (mailbox);
+
+    /* Combine the name and address and index them as a phrase. */
+    if (name && addr)
+       combined = talloc_asprintf (local, "%s %s", name, addr);
+    else if (name)
+       combined = name;
+    else
+       combined = addr;
+
+    if (combined)
+       _notmuch_message_gen_terms (message, prefix_name, combined);
+
+    talloc_free (local);
+}
+
+static void
+_index_address_list (notmuch_message_t *message,
+                    const char *prefix_name,
+                    InternetAddressList *addresses);
+
+/* The outer loop over the InternetAddressList wasn't quite enough.
+ * There can actually be a tree here where a single member of the list
+ * is a "group" containing another list. Recurse please.
+ */
+static void
+_index_address_group (notmuch_message_t *message,
+                     const char *prefix_name,
+                     InternetAddress *address)
+{
+    InternetAddressGroup *group;
+    InternetAddressList *list;
+
+    group = INTERNET_ADDRESS_GROUP (address);
+    list = internet_address_group_get_members (group);
+
+    if (! list)
+       return;
+
+    _index_address_list (message, prefix_name, list);
+}
+
+static void
+_index_address_list (notmuch_message_t *message,
+                    const char *prefix_name,
+                    InternetAddressList *addresses)
+{
+    int i;
+    InternetAddress *address;
+
+    if (addresses == NULL)
+       return;
+
+    for (i = 0; i < internet_address_list_length (addresses); i++) {
+       address = internet_address_list_get_address (addresses, i);
+       if (INTERNET_ADDRESS_IS_MAILBOX (address)) {
+           _index_address_mailbox (message, prefix_name, address);
+       } else if (INTERNET_ADDRESS_IS_GROUP (address)) {
+           _index_address_group (message, prefix_name, address);
+       } else {
+           INTERNAL_ERROR ("GMime InternetAddress is neither a mailbox nor a group.\n");
+       }
+    }
+}
+
+static void
+_index_content_type (notmuch_message_t *message, GMimeObject *part)
+{
+    GMimeContentType *content_type = g_mime_object_get_content_type (part);
+
+    if (content_type) {
+       char *mime_string = g_mime_content_type_get_mime_type (content_type);
+       if (mime_string) {
+           _notmuch_message_gen_terms (message, "mimetype", mime_string);
+           g_free (mime_string);
+       }
+    }
+}
+
+static void
+_index_encrypted_mime_part (notmuch_message_t *message, notmuch_indexopts_t *indexopts,
+                           GMimeObject *part,
+                           _notmuch_message_crypto_t *msg_crypto);
+
+static void
+_index_pkcs7_part (notmuch_message_t *message,
+                  notmuch_indexopts_t *indexopts,
+                  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,
+                 notmuch_indexopts_t *indexopts,
+                 GMimeObject *part,
+                 _notmuch_message_crypto_t *msg_crypto)
+{
+    GMimeStream *stream, *filter;
+    GMimeFilter *discard_non_term_filter;
+    GMimeDataWrapper *wrapper;
+    GByteArray *byte_array;
+    GMimeContentDisposition *disposition;
+    GMimeContentType *content_type;
+    char *body;
+    const char *charset;
+    GMimeObject *repaired_part = NULL;
+
+    if (! part) {
+       _notmuch_database_log (notmuch_message_get_database (message),
+                              "Warning: Not indexing empty mime part.\n");
+       goto DONE;
+    }
+
+    repaired_part = _notmuch_repair_mixed_up_mangled (part);
+    if (repaired_part) {
+       /* This was likely "Mixed Up" in transit!  We will instead use
+        * the more likely-to-be-correct variant. */
+       notmuch_message_add_property (message, "index.repaired", "mixedup");
+       part = repaired_part;
+    }
+
+    _index_content_type (message, part);
+
+    if (GMIME_IS_MULTIPART (part)) {
+       GMimeMultipart *multipart = GMIME_MULTIPART (part);
+       int i;
+
+       if (GMIME_IS_MULTIPART_SIGNED (multipart))
+           _notmuch_message_add_term (message, "tag", "signed");
+
+       if (GMIME_IS_MULTIPART_ENCRYPTED (multipart))
+           _notmuch_message_add_term (message, "tag", "encrypted");
+
+       for (i = 0; i < g_mime_multipart_get_count (multipart); i++) {
+           GMimeObject *child;
+           if (GMIME_IS_MULTIPART_SIGNED (multipart)) {
+               /* Don't index the signature, but index its content type. */
+               if (i == GMIME_MULTIPART_SIGNED_SIGNATURE) {
+                   _index_content_type (message,
+                                        g_mime_multipart_get_part (multipart, i));
+                   continue;
+               } else if (i != GMIME_MULTIPART_SIGNED_CONTENT) {
+                   _notmuch_database_log (notmuch_message_get_database (message),
+                                          "Warning: Unexpected extra parts of multipart/signed. Indexing anyway.\n");
+               }
+           }
+           if (GMIME_IS_MULTIPART_ENCRYPTED (multipart)) {
+               _index_content_type (message,
+                                    g_mime_multipart_get_part (multipart, i));
+               if (i == GMIME_MULTIPART_ENCRYPTED_CONTENT) {
+                   _index_encrypted_mime_part (message, indexopts,
+                                               part,
+                                               msg_crypto);
+               } else {
+                   if (i != GMIME_MULTIPART_ENCRYPTED_VERSION) {
+                       _notmuch_database_log (notmuch_message_get_database (message),
+                                              "Warning: Unexpected extra parts of multipart/encrypted.\n");
+                   }
+               }
+               continue;
+           }
+           child = g_mime_multipart_get_part (multipart, i);
+           GMimeObject *toindex = child;
+           if (_notmuch_message_crypto_potential_payload (msg_crypto, child, part, i) &&
+               msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL) {
+               toindex = _notmuch_repair_crypto_payload_skip_legacy_display (child);
+               if (toindex != child)
+                   notmuch_message_add_property (message, "index.repaired",
+                                                 "skip-protected-headers-legacy-display");
+           }
+           _index_mime_part (message, indexopts, toindex, msg_crypto);
+       }
+       goto DONE;
+    }
+
+    if (GMIME_IS_MESSAGE_PART (part)) {
+       GMimeMessage *mime_message;
+
+       mime_message = g_mime_message_part_get_message (GMIME_MESSAGE_PART (part));
+
+       _index_mime_part (message, indexopts, g_mime_message_get_mime_part (mime_message),
+                         msg_crypto);
+
+       goto DONE;
+    }
+
+    if (GMIME_IS_APPLICATION_PKCS7_MIME (part)) {
+       _index_pkcs7_part (message, indexopts, part, msg_crypto);
+       goto DONE;
+    }
+
+    if (! (GMIME_IS_PART (part))) {
+       _notmuch_database_log (notmuch_message_get_database (message),
+                              "Warning: Not indexing unknown mime part: %s.\n",
+                              g_type_name (G_OBJECT_TYPE (part)));
+       goto DONE;
+    }
+
+    disposition = g_mime_object_get_content_disposition (part);
+    if (disposition &&
+       strcasecmp (g_mime_content_disposition_get_disposition (disposition),
+                   GMIME_DISPOSITION_ATTACHMENT) == 0) {
+       const char *filename = g_mime_part_get_filename (GMIME_PART (part));
+
+       _notmuch_message_add_term (message, "tag", "attachment");
+       _notmuch_message_gen_terms (message, "attachment", filename);
+
+       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 ();
+
+    stream = g_mime_stream_mem_new_with_byte_array (byte_array);
+    g_mime_stream_mem_set_owner (GMIME_STREAM_MEM (stream), false);
+
+    filter = g_mime_stream_filter_new (stream);
+
+    content_type = g_mime_object_get_content_type (part);
+    discard_non_term_filter = notmuch_filter_discard_non_term_new (content_type);
+
+    g_mime_stream_filter_add (GMIME_STREAM_FILTER (filter),
+                             discard_non_term_filter);
+
+    charset = g_mime_object_get_content_type_parameter (part, "charset");
+    if (charset) {
+       GMimeFilter *charset_filter;
+       charset_filter = g_mime_filter_charset_new (charset, "UTF-8");
+       /* This result can be NULL for things like "unknown-8bit".
+        * Don't set a NULL filter as that makes GMime print
+        * annoying assertion-failure messages on stderr. */
+       if (charset_filter) {
+           g_mime_stream_filter_add (GMIME_STREAM_FILTER (filter),
+                                     charset_filter);
+           g_object_unref (charset_filter);
+       }
+    }
+
+    wrapper = g_mime_part_get_content (GMIME_PART (part));
+    if (wrapper)
+       g_mime_data_wrapper_write_to_stream (wrapper, filter);
+
+    g_object_unref (stream);
+    g_object_unref (filter);
+    g_object_unref (discard_non_term_filter);
+
+    g_byte_array_append (byte_array, (guint8 *) "\0", 1);
+    body = (char *) g_byte_array_free (byte_array, false);
+
+    if (body) {
+       _notmuch_message_gen_terms (message, NULL, body);
+
+       free (body);
+    }
+  DONE:
+    if (repaired_part)
+       g_object_unref (repaired_part);
+}
+
+/* descend (if desired) into the cleartext part of an encrypted MIME
+ * part while indexing. */
+static void
+_index_encrypted_mime_part (notmuch_message_t *message,
+                           notmuch_indexopts_t *indexopts,
+                           GMimeObject *encrypted_data,
+                           _notmuch_message_crypto_t *msg_crypto)
+{
+    notmuch_status_t status;
+    GError *err = NULL;
+    notmuch_database_t *notmuch = NULL;
+    GMimeObject *clear = NULL;
+
+    if (! indexopts || (notmuch_indexopts_get_decrypt_policy (indexopts) == NOTMUCH_DECRYPT_FALSE))
+       return;
+
+    notmuch = notmuch_message_get_database (message);
+
+    bool attempted = false;
+    GMimeDecryptResult *decrypt_result = NULL;
+    bool get_sk = (notmuch_indexopts_get_decrypt_policy (indexopts) == NOTMUCH_DECRYPT_TRUE);
+
+    clear = _notmuch_crypto_decrypt (&attempted, notmuch_indexopts_get_decrypt_policy (indexopts),
+                                    message, encrypted_data, get_sk ? &decrypt_result : NULL, &err);
+    if (! attempted)
+       return;
+    if (err || ! clear) {
+       if (decrypt_result)
+           g_object_unref (decrypt_result);
+       if (err) {
+           _notmuch_database_log (notmuch, "Failed to decrypt during indexing. (%d:%d) [%s]\n",
+                                  err->domain, err->code, err->message);
+           g_error_free (err);
+       } else {
+           _notmuch_database_log (notmuch, "Failed to decrypt during indexing. (unknown error)\n");
+       }
+       /* Indicate that we failed to decrypt during indexing */
+       status = notmuch_message_add_property (message, "index.decryption", "failure");
+       if (status)
+           _notmuch_database_log_append (notmuch, "failed to add index.decryption "
+                                         "property (%d)\n", status);
+       return;
+    }
+    if (decrypt_result) {
+       status = _notmuch_message_crypto_successful_decryption (msg_crypto);
+       if (status)
+           _notmuch_database_log_append (notmuch, "failed to mark the message as decrypted (%s)\n",
+                                         notmuch_status_to_string (status));
+       if (get_sk) {
+           status = notmuch_message_add_property (message, "session-key",
+                                                  g_mime_decrypt_result_get_session_key (
+                                                      decrypt_result));
+           if (status)
+               _notmuch_database_log (notmuch, "failed to add session-key "
+                                      "property (%d)\n", status);
+       }
+       g_object_unref (decrypt_result);
+    }
+    GMimeObject *toindex = clear;
+
+    if (_notmuch_message_crypto_potential_payload (msg_crypto, clear, encrypted_data,
+                                                  GMIME_MULTIPART_ENCRYPTED_CONTENT) &&
+       msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL) {
+       toindex = _notmuch_repair_crypto_payload_skip_legacy_display (clear);
+       if (toindex != clear)
+           notmuch_message_add_property (message, "index.repaired",
+                                         "skip-protected-headers-legacy-display");
+    }
+    _index_mime_part (message, indexopts, toindex, msg_crypto);
+    g_object_unref (clear);
+
+    status = notmuch_message_add_property (message, "index.decryption", "success");
+    if (status)
+       _notmuch_database_log (notmuch, "failed to add index.decryption "
+                              "property (%d)\n", status);
+
+}
+
+static void
+_index_pkcs7_part (notmuch_message_t *message,
+                  notmuch_indexopts_t *indexopts,
+                  GMimeObject *part,
+                  _notmuch_message_crypto_t *msg_crypto)
+{
+    GMimeApplicationPkcs7Mime *pkcs7;
+    GMimeSecureMimeType p7type;
+    GMimeObject *mimeobj = NULL;
+    GMimeSignatureList *sigs = NULL;
+    GError *err = NULL;
+    notmuch_database_t *notmuch = NULL;
+
+    pkcs7 = GMIME_APPLICATION_PKCS7_MIME (part);
+    p7type = g_mime_application_pkcs7_mime_get_smime_type (pkcs7);
+    notmuch = notmuch_message_get_database (message);
+    _index_content_type (message, part);
+
+    if (p7type == GMIME_SECURE_MIME_TYPE_SIGNED_DATA) {
+       sigs = g_mime_application_pkcs7_mime_verify (pkcs7, GMIME_VERIFY_NONE, &mimeobj, &err);
+       if (sigs == NULL) {
+           _notmuch_database_log (notmuch,
+                                  "Failed to verify PKCS#7 SignedData during indexing. (%d:%d) [%s]\n",
+                                  err->domain, err->code, err->message);
+           g_error_free (err);
+           goto DONE;
+       }
+       _notmuch_message_add_term (message, "tag", "signed");
+       GMimeObject *toindex = mimeobj;
+       if (_notmuch_message_crypto_potential_payload (msg_crypto, mimeobj, part, 0) &&
+           msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL) {
+           toindex = _notmuch_repair_crypto_payload_skip_legacy_display (mimeobj);
+           if (toindex != mimeobj)
+               notmuch_message_add_property (message, "index.repaired",
+                                             "skip-protected-headers-legacy-display");
+       }
+       _index_mime_part (message, indexopts, toindex, msg_crypto);
+    } else if (p7type == GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA) {
+       _notmuch_message_add_term (message, "tag", "encrypted");
+       _index_encrypted_mime_part (message, indexopts,
+                                   part,
+                                   msg_crypto);
+    } else {
+       _notmuch_database_log (notmuch, "Cannot currently handle PKCS#7 smime-type '%s'\n",
+                              g_mime_object_get_content_type_parameter (part, "smime-type"));
+    }
+  DONE:
+    if (mimeobj)
+       g_object_unref (mimeobj);
+    if (sigs)
+       g_object_unref (sigs);
+}
+
+static notmuch_status_t
+_notmuch_message_index_user_headers (notmuch_message_t *message, GMimeMessage *mime_message)
+{
+
+    notmuch_database_t *notmuch = notmuch_message_get_database (message);
+    notmuch_string_map_iterator_t *iter = _notmuch_database_user_headers (notmuch);
+
+    for (; _notmuch_string_map_iterator_valid (iter);
+        _notmuch_string_map_iterator_move_to_next (iter)) {
+
+       const char *prefix_name = _notmuch_string_map_iterator_key (iter);
+
+       const char *header_name = _notmuch_string_map_iterator_value (iter);
+
+       const char *header = g_mime_object_get_header (GMIME_OBJECT (mime_message), header_name);
+       if (header)
+           _notmuch_message_gen_terms (message, prefix_name, header);
+    }
+
+    if (iter)
+       _notmuch_string_map_iterator_destroy (iter);
+    return NOTMUCH_STATUS_SUCCESS;
+
+}
+
+notmuch_status_t
+_notmuch_message_index_file (notmuch_message_t *message,
+                            notmuch_indexopts_t *indexopts,
+                            notmuch_message_file_t *message_file)
+{
+    GMimeMessage *mime_message;
+    InternetAddressList *addresses;
+    const char *subject;
+    notmuch_status_t status;
+    _notmuch_message_crypto_t *msg_crypto;
+
+    status = _notmuch_message_file_get_mime_message (message_file,
+                                                    &mime_message);
+    if (status)
+       return status;
+
+    addresses = g_mime_message_get_from (mime_message);
+    if (addresses) {
+       _index_address_list (message, "from", addresses);
+    }
+
+    addresses = g_mime_message_get_all_recipients (mime_message);
+    if (addresses) {
+       _index_address_list (message, "to", addresses);
+       g_object_unref (addresses);
+    }
+
+    subject = g_mime_message_get_subject (mime_message);
+    _notmuch_message_gen_terms (message, "subject", subject);
+
+    status = _notmuch_message_index_user_headers (message, mime_message);
+
+    msg_crypto = _notmuch_message_crypto_new (NULL);
+    _index_mime_part (message, indexopts, g_mime_message_get_mime_part (mime_message), msg_crypto);
+    if (msg_crypto && msg_crypto->payload_subject) {
+       _notmuch_message_gen_terms (message, "subject", msg_crypto->payload_subject);
+       _notmuch_message_update_subject (message, msg_crypto->payload_subject);
+    }
+
+    talloc_free (msg_crypto);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
diff --git a/lib/indexopts.c b/lib/indexopts.c
new file mode 100644 (file)
index 0000000..2ffd194
--- /dev/null
@@ -0,0 +1,81 @@
+/* indexopts.c - options for indexing messages (currently a stub)
+ *
+ * Copyright © 2017 Daniel Kahn Gillmor
+ *
+ * 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: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+ */
+
+#include "notmuch-private.h"
+
+struct _notmuch_indexopts {
+    _notmuch_crypto_t crypto;
+};
+
+notmuch_indexopts_t *
+notmuch_database_get_default_indexopts (notmuch_database_t *db)
+{
+    notmuch_indexopts_t *ret = talloc_zero (db, notmuch_indexopts_t);
+
+    if (! ret)
+       return ret;
+    ret->crypto.decrypt = NOTMUCH_DECRYPT_AUTO;
+
+    char *decrypt_policy;
+    notmuch_status_t err = notmuch_database_get_config (db, "index.decrypt", &decrypt_policy);
+
+    if (err)
+       return NULL;
+
+    if (decrypt_policy) {
+       if ((! (strcasecmp (decrypt_policy, "true"))) ||
+           (! (strcasecmp (decrypt_policy, "yes"))) ||
+           (! (strcasecmp (decrypt_policy, "1"))))
+           notmuch_indexopts_set_decrypt_policy (ret, NOTMUCH_DECRYPT_TRUE);
+       else if ((! (strcasecmp (decrypt_policy, "false"))) ||
+                (! (strcasecmp (decrypt_policy, "no"))) ||
+                (! (strcasecmp (decrypt_policy, "0"))))
+           notmuch_indexopts_set_decrypt_policy (ret, NOTMUCH_DECRYPT_FALSE);
+       else if (! strcasecmp (decrypt_policy, "nostash"))
+           notmuch_indexopts_set_decrypt_policy (ret, NOTMUCH_DECRYPT_NOSTASH);
+    }
+
+    free (decrypt_policy);
+    return ret;
+}
+
+notmuch_status_t
+notmuch_indexopts_set_decrypt_policy (notmuch_indexopts_t *indexopts,
+                                     notmuch_decryption_policy_t decrypt_policy)
+{
+    if (! indexopts)
+       return NOTMUCH_STATUS_NULL_POINTER;
+    indexopts->crypto.decrypt = decrypt_policy;
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_decryption_policy_t
+notmuch_indexopts_get_decrypt_policy (const notmuch_indexopts_t *indexopts)
+{
+    if (! indexopts)
+       return false;
+    return indexopts->crypto.decrypt;
+}
+
+void
+notmuch_indexopts_destroy (notmuch_indexopts_t *indexopts)
+{
+    talloc_free (indexopts);
+}
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 */
diff --git a/lib/message-file.c b/lib/message-file.c
new file mode 100644 (file)
index 0000000..68f646a
--- /dev/null
@@ -0,0 +1,402 @@
+/* message.c - Utility functions for parsing an email message for notmuch.
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include <stdarg.h>
+
+#include "notmuch-private.h"
+
+#include <gmime/gmime.h>
+
+#include <glib.h> /* GHashTable */
+
+struct _notmuch_message_file {
+    /* open stream to (possibly gzipped) file */
+    GMimeStream *stream;
+    char *filename;
+
+    /* Cache for decoded headers */
+    GHashTable *headers;
+
+    GMimeMessage *message;
+};
+
+static int
+_notmuch_message_file_destructor (notmuch_message_file_t *message)
+{
+    if (message->headers)
+       g_hash_table_destroy (message->headers);
+
+    if (message->message)
+       g_object_unref (message->message);
+
+    if (message->stream)
+       g_object_unref (message->stream);
+
+    return 0;
+}
+
+/* Create a new notmuch_message_file_t for 'filename' with 'ctx' as
+ * the talloc owner. */
+notmuch_message_file_t *
+_notmuch_message_file_open_ctx (notmuch_database_t *notmuch,
+                               void *ctx, const char *filename)
+{
+    notmuch_message_file_t *message;
+
+    message = talloc_zero (ctx, notmuch_message_file_t);
+    if (unlikely (message == NULL))
+       return NULL;
+
+    const char *prefix = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT);
+
+    if (prefix == NULL)
+       goto FAIL;
+
+    if (*filename == '/') {
+       if (strncmp (filename, prefix, strlen (prefix)) != 0) {
+           _notmuch_database_log (notmuch, "Error opening %s: path outside mail root\n",
+                                  filename);
+           errno = 0;
+           goto FAIL;
+       }
+       message->filename = talloc_strdup (message, filename);
+    } else {
+       message->filename = talloc_asprintf (message, "%s/%s", prefix, filename);
+    }
+
+    if (message->filename == NULL)
+       goto FAIL;
+
+    talloc_set_destructor (message, _notmuch_message_file_destructor);
+
+    message->stream = g_mime_stream_gzfile_open (message->filename);
+    if (message->stream == NULL)
+       goto FAIL;
+
+    return message;
+
+  FAIL:
+    if (errno)
+       _notmuch_database_log (notmuch, "Error opening %s: %s\n",
+                              filename, strerror (errno));
+    _notmuch_message_file_close (message);
+
+    return NULL;
+}
+
+notmuch_message_file_t *
+_notmuch_message_file_open (notmuch_database_t *notmuch,
+                           const char *filename)
+{
+    return _notmuch_message_file_open_ctx (notmuch, NULL, filename);
+}
+
+const char *
+_notmuch_message_file_get_filename (notmuch_message_file_t *message_file)
+{
+    return message_file->filename;
+}
+
+void
+_notmuch_message_file_close (notmuch_message_file_t *message)
+{
+    talloc_free (message);
+}
+
+static bool
+_is_mbox (GMimeStream *stream)
+{
+    char from_buf[5];
+    bool ret = false;
+
+    /* Is this mbox? */
+    if (g_mime_stream_read (stream, from_buf, sizeof (from_buf)) == sizeof (from_buf) &&
+       strncmp (from_buf, "From ", 5) == 0)
+       ret = true;
+
+    g_mime_stream_reset (stream);
+
+    return ret;
+}
+
+notmuch_status_t
+_notmuch_message_file_parse (notmuch_message_file_t *message)
+{
+    GMimeParser *parser;
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+    bool is_mbox;
+
+    if (message->message)
+       return NOTMUCH_STATUS_SUCCESS;
+
+    is_mbox = _is_mbox (message->stream);
+
+    _notmuch_init ();
+
+    message->headers = g_hash_table_new_full (strcase_hash, strcase_equal,
+                                             free, g_free);
+    if (! message->headers)
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+    parser = g_mime_parser_new_with_stream (message->stream);
+    g_mime_parser_set_scan_from (parser, is_mbox);
+
+    message->message = g_mime_parser_construct_message (parser, NULL);
+    if (! message->message) {
+       status = NOTMUCH_STATUS_FILE_NOT_EMAIL;
+       goto DONE;
+    }
+
+    if (is_mbox && ! g_mime_parser_eos (parser)) {
+       /*
+        * This is a multi-message mbox. (For historical reasons, we
+        * do support single-message mboxes.)
+        */
+       status = NOTMUCH_STATUS_FILE_NOT_EMAIL;
+    }
+
+  DONE:
+    g_mime_stream_reset (message->stream);
+    g_object_unref (parser);
+
+    if (status) {
+       g_hash_table_destroy (message->headers);
+       message->headers = NULL;
+
+       if (message->message) {
+           g_object_unref (message->message);
+           message->message = NULL;
+       }
+
+    }
+
+    return status;
+}
+
+notmuch_status_t
+_notmuch_message_file_get_mime_message (notmuch_message_file_t *message,
+                                       GMimeMessage **mime_message)
+{
+    notmuch_status_t status;
+
+    status = _notmuch_message_file_parse (message);
+    if (status)
+       return status;
+
+    *mime_message = message->message;
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/*
+ * Get all instances of a header decoded and concatenated.
+ *
+ * The result must be freed using g_free().
+ *
+ * Return NULL on errors, empty string for non-existing headers.
+ */
+
+static char *
+_extend_header (char *combined, const char *value)
+{
+    char *decoded;
+
+    decoded = g_mime_utils_header_decode_text (NULL, value);
+    if (! decoded) {
+       if (combined) {
+           g_free (combined);
+           combined = NULL;
+       }
+       goto DONE;
+    }
+
+    if (combined) {
+       char *tmp = g_strdup_printf ("%s %s", combined, decoded);
+       g_free (decoded);
+       g_free (combined);
+       if (! tmp) {
+           combined = NULL;
+           goto DONE;
+       }
+
+       combined = tmp;
+    } else {
+       combined = decoded;
+    }
+  DONE:
+    return combined;
+}
+
+static char *
+_notmuch_message_file_get_combined_header (notmuch_message_file_t *message,
+                                          const char *header)
+{
+    char *combined = NULL;
+    GMimeHeaderList *headers;
+
+    headers = g_mime_object_get_header_list (GMIME_OBJECT (message->message));
+    if (! headers)
+       return NULL;
+
+
+    for (int i = 0; i < g_mime_header_list_get_count (headers); i++) {
+       const char *value;
+       GMimeHeader *g_header = g_mime_header_list_get_header_at (headers, i);
+
+       if (strcasecmp (g_mime_header_get_name (g_header), header) != 0)
+           continue;
+
+       /* GMime retains ownership of value, we hope */
+       value = g_mime_header_get_value (g_header);
+
+       combined = _extend_header (combined, value);
+    }
+
+    /* Return empty string for non-existing headers. */
+    if (! combined)
+       combined = g_strdup ("");
+
+    return combined;
+}
+
+const char *
+_notmuch_message_file_get_header (notmuch_message_file_t *message,
+                                 const char *header)
+{
+    const char *value;
+    char *decoded;
+
+    if (_notmuch_message_file_parse (message))
+       return NULL;
+
+    /* If we have a cached decoded value, use it. */
+    value = g_hash_table_lookup (message->headers, header);
+    if (value)
+       return value;
+
+    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.
+        *
+        * 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 {
+       value = g_mime_object_get_header (GMIME_OBJECT (message->message),
+                                         header);
+       if (value)
+           decoded = g_mime_utils_header_decode_text (NULL, value);
+       else
+           decoded = g_strdup ("");
+    }
+
+    if (! decoded)
+       return NULL;
+
+    /* Cache the decoded value. We also own the strings. */
+    g_hash_table_insert (message->headers, xstrdup (header), decoded);
+
+    return decoded;
+}
+
+notmuch_status_t
+_notmuch_message_file_get_headers (notmuch_message_file_t *message_file,
+                                  const char **from_out,
+                                  const char **subject_out,
+                                  const char **to_out,
+                                  const char **date_out,
+                                  char **message_id_out)
+{
+    notmuch_status_t ret;
+    const char *header;
+    const char *from, *to, *subject, *date;
+    char *message_id = NULL;
+
+    /* Parse message up front to get better error status. */
+    ret = _notmuch_message_file_parse (message_file);
+    if (ret)
+       goto DONE;
+
+    /* Before we do any real work, (especially before doing a
+     * potential SHA-1 computation on the entire file's contents),
+     * let's make sure that what we're looking at looks like an
+     * actual email message.
+     */
+    from = _notmuch_message_file_get_header (message_file, "from");
+    subject = _notmuch_message_file_get_header (message_file, "subject");
+    to = _notmuch_message_file_get_header (message_file, "to");
+    date = _notmuch_message_file_get_header (message_file, "date");
+
+    if ((from == NULL || *from == '\0') &&
+       (subject == NULL || *subject == '\0') &&
+       (to == NULL || *to == '\0')) {
+       ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
+       goto DONE;
+    }
+
+    /* Now that we're sure it's mail, the first order of business
+     * is to find a message ID (or else create one ourselves).
+     */
+    header = _notmuch_message_file_get_header (message_file, "message-id");
+    if (header && *header != '\0') {
+       message_id = _notmuch_message_id_parse (message_file, header, NULL);
+
+       /* So the header value isn't RFC-compliant, but it's
+        * better than no message-id at all.
+        */
+       if (message_id == NULL)
+           message_id = talloc_strdup (message_file, header);
+    }
+
+    if (message_id == NULL ) {
+       /* No message-id at all, let's generate one by taking a
+        * hash over the file's contents.
+        */
+       char *sha1 = _notmuch_sha1_of_file (_notmuch_message_file_get_filename (message_file));
+
+       /* If that failed too, something is really wrong. Give up. */
+       if (sha1 == NULL) {
+           ret = NOTMUCH_STATUS_FILE_ERROR;
+           goto DONE;
+       }
+
+       message_id = talloc_asprintf (message_file, "notmuch-sha1-%s", sha1);
+       free (sha1);
+    }
+  DONE:
+    if (ret == NOTMUCH_STATUS_SUCCESS) {
+       if (from_out)
+           *from_out = from;
+       if (subject_out)
+           *subject_out = subject;
+       if (to_out)
+           *to_out = to;
+       if (date_out)
+           *date_out = date;
+       if (message_id_out)
+           *message_id_out = message_id;
+    }
+    return ret;
+}
diff --git a/lib/message-id.c b/lib/message-id.c
new file mode 100644 (file)
index 0000000..5401235
--- /dev/null
@@ -0,0 +1,126 @@
+#include "notmuch-private.h"
+#include "string-util.h"
+
+/* Advance 'str' past any whitespace or RFC 822 comments. A comment is
+ * a (potentially nested) parenthesized sequence with '\' used to
+ * escape any character (including parentheses).
+ *
+ * If the sequence to be skipped continues to the end of the string,
+ * then 'str' will be left pointing at the final terminating '\0'
+ * character.
+ */
+static void
+skip_space_and_comments (const char **str)
+{
+    const char *s;
+
+    s = *str;
+    while (*s && (isspace (*s) || *s == '(')) {
+       while (*s && isspace (*s))
+           s++;
+       if (*s == '(') {
+           int nesting = 1;
+           s++;
+           while (*s && nesting) {
+               if (*s == '(') {
+                   nesting++;
+               } else if (*s == ')') {
+                   nesting--;
+               } else if (*s == '\\') {
+                   if (*(s + 1))
+                       s++;
+               }
+               s++;
+           }
+       }
+    }
+
+    *str = s;
+}
+
+char *
+_notmuch_message_id_parse (void *ctx, const char *message_id, const char **next)
+{
+    const char *s, *end;
+    char *result;
+
+    if (message_id == NULL || *message_id == '\0')
+       return NULL;
+
+    s = message_id;
+
+    skip_space_and_comments (&s);
+
+    /* Skip any unstructured text as well. */
+    while (*s && *s != '<')
+       s++;
+
+    if (*s == '<') {
+       s++;
+    } else {
+       if (next)
+           *next = s;
+       return NULL;
+    }
+
+    skip_space_and_comments (&s);
+
+    end = s;
+    while (*end && *end != '>')
+       end++;
+    if (next) {
+       if (*end)
+           *next = end + 1;
+       else
+           *next = end;
+    }
+
+    if (end > s && *end == '>')
+       end--;
+    if (end <= s)
+       return NULL;
+
+    result = talloc_strndup (ctx, s, end - s + 1);
+
+    /* Finally, collapse any whitespace that is within the message-id
+     * itself. */
+    {
+       char *r;
+       int len;
+
+       for (r = result, len = strlen (r); *r; r++, len--)
+           if (*r == ' ' || *r == '\t')
+               memmove (r, r + 1, len);
+    }
+
+    return result;
+}
+
+char *
+_notmuch_message_id_parse_strict (void *ctx, const char *message_id)
+{
+    const char *s, *end;
+
+    if (message_id == NULL || *message_id == '\0')
+       return NULL;
+
+    s = skip_space (message_id);
+    if (*s == '<')
+       s++;
+    else
+       return NULL;
+
+    for (end = s; *end && *end != '>'; end++)
+       if (isspace (*end))
+           return NULL;
+
+    if (*end != '>')
+       return NULL;
+    else {
+       const char *last = skip_space (end + 1);
+       if (*last != '\0')
+           return NULL;
+    }
+
+    return talloc_strndup (ctx, s, end - s);
+}
diff --git a/lib/message-private.h b/lib/message-private.h
new file mode 100644 (file)
index 0000000..73b080e
--- /dev/null
@@ -0,0 +1,16 @@
+#ifndef MESSAGE_PRIVATE_H
+#define MESSAGE_PRIVATE_H
+
+notmuch_string_map_t *
+_notmuch_message_property_map (notmuch_message_t *message);
+
+bool
+_notmuch_message_frozen (notmuch_message_t *message);
+
+void
+_notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix);
+
+void
+_notmuch_message_invalidate_metadata (notmuch_message_t *message,  const char *prefix_name);
+
+#endif
diff --git a/lib/message-property.cc b/lib/message-property.cc
new file mode 100644 (file)
index 0000000..7f52034
--- /dev/null
@@ -0,0 +1,214 @@
+/* message-property.cc - Properties are like tags, but (key,value) pairs.
+ * keys are allowed to repeat.
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2016 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 "notmuch-private.h"
+#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)
+{
+    if (! value)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    *value = _notmuch_string_map_get (_notmuch_message_property_map (message), key);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_count_properties (notmuch_message_t *message, const char *key, unsigned int *count)
+{
+    if (! count || ! key || ! message)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    notmuch_string_map_t *map;
+
+    map = _notmuch_message_property_map (message);
+    if (! map)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    notmuch_string_map_iterator_t *matcher = _notmuch_string_map_iterator_create (map, key, true);
+
+    if (! matcher)
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+    *count = 0;
+    while (_notmuch_string_map_iterator_valid (matcher)) {
+       (*count)++;
+       _notmuch_string_map_iterator_move_to_next (matcher);
+    }
+
+    _notmuch_string_map_iterator_destroy (matcher);
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_notmuch_message_modify_property (notmuch_message_t *message, const char *key, const char *value,
+                                 bool delete_it)
+{
+    notmuch_private_status_t private_status;
+    notmuch_status_t status;
+    char *term = NULL;
+
+    status = _notmuch_database_ensure_writable (notmuch_message_get_database (message));
+    if (status)
+       return status;
+
+    if (key == NULL || value == NULL)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    if (strchr (key, '='))
+       return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
+
+    term = talloc_asprintf (message, "%s=%s", key, value);
+
+    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,
+                             "Unhandled error modifying message property");
+    if (! _notmuch_message_frozen (message))
+       _notmuch_message_sync (message);
+
+    if (term)
+       talloc_free (term);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_add_property (notmuch_message_t *message, const char *key, const char *value)
+{
+    return _notmuch_message_modify_property (message, key, value, false);
+}
+
+notmuch_status_t
+notmuch_message_remove_property (notmuch_message_t *message, const char *key, const char *value)
+{
+    return _notmuch_message_modify_property (message, key, value, true);
+}
+
+static
+notmuch_status_t
+_notmuch_message_remove_all_properties (notmuch_message_t *message, const char *key, bool prefix)
+{
+    notmuch_status_t status;
+    const char *term_prefix;
+
+    status = _notmuch_database_ensure_writable (notmuch_message_get_database (message));
+    if (status)
+       return status;
+
+    if (key)
+       term_prefix = talloc_asprintf (message, "%s%s%s", _find_prefix ("property"), key,
+                                      prefix ? "" : "=");
+    else
+       term_prefix = _find_prefix ("property");
+
+    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;
+}
+
+notmuch_status_t
+notmuch_message_remove_all_properties (notmuch_message_t *message, const char *key)
+{
+    return _notmuch_message_remove_all_properties (message, key, false);
+}
+
+notmuch_status_t
+notmuch_message_remove_all_properties_with_prefix (notmuch_message_t *message, const char *prefix)
+{
+    return _notmuch_message_remove_all_properties (message, prefix, true);
+}
+
+notmuch_message_properties_t *
+notmuch_message_get_properties (notmuch_message_t *message, const char *key, notmuch_bool_t exact)
+{
+    notmuch_string_map_t *map;
+
+    map = _notmuch_message_property_map (message);
+    return _notmuch_string_map_iterator_create (map, key, exact);
+}
+
+notmuch_bool_t
+notmuch_message_properties_valid (notmuch_message_properties_t *properties)
+{
+    return _notmuch_string_map_iterator_valid (properties);
+}
+
+void
+notmuch_message_properties_move_to_next (notmuch_message_properties_t *properties)
+{
+    return _notmuch_string_map_iterator_move_to_next (properties);
+}
+
+const char *
+notmuch_message_properties_key (notmuch_message_properties_t *properties)
+{
+    return _notmuch_string_map_iterator_key (properties);
+}
+
+const char *
+notmuch_message_properties_value (notmuch_message_properties_t *properties)
+{
+    return _notmuch_string_map_iterator_value (properties);
+}
+
+void
+notmuch_message_properties_destroy (notmuch_message_properties_t *properties)
+{
+    _notmuch_string_map_iterator_destroy (properties);
+}
diff --git a/lib/message.cc b/lib/message.cc
new file mode 100644 (file)
index 0000000..46638f8
--- /dev/null
@@ -0,0 +1,2358 @@
+/* message.cc - Results of message-based searches from a notmuch database
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+#include "database-private.h"
+#include "message-private.h"
+
+#include <stdint.h>
+
+#include <gmime/gmime.h>
+
+struct _notmuch_message {
+    notmuch_database_t *notmuch;
+    Xapian::docid doc_id;
+    int frozen;
+    char *message_id;
+    char *thread_id;
+    size_t thread_depth;
+    char *in_reply_to;
+    notmuch_string_list_t *tag_list;
+    notmuch_string_list_t *filename_term_list;
+    notmuch_string_list_t *filename_list;
+    char *maildir_flags;
+    char *author;
+    notmuch_message_file_t *message_file;
+    notmuch_string_list_t *property_term_list;
+    notmuch_string_map_t *property_map;
+    notmuch_string_list_t *reference_list;
+    notmuch_message_list_t *replies;
+    unsigned long flags;
+    /* For flags that are initialized on-demand, lazy_flags indicates
+     * if each flag has been initialized. */
+    unsigned long lazy_flags;
+
+    /* Message document modified since last sync */
+    bool modified;
+
+    /* last view of database the struct is synced with */
+    unsigned long last_view;
+
+    Xapian::Document doc;
+    Xapian::termcount termpos;
+};
+
+#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
+
+struct maildir_flag_tag {
+    char flag;
+    const char *tag;
+    bool inverse;
+};
+
+/* ASCII ordered table of Maildir flags and associated tags */
+static const struct maildir_flag_tag flag2tag[] = {
+    { 'D', "draft",   false },
+    { 'F', "flagged", false },
+    { 'P', "passed",  false },
+    { 'R', "replied", false },
+    { 'S', "unread",  true }
+};
+
+/* We end up having to call the destructor explicitly because we had
+ * to use "placement new" in order to initialize C++ objects within a
+ * block that we allocated with talloc. So C++ is making talloc
+ * slightly less simple to use, (we wouldn't need
+ * talloc_set_destructor at all otherwise).
+ */
+static int
+_notmuch_message_destructor (notmuch_message_t *message)
+{
+    message->doc.~Document ();
+
+    return 0;
+}
+
+#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;
+}
+
+static notmuch_message_t *
+_notmuch_message_create_for_document (const void *talloc_owner,
+                                     notmuch_database_t *notmuch,
+                                     unsigned int doc_id,
+                                     Xapian::Document doc,
+                                     notmuch_private_status_t *status)
+{
+    notmuch_message_t *message;
+
+    if (status)
+       *status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
+
+    message = talloc (talloc_owner, notmuch_message_t);
+    if (unlikely (message == NULL)) {
+       if (status)
+           *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
+       return NULL;
+    }
+
+    message->notmuch = notmuch;
+    message->doc_id = doc_id;
+
+    message->frozen = 0;
+    message->flags = 0;
+    message->lazy_flags = 0;
+
+    /* the message is initially not synchronized with Xapian */
+    message->last_view = 0;
+
+    /* Calculated after the thread structure is computed */
+    message->thread_depth = 0;
+
+    /* Each of these will be lazily created as needed. */
+    message->message_id = NULL;
+    message->thread_id = NULL;
+    message->in_reply_to = NULL;
+    message->tag_list = NULL;
+    message->filename_term_list = NULL;
+    message->filename_list = NULL;
+    message->maildir_flags = NULL;
+    message->message_file = NULL;
+    message->author = NULL;
+    message->property_term_list = NULL;
+    message->property_map = NULL;
+    message->reference_list = NULL;
+
+    message->replies = _notmuch_message_list_create (message);
+    if (unlikely (message->replies == NULL)) {
+       if (status)
+           *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
+       return NULL;
+    }
+
+    /* This is C++'s creepy "placement new", which is really just an
+     * ugly way to call a constructor for a pre-allocated object. So
+     * it's really not an error to not be checking for OUT_OF_MEMORY
+     * here, since this "new" isn't actually allocating memory. This
+     * is language-design comedy of the wrong kind. */
+
+    new (&message->doc) Xapian::Document;
+
+    talloc_set_destructor (message, _notmuch_message_destructor);
+
+    message->doc = doc;
+    message->termpos = 0;
+    message->modified = false;
+
+    return message;
+}
+
+/* Create a new notmuch_message_t object for an existing document in
+ * the database.
+ *
+ * Here, 'talloc owner' is an optional talloc context to which the new
+ * message will belong. This allows for the caller to not bother
+ * calling notmuch_message_destroy on the message, and know that all
+ * memory will be reclaimed when 'talloc_owner' is freed. The caller
+ * still can call notmuch_message_destroy when finished with the
+ * message if desired.
+ *
+ * The 'talloc_owner' argument can also be NULL, in which case the
+ * caller *is* responsible for calling notmuch_message_destroy.
+ *
+ * If no document exists in the database with document ID of 'doc_id'
+ * then this function returns NULL and optionally sets *status to
+ * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
+ *
+ * This function can also fail to due lack of available memory,
+ * returning NULL and optionally setting *status to
+ * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
+ *
+ * The caller can pass NULL for status if uninterested in
+ * distinguishing these two cases.
+ */
+notmuch_message_t *
+_notmuch_message_create (const void *talloc_owner,
+                        notmuch_database_t *notmuch,
+                        unsigned int doc_id,
+                        notmuch_private_status_t *status)
+{
+    Xapian::Document doc;
+
+    try {
+       doc = notmuch->xapian_db->get_document (doc_id);
+    } catch (const Xapian::DocNotFoundError &error) {
+       if (status)
+           *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
+       return NULL;
+    }
+
+    return _notmuch_message_create_for_document (talloc_owner, notmuch,
+                                                doc_id, doc, status);
+}
+
+/* Create a new notmuch_message_t object for a specific message ID,
+ * (which may or may not already exist in the database).
+ *
+ * The 'notmuch' database will be the talloc owner of the returned
+ * message.
+ *
+ * This function returns a valid notmuch_message_t whether or not
+ * there is already a document in the database with the given message
+ * ID. These two cases can be distinguished by the value of *status:
+ *
+ *
+ *   NOTMUCH_PRIVATE_STATUS_SUCCESS:
+ *
+ *     There is already a document with message ID 'message_id' in the
+ *     database. The returned message can be used to query/modify the
+ *     document. The message may be a ghost message.
+ *
+ *   NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
+ *
+ *     No document with 'message_id' exists in the database. The
+ *     returned message contains a newly created document (not yet
+ *     added to the database) and a document ID that is known not to
+ *     exist in the database.  This message is "blank"; that is, it
+ *     contains only a message ID and no other metadata. The caller
+ *     can modify the message, and a call to _notmuch_message_sync
+ *     will add the document to the database.
+ *
+ * If an error occurs, this function will return NULL and *status
+ * will be set as appropriate. (The status pointer argument must
+ * not be NULL.)
+ */
+notmuch_message_t *
+_notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
+                                       const char *message_id,
+                                       notmuch_private_status_t *status_ret)
+{
+    notmuch_message_t *message;
+    Xapian::Document doc;
+    unsigned int doc_id;
+    char *term;
+
+    *status_ret = (notmuch_private_status_t) notmuch_database_find_message (notmuch,
+                                                                           message_id,
+                                                                           &message);
+    if (message)
+       return talloc_steal (notmuch, message);
+    else if (*status_ret)
+       return NULL;
+
+    /* If the message ID is too long, substitute its sha1 instead. */
+    if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
+       message_id = _notmuch_message_id_compressed (message, message_id);
+
+    term = talloc_asprintf (NULL, "%s%s",
+                           _find_prefix ("id"), message_id);
+    if (term == NULL) {
+       *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
+       return NULL;
+    }
+
+    if (_notmuch_database_mode (notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY)
+       INTERNAL_ERROR ("Failure to ensure database is writable.");
+
+    try {
+       doc.add_term (term, 0);
+       talloc_free (term);
+
+       doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
+
+       doc_id = _notmuch_database_generate_doc_id (notmuch);
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch,
+                              "A Xapian exception occurred creating message: %s\n",
+                              error.get_msg ().c_str ());
+       notmuch->exception_reported = true;
+       *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
+       return NULL;
+    }
+
+    message = _notmuch_message_create_for_document (notmuch, notmuch,
+                                                   doc_id, doc, status_ret);
+
+    /* We want to inform the caller that we had to create a new
+     * document. */
+    if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS)
+       *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
+
+    return message;
+}
+
+static char *
+_notmuch_message_get_term (notmuch_message_t *message,
+                          Xapian::TermIterator &i, Xapian::TermIterator &end,
+                          const char *prefix)
+{
+    int prefix_len = strlen (prefix);
+    char *value;
+
+    i.skip_to (prefix);
+
+    if (i == end)
+       return NULL;
+
+    const std::string &term = *i;
+
+    if (strncmp (term.c_str (), prefix, prefix_len))
+       return NULL;
+
+    value = talloc_strdup (message, term.c_str () + prefix_len);
+
+#if DEBUG_DATABASE_SANITY
+    i++;
+
+    if (i != end && strncmp ((*i).c_str (), prefix, prefix_len) == 0) {
+       INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate %s terms: %s and %s\n",
+                       message->doc_id, prefix, value,
+                       (*i).c_str () + prefix_len);
+    }
+#endif
+
+    return value;
+}
+
+static void
+_notmuch_message_ensure_metadata (notmuch_message_t *message, void *field)
+{
+    Xapian::TermIterator i, end;
+
+    if (field && (message->last_view >= message->notmuch->view))
+       return;
+
+    const char *thread_prefix = _find_prefix ("thread"),
+              *tag_prefix = _find_prefix ("tag"),
+              *id_prefix = _find_prefix ("id"),
+              *type_prefix = _find_prefix ("type"),
+              *filename_prefix = _find_prefix ("file-direntry"),
+              *property_prefix = _find_prefix ("property"),
+              *reference_prefix = _find_prefix ("reference"),
+              *replyto_prefix = _find_prefix ("replyto");
+
+    /* We do this all in a single pass because Xapian decompresses the
+     * term list every time you iterate over it.  Thus, while this is
+     * slightly more costly than looking up individual fields if only
+     * one field of the message object is actually used, it's a huge
+     * win as more fields are used. */
+    for (int count = 0; count < 3; count++) {
+       try {
+           i = message->doc.termlist_begin ();
+           end = message->doc.termlist_end ();
+
+           /* Get thread */
+           if (! message->thread_id)
+               message->thread_id =
+                   _notmuch_message_get_term (message, i, end, thread_prefix);
+
+           /* Get tags */
+           assert (strcmp (thread_prefix, tag_prefix) < 0);
+           if (! message->tag_list) {
+               message->tag_list =
+                   _notmuch_database_get_terms_with_prefix (message, i, end,
+                                                            tag_prefix);
+               _notmuch_string_list_sort (message->tag_list);
+           }
+
+           /* Get id */
+           assert (strcmp (tag_prefix, id_prefix) < 0);
+           if (! message->message_id)
+               message->message_id =
+                   _notmuch_message_get_term (message, i, end, id_prefix);
+
+           /* Get document type */
+           assert (strcmp (id_prefix, type_prefix) < 0);
+           if (! NOTMUCH_TEST_BIT (message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST)) {
+               i.skip_to (type_prefix);
+               /* "T" is the prefix "type" fields.  See
+                * BOOLEAN_PREFIX_INTERNAL. */
+               if (*i == "Tmail")
+                   NOTMUCH_CLEAR_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
+               else if (*i == "Tghost")
+                   NOTMUCH_SET_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
+               else
+                   INTERNAL_ERROR ("Message without type term");
+               NOTMUCH_SET_BIT (&message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST);
+           }
+
+           /* Get filename list.  Here we get only the terms.  We lazily
+            * expand them to full file names when needed in
+            * _notmuch_message_ensure_filename_list. */
+           assert (strcmp (type_prefix, filename_prefix) < 0);
+           if (! message->filename_term_list && ! message->filename_list)
+               message->filename_term_list =
+                   _notmuch_database_get_terms_with_prefix (message, i, end,
+                                                            filename_prefix);
+
+
+           /* Get property terms. Mimic the setup with filenames above */
+           assert (strcmp (filename_prefix, property_prefix) < 0);
+           if (! message->property_map && ! message->property_term_list)
+               message->property_term_list =
+                   _notmuch_database_get_terms_with_prefix (message, i, end,
+                                                            property_prefix);
+
+           /* get references */
+           assert (strcmp (property_prefix, reference_prefix) < 0);
+           if (! message->reference_list) {
+               message->reference_list =
+                   _notmuch_database_get_terms_with_prefix (message, i, end,
+                                                            reference_prefix);
+           }
+
+           /* Get reply to */
+           assert (strcmp (property_prefix, replyto_prefix) < 0);
+           if (! message->in_reply_to)
+               message->in_reply_to =
+                   _notmuch_message_get_term (message, i, end, replyto_prefix);
+
+
+           /* It's perfectly valid for a message to have no In-Reply-To
+            * header. For these cases, we return an empty string. */
+           if (! message->in_reply_to)
+               message->in_reply_to = talloc_strdup (message, "");
+
+           /* all the way without an exception */
+           break;
+       } catch (const Xapian::DatabaseModifiedError &error) {
+           notmuch_status_t status = notmuch_database_reopen (message->notmuch,
+                                                              NOTMUCH_DATABASE_MODE_READ_ONLY);
+           if (status != NOTMUCH_STATUS_SUCCESS)
+               INTERNAL_ERROR ("unhandled error from notmuch_database_reopen: %s\n",
+                               notmuch_status_to_string (status));
+       }
+    }
+    message->last_view = message->notmuch->view;
+}
+
+void
+_notmuch_message_invalidate_metadata (notmuch_message_t *message,
+                                     const char *prefix_name)
+{
+    if (strcmp ("thread", prefix_name) == 0) {
+       talloc_free (message->thread_id);
+       message->thread_id = NULL;
+    }
+
+    if (strcmp ("tag", prefix_name) == 0) {
+       talloc_unlink (message, message->tag_list);
+       message->tag_list = NULL;
+    }
+
+    if (strcmp ("type", prefix_name) == 0) {
+       NOTMUCH_CLEAR_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
+       NOTMUCH_CLEAR_BIT (&message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST);
+    }
+
+    if (strcmp ("file-direntry", prefix_name) == 0) {
+       talloc_free (message->filename_term_list);
+       talloc_free (message->filename_list);
+       message->filename_term_list = message->filename_list = NULL;
+    }
+
+    if (strcmp ("property", prefix_name) == 0) {
+
+       if (message->property_term_list)
+           talloc_free (message->property_term_list);
+       message->property_term_list = NULL;
+
+       if (message->property_map)
+           talloc_unlink (message, message->property_map);
+
+       message->property_map = NULL;
+    }
+
+    if (strcmp ("replyto", prefix_name) == 0) {
+       talloc_free (message->in_reply_to);
+       message->in_reply_to = NULL;
+    }
+}
+
+unsigned int
+_notmuch_message_get_doc_id (notmuch_message_t *message)
+{
+    return message->doc_id;
+}
+
+const char *
+notmuch_message_get_message_id (notmuch_message_t *message)
+{
+    try {
+       _notmuch_message_ensure_metadata (message, message->message_id);
+    } catch (const Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return NULL;
+    }
+
+    if (! message->message_id)
+       INTERNAL_ERROR ("Message with document ID of %u has no message ID.\n",
+                       message->doc_id);
+    return message->message_id;
+}
+
+static void
+_notmuch_message_ensure_message_file (notmuch_message_t *message)
+{
+    const char *filename;
+
+    if (message->message_file)
+       return;
+
+    filename = notmuch_message_get_filename (message);
+    if (unlikely (filename == NULL))
+       return;
+
+    message->message_file = _notmuch_message_file_open_ctx (
+       notmuch_message_get_database (message), message, filename);
+}
+
+const char *
+notmuch_message_get_header (notmuch_message_t *message, const char *header)
+{
+    Xapian::valueno slot = Xapian::BAD_VALUENO;
+
+    /* Fetch header from the appropriate xapian value field if
+     * available */
+    if (strcasecmp (header, "from") == 0)
+       slot = NOTMUCH_VALUE_FROM;
+    else if (strcasecmp (header, "subject") == 0)
+       slot = NOTMUCH_VALUE_SUBJECT;
+    else if (strcasecmp (header, "message-id") == 0)
+       slot = NOTMUCH_VALUE_MESSAGE_ID;
+
+    if (slot != Xapian::BAD_VALUENO) {
+       try {
+           std::string value = message->doc.get_value (slot);
+
+           /* If we have NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES, then
+            * empty values indicate empty headers.  If we don't, then
+            * it could just mean we didn't record the header. */
+           if ((message->notmuch->features &
+                NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES) ||
+               ! value.empty ())
+               return talloc_strdup (message, value.c_str ());
+
+       } catch (Xapian::Error &error) {
+           LOG_XAPIAN_EXCEPTION (message, error);
+           return NULL;
+       }
+    }
+
+    /* Otherwise fall back to parsing the file */
+    _notmuch_message_ensure_message_file (message);
+    if (message->message_file == NULL)
+       return NULL;
+
+    return _notmuch_message_file_get_header (message->message_file, header);
+}
+
+/* Return the message ID from the In-Reply-To header of 'message'.
+ *
+ * Returns an empty string ("") if 'message' has no In-Reply-To
+ * header.
+ *
+ * Returns NULL if any error occurs.
+ */
+const char *
+_notmuch_message_get_in_reply_to (notmuch_message_t *message)
+{
+    _notmuch_message_ensure_metadata (message, message->in_reply_to);
+    return message->in_reply_to;
+}
+
+const char *
+notmuch_message_get_thread_id (notmuch_message_t *message)
+{
+    try {
+       _notmuch_message_ensure_metadata (message, message->thread_id);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return NULL;
+    }
+    if (! message->thread_id)
+       INTERNAL_ERROR ("Message with document ID of %u has no thread ID.\n",
+                       message->doc_id);
+    return message->thread_id;
+}
+
+void
+_notmuch_message_add_reply (notmuch_message_t *message,
+                           notmuch_message_t *reply)
+{
+    _notmuch_message_list_add_message (message->replies, reply);
+}
+
+size_t
+_notmuch_message_get_thread_depth (notmuch_message_t *message)
+{
+    return message->thread_depth;
+}
+
+void
+_notmuch_message_label_depths (notmuch_message_t *message,
+                              size_t depth)
+{
+    message->thread_depth = depth;
+
+    for (notmuch_messages_t *messages = _notmuch_messages_create (message->replies);
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages)) {
+       notmuch_message_t *child = notmuch_messages_get (messages);
+       _notmuch_message_label_depths (child, depth + 1);
+    }
+}
+
+const notmuch_string_list_t *
+_notmuch_message_get_references (notmuch_message_t *message)
+{
+    _notmuch_message_ensure_metadata (message, message->reference_list);
+    return message->reference_list;
+}
+
+static int
+_cmpmsg (const void *pa, const void *pb)
+{
+    notmuch_message_t **a = (notmuch_message_t **) pa;
+    notmuch_message_t **b = (notmuch_message_t **) pb;
+    time_t time_a = notmuch_message_get_date (*a);
+    time_t time_b = notmuch_message_get_date (*b);
+
+    if (time_a == time_b)
+       return 0;
+    else if (time_a < time_b)
+       return -1;
+    else
+       return 1;
+}
+
+notmuch_message_list_t *
+_notmuch_message_sort_subtrees (void *ctx, notmuch_message_list_t *list)
+{
+
+    size_t count = 0;
+    size_t capacity = 16;
+
+    if (! list)
+       return list;
+
+    void *local = talloc_new (NULL);
+    notmuch_message_list_t *new_list = _notmuch_message_list_create (ctx);
+    notmuch_message_t **message_array = talloc_zero_array (local, notmuch_message_t *, capacity);
+
+    for (notmuch_messages_t *messages = _notmuch_messages_create (list);
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages)) {
+       notmuch_message_t *root = notmuch_messages_get (messages);
+       if (count >= capacity) {
+           capacity *= 2;
+           message_array = talloc_realloc (local, message_array, notmuch_message_t *, capacity);
+       }
+       message_array[count++] = root;
+       root->replies = _notmuch_message_sort_subtrees (root, root->replies);
+    }
+
+    qsort (message_array, count, sizeof (notmuch_message_t *), _cmpmsg);
+    for (size_t i = 0; i < count; i++) {
+       _notmuch_message_list_add_message (new_list, message_array[i]);
+    }
+
+    talloc_free (local);
+    talloc_free (list);
+    return new_list;
+}
+
+notmuch_messages_t *
+notmuch_message_get_replies (notmuch_message_t *message)
+{
+    return _notmuch_messages_create (message->replies);
+}
+
+void
+_notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix)
+{
+    Xapian::TermIterator i;
+    size_t prefix_len = 0;
+
+    prefix_len = strlen (prefix);
+
+    while (1) {
+       i = message->doc.termlist_begin ();
+       i.skip_to (prefix);
+
+       /* Terminate loop when no terms remain with desired prefix. */
+       if (i == message->doc.termlist_end () ||
+           strncmp ((*i).c_str (), prefix, prefix_len))
+           break;
+
+       try {
+           message->doc.remove_term ((*i));
+           message->modified = true;
+       } catch (const Xapian::InvalidArgumentError) {
+           /* Ignore failure to remove non-existent term. */
+       }
+    }
+
+    _notmuch_message_invalidate_metadata (message, "property");
+}
+
+
+/* Remove all terms generated by indexing, i.e. not tags or
+ * properties, along with any automatic tags*/
+/* According to Xapian API docs, none of these calls throw
+ * exceptions */
+static notmuch_private_status_t
+_notmuch_message_remove_indexed_terms (notmuch_message_t *message)
+{
+    Xapian::TermIterator i;
+
+    const std::string
+       id_prefix = _find_prefix ("id"),
+       property_prefix = _find_prefix ("property"),
+       tag_prefix = _find_prefix ("tag"),
+       type_prefix = _find_prefix ("type");
+
+    /* Make sure we have the data to restore to Xapian*/
+    _notmuch_message_ensure_metadata (message, NULL);
+
+    /* Empirically, it turns out to be faster to remove all the terms,
+     * and add back the ones we want. */
+    message->doc.clear_terms ();
+    message->modified = true;
+
+    /* still a mail message */
+    message->doc.add_term (type_prefix + "mail");
+
+    /* Put back message-id */
+    message->doc.add_term (id_prefix + message->message_id);
+
+    /* Put back non-automatic tags */
+    for (notmuch_tags_t *tags = notmuch_message_get_tags (message);
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags)) {
+
+       const char *tag = notmuch_tags_get (tags);
+
+       if (strcmp (tag, "encrypted") != 0 &&
+           strcmp (tag, "signed") != 0 &&
+           strcmp (tag, "attachment") != 0) {
+           std::string term = tag_prefix + tag;
+           message->doc.add_term (term);
+       }
+    }
+
+    /* Put back properties */
+    notmuch_message_properties_t *list;
+
+    for (list = notmuch_message_get_properties (message, "", false);
+        notmuch_message_properties_valid (list); notmuch_message_properties_move_to_next (list)) {
+       std::string term = property_prefix +
+                          notmuch_message_properties_key (list) + "=" +
+                          notmuch_message_properties_value (list);
+
+       message->doc.add_term (term);
+    }
+
+    notmuch_message_properties_destroy (list);
+
+    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+
+/* Return true if p points at "new" or "cur". */
+static bool
+is_maildir (const char *p)
+{
+    return strcmp (p, "cur") == 0 || strcmp (p, "new") == 0;
+}
+
+/* Add "folder:" term for directory. */
+NODISCARD static notmuch_status_t
+_notmuch_message_add_folder_terms (notmuch_message_t *message,
+                                  const char *directory)
+{
+    char *folder, *last;
+
+    folder = talloc_strdup (NULL, directory);
+    if (! folder)
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+    /*
+     * If the message file is in a leaf directory named "new" or
+     * "cur", presume maildir and index the parent directory. Thus a
+     * "folder:" prefix search matches messages in the specified
+     * maildir folder, i.e. in the specified directory and its "new"
+     * and "cur" subdirectories.
+     *
+     * Note that this means the "folder:" prefix can't be used for
+     * distinguishing between message files in "new" or "cur". The
+     * "path:" prefix needs to be used for that.
+     *
+     * Note the deliberate difference to _filename_is_in_maildir(). We
+     * don't want to index different things depending on the existence
+     * or non-existence of all maildir sibling directories "new",
+     * "cur", and "tmp". Doing so would be surprising, and difficult
+     * for the user to fix in case all subdirectories were not in
+     * place during indexing.
+     */
+    last = strrchr (folder, '/');
+    if (last) {
+       if (is_maildir (last + 1))
+           *last = '\0';
+    } else if (is_maildir (folder)) {
+       *folder = '\0';
+    }
+
+    if (notmuch_status_t status = COERCE_STATUS (_notmuch_message_add_term (message, "folder",
+                                                                           folder),
+                                                "adding folder term"))
+       return status;
+
+    talloc_free (folder);
+
+    message->modified = true;
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+#define RECURSIVE_SUFFIX "/**"
+
+/* Add "path:" terms for directory. */
+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. */
+    status = COERCE_STATUS (_notmuch_message_add_term (message, "path", directory),
+                           "adding path term");
+    if (status)
+       return status;
+
+    if (strlen (directory)) {
+       char *path, *p;
+
+       path = talloc_asprintf (NULL, "%s%s", directory, RECURSIVE_SUFFIX);
+       if (! path)
+           return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+       /* Add recursive "path:" terms for directory and all parents. */
+       for (p = path + strlen (path) - 1; p > path; p--) {
+           if (*p == '/') {
+               strcpy (p, RECURSIVE_SUFFIX);
+               status = COERCE_STATUS (_notmuch_message_add_term (message, "path", path),
+                                       "adding path term");
+               if (status)
+                   return status;
+           }
+       }
+
+       talloc_free (path);
+    }
+
+    /* Recursive all-matching path:** for consistency. */
+    status = COERCE_STATUS (_notmuch_message_add_term (message, "path", "**"),
+                           "adding path term");
+    if (status)
+       return status;
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Add directory based terms for all filenames of the message. */
+static notmuch_status_t
+_notmuch_message_add_directory_terms (void *ctx, notmuch_message_t *message)
+{
+    const char *direntry_prefix = _find_prefix ("file-direntry");
+    int direntry_prefix_len = strlen (direntry_prefix);
+    Xapian::TermIterator i = message->doc.termlist_begin ();
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+
+    for (i.skip_to (direntry_prefix); i != message->doc.termlist_end (); i++) {
+       unsigned int directory_id;
+       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))
+           break;
+
+       /* Indicate that there are filenames remaining. */
+       status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+
+       direntry = term.c_str ();
+       direntry += direntry_prefix_len;
+
+       directory_id = strtol (direntry, &colon, 10);
+
+       if (colon == NULL || *colon != ':')
+           INTERNAL_ERROR ("malformed direntry");
+
+       directory = _notmuch_database_get_directory_path (ctx,
+                                                         message->notmuch,
+                                                         directory_id);
+
+       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;
+}
+
+/* Add an additional 'filename' for 'message'.
+ *
+ * This change will not be reflected in the database until the next
+ * call to _notmuch_message_sync. */
+notmuch_status_t
+_notmuch_message_add_filename (notmuch_message_t *message,
+                              const char *filename)
+{
+    const char *relative, *directory;
+    notmuch_status_t status;
+    notmuch_private_status_t private_status;
+    void *local = talloc_new (message);
+    char *direntry;
+
+    if (filename == NULL)
+       INTERNAL_ERROR ("Message filename cannot be NULL.");
+
+    if (! (message->notmuch->features & NOTMUCH_FEATURE_FILE_TERMS) ||
+       ! (message->notmuch->features & NOTMUCH_FEATURE_BOOL_FOLDER))
+       return NOTMUCH_STATUS_UPGRADE_REQUIRED;
+
+    relative = _notmuch_database_relative_path (message->notmuch, filename);
+
+    status = _notmuch_database_split_path (local, relative, &directory, NULL);
+    if (status)
+       return status;
+
+    status = _notmuch_database_filename_to_direntry (
+       local, message->notmuch, filename, NOTMUCH_FIND_CREATE, &direntry);
+    if (status)
+       return status;
+
+    /* New file-direntry allows navigating to this message with
+     * notmuch_directory_get_child_files() . */
+    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;
+
+    status = _notmuch_message_add_path_terms (message, directory);
+    if (status)
+       return status;
+
+    talloc_free (local);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Remove a particular 'filename' from 'message'.
+ *
+ * This change will not be reflected in the database until the next
+ * call to _notmuch_message_sync.
+ *
+ * If this message still has other filenames, returns
+ * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID.
+ *
+ * Note: This function does not remove a document from the database,
+ * even if the specified filename is the only filename for this
+ * message. For that functionality, see
+ * notmuch_database_remove_message. */
+notmuch_status_t
+_notmuch_message_remove_filename (notmuch_message_t *message,
+                                 const char *filename)
+{
+    void *local = talloc_new (message);
+    char *direntry;
+    notmuch_private_status_t private_status;
+    notmuch_status_t status;
+
+    if (! (message->notmuch->features & NOTMUCH_FEATURE_FILE_TERMS) ||
+       ! (message->notmuch->features & NOTMUCH_FEATURE_BOOL_FOLDER))
+       return NOTMUCH_STATUS_UPGRADE_REQUIRED;
+
+    status = _notmuch_database_filename_to_direntry (
+       local, message->notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
+    if (status || ! direntry)
+       return status;
+
+    /* Unlink this file from its parent directory. */
+    private_status = _notmuch_message_remove_term (message,
+                                                  "file-direntry", direntry);
+    status = COERCE_STATUS (private_status,
+                           "Unexpected error from _notmuch_message_remove_term");
+    if (status)
+       return status;
+
+    /* Re-synchronize "folder:" and "path:" terms for this message. */
+
+    /* Remove all "folder:" terms. */
+    _notmuch_message_remove_terms (message, _find_prefix ("folder"));
+
+    /* Remove all "path:" terms. */
+    _notmuch_message_remove_terms (message, _find_prefix ("path"));
+
+    /* Add back terms for all remaining filenames of the message. */
+    status = _notmuch_message_add_directory_terms (local, message);
+
+    talloc_free (local);
+
+    return status;
+}
+
+/* Upgrade the "folder:" prefix from V1 to V2. */
+#define FOLDER_PREFIX_V1       "XFOLDER"
+#define ZFOLDER_PREFIX_V1      "Z" FOLDER_PREFIX_V1
+void
+_notmuch_message_upgrade_folder (notmuch_message_t *message)
+{
+    /* Remove all old "folder:" terms. */
+    _notmuch_message_remove_terms (message, FOLDER_PREFIX_V1);
+
+    /* Remove all old "folder:" stemmed terms. */
+    _notmuch_message_remove_terms (message, ZFOLDER_PREFIX_V1);
+
+    /* Add new boolean "folder:" and "path:" terms. */
+    _notmuch_message_add_directory_terms (message, message);
+}
+
+char *
+_notmuch_message_talloc_copy_data (notmuch_message_t *message)
+{
+    return talloc_strdup (message, message->doc.get_data ().c_str ());
+}
+
+void
+_notmuch_message_clear_data (notmuch_message_t *message)
+{
+    message->doc.set_data ("");
+    message->modified = true;
+}
+
+static void
+_notmuch_message_ensure_filename_list (notmuch_message_t *message)
+{
+    notmuch_string_node_t *node;
+
+    if (message->filename_list)
+       return;
+
+    _notmuch_message_ensure_metadata (message, message->filename_term_list);
+
+    message->filename_list = _notmuch_string_list_create (message);
+    node = message->filename_term_list->head;
+
+    if (! node) {
+       /* A message document created by an old version of notmuch
+        * (prior to rename support) will have the filename in the
+        * data of the document rather than as a file-direntry term.
+        *
+        * It would be nice to do the upgrade of the document directly
+        * here, but the database is likely open in read-only mode. */
+
+       std::string datastr = message->doc.get_data ();
+       const char *data = datastr.c_str ();
+
+       if (data == NULL)
+           INTERNAL_ERROR ("message with no filename");
+
+       _notmuch_string_list_append (message->filename_list, data);
+
+       return;
+    }
+
+    for (; node; node = node->next) {
+       void *local = talloc_new (message);
+       const char *db_path, *directory, *basename, *filename;
+       char *colon, *direntry = NULL;
+       unsigned int directory_id;
+
+       direntry = node->string;
+
+       directory_id = strtol (direntry, &colon, 10);
+
+       if (colon == NULL || *colon != ':')
+           INTERNAL_ERROR ("malformed direntry");
+
+       basename = colon + 1;
+
+       *colon = '\0';
+
+       db_path = notmuch_config_get (message->notmuch, NOTMUCH_CONFIG_MAIL_ROOT);
+
+       directory = _notmuch_database_get_directory_path (local,
+                                                         message->notmuch,
+                                                         directory_id);
+
+       if (strlen (directory))
+           filename = talloc_asprintf (message, "%s/%s/%s",
+                                       db_path, directory, basename);
+       else
+           filename = talloc_asprintf (message, "%s/%s",
+                                       db_path, basename);
+
+       _notmuch_string_list_append (message->filename_list, filename);
+
+       talloc_free (local);
+    }
+
+    talloc_free (message->filename_term_list);
+    message->filename_term_list = NULL;
+}
+
+const char *
+notmuch_message_get_filename (notmuch_message_t *message)
+{
+    try {
+       _notmuch_message_ensure_filename_list (message);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return NULL;
+    }
+
+    if (message->filename_list == NULL)
+       return NULL;
+
+    if (message->filename_list->head == NULL ||
+       message->filename_list->head->string == NULL) {
+       INTERNAL_ERROR ("message with no filename");
+    }
+
+    return message->filename_list->head->string;
+}
+
+notmuch_filenames_t *
+notmuch_message_get_filenames (notmuch_message_t *message)
+{
+    try {
+       _notmuch_message_ensure_filename_list (message);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return NULL;
+    }
+
+    return _notmuch_filenames_create (message, message->filename_list);
+}
+
+int
+notmuch_message_count_files (notmuch_message_t *message)
+{
+    try {
+       _notmuch_message_ensure_filename_list (message);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return -1;
+    }
+
+    return _notmuch_string_list_length (message->filename_list);
+}
+
+notmuch_status_t
+notmuch_message_get_flag_st (notmuch_message_t *message,
+                            notmuch_message_flag_t flag,
+                            notmuch_bool_t *is_set)
+{
+    if (! is_set)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    try {
+       if (flag == NOTMUCH_MESSAGE_FLAG_GHOST &&
+           ! NOTMUCH_TEST_BIT (message->lazy_flags, flag))
+           _notmuch_message_ensure_metadata (message, NULL);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    *is_set = NOTMUCH_TEST_BIT (message->flags, flag);
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_bool_t
+notmuch_message_get_flag (notmuch_message_t *message,
+                         notmuch_message_flag_t flag)
+{
+    notmuch_bool_t is_set;
+    notmuch_status_t status;
+
+    status = notmuch_message_get_flag_st (message, flag, &is_set);
+
+    if (status)
+       return FALSE;
+    else
+       return is_set;
+}
+
+void
+notmuch_message_set_flag (notmuch_message_t *message,
+                         notmuch_message_flag_t flag, notmuch_bool_t enable)
+{
+    if (enable)
+       NOTMUCH_SET_BIT (&message->flags, flag);
+    else
+       NOTMUCH_CLEAR_BIT (&message->flags, flag);
+    NOTMUCH_SET_BIT (&message->lazy_flags, flag);
+}
+
+time_t
+notmuch_message_get_date (notmuch_message_t *message)
+{
+    std::string value;
+
+    try {
+       value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return 0;
+    }
+
+    if (value.empty ())
+       /* sortable_unserialise is undefined on empty string */
+       return 0;
+    return Xapian::sortable_unserialise (value);
+}
+
+notmuch_tags_t *
+notmuch_message_get_tags (notmuch_message_t *message)
+{
+    notmuch_tags_t *tags;
+
+    try {
+       _notmuch_message_ensure_metadata (message, message->tag_list);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return NULL;
+    }
+
+    tags = _notmuch_tags_create (message, message->tag_list);
+    /* _notmuch_tags_create steals the reference to the tag_list, but
+     * in this case it's still used by the message, so we add an
+     * *additional* talloc reference to the list.  As a result, it's
+     * possible to modify the message tags (which talloc_unlink's the
+     * current list from the message) while still iterating because
+     * the iterator will keep the current list alive. */
+    if (! talloc_reference (message, message->tag_list))
+       return NULL;
+
+    return tags;
+}
+
+const char *
+_notmuch_message_get_author (notmuch_message_t *message)
+{
+    return message->author;
+}
+
+void
+_notmuch_message_set_author (notmuch_message_t *message,
+                            const char *author)
+{
+    if (message->author)
+       talloc_free (message->author);
+    message->author = talloc_strdup (message, author);
+    return;
+}
+
+void
+_notmuch_message_set_header_values (notmuch_message_t *message,
+                                   const char *date,
+                                   const char *from,
+                                   const char *subject)
+{
+    time_t time_value;
+
+    /* GMime really doesn't want to see a NULL date, so protect its
+     * sensibilities. */
+    if (date == NULL || *date == '\0') {
+       time_value = 0;
+    } else {
+       time_value = g_mime_utils_header_decode_date_unix (date);
+       /*
+        * Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=779923
+        */
+       if (time_value < 0)
+           time_value = 0;
+    }
+
+    message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
+                           Xapian::sortable_serialise (time_value));
+    message->doc.add_value (NOTMUCH_VALUE_FROM, from);
+    message->doc.add_value (NOTMUCH_VALUE_SUBJECT, subject);
+    message->modified = true;
+}
+
+void
+_notmuch_message_update_subject (notmuch_message_t *message,
+                                const char *subject)
+{
+    message->doc.add_value (NOTMUCH_VALUE_SUBJECT, subject);
+    message->modified = true;
+}
+
+/* Upgrade a message to support NOTMUCH_FEATURE_LAST_MOD.  The caller
+ * must call _notmuch_message_sync. */
+void
+_notmuch_message_upgrade_last_mod (notmuch_message_t *message)
+{
+    /* _notmuch_message_sync will update the last modification
+     * revision; we just have to ask it to. */
+    message->modified = true;
+}
+
+/* Synchronize changes made to message->doc out into the database. */
+void
+_notmuch_message_sync (notmuch_message_t *message)
+{
+    if (_notmuch_database_mode (message->notmuch) == NOTMUCH_DATABASE_MODE_READ_ONLY)
+       return;
+
+    if (! message->modified)
+       return;
+
+    /* Update the last modification of this message. */
+    if (message->notmuch->features & NOTMUCH_FEATURE_LAST_MOD)
+       /* sortable_serialise gives a reasonably compact encoding,
+        * which directly translates to reduced IO when scanning the
+        * value stream.  Since it's built for doubles, we only get 53
+        * effective bits, but that's still enough for the database to
+        * last a few centuries at 1 million revisions per second. */
+       message->doc.add_value (NOTMUCH_VALUE_LAST_MOD,
+                               Xapian::sortable_serialise (
+                                   _notmuch_database_new_revision (
+                                       message->notmuch)));
+
+    message->notmuch->writable_xapian_db->
+       replace_document (message->doc_id, message->doc);
+    message->modified = false;
+}
+
+/* Delete a message document from the database, leaving a ghost
+ * message in its place */
+notmuch_status_t
+_notmuch_message_delete (notmuch_message_t *message)
+{
+    notmuch_status_t status;
+    const char *mid, *tid;
+    notmuch_message_t *ghost;
+    notmuch_private_status_t private_status;
+    notmuch_database_t *notmuch;
+    unsigned int count = 0;
+    bool is_ghost;
+
+    mid = notmuch_message_get_message_id (message);
+    tid = notmuch_message_get_thread_id (message);
+    notmuch = message->notmuch;
+
+    status = _notmuch_database_ensure_writable (message->notmuch);
+    if (status)
+       return status;
+
+    try {
+       Xapian::PostingIterator thread_doc, thread_doc_end;
+       Xapian::PostingIterator mail_doc, mail_doc_end;
+
+       /* look for a non-ghost message in the same thread */
+       /* if this was a ghost to begin with, we are done */
+       private_status = _notmuch_message_has_term (message, "type", "ghost", &is_ghost);
+       if (private_status)
+           return COERCE_STATUS (private_status,
+                                 "Error trying to determine whether message was a ghost");
+
+       message->notmuch->writable_xapian_db->delete_document (message->doc_id);
+
+       if (is_ghost)
+           return NOTMUCH_STATUS_SUCCESS;
+
+       _notmuch_database_find_doc_ids (message->notmuch, "thread", tid, &thread_doc,
+                                       &thread_doc_end);
+       _notmuch_database_find_doc_ids (message->notmuch, "type", "mail", &mail_doc, &mail_doc_end);
+
+       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: */
+       ghost = _notmuch_message_create_for_message_id (notmuch, mid, &private_status);
+       if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+           private_status = _notmuch_message_initialize_ghost (ghost, tid);
+           if (! private_status)
+               _notmuch_message_sync (ghost);
+       } else if (private_status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
+           /* this is deeply weird, and we should not have gotten
+            * into this state.  is there a better error message to
+            * return here? */
+           status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+       }
+
+       notmuch_message_destroy (ghost);
+       status = COERCE_STATUS (private_status, "Error converting to ghost message");
+    } else {
+       /* 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);
+           }
+       } catch (Xapian::Error &error) {
+           LOG_XAPIAN_EXCEPTION (message, error);
+           return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+       }
+
+    }
+    return status;
+}
+
+/* Transform a blank message into a ghost message.  The caller must
+ * _notmuch_message_sync the message. */
+notmuch_private_status_t
+_notmuch_message_initialize_ghost (notmuch_message_t *message,
+                                  const char *thread_id)
+{
+    notmuch_private_status_t status;
+
+    status = _notmuch_message_add_term (message, "type", "ghost");
+    if (status)
+       return status;
+    status = _notmuch_message_add_term (message, "thread", thread_id);
+    if (status)
+       return status;
+
+    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+/* Ensure that 'message' is not holding any file object open. Future
+ * calls to various functions will still automatically open the
+ * message file as needed.
+ */
+void
+_notmuch_message_close (notmuch_message_t *message)
+{
+    if (message->message_file) {
+       _notmuch_message_file_close (message->message_file);
+       message->message_file = NULL;
+    }
+}
+
+/* Add a name:value term to 'message', (the actual term will be
+ * encoded by prefixing the value with a short prefix). See
+ * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
+ * names to prefix values.
+ *
+ * This change will not be reflected in the database until the next
+ * call to _notmuch_message_sync. */
+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;
+    }
+
+    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);
+    return status;
+}
+
+/* Parse 'text' and add a term to 'message' for each parsed word. Each
+ * term will be added with the appropriate prefix if prefix_name is
+ * non-NULL.
+ */
+notmuch_private_status_t
+_notmuch_message_gen_terms (notmuch_message_t *message,
+                           const char *prefix_name,
+                           const char *text)
+{
+    Xapian::TermGenerator *term_gen = message->notmuch->term_gen;
+
+    if (text == NULL)
+       return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
+
+    term_gen->set_document (message->doc);
+    term_gen->set_termpos (message->termpos);
+
+    if (prefix_name) {
+       const char *prefix = _notmuch_database_prefix (message->notmuch, prefix_name);
+       if (prefix == NULL)
+           return NOTMUCH_PRIVATE_STATUS_BAD_PREFIX;
+
+       _notmuch_message_invalidate_metadata (message, prefix_name);
+       term_gen->index_text (text, 1, prefix);
+    } else {
+       term_gen->index_text (text);
+    }
+
+    /* Create a gap between this an the next terms so they don't
+     * appear to be a phrase. */
+    message->termpos = term_gen->get_termpos () + 100;
+
+    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+/* Remove a name:value term from 'message', (the actual term will be
+ * encoded by prefixing the value with a short prefix). See
+ * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
+ * names to prefix values.
+ *
+ * This change will not be reflected in the database until the next
+ * call to _notmuch_message_sync. */
+NODISCARD notmuch_private_status_t
+_notmuch_message_remove_term (notmuch_message_t *message,
+                             const char *prefix_name,
+                             const char *value)
+{
+    char *term;
+
+    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)
+       return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
+
+    try {
+       message->doc.remove_term (term);
+       message->modified = true;
+    } 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);
+
+    _notmuch_message_invalidate_metadata (message, prefix_name);
+
+    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+notmuch_private_status_t
+_notmuch_message_has_term (notmuch_message_t *message,
+                          const char *prefix_name,
+                          const char *value,
+                          bool *result)
+{
+    char *term;
+    bool out = false;
+    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)
+       return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
+
+    try {
+       /* Look for the exact term */
+       Xapian::TermIterator i = message->doc.termlist_begin ();
+       i.skip_to (term);
+       if (i != message->doc.termlist_end () &&
+           ! strcmp ((*i).c_str (), term))
+           out = true;
+    } catch (Xapian::Error &error) {
+       status = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
+    }
+    talloc_free (term);
+
+    *result = out;
+    return status;
+}
+
+notmuch_status_t
+notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
+{
+    notmuch_private_status_t private_status;
+    notmuch_status_t status;
+
+    try {
+       status = _notmuch_database_ensure_writable (message->notmuch);
+       if (status)
+           return status;
+
+       if (tag == NULL)
+           return NOTMUCH_STATUS_NULL_POINTER;
+
+       if (strlen (tag) > NOTMUCH_TAG_MAX)
+           return NOTMUCH_STATUS_TAG_TOO_LONG;
+
+       private_status = _notmuch_message_add_term (message, "tag", tag);
+       if (private_status) {
+           return COERCE_STATUS (private_status,
+                                 "_notmuch_message_remove_term return unexpected value: %d\n",
+                                 private_status);
+       }
+
+       if (! message->frozen)
+           _notmuch_message_sync (message);
+
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
+{
+    notmuch_private_status_t private_status;
+    notmuch_status_t status;
+
+    try {
+       status = _notmuch_database_ensure_writable (message->notmuch);
+       if (status)
+           return status;
+
+       if (tag == NULL)
+           return NOTMUCH_STATUS_NULL_POINTER;
+
+       if (strlen (tag) > NOTMUCH_TAG_MAX)
+           return NOTMUCH_STATUS_TAG_TOO_LONG;
+
+       private_status = _notmuch_message_remove_term (message, "tag", tag);
+       if (private_status) {
+           return COERCE_STATUS (private_status,
+                                 "_notmuch_message_remove_term return unexpected value: %d\n",
+                                 private_status);
+       }
+
+       if (! message->frozen)
+           _notmuch_message_sync (message);
+    } catch (Xapian::Error &error) {
+       LOG_XAPIAN_EXCEPTION (message, error);
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Is the given filename within a maildir directory?
+ *
+ * Specifically, is the final directory component of 'filename' either
+ * "cur" or "new". If so, return a pointer to that final directory
+ * component within 'filename'. If not, return NULL.
+ *
+ * A non-NULL return value is guaranteed to be a valid string pointer
+ * pointing to the characters "new/" or "cur/", (but not
+ * NUL-terminated).
+ */
+static const char *
+_filename_is_in_maildir (const char *filename)
+{
+    const char *slash, *dir = NULL;
+
+    /* Find the last '/' separating directory from filename. */
+    slash = strrchr (filename, '/');
+    if (slash == NULL)
+       return NULL;
+
+    /* Jump back 4 characters to where the previous '/' will be if the
+     * directory is named "cur" or "new". */
+    if (slash - filename < 4)
+       return NULL;
+
+    slash -= 4;
+
+    if (*slash != '/')
+       return NULL;
+
+    dir = slash + 1;
+
+    if (STRNCMP_LITERAL (dir, "cur/") == 0 ||
+       STRNCMP_LITERAL (dir, "new/") == 0) {
+       return dir;
+    }
+
+    return NULL;
+}
+
+static notmuch_status_t
+_ensure_maildir_flags (notmuch_message_t *message, bool force)
+{
+    const char *flags;
+    notmuch_filenames_t *filenames;
+    const char *filename, *dir;
+    char *combined_flags = talloc_strdup (message, "");
+    int seen_maildir_info = 0;
+
+    if (message->maildir_flags) {
+       if (force) {
+           talloc_free (message->maildir_flags);
+           message->maildir_flags = NULL;
+       }
+    }
+    filenames = notmuch_message_get_filenames (message);
+    if (! filenames)
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    for (;
+        notmuch_filenames_valid (filenames);
+        notmuch_filenames_move_to_next (filenames)) {
+       filename = notmuch_filenames_get (filenames);
+       dir = _filename_is_in_maildir (filename);
+
+       if (! dir)
+           continue;
+
+       flags = strstr (filename, ":2,");
+       if (flags) {
+           seen_maildir_info = 1;
+           flags += 3;
+           combined_flags = talloc_strdup_append (combined_flags, flags);
+       } else if (STRNCMP_LITERAL (dir, "new/") == 0) {
+           /* Messages are delivered to new/ with no "info" part, but
+            * they effectively have default maildir flags.  According
+            * to the spec, we should ignore the info part for
+            * messages in new/, but some MUAs (mutt) can set maildir
+            * flags on messages in new/, so we're liberal in what we
+            * accept. */
+           seen_maildir_info = 1;
+       }
+    }
+    if (seen_maildir_info)
+       message->maildir_flags = combined_flags;
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_bool_t
+notmuch_message_has_maildir_flag (notmuch_message_t *message, char flag)
+{
+    notmuch_status_t status;
+    notmuch_bool_t ret;
+
+    status = notmuch_message_has_maildir_flag_st (message, flag, &ret);
+    if (status)
+       return FALSE;
+
+    return ret;
+}
+
+notmuch_status_t
+notmuch_message_has_maildir_flag_st (notmuch_message_t *message,
+                                    char flag,
+                                    notmuch_bool_t *is_set)
+{
+    notmuch_status_t status;
+
+    if (! is_set)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    status = _ensure_maildir_flags (message, false);
+    if (status)
+       return status;
+
+    *is_set =  message->maildir_flags && (strchr (message->maildir_flags, flag) != NULL);
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
+{
+    notmuch_status_t status;
+    unsigned i;
+
+    status = _ensure_maildir_flags (message, true);
+    if (status)
+       return status;
+    /* If none of the filenames have any maildir info field (not even
+     * an empty info with no flags set) then there's no information to
+     * go on, so do nothing. */
+    if (! message->maildir_flags)
+       return NOTMUCH_STATUS_SUCCESS;
+
+    status = notmuch_message_freeze (message);
+    if (status)
+       return status;
+
+    for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
+       if ((strchr (message->maildir_flags, flag2tag[i].flag) != NULL)
+           ^
+           flag2tag[i].inverse) {
+           status = notmuch_message_add_tag (message, flag2tag[i].tag);
+       } else {
+           status = notmuch_message_remove_tag (message, flag2tag[i].tag);
+       }
+       if (status)
+           return status;
+    }
+    status = notmuch_message_thaw (message);
+
+    return status;
+}
+
+/* From the set of tags on 'message' and the flag2tag table, compute a
+ * set of maildir-flag actions to be taken, (flags that should be
+ * either set or cleared).
+ *
+ * The result is returned as two talloced strings: to_set, and to_clear
+ */
+static void
+_get_maildir_flag_actions (notmuch_message_t *message,
+                          char **to_set_ret,
+                          char **to_clear_ret)
+{
+    char *to_set, *to_clear;
+    notmuch_tags_t *tags;
+    const char *tag;
+    unsigned i;
+
+    to_set = talloc_strdup (message, "");
+    to_clear = talloc_strdup (message, "");
+
+    /* First, find flags for all set tags. */
+    for (tags = notmuch_message_get_tags (message);
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags)) {
+       tag = notmuch_tags_get (tags);
+
+       for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
+           if (strcmp (tag, flag2tag[i].tag) == 0) {
+               if (flag2tag[i].inverse)
+                   to_clear = talloc_asprintf_append (to_clear,
+                                                      "%c",
+                                                      flag2tag[i].flag);
+               else
+                   to_set = talloc_asprintf_append (to_set,
+                                                    "%c",
+                                                    flag2tag[i].flag);
+           }
+       }
+    }
+
+    /* Then, find the flags for all tags not present. */
+    for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
+       if (flag2tag[i].inverse) {
+           if (strchr (to_clear, flag2tag[i].flag) == NULL)
+               to_set = talloc_asprintf_append (to_set, "%c", flag2tag[i].flag);
+       } else {
+           if (strchr (to_set, flag2tag[i].flag) == NULL)
+               to_clear = talloc_asprintf_append (to_clear, "%c", flag2tag[i].flag);
+       }
+    }
+
+    *to_set_ret = to_set;
+    *to_clear_ret = to_clear;
+}
+
+/* Given 'filename' and a set of maildir flags to set and to clear,
+ * compute the new maildir filename.
+ *
+ * If the existing filename is in the directory "new", the new
+ * filename will be in the directory "cur", except for the case when
+ * no flags are changed and the existing filename does not contain
+ * maildir info (starting with ",2:").
+ *
+ * After a sequence of ":2," in the filename, any subsequent
+ * single-character flags will be added or removed according to the
+ * characters in flags_to_set and flags_to_clear. Any existing flags
+ * not mentioned in either string will remain. The final list of flags
+ * will be in ASCII order.
+ *
+ * If the original flags seem invalid, (repeated characters or
+ * non-ASCII ordering of flags), this function will return NULL
+ * (meaning that renaming would not be safe and should not occur).
+ */
+static char *
+_new_maildir_filename (void *ctx,
+                      const char *filename,
+                      const char *flags_to_set,
+                      const char *flags_to_clear)
+{
+    const char *info, *flags;
+    unsigned int flag, last_flag;
+    char *filename_new, *dir;
+    char flag_map[128];
+    int flags_in_map = 0;
+    bool flags_changed = false;
+    unsigned int i;
+    char *s;
+
+    memset (flag_map, 0, sizeof (flag_map));
+
+    info = strstr (filename, ":2,");
+
+    if (info == NULL) {
+       info = filename + strlen (filename);
+    } else {
+       /* Loop through existing flags in filename. */
+       for (flags = info + 3, last_flag = 0;
+            *flags;
+            last_flag = flag, flags++) {
+           flag = *flags;
+
+           /* Original flags not in ASCII order. Abort. */
+           if (flag < last_flag)
+               return NULL;
+
+           /* Non-ASCII flag. Abort. */
+           if (flag > sizeof (flag_map) - 1)
+               return NULL;
+
+           /* Repeated flag value. Abort. */
+           if (flag_map[flag])
+               return NULL;
+
+           flag_map[flag] = 1;
+           flags_in_map++;
+       }
+    }
+
+    /* Then set and clear our flags from tags. */
+    for (flags = flags_to_set; *flags; flags++) {
+       flag = *flags;
+       if (flag_map[flag] == 0) {
+           flag_map[flag] = 1;
+           flags_in_map++;
+           flags_changed = true;
+       }
+    }
+
+    for (flags = flags_to_clear; *flags; flags++) {
+       flag = *flags;
+       if (flag_map[flag]) {
+           flag_map[flag] = 0;
+           flags_in_map--;
+           flags_changed = true;
+       }
+    }
+
+    /* Messages in new/ without maildir info can be kept in new/ if no
+     * flags have changed. */
+    dir = (char *) _filename_is_in_maildir (filename);
+    if (dir && STRNCMP_LITERAL (dir, "new/") == 0 && ! *info && ! flags_changed)
+       return talloc_strdup (ctx, filename);
+
+    filename_new = (char *) talloc_size (ctx,
+                                        info - filename +
+                                        strlen (":2,") + flags_in_map + 1);
+    if (unlikely (filename_new == NULL))
+       return NULL;
+
+    strncpy (filename_new, filename, info - filename);
+    filename_new[info - filename] = '\0';
+
+    strcat (filename_new, ":2,");
+
+    s = filename_new + strlen (filename_new);
+    for (i = 0; i < sizeof (flag_map); i++) {
+       if (flag_map[i]) {
+           *s = i;
+           s++;
+       }
+    }
+    *s = '\0';
+
+    /* If message is in new/ move it under cur/. */
+    dir = (char *) _filename_is_in_maildir (filename_new);
+    if (dir && STRNCMP_LITERAL (dir, "new/") == 0)
+       memcpy (dir, "cur/", 4);
+
+    return filename_new;
+}
+
+notmuch_status_t
+notmuch_message_tags_to_maildir_flags (notmuch_message_t *message)
+{
+    notmuch_filenames_t *filenames;
+    const char *filename;
+    char *filename_new;
+    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);
+        notmuch_filenames_valid (filenames);
+        notmuch_filenames_move_to_next (filenames)) {
+       filename = notmuch_filenames_get (filenames);
+
+       if (! _filename_is_in_maildir (filename))
+           continue;
+
+       filename_new = _new_maildir_filename (message, filename,
+                                             to_set, to_clear);
+       if (filename_new == NULL)
+           continue;
+
+       if (strcmp (filename, filename_new)) {
+           int err;
+           notmuch_status_t new_status;
+
+           err = rename (filename, filename_new);
+           if (err)
+               continue;
+
+           new_status = _notmuch_message_remove_filename (message,
+                                                          filename);
+           /* Hold on to only the first error. */
+           if (! status && new_status
+               && new_status != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
+               status = new_status;
+               continue;
+           }
+
+           new_status = _notmuch_message_add_filename (message,
+                                                       filename_new);
+           /* Hold on to only the first error. */
+           if (! status && new_status) {
+               status = new_status;
+               continue;
+           }
+
+           _notmuch_message_sync (message);
+       }
+
+       talloc_free (filename_new);
+    }
+
+    talloc_free (to_set);
+    talloc_free (to_clear);
+
+    return status;
+}
+
+notmuch_status_t
+notmuch_message_remove_all_tags (notmuch_message_t *message)
+{
+    notmuch_private_status_t private_status;
+    notmuch_status_t status;
+    notmuch_tags_t *tags;
+    const char *tag;
+
+    status = _notmuch_database_ensure_writable (message->notmuch);
+    if (status)
+       return status;
+    tags = notmuch_message_get_tags (message);
+    if (! tags)
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+
+    for (;
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags)) {
+       tag = notmuch_tags_get (tags);
+
+       private_status = _notmuch_message_remove_term (message, "tag", tag);
+       if (private_status) {
+           return COERCE_STATUS (private_status,
+                                 "_notmuch_message_remove_term return unexpected value: %d\n",
+                                 private_status);
+       }
+    }
+
+    if (! message->frozen)
+       _notmuch_message_sync (message);
+
+    talloc_free (tags);
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_freeze (notmuch_message_t *message)
+{
+    notmuch_status_t status;
+
+    status = _notmuch_database_ensure_writable (message->notmuch);
+    if (status)
+       return status;
+
+    message->frozen++;
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_thaw (notmuch_message_t *message)
+{
+    notmuch_status_t status;
+
+    status = _notmuch_database_ensure_writable (message->notmuch);
+    if (status)
+       return status;
+
+    if (message->frozen > 0) {
+       message->frozen--;
+       if (message->frozen == 0)
+           _notmuch_message_sync (message);
+       return NOTMUCH_STATUS_SUCCESS;
+    } else {
+       return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW;
+    }
+}
+
+void
+notmuch_message_destroy (notmuch_message_t *message)
+{
+    talloc_free (message);
+}
+
+notmuch_database_t *
+notmuch_message_get_database (const notmuch_message_t *message)
+{
+    return message->notmuch;
+}
+
+static void
+_notmuch_message_ensure_property_map (notmuch_message_t *message)
+{
+    notmuch_string_node_t *node;
+
+    if (message->property_map)
+       return;
+
+    _notmuch_message_ensure_metadata (message, message->property_term_list);
+
+    message->property_map = _notmuch_string_map_create (message);
+
+    for (node = message->property_term_list->head; node; node = node->next) {
+       const char *key;
+       char *value;
+
+       value = strchr (node->string, '=');
+       if (! value)
+           INTERNAL_ERROR ("malformed property term");
+
+       *value = '\0';
+       value++;
+       key = node->string;
+
+       _notmuch_string_map_append (message->property_map, key, value);
+
+    }
+
+    talloc_free (message->property_term_list);
+    message->property_term_list = NULL;
+}
+
+notmuch_string_map_t *
+_notmuch_message_property_map (notmuch_message_t *message)
+{
+    _notmuch_message_ensure_property_map (message);
+
+    return message->property_map;
+}
+
+bool
+_notmuch_message_frozen (notmuch_message_t *message)
+{
+    return message->frozen;
+}
+
+notmuch_status_t
+notmuch_message_reindex (notmuch_message_t *message,
+                        notmuch_indexopts_t *indexopts)
+{
+    notmuch_database_t *notmuch = NULL;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    notmuch_private_status_t private_status;
+    notmuch_filenames_t *orig_filenames = NULL;
+    const char *orig_thread_id = NULL;
+    notmuch_message_file_t *message_file = NULL;
+
+    int found = 0;
+
+    if (message == NULL)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    /* Save in case we need to delete message */
+    orig_thread_id = notmuch_message_get_thread_id (message);
+    if (! orig_thread_id) {
+       /* the following is correct as long as there is only one reason
+        * n_m_get_thread_id returns NULL
+        */
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    /* strdup it because the metadata may be invalidated */
+    orig_thread_id = talloc_strdup (message, orig_thread_id);
+
+    notmuch = notmuch_message_get_database (message);
+
+    ret = _notmuch_database_ensure_writable (notmuch);
+    if (ret)
+       return ret;
+
+    orig_filenames = notmuch_message_get_filenames (message);
+
+    private_status = _notmuch_message_remove_indexed_terms (message);
+    if (private_status) {
+       ret = COERCE_STATUS (private_status, "error removing terms");
+       goto DONE;
+    }
+
+    ret = notmuch_message_remove_all_properties_with_prefix (message, "index.");
+    if (ret)
+       goto DONE; /* XXX TODO: distinguish from other error returns above? */
+    if (indexopts && notmuch_indexopts_get_decrypt_policy (indexopts) == NOTMUCH_DECRYPT_FALSE) {
+       ret = notmuch_message_remove_all_properties (message, "session-key");
+       if (ret)
+           goto DONE;
+    }
+
+    /* re-add the filenames with the associated indexopts */
+    for (; notmuch_filenames_valid (orig_filenames);
+        notmuch_filenames_move_to_next (orig_filenames)) {
+
+       const char *date;
+       const char *from, *to, *subject;
+       char *message_id = NULL;
+       const char *thread_id = NULL;
+
+       const char *filename = notmuch_filenames_get (orig_filenames);
+
+       message_file = _notmuch_message_file_open (notmuch, filename);
+       if (message_file == NULL)
+           continue;
+
+       ret = _notmuch_message_file_get_headers (message_file,
+                                                &from, &subject, &to, &date,
+                                                &message_id);
+       if (ret)
+           goto DONE;
+
+       /* XXX TODO: deal with changing message id? */
+
+       _notmuch_message_add_filename (message, filename);
+
+       ret = _notmuch_database_link_message_to_parents (notmuch, message,
+                                                        message_file,
+                                                        &thread_id);
+       if (ret)
+           goto DONE;
+
+       if (thread_id == NULL)
+           thread_id = orig_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);
+
+       ret = _notmuch_message_index_file (message, indexopts, message_file);
+
+       if (ret == NOTMUCH_STATUS_FILE_ERROR)
+           continue;
+       if (ret)
+           goto DONE;
+
+       found++;
+       _notmuch_message_file_close (message_file);
+       message_file = NULL;
+    }
+    if (found == 0) {
+       /* put back thread id to help cleanup */
+       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);
+    }
+
+  DONE:
+    if (message_file)
+       _notmuch_message_file_close (message_file);
+
+    /* XXX TODO destroy orig_filenames? */
+    return ret;
+}
diff --git a/lib/messages.c b/lib/messages.c
new file mode 100644 (file)
index 0000000..eec0a16
--- /dev/null
@@ -0,0 +1,194 @@
+/* messages.c - Iterator for a set of messages
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+
+#include <glib.h>
+
+/* Create a new notmuch_message_list_t object, with 'ctx' as its
+ * talloc owner.
+ *
+ * This function can return NULL in case of out-of-memory.
+ */
+notmuch_message_list_t *
+_notmuch_message_list_create (const void *ctx)
+{
+    notmuch_message_list_t *list;
+
+    list = talloc (ctx, notmuch_message_list_t);
+    if (unlikely (list == NULL))
+       return NULL;
+
+    list->head = NULL;
+    list->tail = &list->head;
+
+    return list;
+}
+
+/* Append 'message' to the end of 'list'. */
+void
+_notmuch_message_list_add_message (notmuch_message_list_t *list,
+                                  notmuch_message_t *message)
+{
+    notmuch_message_node_t *node = talloc (list, notmuch_message_node_t);
+
+    node->message = message;
+    node->next = NULL;
+
+    *(list->tail) = node;
+    list->tail = &node->next;
+}
+
+bool
+_notmuch_message_list_empty (notmuch_message_list_t *list)
+{
+    if (list == NULL)
+       return TRUE;
+
+    return (list->head == NULL);
+}
+
+notmuch_messages_t *
+_notmuch_messages_create (notmuch_message_list_t *list)
+{
+    notmuch_messages_t *messages;
+
+    if (list->head == NULL)
+       return NULL;
+
+    messages = talloc (list, notmuch_messages_t);
+    if (unlikely (messages == NULL))
+       return NULL;
+
+    messages->is_of_list_type = true;
+    messages->iterator = list->head;
+
+    return messages;
+}
+
+/* We're using the "is_of_type_list" to conditionally defer to the
+ * notmuch_mset_messages_t implementation of notmuch_messages_t in
+ * query.cc. It's ugly that that's over in query.cc, and it's ugly
+ * that we're not using a union here. Both of those uglies are due to
+ * C++:
+ *
+ *     1. I didn't want to force a C++ header file onto
+ *        notmuch-private.h and suddenly subject all our code to a
+ *        C++ compiler and its rules.
+ *
+ *     2. C++ won't allow me to put C++ objects, (with non-trivial
+ *        constructors) into a union anyway. Even though I'd
+ *        carefully control object construction with placement new
+ *        anyway. *sigh*
+ */
+notmuch_bool_t
+notmuch_messages_valid (notmuch_messages_t *messages)
+{
+    if (messages == NULL)
+       return false;
+
+    if (! messages->is_of_list_type)
+       return _notmuch_mset_messages_valid (messages);
+
+    return (messages->iterator != NULL);
+}
+
+bool
+_notmuch_messages_has_next (notmuch_messages_t *messages)
+{
+    if (! notmuch_messages_valid (messages))
+       return false;
+
+    if (! messages->is_of_list_type)
+       INTERNAL_ERROR ("_notmuch_messages_has_next not implemented for msets");
+
+    return (messages->iterator->next != NULL);
+}
+
+notmuch_message_t *
+notmuch_messages_get (notmuch_messages_t *messages)
+{
+    if (! messages->is_of_list_type)
+       return _notmuch_mset_messages_get (messages);
+
+    if (messages->iterator == NULL)
+       return NULL;
+
+    return messages->iterator->message;
+}
+
+void
+notmuch_messages_move_to_next (notmuch_messages_t *messages)
+{
+    if (! messages->is_of_list_type) {
+       _notmuch_mset_messages_move_to_next (messages);
+       return;
+    }
+
+    if (messages->iterator == NULL)
+       return;
+
+    messages->iterator = messages->iterator->next;
+}
+
+void
+notmuch_messages_destroy (notmuch_messages_t *messages)
+{
+    talloc_free (messages);
+}
+
+
+notmuch_tags_t *
+notmuch_messages_collect_tags (notmuch_messages_t *messages)
+{
+    notmuch_string_list_t *tags;
+    notmuch_tags_t *msg_tags;
+    notmuch_message_t *msg;
+    GHashTable *htable;
+    GList *keys, *l;
+    const char *tag;
+
+    tags = _notmuch_string_list_create (messages);
+    if (tags == NULL) return NULL;
+
+    htable = g_hash_table_new_full (g_str_hash, g_str_equal, free, NULL);
+
+    while ((msg = notmuch_messages_get (messages))) {
+       msg_tags = notmuch_message_get_tags (msg);
+       while ((tag = notmuch_tags_get (msg_tags))) {
+           g_hash_table_insert (htable, xstrdup (tag), NULL);
+           notmuch_tags_move_to_next (msg_tags);
+       }
+       notmuch_tags_destroy (msg_tags);
+       notmuch_message_destroy (msg);
+       notmuch_messages_move_to_next (messages);
+    }
+
+    keys = g_hash_table_get_keys (htable);
+    for (l = keys; l; l = l->next) {
+       _notmuch_string_list_append (tags, (char *) l->data);
+    }
+
+    g_list_free (keys);
+    g_hash_table_destroy (htable);
+
+    _notmuch_string_list_sort (tags);
+    return _notmuch_tags_create (messages, tags);
+}
diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h
new file mode 100644 (file)
index 0000000..367e23e
--- /dev/null
@@ -0,0 +1,768 @@
+/* notmuch-private.h - Internal interfaces for notmuch.
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#ifndef NOTMUCH_PRIVATE_H
+#define NOTMUCH_PRIVATE_H
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* For getline and asprintf */
+#endif
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "compat.h"
+
+#include "notmuch.h"
+
+#include "xutil.h"
+#include "error_util.h"
+#include "string-util.h"
+#include "crypto.h"
+#include "repair.h"
+
+NOTMUCH_BEGIN_DECLS
+
+#include <stdlib.h>
+#include <stdarg.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <assert.h>
+
+#include <talloc.h>
+
+#ifdef DEBUG
+# define DEBUG_DATABASE_SANITY 1
+# define DEBUG_THREADING 1
+# define DEBUG_QUERY 1
+#endif
+
+#define COMPILE_TIME_ASSERT(pred) ((void) sizeof (char[1 - 2 * ! (pred)]))
+
+#define STRNCMP_LITERAL(var, literal) \
+    strncmp ((var), (literal), sizeof (literal) - 1)
+
+/* Robust bit test/set/reset macros */
+#define _NOTMUCH_VALID_BIT(bit) \
+    ((bit) >= 0 && ((unsigned long) bit) < CHAR_BIT * sizeof (unsigned long long))
+#define NOTMUCH_TEST_BIT(val, bit) \
+    (_NOTMUCH_VALID_BIT (bit) ? ! ! ((val) & (1ull << (bit))) : 0)
+#define NOTMUCH_SET_BIT(valp, bit) \
+    (_NOTMUCH_VALID_BIT (bit) ? (*(valp) |= (1ull << (bit))) : *(valp))
+#define NOTMUCH_CLEAR_BIT(valp,  bit) \
+    (_NOTMUCH_VALID_BIT (bit) ? (*(valp) &= ~(1ull << (bit))) : *(valp))
+
+#define unused(x) x ## _unused __attribute__ ((unused))
+
+/* Thanks to Andrew Tridgell's (SAMBA's) talloc for this definition of
+ * unlikely. The talloc source code comes to us via the GNU LGPL v. 3.
+ */
+/* these macros gain us a few percent of speed on gcc */
+#if (__GNUC__ >= 3)
+/* the strange !! is to ensure that __builtin_expect() takes either 0 or 1
+ * as its first argument */
+#ifndef likely
+#define likely(x)   __builtin_expect (! ! (x), 1)
+#endif
+#ifndef unlikely
+#define unlikely(x) __builtin_expect (! ! (x), 0)
+#endif
+#else
+#ifndef likely
+#define likely(x) (x)
+#endif
+#ifndef unlikely
+#define unlikely(x) (x)
+#endif
+#endif
+
+typedef enum {
+    NOTMUCH_VALUE_TIMESTAMP = 0,
+    NOTMUCH_VALUE_MESSAGE_ID,
+    NOTMUCH_VALUE_FROM,
+    NOTMUCH_VALUE_SUBJECT,
+    NOTMUCH_VALUE_LAST_MOD,
+} notmuch_value_t;
+
+/* Xapian (with flint backend) complains if we provide a term longer
+ * than this, but I haven't yet found a way to query the limit
+ * programmatically. */
+#define NOTMUCH_TERM_MAX 245
+
+#define NOTMUCH_METADATA_THREAD_ID_PREFIX "thread_id_"
+
+/* For message IDs we have to be even more restrictive. Beyond fitting
+ * into the term limit, we also use message IDs to construct
+ * metadata-key values. And the documentation says that these should
+ * be restricted to about 200 characters. (The actual limit for the
+ * chert backend at least is 252.)
+ */
+#define NOTMUCH_MESSAGE_ID_MAX (200 - sizeof (NOTMUCH_METADATA_THREAD_ID_PREFIX))
+
+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,
+    NOTMUCH_PRIVATE_STATUS_READ_ONLY_DATABASE          = NOTMUCH_STATUS_READ_ONLY_DATABASE,
+    NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION            = NOTMUCH_STATUS_XAPIAN_EXCEPTION,
+    NOTMUCH_PRIVATE_STATUS_FILE_ERROR                  = NOTMUCH_STATUS_FILE_ERROR,
+    NOTMUCH_PRIVATE_STATUS_FILE_NOT_EMAIL              = NOTMUCH_STATUS_FILE_NOT_EMAIL,
+    NOTMUCH_PRIVATE_STATUS_NULL_POINTER                        = NOTMUCH_STATUS_NULL_POINTER,
+    NOTMUCH_PRIVATE_STATUS_TAG_TOO_LONG                        = NOTMUCH_STATUS_TAG_TOO_LONG,
+    NOTMUCH_PRIVATE_STATUS_UNBALANCED_FREEZE_THAW      = NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
+    NOTMUCH_PRIVATE_STATUS_UNBALANCED_ATOMIC           = NOTMUCH_STATUS_UNBALANCED_ATOMIC,
+    NOTMUCH_PRIVATE_STATUS_UNSUPPORTED_OPERATION       = NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
+    NOTMUCH_PRIVATE_STATUS_UPGRADE_REQUIRED            = NOTMUCH_STATUS_UPGRADE_REQUIRED,
+    NOTMUCH_PRIVATE_STATUS_PATH_ERROR                  = NOTMUCH_STATUS_PATH_ERROR,
+    NOTMUCH_PRIVATE_STATUS_IGNORED                     = NOTMUCH_STATUS_IGNORED,
+    NOTMUCH_PRIVATE_STATUS_ILLEGAL_ARGUMENT            = NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
+    NOTMUCH_PRIVATE_STATUS_MALFORMED_CRYPTO_PROTOCOL           = NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL,
+    NOTMUCH_PRIVATE_STATUS_FAILED_CRYPTO_CONTEXT_CREATION      = NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION,
+    NOTMUCH_PRIVATE_STATUS_UNKNOWN_CRYPTO_PROTOCOL             = NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL,
+    NOTMUCH_PRIVATE_STATUS_NO_CONFIG                           = NOTMUCH_STATUS_NO_CONFIG,
+    NOTMUCH_PRIVATE_STATUS_NO_DATABASE                         = NOTMUCH_STATUS_NO_DATABASE,
+    NOTMUCH_PRIVATE_STATUS_DATABASE_EXISTS                     = NOTMUCH_STATUS_DATABASE_EXISTS,
+    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,
+    NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND,
+    NOTMUCH_PRIVATE_STATUS_BAD_PREFIX,
+
+    NOTMUCH_PRIVATE_STATUS_LAST_STATUS
+} notmuch_private_status_t;
+
+/* Coerce a notmuch_private_status_t value to a notmuch_status_t
+ * value, generating an internal error if the private value is equal
+ * to or greater than NOTMUCH_STATUS_LAST_STATUS. (The idea here is
+ * that the caller has previously handled any expected
+ * notmuch_private_status_t values.)
+ *
+ * Note that the function _internal_error does not return. Evaluating
+ * to NOTMUCH_STATUS_SUCCESS is done purely to appease the compiler.
+ */
+#define COERCE_STATUS(private_status, format, ...)                      \
+    ((private_status >= (notmuch_private_status_t) NOTMUCH_STATUS_LAST_STATUS) \
+     ?                                                                  \
+     _internal_error (format " (%s).\n",                                \
+                     ##__VA_ARGS__,                                    \
+                     __location__),                                    \
+     (notmuch_status_t) NOTMUCH_PRIVATE_STATUS_SUCCESS                  \
+     :                                                                  \
+     (notmuch_status_t) private_status)
+
+/* Flags shared by various lookup functions. */
+typedef enum {
+    /* Lookup without creating any documents.  This is the default
+     * behavior. */
+    NOTMUCH_FIND_LOOKUP = 0,
+    /* If set, create the necessary document (or documents) if they
+     * are missing.  Requires a read/write database. */
+    NOTMUCH_FIND_CREATE = 1 << 0,
+} notmuch_find_flags_t;
+
+typedef struct _notmuch_doc_id_set notmuch_doc_id_set_t;
+
+/* database.cc */
+
+/* Lookup a prefix value by name.
+ *
+ * XXX: This should really be static inside of message.cc, and we can
+ * do that once we convert database.cc to use the
+ * _notmuch_message_add/remove_term functions. */
+const char *
+_find_prefix (const char *name);
+
+/* Lookup a prefix value by name, including possibly user defined prefixes
+ */
+const char *
+_notmuch_database_prefix (notmuch_database_t *notmuch, const char *name);
+
+char *
+_notmuch_message_id_compressed (void *ctx, const char *message_id);
+
+notmuch_status_t
+_notmuch_database_ensure_writable (notmuch_database_t *notmuch);
+
+void
+_notmuch_database_log (notmuch_database_t *notmuch,
+                      const char *format, ...);
+
+void
+_notmuch_database_log_append (notmuch_database_t *notmuch,
+                             const char *format, ...);
+
+unsigned long
+_notmuch_database_new_revision (notmuch_database_t *notmuch);
+
+const char *
+_notmuch_database_relative_path (notmuch_database_t *notmuch,
+                                const char *path);
+
+notmuch_status_t
+_notmuch_database_split_path (void *ctx,
+                             const char *path,
+                             const char **directory,
+                             const char **basename);
+
+const char *
+_notmuch_database_get_directory_db_path (const char *path);
+
+unsigned int
+_notmuch_database_generate_doc_id (notmuch_database_t *notmuch);
+
+notmuch_private_status_t
+_notmuch_database_find_unique_doc_id (notmuch_database_t *notmuch,
+                                     const char *prefix_name,
+                                     const char *value,
+                                     unsigned int *doc_id);
+
+notmuch_status_t
+_notmuch_database_find_directory_id (notmuch_database_t *database,
+                                    const char *path,
+                                    notmuch_find_flags_t flags,
+                                    unsigned int *directory_id);
+
+const char *
+_notmuch_database_get_directory_path (void *ctx,
+                                     notmuch_database_t *notmuch,
+                                     unsigned int doc_id);
+
+notmuch_status_t
+_notmuch_database_filename_to_direntry (void *ctx,
+                                       notmuch_database_t *notmuch,
+                                       const char *filename,
+                                       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 *
+_notmuch_directory_find_or_create (notmuch_database_t *notmuch,
+                                  const char *path,
+                                  notmuch_find_flags_t flags,
+                                  notmuch_status_t *status_ret);
+
+unsigned int
+_notmuch_directory_get_document_id (notmuch_directory_t *directory);
+
+notmuch_database_mode_t
+_notmuch_database_mode (notmuch_database_t *notmuch);
+
+/* message.cc */
+
+notmuch_message_t *
+_notmuch_message_create (const void *talloc_owner,
+                        notmuch_database_t *notmuch,
+                        unsigned int doc_id,
+                        notmuch_private_status_t *status);
+
+notmuch_message_t *
+_notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
+                                       const char *message_id,
+                                       notmuch_private_status_t *status);
+
+unsigned int
+_notmuch_message_get_doc_id (notmuch_message_t *message);
+
+const char *
+_notmuch_message_get_in_reply_to (notmuch_message_t *message);
+
+notmuch_private_status_t
+_notmuch_message_add_term (notmuch_message_t *message,
+                          const char *prefix_name,
+                          const char *value);
+
+notmuch_private_status_t
+_notmuch_message_remove_term (notmuch_message_t *message,
+                             const char *prefix_name,
+                             const char *value);
+
+notmuch_private_status_t
+_notmuch_message_has_term (notmuch_message_t *message,
+                          const char *prefix_name,
+                          const char *value,
+                          bool *result);
+
+notmuch_private_status_t
+_notmuch_message_gen_terms (notmuch_message_t *message,
+                           const char *prefix_name,
+                           const char *text);
+
+void
+_notmuch_message_upgrade_filename_storage (notmuch_message_t *message);
+
+void
+_notmuch_message_upgrade_folder (notmuch_message_t *message);
+
+notmuch_status_t
+_notmuch_message_add_filename (notmuch_message_t *message,
+                              const char *filename);
+
+notmuch_status_t
+_notmuch_message_remove_filename (notmuch_message_t *message,
+                                 const char *filename);
+
+notmuch_status_t
+_notmuch_message_rename (notmuch_message_t *message,
+                        const char *new_filename);
+
+void
+_notmuch_message_ensure_thread_id (notmuch_message_t *message);
+
+void
+_notmuch_message_set_header_values (notmuch_message_t *message,
+                                   const char *date,
+                                   const char *from,
+                                   const char *subject);
+
+void
+_notmuch_message_update_subject (notmuch_message_t *message,
+                                const char *subject);
+
+void
+_notmuch_message_upgrade_last_mod (notmuch_message_t *message);
+
+void
+_notmuch_message_sync (notmuch_message_t *message);
+
+notmuch_status_t
+_notmuch_message_delete (notmuch_message_t *message);
+
+notmuch_private_status_t
+_notmuch_message_initialize_ghost (notmuch_message_t *message,
+                                  const char *thread_id);
+
+void
+_notmuch_message_close (notmuch_message_t *message);
+
+/* Get a copy of the data in this message document.
+ *
+ * Caller should talloc_free the result when done.
+ *
+ * This function is intended to support database upgrade and really
+ * shouldn't be used otherwise. */
+char *
+_notmuch_message_talloc_copy_data (notmuch_message_t *message);
+
+/* Clear the data in this message document.
+ *
+ * This function is intended to support database upgrade and really
+ * shouldn't be used otherwise. */
+void
+_notmuch_message_clear_data (notmuch_message_t *message);
+
+/* Set the author member of 'message' - this is the representation used
+ * when displaying the message */
+void
+_notmuch_message_set_author (notmuch_message_t *message, const char *author);
+
+/* Get the author member of 'message' */
+const char *
+_notmuch_message_get_author (notmuch_message_t *message);
+
+/* message-file.c */
+
+/* XXX: I haven't decided yet whether these will actually get exported
+ * into the public interface in notmuch.h
+ */
+
+typedef struct _notmuch_message_file notmuch_message_file_t;
+
+/* Open a file containing a single email message.
+ *
+ * The caller should call notmuch_message_close when done with this.
+ *
+ * Returns NULL if any error occurs.
+ */
+notmuch_message_file_t *
+_notmuch_message_file_open (notmuch_database_t *notmuch, const char *filename);
+
+/* Like notmuch_message_file_open but with 'ctx' as the talloc owner. */
+notmuch_message_file_t *
+_notmuch_message_file_open_ctx (notmuch_database_t *notmuch,
+                               void *ctx, const char *filename);
+
+/* Close a notmuch message previously opened with notmuch_message_open. */
+void
+_notmuch_message_file_close (notmuch_message_file_t *message);
+
+/* Parse the message.
+ *
+ * This will be done automatically as necessary on other calls
+ * depending on it, but an explicit call allows for better error
+ * status reporting.
+ */
+notmuch_status_t
+_notmuch_message_file_parse (notmuch_message_file_t *message);
+
+/* Get the gmime message of a message file.
+ *
+ * The message file is parsed as necessary.
+ *
+ * The GMimeMessage* is set to *mime_message on success (which the
+ * caller must not unref).
+ *
+ * XXX: Would be nice to not have to expose GMimeMessage here.
+ */
+notmuch_status_t
+_notmuch_message_file_get_mime_message (notmuch_message_file_t *message,
+                                       GMimeMessage **mime_message);
+
+/* Get the value of the specified header from the message as a UTF-8 string.
+ *
+ * The message file is parsed as necessary.
+ *
+ * The header name is case insensitive.
+ *
+ * The Received: header is special - for it all Received: headers in
+ * the message are concatenated
+ *
+ * The returned value is owned by the notmuch message and is valid
+ * only until the message is closed. The caller should copy it if
+ * needing to modify the value or to hold onto it for longer.
+ *
+ * Returns NULL on errors, empty string if the message does not
+ * contain a header line matching 'header'.
+ */
+const char *
+_notmuch_message_file_get_header (notmuch_message_file_t *message,
+                                 const char *header);
+
+notmuch_status_t
+_notmuch_message_file_get_headers (notmuch_message_file_t *message_file,
+                                  const char **from_out,
+                                  const char **subject_out,
+                                  const char **to_out,
+                                  const char **date_out,
+                                  char **message_id_out);
+
+const char *
+_notmuch_message_file_get_filename (notmuch_message_file_t *message);
+
+/* add-message.cc */
+notmuch_status_t
+_notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
+                                          notmuch_message_t *message,
+                                          notmuch_message_file_t *message_file,
+                                          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 {
+    notmuch_message_t *message;
+    struct _notmuch_message_node *next;
+} notmuch_message_node_t;
+
+typedef struct _notmuch_message_list {
+    notmuch_message_node_t *head;
+    notmuch_message_node_t **tail;
+} notmuch_message_list_t;
+
+/* There's a rumor that there's an alternate struct _notmuch_messages
+ * somewhere with some nasty C++ objects in it. We'll try to maintain
+ * ignorance of that here. (See notmuch_mset_messages_t in query.cc)
+ */
+struct _notmuch_messages {
+    bool is_of_list_type;
+    notmuch_doc_id_set_t *excluded_doc_ids;
+    notmuch_message_node_t *iterator;
+};
+
+notmuch_message_list_t *
+_notmuch_message_list_create (const void *ctx);
+
+bool
+_notmuch_message_list_empty (notmuch_message_list_t *list);
+
+void
+_notmuch_message_list_add_message (notmuch_message_list_t *list,
+                                  notmuch_message_t *message);
+
+notmuch_messages_t *
+_notmuch_messages_create (notmuch_message_list_t *list);
+
+bool
+_notmuch_messages_has_next (notmuch_messages_t *messages);
+
+/* query.cc */
+
+bool
+_notmuch_mset_messages_valid (notmuch_messages_t *messages);
+
+notmuch_message_t *
+_notmuch_mset_messages_get (notmuch_messages_t *messages);
+
+void
+_notmuch_mset_messages_move_to_next (notmuch_messages_t *messages);
+
+bool
+_notmuch_doc_id_set_contains (notmuch_doc_id_set_t *doc_ids,
+                             unsigned int doc_id);
+
+void
+_notmuch_doc_id_set_remove (notmuch_doc_id_set_t *doc_ids,
+                           unsigned int doc_id);
+
+/* querying xapian documents by type (e.g. "mail" or "ghost"): */
+notmuch_status_t
+_notmuch_query_search_documents (notmuch_query_t *query,
+                                const char *type,
+                                notmuch_messages_t **out);
+
+notmuch_status_t
+_notmuch_query_count_documents (notmuch_query_t *query,
+                               const char *type,
+                               unsigned *count_out);
+/* message-id.c */
+
+/* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
+ * comments, and the '<' and '>' delimiters.
+ *
+ * If not NULL, then *next will be made to point to the first character
+ * not parsed, (possibly pointing to the final '\0' terminator.
+ *
+ * Returns a newly talloc'ed string belonging to 'ctx'.
+ *
+ * Returns NULL if there is any error parsing the message-id. */
+char *
+_notmuch_message_id_parse (void *ctx, const char *message_id, const char **next);
+
+/* Parse a message-id, discarding leading and trailing whitespace, and
+ * '<' and '>' delimiters.
+ *
+ * Apply a probably-stricter-than RFC definition of what is allowed in
+ * a message-id. In particular, forbid whitespace.
+ *
+ * Returns a newly talloc'ed string belonging to 'ctx'.
+ *
+ * Returns NULL if there is any error parsing the message-id.
+ */
+
+char *
+_notmuch_message_id_parse_strict (void *ctx, const char *message_id);
+
+
+/* message.cc */
+
+void
+_notmuch_message_add_reply (notmuch_message_t *message,
+                           notmuch_message_t *reply);
+
+void
+_notmuch_message_remove_unprefixed_terms (notmuch_message_t *message);
+
+size_t _notmuch_message_get_thread_depth (notmuch_message_t *message);
+
+void
+_notmuch_message_label_depths (notmuch_message_t *message,
+                              size_t depth);
+
+notmuch_message_list_t *
+_notmuch_message_sort_subtrees (void *ctx, notmuch_message_list_t *list);
+
+/* sha1.c */
+
+char *
+_notmuch_sha1_of_string (const char *str);
+
+char *
+_notmuch_sha1_of_file (const char *filename);
+
+/* string-list.c */
+
+typedef struct _notmuch_string_node {
+    char *string;
+    struct _notmuch_string_node *next;
+} notmuch_string_node_t;
+
+typedef struct _notmuch_string_list {
+    int length;
+    notmuch_string_node_t *head;
+    notmuch_string_node_t **tail;
+} notmuch_string_list_t;
+
+notmuch_string_list_t *
+_notmuch_string_list_create (const void *ctx);
+
+/*
+ * return the number of strings in 'list'
+ */
+int
+_notmuch_string_list_length (notmuch_string_list_t *list);
+
+/* Add 'string' to 'list'.
+ *
+ * The list will create its own talloced copy of 'string'.
+ */
+void
+_notmuch_string_list_append (notmuch_string_list_t *list,
+                            const char *string);
+
+void
+_notmuch_string_list_sort (notmuch_string_list_t *list);
+
+const notmuch_string_list_t *
+_notmuch_message_get_references (notmuch_message_t *message);
+
+/* string-map.c */
+typedef struct _notmuch_string_map notmuch_string_map_t;
+typedef struct _notmuch_string_map_iterator notmuch_string_map_iterator_t;
+notmuch_string_map_t *
+_notmuch_string_map_create (const void *ctx);
+
+void
+_notmuch_string_map_append (notmuch_string_map_t *map,
+                           const char *key,
+                           const char *value);
+
+void
+_notmuch_string_map_set (notmuch_string_map_t *map,
+                        const char *key,
+                        const char *value);
+
+const char *
+_notmuch_string_map_get (notmuch_string_map_t *map, const char *key);
+
+notmuch_string_map_iterator_t *
+_notmuch_string_map_iterator_create (notmuch_string_map_t *map, const char *key,
+                                    bool exact);
+
+bool
+_notmuch_string_map_iterator_valid (notmuch_string_map_iterator_t *iter);
+
+void
+_notmuch_string_map_iterator_move_to_next (notmuch_string_map_iterator_t *iter);
+
+const char *
+_notmuch_string_map_iterator_key (notmuch_string_map_iterator_t *iterator);
+
+const char *
+_notmuch_string_map_iterator_value (notmuch_string_map_iterator_t *iterator);
+
+void
+_notmuch_string_map_iterator_destroy (notmuch_string_map_iterator_t *iterator);
+
+/* Create an iterator for user headers. Destroy with
+ * _notmuch_string_map_iterator_destroy. Actually in database.cc*/
+notmuch_string_map_iterator_t *
+_notmuch_database_user_headers (notmuch_database_t *notmuch);
+
+/* tags.c */
+
+notmuch_tags_t *
+_notmuch_tags_create (const void *ctx, notmuch_string_list_t *list);
+
+/* filenames.c */
+
+/* The notmuch_filenames_t iterates over a notmuch_string_list_t of
+ * file names */
+notmuch_filenames_t *
+_notmuch_filenames_create (const void *ctx,
+                          notmuch_string_list_t *list);
+
+/* thread.cc */
+
+notmuch_thread_t *
+_notmuch_thread_create (void *ctx,
+                       notmuch_database_t *notmuch,
+                       unsigned int seed_doc_id,
+                       notmuch_doc_id_set_t *match_set,
+                       notmuch_string_list_t *excluded_terms,
+                       notmuch_exclude_t omit_exclude,
+                       notmuch_sort_t sort);
+
+/* indexopts.c */
+
+struct _notmuch_indexopts;
+
+#define CONFIG_HEADER_PREFIX "index.header."
+
+#define EMPTY_STRING(s) ((s)[0] == '\0')
+
+/* config.cc */
+notmuch_status_t
+_notmuch_config_load_from_database (notmuch_database_t *db);
+
+notmuch_status_t
+_notmuch_config_load_from_file (notmuch_database_t *db, GKeyFile *file, char **status_string);
+
+notmuch_status_t
+_notmuch_config_load_defaults (notmuch_database_t *db);
+
+void
+_notmuch_config_cache (notmuch_database_t *db, notmuch_config_key_t key, const char *val);
+
+/* open.cc */
+notmuch_status_t
+_notmuch_choose_xapian_path (void *ctx, const char *database_path, const char **xapian_path,
+                            char **message);
+
+NOTMUCH_END_DECLS
+
+#ifdef __cplusplus
+/* Implicit typecast from 'void *' to 'T *' is okay in C, but not in
+ * C++. In talloc_steal, an explicit cast is provided for type safety
+ * in some GCC versions. Otherwise, a cast is required. Provide a
+ * template function for this to maintain type safety, and redefine
+ * talloc_steal to use it.
+ */
+#if ! (__GNUC__ >= 3)
+template <class T> T *
+_notmuch_talloc_steal (const void *new_ctx, const T *ptr)
+{
+    return static_cast<T *> (talloc_steal (new_ctx, ptr));
+}
+#undef talloc_steal
+#define talloc_steal _notmuch_talloc_steal
+#endif
+
+#if __cplusplus >= 201703L || __cppcheck__
+#define NODISCARD [[nodiscard]]
+#else
+#define NODISCARD /**/
+#endif
+#endif
+
+#endif
diff --git a/lib/notmuch.h b/lib/notmuch.h
new file mode 100644 (file)
index 0000000..4e2b0fa
--- /dev/null
@@ -0,0 +1,2872 @@
+/* notmuch - Not much of an email library, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+/**
+ * @defgroup notmuch The notmuch API
+ *
+ * Not much of an email library, (just index and search)
+ *
+ * @{
+ */
+
+#ifndef NOTMUCH_H
+#define NOTMUCH_H
+
+#ifndef __DOXYGEN__
+
+#ifdef  __cplusplus
+# define NOTMUCH_BEGIN_DECLS  extern "C" {
+# define NOTMUCH_END_DECLS    }
+#else
+# define NOTMUCH_BEGIN_DECLS
+# define NOTMUCH_END_DECLS
+#endif
+
+NOTMUCH_BEGIN_DECLS
+
+#include <time.h>
+
+#pragma GCC visibility push(default)
+
+#ifndef FALSE
+#define FALSE 0
+#endif
+
+#ifndef TRUE
+#define TRUE 1
+#endif
+
+/*
+ * The library version number.  This must agree with the soname
+ * version in Makefile.local.
+ */
+#define LIBNOTMUCH_MAJOR_VERSION        5
+#define LIBNOTMUCH_MINOR_VERSION        6
+#define LIBNOTMUCH_MICRO_VERSION        0
+
+
+#if defined (__clang_major__) && __clang_major__ >= 3 \
+    || defined (__GNUC__) && __GNUC__ >= 5 \
+    || defined (__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ >= 5
+#define NOTMUCH_DEPRECATED(major, minor) \
+    __attribute__ ((deprecated ("function deprecated as of libnotmuch " #major "." #minor)))
+#else
+#define NOTMUCH_DEPRECATED(major, minor) __attribute__ ((deprecated))
+#endif
+
+
+#endif /* __DOXYGEN__ */
+
+/**
+ * Check the version of the notmuch library being compiled against.
+ *
+ * Return true if the library being compiled against is of the
+ * specified version or above. For example:
+ *
+ * @code
+ * #if LIBNOTMUCH_CHECK_VERSION(3, 1, 0)
+ *     (code requiring libnotmuch 3.1.0 or above)
+ * #endif
+ * @endcode
+ *
+ * LIBNOTMUCH_CHECK_VERSION has been defined since version 3.1.0; to
+ * check for versions prior to that, use:
+ *
+ * @code
+ * #if !defined(NOTMUCH_CHECK_VERSION)
+ *     (code requiring libnotmuch prior to 3.1.0)
+ * #endif
+ * @endcode
+ */
+#define LIBNOTMUCH_CHECK_VERSION(major, minor, micro)                   \
+    (LIBNOTMUCH_MAJOR_VERSION > (major) ||                                      \
+     (LIBNOTMUCH_MAJOR_VERSION == (major) && LIBNOTMUCH_MINOR_VERSION > (minor)) || \
+     (LIBNOTMUCH_MAJOR_VERSION == (major) && LIBNOTMUCH_MINOR_VERSION == (minor) && \
+      LIBNOTMUCH_MICRO_VERSION >= (micro)))
+
+/**
+ * Notmuch boolean type.
+ */
+typedef int notmuch_bool_t;
+
+/**
+ * Status codes used for the return values of most functions.
+ *
+ * A zero value (NOTMUCH_STATUS_SUCCESS) indicates that the function
+ * completed without error. Any other value indicates an error.
+ */
+typedef enum {
+    /**
+     * No error occurred.
+     */
+    NOTMUCH_STATUS_SUCCESS = 0,
+    /**
+     * Out of memory.
+     */
+    NOTMUCH_STATUS_OUT_OF_MEMORY,
+    /**
+     * An attempt was made to write to a database opened in read-only
+     * mode.
+     */
+    NOTMUCH_STATUS_READ_ONLY_DATABASE,
+    /**
+     * A Xapian exception occurred.
+     *
+     * @todo We don't really want to expose this lame XAPIAN_EXCEPTION
+     * value. Instead we should map to things like DATABASE_LOCKED or
+     * whatever.
+     */
+    NOTMUCH_STATUS_XAPIAN_EXCEPTION,
+    /**
+     * An error occurred trying to read or write to a file (this could
+     * be file not found, permission denied, etc.)
+     */
+    NOTMUCH_STATUS_FILE_ERROR,
+    /**
+     * A file was presented that doesn't appear to be an email
+     * message.
+     */
+    NOTMUCH_STATUS_FILE_NOT_EMAIL,
+    /**
+     * A file contains a message ID that is identical to a message
+     * already in the database.
+     */
+    NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID,
+    /**
+     * The user erroneously passed a NULL pointer to a notmuch
+     * function.
+     */
+    NOTMUCH_STATUS_NULL_POINTER,
+    /**
+     * A tag value is too long (exceeds NOTMUCH_TAG_MAX).
+     */
+    NOTMUCH_STATUS_TAG_TOO_LONG,
+    /**
+     * The notmuch_message_thaw function has been called more times
+     * than notmuch_message_freeze.
+     */
+    NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
+    /**
+     * notmuch_database_end_atomic has been called more times than
+     * notmuch_database_begin_atomic.
+     */
+    NOTMUCH_STATUS_UNBALANCED_ATOMIC,
+    /**
+     * The operation is not supported.
+     */
+    NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
+    /**
+     * The operation requires a database upgrade.
+     */
+    NOTMUCH_STATUS_UPGRADE_REQUIRED,
+    /**
+     * There is a problem with the proposed path, e.g. a relative path
+     * passed to a function expecting an absolute path.
+     */
+    NOTMUCH_STATUS_PATH_ERROR,
+    /**
+     * The requested operation was ignored. Depending on the function,
+     * this may not be an actual error.
+     */
+    NOTMUCH_STATUS_IGNORED,
+    /**
+     * One of the arguments violates the preconditions for the
+     * function, in a way not covered by a more specific argument.
+     */
+    NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
+    /**
+     * A MIME object claimed to have cryptographic protection which
+     * notmuch tried to handle, but the protocol was not specified in
+     * an intelligible way.
+     */
+    NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL,
+    /**
+     * Notmuch attempted to do crypto processing, but could not
+     * initialize the engine needed to do so.
+     */
+    NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION,
+    /**
+     * A MIME object claimed to have cryptographic protection, and
+     * notmuch attempted to process it, but the specific protocol was
+     * something that notmuch doesn't know how to handle.
+     */
+    NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL,
+    /**
+     * Unable to load a config file
+     */
+    NOTMUCH_STATUS_NO_CONFIG,
+    /**
+     * Unable to load a database
+     */
+    NOTMUCH_STATUS_NO_DATABASE,
+    /**
+     * Database exists, so not (re)-created
+     */
+    NOTMUCH_STATUS_DATABASE_EXISTS,
+    /**
+     * 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.
+     */
+    NOTMUCH_STATUS_LAST_STATUS
+} notmuch_status_t;
+
+/**
+ * Get a string representation of a notmuch_status_t value.
+ *
+ * The result is read-only.
+ */
+const char *
+notmuch_status_to_string (notmuch_status_t status);
+
+/* Various opaque data types. For each notmuch_<foo>_t see the various
+ * notmuch_<foo> functions below. */
+#ifndef __DOXYGEN__
+typedef struct _notmuch_database notmuch_database_t;
+typedef struct _notmuch_query notmuch_query_t;
+typedef struct _notmuch_threads notmuch_threads_t;
+typedef struct _notmuch_thread notmuch_thread_t;
+typedef struct _notmuch_messages notmuch_messages_t;
+typedef struct _notmuch_message notmuch_message_t;
+typedef struct _notmuch_tags notmuch_tags_t;
+typedef struct _notmuch_directory notmuch_directory_t;
+typedef struct _notmuch_filenames notmuch_filenames_t;
+typedef struct _notmuch_config_list notmuch_config_list_t;
+typedef struct _notmuch_config_values notmuch_config_values_t;
+typedef struct _notmuch_config_pairs notmuch_config_pairs_t;
+typedef struct _notmuch_indexopts notmuch_indexopts_t;
+#endif /* __DOXYGEN__ */
+
+/**
+ * Create a new, empty notmuch database located at 'path'.
+ *
+ * The path should be a top-level directory to a collection of
+ * plain-text email messages (one message per file). This call will
+ * create a new ".notmuch" directory within 'path' where notmuch will
+ * store its data.
+ *
+ * After a successful call to notmuch_database_create, the returned
+ * database will be open so the caller should call
+ * notmuch_database_destroy when finished with it.
+ *
+ * The database will not yet have any data in it
+ * (notmuch_database_create itself is a very cheap function). Messages
+ * contained within 'path' can be added to the database by calling
+ * notmuch_database_index_file.
+ *
+ * In case of any failure, this function returns an error status and
+ * sets *database to NULL (after printing an error message on stderr).
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successfully created the database.
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The given 'path' argument is NULL.
+ *
+ * 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.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred.
+ */
+notmuch_status_t
+notmuch_database_create (const char *path, notmuch_database_t **database);
+
+/**
+ * Like notmuch_database_create, except optionally return an error
+ * message. This message is allocated by malloc and should be freed by
+ * the caller.
+ */
+notmuch_status_t
+notmuch_database_create_verbose (const char *path,
+                                notmuch_database_t **database,
+                                char **error_message);
+
+/**
+ * Database open mode for notmuch_database_open.
+ */
+typedef enum {
+    /**
+     * Open database for reading only.
+     */
+    NOTMUCH_DATABASE_MODE_READ_ONLY = 0,
+    /**
+     * Open database for reading and writing.
+     */
+    NOTMUCH_DATABASE_MODE_READ_WRITE
+} notmuch_database_mode_t;
+
+/**
+ * Deprecated alias for notmuch_database_open_with_config with
+ * config_path="" and error_message=NULL
+ * @deprecated Deprecated as of libnotmuch 5.4 (notmuch 0.32)
+ */
+NOTMUCH_DEPRECATED(5, 4)
+notmuch_status_t
+notmuch_database_open (const char *path,
+                      notmuch_database_mode_t mode,
+                      notmuch_database_t **database);
+/**
+ * Deprecated alias for notmuch_database_open_with_config with
+ * config_path=""
+ *
+ * @deprecated Deprecated as of libnotmuch 5.4 (notmuch 0.32)
+ *
+ */
+NOTMUCH_DEPRECATED(5, 4)
+notmuch_status_t
+notmuch_database_open_verbose (const char *path,
+                              notmuch_database_mode_t mode,
+                              notmuch_database_t **database,
+                              char **error_message);
+
+/**
+ * Open an existing notmuch database located at 'database_path', using
+ * configuration in 'config_path'.
+ *
+ * @param[in]  database_path
+ * @parblock
+ * Path to existing database.
+ *
+ * A notmuch database is a Xapian database containing appropriate
+ * metadata.
+ *
+ * The database should have been created at some time in the past,
+ * (not necessarily by this process), by calling
+ * notmuch_database_create.
+ *
+ * If 'database_path' is NULL, use the location specified
+ *
+ * - in the environment variable NOTMUCH_DATABASE, if non-empty
+ *
+ * - in a configuration file, located as described under 'config_path'
+ *
+ * - by $XDG_DATA_HOME/notmuch/$PROFILE where XDG_DATA_HOME defaults
+ *   to "$HOME/.local/share" and PROFILE as as discussed in
+ *   'profile'
+ *
+ * If 'database_path' is non-NULL, but does not appear to be a Xapian
+ * database, check for a directory '.notmuch/xapian' below
+ * 'database_path' (this is the behavior of
+ * notmuch_database_open_verbose pre-0.32).
+ *
+ * @endparblock
+ * @param[in]  mode
+ * @parblock
+ * Mode to open database. Use one of #NOTMUCH_DATABASE_MODE_READ_WRITE
+ * or #NOTMUCH_DATABASE_MODE_READ_ONLY
+ *
+ * @endparblock
+ * @param[in]  config_path
+ * @parblock
+ * Path to config file.
+ *
+ * Config file is key-value, with mandatory sections. See
+ * <em>notmuch-config(5)</em> for more information. The key-value pair
+ * overrides the corresponding configuration data stored in the
+ * database (see <em>notmuch_database_get_config</em>)
+ *
+ * If <em>config_path</em> is NULL use the path specified
+ *
+ * - in environment variable <em>NOTMUCH_CONFIG</em>, if non-empty
+ *
+ * - by  <em>XDG_CONFIG_HOME</em>/notmuch/ where
+ *   XDG_CONFIG_HOME defaults to "$HOME/.config".
+ *
+ * - by $HOME/.notmuch-config
+ *
+ * If <em>config_path</em> is "" (empty string) then do not
+ * open any configuration file.
+ * @endparblock
+ * @param[in] profile:
+ * @parblock
+ * Name of profile (configuration/database variant).
+ *
+ * If non-NULL, append to the directory / file path determined for
+ * <em>config_path</em> and <em>database_path</em>.
+ *
+ * If NULL then use
+ * - environment variable NOTMUCH_PROFILE if defined,
+ * - otherwise "default" for directories and "" (empty string) for paths.
+ *
+ * @endparblock
+ * @param[out] database
+ * @parblock
+ * Pointer to database object. May not be NULL.
+ *
+ * The caller should call notmuch_database_destroy when finished with
+ * this database.
+ *
+ * In case of any failure, this function returns an error status and
+ * sets *database to NULL.
+ *
+ * @endparblock
+ * @param[out] error_message
+ * If non-NULL, store error message from opening the database.
+ * Any such message is allocated by \a malloc(3) and should be freed
+ * by the caller.
+ *
+ * @retval NOTMUCH_STATUS_SUCCESS: Successfully opened the database.
+ *
+ * @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
+ *     database or config file (such as permission denied, or file not found,
+ *     etc.), or the database version is unknown.
+ *
+ * @retval NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred.
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ */
+
+notmuch_status_t
+notmuch_database_open_with_config (const char *database_path,
+                                  notmuch_database_mode_t mode,
+                                  const char *config_path,
+                                  const char *profile,
+                                  notmuch_database_t **database,
+                                  char **error_message);
+
+
+/**
+ * Loads configuration from config file, database, and/or defaults
+ *
+ * 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.
+ *
+ * @retval NOTMUCH_STATUS_NO_DATABASE: No config information was
+ *     loaded from a database. Not fatal.
+ *
+ * @retval NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory.
+ *
+ * @retval NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the
+ *     database or config file (such as permission denied, or file not found,
+ *     etc.)
+ *
+ * @retval NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred.
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ */
+
+notmuch_status_t
+notmuch_database_load_config (const char *database_path,
+                             const char *config_path,
+                             const char *profile,
+                             notmuch_database_t **database,
+                             char **error_message);
+
+/**
+ * Create a new notmuch database located at 'database_path', using
+ * configuration in 'config_path'.
+ *
+ * For description of arguments, @see notmuch_database_open_with_config
+ *
+ * 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
+ *
+ * @retval NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory.
+ *
+ * @retval NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the
+ *     database or config file (such as permission denied, or file not found,
+ *     etc.)
+ *
+ * @retval NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred.
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ */
+
+notmuch_status_t
+notmuch_database_create_with_config (const char *database_path,
+                                    const char *config_path,
+                                    const char *profile,
+                                    notmuch_database_t **database,
+                                    char **error_message);
+
+/**
+ * Retrieve last status string for given database.
+ *
+ */
+const char *
+notmuch_database_status_string (const notmuch_database_t *notmuch);
+
+/**
+ * Commit changes and close the given notmuch database.
+ *
+ * After notmuch_database_close has been called, calls to other
+ * functions on objects derived from this database may either behave
+ * as if the database had not been closed (e.g., if the required data
+ * has been cached) or may fail with a
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION. The only further operation
+ * permitted on the database itself is to call
+ * notmuch_database_destroy.
+ *
+ * notmuch_database_close can be called multiple times.  Later calls
+ * have no effect.
+ *
+ * For writable databases, notmuch_database_close commits all changes
+ * 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:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successfully closed the database.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred; the
+ *     database has been closed but there are no guarantees the
+ *     changes to the database, if any, have been flushed to disk.
+ */
+notmuch_status_t
+notmuch_database_close (notmuch_database_t *database);
+
+/**
+ * A callback invoked by notmuch_database_compact to notify the user
+ * of the progress of the compaction process.
+ */
+typedef void (*notmuch_compact_status_cb_t)(const char *message, void *closure);
+
+/**
+ * Compact a notmuch database, backing up the original database to the
+ * given path.
+ *
+ * The database will be opened with NOTMUCH_DATABASE_MODE_READ_WRITE
+ * during the compaction process to ensure no writes are made.
+ *
+ * If the optional callback function 'status_cb' is non-NULL, it will
+ * be called with diagnostic and informational messages. The argument
+ * 'closure' is passed verbatim to any callback invoked.
+ */
+notmuch_status_t
+notmuch_database_compact (const char *path,
+                         const char *backup_path,
+                         notmuch_compact_status_cb_t status_cb,
+                         void *closure);
+
+/**
+ * Like notmuch_database_compact, but take an open database as a
+ * parameter.
+ *
+ * @since libnnotmuch 5.4 (notmuch 0.32)
+ */
+notmuch_status_t
+notmuch_database_compact_db (notmuch_database_t *database,
+                            const char *backup_path,
+                            notmuch_compact_status_cb_t status_cb,
+                            void *closure);
+
+/**
+ * Destroy the notmuch database, closing it if necessary and freeing
+ * all associated resources.
+ *
+ * Return value as in notmuch_database_close if the database was open;
+ * notmuch_database_destroy itself has no failure modes.
+ */
+notmuch_status_t
+notmuch_database_destroy (notmuch_database_t *database);
+
+/**
+ * Return the database path of the given database.
+ *
+ * The return value is a string owned by notmuch so should not be
+ * modified nor freed by the caller.
+ */
+const char *
+notmuch_database_get_path (notmuch_database_t *database);
+
+/**
+ * Return the database format version of the given database.
+ *
+ * @retval 0 on error
+ */
+unsigned int
+notmuch_database_get_version (notmuch_database_t *database);
+
+/**
+ * Can the database be upgraded to a newer database version?
+ *
+ * If this function returns TRUE, then the caller may call
+ * notmuch_database_upgrade to upgrade the database.  If the caller
+ * does not upgrade an out-of-date database, then some functions may
+ * fail with NOTMUCH_STATUS_UPGRADE_REQUIRED.  This always returns
+ * FALSE for a read-only database because there's no way to upgrade a
+ * read-only database.
+ *
+ * Also returns FALSE if an error occurs accessing the database.
+ *
+ */
+notmuch_bool_t
+notmuch_database_needs_upgrade (notmuch_database_t *database);
+
+/**
+ * Upgrade the current database to the latest supported version.
+ *
+ * This ensures that all current notmuch functionality will be
+ * available on the database.  After opening a database in read-write
+ * mode, it is recommended that clients check if an upgrade is needed
+ * (notmuch_database_needs_upgrade) and if so, upgrade with this
+ * function before making any modifications.  If
+ * notmuch_database_needs_upgrade returns FALSE, this will be a no-op.
+ *
+ * The optional progress_notify callback can be used by the caller to
+ * provide progress indication to the user. If non-NULL it will be
+ * called periodically with 'progress' as a floating-point value in
+ * the range of [0.0 .. 1.0] indicating the progress made so far in
+ * the upgrade process.  The argument 'closure' is passed verbatim to
+ * any callback invoked.
+ */
+notmuch_status_t
+notmuch_database_upgrade (notmuch_database_t *database,
+                         void (*progress_notify)(void *closure,
+                                                 double progress),
+                         void *closure);
+
+/**
+ * Begin an atomic database operation.
+ *
+ * Any modifications performed between a successful begin and a
+ * notmuch_database_end_atomic will be applied to the database
+ * atomically.  Note that, unlike a typical database transaction, this
+ * only ensures atomicity, not durability; neither begin nor end
+ * necessarily flush modifications to disk.
+ *
+ * Atomic sections may be nested.  begin_atomic and end_atomic must
+ * always be called in pairs.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successfully entered atomic section.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred;
+ *     atomic section not entered.
+ */
+notmuch_status_t
+notmuch_database_begin_atomic (notmuch_database_t *notmuch);
+
+/**
+ * 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:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successfully completed atomic section.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred;
+ *     atomic section not ended.
+ *
+ * NOTMUCH_STATUS_UNBALANCED_ATOMIC: The database is not currently in
+ *     an atomic section.
+ */
+notmuch_status_t
+notmuch_database_end_atomic (notmuch_database_t *notmuch);
+
+/**
+ * Return the committed database revision and UUID.
+ *
+ * The database revision number increases monotonically with each
+ * commit to the database.  Hence, all messages and message changes
+ * committed to the database (that is, visible to readers) have a last
+ * modification revision <= the committed database revision.  Any
+ * messages committed in the future will be assigned a modification
+ * revision > the committed database revision.
+ *
+ * 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. 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,
+                              const char **uuid);
+
+/**
+ * Retrieve a directory object from the database for 'path'.
+ *
+ * Here, 'path' should be a path relative to the path of 'database'
+ * (see notmuch_database_get_path), or else should be an absolute path
+ * with initial components that match the path of 'database'.
+ *
+ * If this directory object does not exist in the database, this
+ * returns NOTMUCH_STATUS_SUCCESS and sets *directory to NULL.
+ *
+ * Otherwise the returned directory object is owned by the database
+ * and as such, will only be valid until notmuch_database_destroy is
+ * called.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successfully retrieved directory.
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The given 'directory' argument is NULL.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred;
+ *     directory not retrieved.
+ *
+ * NOTMUCH_STATUS_UPGRADE_REQUIRED: The caller must upgrade the
+ *      database to use this function.
+ */
+notmuch_status_t
+notmuch_database_get_directory (notmuch_database_t *database,
+                               const char *path,
+                               notmuch_directory_t **directory);
+
+/**
+ * Add a message file to a database, indexing it for retrieval by
+ * future searches.  If a message already exists with the same message
+ * ID as the specified file, their indexes will be merged, and this
+ * new filename will also be associated with the existing message.
+ *
+ * Here, 'filename' should be a path relative to the path of
+ * 'database' (see notmuch_database_get_path), or else should be an
+ * absolute filename with initial components that match the path of
+ * 'database'.
+ *
+ * The file should be a single mail message (not a multi-message mbox)
+ * that is expected to remain at its current location, (since the
+ * notmuch database will reference the filename, and will not copy the
+ * entire contents of the file.
+ *
+ * If another message with the same message ID already exists in the
+ * database, rather than creating a new message, this adds the search
+ * terms from the identified file to the existing message's index, and
+ * adds 'filename' to the list of filenames known for the message.
+ *
+ * The 'indexopts' parameter can be NULL (meaning, use the indexing
+ * defaults from the database), or can be an explicit choice of
+ * indexing options that should govern the indexing of this specific
+ * 'filename'.
+ *
+ * If 'message' is not NULL, then, on successful return
+ * (NOTMUCH_STATUS_SUCCESS or NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) '*message'
+ * will be initialized to a message object that can be used for things
+ * such as adding tags to the just-added message. The user should call
+ * notmuch_message_destroy when done with the message. On any failure
+ * '*message' will be set to NULL.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully added to database.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred,
+ *     message not added.
+ *
+ * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: Message has the same message
+ *     ID as another message already in the database. The new
+ *     filename was successfully added to the message in the database
+ *     (if not already present) and the existing message is returned.
+ *
+ * NOTMUCH_STATUS_FILE_ERROR: an error occurred trying to open the
+ *     file, (such as permission denied, or file not found,
+ *     etc.). Nothing added to the database.
+ *
+ * NOTMUCH_STATUS_FILE_NOT_EMAIL: the contents of filename don't look
+ *     like an email message. Nothing added to the database.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so no message can be added.
+ *
+ * NOTMUCH_STATUS_UPGRADE_REQUIRED: The caller must upgrade the
+ *      database to use this function.
+ *
+ * @since libnotmuch 5.1 (notmuch 0.26)
+ */
+notmuch_status_t
+notmuch_database_index_file (notmuch_database_t *database,
+                            const char *filename,
+                            notmuch_indexopts_t *indexopts,
+                            notmuch_message_t **message);
+
+/**
+ * Deprecated alias for notmuch_database_index_file called with
+ * NULL indexopts.
+ *
+ * @deprecated Deprecated as of libnotmuch 5.1 (notmuch 0.26). Please
+ * use notmuch_database_index_file instead.
+ *
+ */
+NOTMUCH_DEPRECATED (5, 1)
+notmuch_status_t
+notmuch_database_add_message (notmuch_database_t *database,
+                             const char *filename,
+                             notmuch_message_t **message);
+
+/**
+ * Remove a message filename from the given notmuch database. If the
+ * message has no more filenames, remove the message.
+ *
+ * If the same message (as determined by the message ID) is still
+ * available via other filenames, then the message will persist in the
+ * database for those filenames. When the last filename is removed for
+ * a particular message, the database content for that message will be
+ * entirely removed.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: The last filename was removed and the
+ *     message was removed from the database.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred,
+ *     message not removed.
+ *
+ * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: This filename was removed but
+ *     the message persists in the database with at least one other
+ *     filename.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so no message can be removed.
+ *
+ * NOTMUCH_STATUS_UPGRADE_REQUIRED: The caller must upgrade the
+ *      database to use this function.
+ */
+notmuch_status_t
+notmuch_database_remove_message (notmuch_database_t *database,
+                                const char *filename);
+
+/**
+ * Find a message with the given message_id.
+ *
+ * If a message with the given message_id is found then, on successful return
+ * (NOTMUCH_STATUS_SUCCESS) '*message' will be initialized to a message
+ * object.  The caller should call notmuch_message_destroy when done with the
+ * message.
+ *
+ * On any failure or when the message is not found, this function initializes
+ * '*message' to NULL. This means, when NOTMUCH_STATUS_SUCCESS is returned, the
+ * caller is supposed to check '*message' for NULL to find out whether the
+ * message with the given message_id was found.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successful return, check '*message'.
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The given 'message' argument is NULL
+ *
+ * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory, creating message object
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred
+ */
+notmuch_status_t
+notmuch_database_find_message (notmuch_database_t *database,
+                              const char *message_id,
+                              notmuch_message_t **message);
+
+/**
+ * Find a message with the given filename.
+ *
+ * If the database contains a message with the given filename then, on
+ * successful return (NOTMUCH_STATUS_SUCCESS) '*message' will be initialized to
+ * a message object. The caller should call notmuch_message_destroy when done
+ * with the message.
+ *
+ * On any failure or when the message is not found, this function initializes
+ * '*message' to NULL. This means, when NOTMUCH_STATUS_SUCCESS is returned, the
+ * caller is supposed to check '*message' for NULL to find out whether the
+ * message with the given filename is found.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Successful return, check '*message'
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The given 'message' argument is NULL
+ *
+ * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory, creating the message object
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred
+ *
+ * NOTMUCH_STATUS_UPGRADE_REQUIRED: The caller must upgrade the
+ *      database to use this function.
+ */
+notmuch_status_t
+notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
+                                          const char *filename,
+                                          notmuch_message_t **message);
+
+/**
+ * Return a list of all tags found in the database.
+ *
+ * This function creates a list of all tags found in the database. The
+ * resulting list contains all tags from all messages found in the database.
+ *
+ * On error this function returns NULL.
+ */
+notmuch_tags_t *
+notmuch_database_get_all_tags (notmuch_database_t *db);
+
+/**
+ * Reopen an open notmuch database.
+ *
+ * @param [in] db      open notmuch database
+ * @param [in] mode    mode (read only or read-write) for reopened database.
+ *
+ * @retval #NOTMUCH_STATUS_SUCCESS
+ * @retval #NOTMUCH_STATUS_ILLEGAL_ARGUMENT    The passed database was not open.
+ * @retval #NOTMUCH_STATUS_XAPIAN_EXCEPTION    A Xapian exception occured
+ */
+notmuch_status_t
+notmuch_database_reopen (notmuch_database_t *db, notmuch_database_mode_t mode);
+
+/**
+ * Create a new query for 'database'.
+ *
+ * Here, 'database' should be an open database, (see
+ * notmuch_database_open and notmuch_database_create).
+ *
+ * For the query string, we'll document the syntax here more
+ * completely in the future, but it's likely to be a specialized
+ * version of the general Xapian query syntax:
+ *
+ * https://xapian.org/docs/queryparser.html
+ *
+ * As a special case, passing either a length-zero string, (that is ""),
+ * or a string consisting of a single asterisk (that is "*"), will
+ * result in a query that returns all messages in the database.
+ *
+ * See notmuch_query_set_sort for controlling the order of results.
+ * See notmuch_query_search_messages and notmuch_query_search_threads
+ * to actually execute the query.
+ *
+ * User should call notmuch_query_destroy when finished with this
+ * query.
+ *
+ * Will return NULL if insufficient memory is available.
+ */
+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.
+ */
+typedef enum {
+    /**
+     * Oldest first.
+     */
+    NOTMUCH_SORT_OLDEST_FIRST,
+    /**
+     * Newest first.
+     */
+    NOTMUCH_SORT_NEWEST_FIRST,
+    /**
+     * Sort by message-id.
+     */
+    NOTMUCH_SORT_MESSAGE_ID,
+    /**
+     * Do not sort.
+     */
+    NOTMUCH_SORT_UNSORTED
+} notmuch_sort_t;
+
+/**
+ * Return the query_string of this query. See notmuch_query_create.
+ */
+const char *
+notmuch_query_get_query_string (const notmuch_query_t *query);
+
+/**
+ * Return the notmuch database of this query. See notmuch_query_create.
+ */
+notmuch_database_t *
+notmuch_query_get_database (const notmuch_query_t *query);
+
+/**
+ * Exclude values for notmuch_query_set_omit_excluded. The strange
+ * order is to maintain backward compatibility: the old FALSE/TRUE
+ * options correspond to the new
+ * NOTMUCH_EXCLUDE_FLAG/NOTMUCH_EXCLUDE_TRUE options.
+ */
+typedef enum {
+    NOTMUCH_EXCLUDE_FLAG,
+    NOTMUCH_EXCLUDE_TRUE,
+    NOTMUCH_EXCLUDE_FALSE,
+    NOTMUCH_EXCLUDE_ALL
+} notmuch_exclude_t;
+
+/**
+ * Specify whether to omit excluded results or simply flag them.  By
+ * default, this is set to TRUE.
+ *
+ * If set to TRUE or ALL, notmuch_query_search_messages will omit excluded
+ * messages from the results, and notmuch_query_search_threads will omit
+ * threads that match only in excluded messages.  If set to TRUE,
+ * notmuch_query_search_threads will include all messages in threads that
+ * match in at least one non-excluded message.  Otherwise, if set to ALL,
+ * notmuch_query_search_threads will omit excluded messages from all threads.
+ *
+ * If set to FALSE or FLAG then both notmuch_query_search_messages and
+ * notmuch_query_search_threads will return all matching
+ * messages/threads regardless of exclude status. If set to FLAG then
+ * the exclude flag will be set for any excluded message that is
+ * returned by notmuch_query_search_messages, and the thread counts
+ * for threads returned by notmuch_query_search_threads will be the
+ * number of non-excluded messages/matches. Otherwise, if set to
+ * FALSE, then the exclude status is completely ignored.
+ *
+ * The performance difference when calling
+ * notmuch_query_search_messages should be relatively small (and both
+ * should be very fast).  However, in some cases,
+ * notmuch_query_search_threads is very much faster when omitting
+ * excluded messages as it does not need to construct the threads that
+ * only match in excluded messages.
+ */
+void
+notmuch_query_set_omit_excluded (notmuch_query_t *query,
+                                notmuch_exclude_t omit_excluded);
+
+/**
+ * Specify the sorting desired for this query.
+ */
+void
+notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
+
+/**
+ * Return the sort specified for this query. See
+ * notmuch_query_set_sort.
+ */
+notmuch_sort_t
+notmuch_query_get_sort (const notmuch_query_t *query);
+
+/**
+ * Add a tag that will be excluded from the query results by default.
+ * This exclusion will be ignored if this tag appears explicitly in
+ * the query.
+ *
+ * @returns
+ *
+ * NOTMUCH_STATUS_SUCCESS: excluded was added successfully.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: a Xapian exception occurred.
+ *      Most likely a problem lazily parsing the query string.
+ *
+ * NOTMUCH_STATUS_IGNORED: tag is explicitly present in the query, so
+ *             not excluded.
+ */
+notmuch_status_t
+notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag);
+
+/**
+ * Execute a query for threads, returning a notmuch_threads_t object
+ * which can be used to iterate over the results. The returned threads
+ * object is owned by the query and as such, will only be valid until
+ * notmuch_query_destroy.
+ *
+ * Typical usage might be:
+ *
+ *     notmuch_query_t *query;
+ *     notmuch_threads_t *threads;
+ *     notmuch_thread_t *thread;
+ *     notmuch_status_t stat;
+ *
+ *     query = notmuch_query_create (database, query_string);
+ *
+ *     for (stat = notmuch_query_search_threads (query, &threads);
+ *         stat == NOTMUCH_STATUS_SUCCESS &&
+ *          notmuch_threads_valid (threads);
+ *          notmuch_threads_move_to_next (threads))
+ *     {
+ *         thread = notmuch_threads_get (threads);
+ *         ....
+ *         notmuch_thread_destroy (thread);
+ *     }
+ *
+ *     notmuch_query_destroy (query);
+ *
+ * Note: If you are finished with a thread before its containing
+ * query, you can call notmuch_thread_destroy to clean up some memory
+ * sooner (as in the above example). Otherwise, if your thread objects
+ * are long-lived, then you don't need to call notmuch_thread_destroy
+ * and all the memory will still be reclaimed when the query is
+ * destroyed.
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_threads_t object. (For consistency, we do provide a
+ * notmuch_threads_destroy function, but there's no good reason
+ * to call it if the query is about to be destroyed).
+ *
+ * @since libnotmuch 5.0 (notmuch 0.25)
+ */
+notmuch_status_t
+notmuch_query_search_threads (notmuch_query_t *query,
+                             notmuch_threads_t **out);
+
+/**
+ * Deprecated alias for notmuch_query_search_threads.
+ *
+ * @deprecated Deprecated as of libnotmuch 5 (notmuch 0.25). Please
+ * use notmuch_query_search_threads instead.
+ *
+ */
+NOTMUCH_DEPRECATED (5, 0)
+notmuch_status_t
+notmuch_query_search_threads_st (notmuch_query_t *query, notmuch_threads_t **out);
+
+/**
+ * Execute a query for messages, returning a notmuch_messages_t object
+ * which can be used to iterate over the results. The returned
+ * messages object is owned by the query and as such, will only be
+ * valid until notmuch_query_destroy.
+ *
+ * Typical usage might be:
+ *
+ *     notmuch_query_t *query;
+ *     notmuch_messages_t *messages;
+ *     notmuch_message_t *message;
+ *
+ *     query = notmuch_query_create (database, query_string);
+ *
+ *     if (notmuch_query_search_messages (query, &messages) != NOTMUCH_STATUS_SUCCESS)
+ *         return EXIT_FAILURE;
+ *
+ *     for (;
+ *          notmuch_messages_valid (messages);
+ *          notmuch_messages_move_to_next (messages))
+ *     {
+ *         message = notmuch_messages_get (messages);
+ *         ....
+ *         notmuch_message_destroy (message);
+ *     }
+ *
+ *     notmuch_query_destroy (query);
+ *
+ * Note: If you are finished with a message before its containing
+ * query, you can call notmuch_message_destroy to clean up some memory
+ * sooner (as in the above example). Otherwise, if your message
+ * objects are long-lived, then you don't need to call
+ * notmuch_message_destroy and all the memory will still be reclaimed
+ * when the query is destroyed.
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_messages_t object. (For consistency, we do provide a
+ * notmuch_messages_destroy function, but there's no good
+ * reason to call it if the query is about to be destroyed).
+ *
+ * If a Xapian exception occurs this function will return NULL.
+ *
+ * @since libnotmuch 5 (notmuch 0.25)
+ */
+notmuch_status_t
+notmuch_query_search_messages (notmuch_query_t *query,
+                              notmuch_messages_t **out);
+/**
+ * Deprecated alias for notmuch_query_search_messages
+ *
+ * @deprecated Deprecated as of libnotmuch 5 (notmuch 0.25). Please use
+ * notmuch_query_search_messages instead.
+ *
+ */
+
+NOTMUCH_DEPRECATED (5, 0)
+notmuch_status_t
+notmuch_query_search_messages_st (notmuch_query_t *query,
+                                 notmuch_messages_t **out);
+
+/**
+ * Destroy a notmuch_query_t along with any associated resources.
+ *
+ * This will in turn destroy any notmuch_threads_t and
+ * notmuch_messages_t objects generated by this query, (and in
+ * turn any notmuch_thread_t and notmuch_message_t objects generated
+ * from those results, etc.), if such objects haven't already been
+ * destroyed.
+ */
+void
+notmuch_query_destroy (notmuch_query_t *query);
+
+/**
+ * Is the given 'threads' iterator pointing at a valid thread.
+ *
+ * When this function returns TRUE, notmuch_threads_get will return a
+ * valid object. Whereas when this function returns FALSE,
+ * notmuch_threads_get will return NULL.
+ *
+ * If passed a NULL pointer, this function returns FALSE
+ *
+ * See the documentation of notmuch_query_search_threads for example
+ * code showing how to iterate over a notmuch_threads_t object.
+ */
+notmuch_bool_t
+notmuch_threads_valid (notmuch_threads_t *threads);
+
+/**
+ * Get the current thread from 'threads' as a notmuch_thread_t.
+ *
+ * Note: The returned thread belongs to 'threads' and has a lifetime
+ * identical to it (and the query to which it belongs).
+ *
+ * See the documentation of notmuch_query_search_threads for example
+ * code showing how to iterate over a notmuch_threads_t object.
+ *
+ * If an out-of-memory situation occurs, this function will return
+ * NULL.
+ */
+notmuch_thread_t *
+notmuch_threads_get (notmuch_threads_t *threads);
+
+/**
+ * Move the 'threads' iterator to the next thread.
+ *
+ * If 'threads' is already pointing at the last thread then the
+ * iterator will be moved to a point just beyond that last thread,
+ * (where notmuch_threads_valid will return FALSE and
+ * notmuch_threads_get will return NULL).
+ *
+ * See the documentation of notmuch_query_search_threads for example
+ * code showing how to iterate over a notmuch_threads_t object.
+ */
+void
+notmuch_threads_move_to_next (notmuch_threads_t *threads);
+
+/**
+ * Destroy a notmuch_threads_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_threads_t object will be reclaimed when the
+ * containing query object is destroyed.
+ */
+void
+notmuch_threads_destroy (notmuch_threads_t *threads);
+
+/**
+ * Return the number of messages matching a search.
+ *
+ * This function performs a search and returns the number of matching
+ * messages.
+ *
+ * @returns
+ *
+ * NOTMUCH_STATUS_SUCCESS: query completed successfully.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: a Xapian exception occurred. The
+ *      value of *count is not defined.
+ *
+ * @since libnotmuch 5 (notmuch 0.25)
+ */
+notmuch_status_t
+notmuch_query_count_messages (notmuch_query_t *query, unsigned int *count);
+
+/**
+ * Deprecated alias for notmuch_query_count_messages
+ *
+ *
+ * @deprecated Deprecated since libnotmuch 5.0 (notmuch 0.25). Please
+ * use notmuch_query_count_messages instead.
+ */
+NOTMUCH_DEPRECATED (5, 0)
+notmuch_status_t
+notmuch_query_count_messages_st (notmuch_query_t *query, unsigned int *count);
+
+/**
+ * Return the number of threads matching a search.
+ *
+ * This function performs a search and returns the number of unique thread IDs
+ * in the matching messages. This is the same as number of threads matching a
+ * search.
+ *
+ * Note that this is a significantly heavier operation than
+ * notmuch_query_count_messages{_st}().
+ *
+ * @returns
+ *
+ * NOTMUCH_STATUS_OUT_OF_MEMORY: Memory allocation failed. The value
+ *      of *count is not defined
+ *
+ * NOTMUCH_STATUS_SUCCESS: query completed successfully.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: a Xapian exception occurred. The
+ *      value of *count is not defined.
+ *
+ * @since libnotmuch 5 (notmuch 0.25)
+ */
+notmuch_status_t
+notmuch_query_count_threads (notmuch_query_t *query, unsigned *count);
+
+/**
+ * Deprecated alias for notmuch_query_count_threads
+ *
+ * @deprecated Deprecated as of libnotmuch 5.0 (notmuch 0.25). Please
+ * use notmuch_query_count_threads_st instead.
+ */
+NOTMUCH_DEPRECATED (5, 0)
+notmuch_status_t
+notmuch_query_count_threads_st (notmuch_query_t *query, unsigned *count);
+
+/**
+ * Get the thread ID of 'thread'.
+ *
+ * The returned string belongs to 'thread' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * thread is valid, (which is until notmuch_thread_destroy or until
+ * the query from which it derived is destroyed).
+ */
+const char *
+notmuch_thread_get_thread_id (notmuch_thread_t *thread);
+
+/**
+ * Get the total number of messages in 'thread'.
+ *
+ * This count consists of all messages in the database belonging to
+ * this thread. Contrast with notmuch_thread_get_matched_messages() .
+ */
+int
+notmuch_thread_get_total_messages (notmuch_thread_t *thread);
+
+/**
+ * Get the total number of files in 'thread'.
+ *
+ * This sums notmuch_message_count_files over all messages in the
+ * thread
+ * @returns Non-negative integer
+ * @since libnotmuch 5.0 (notmuch 0.25)
+ */
+
+int
+notmuch_thread_get_total_files (notmuch_thread_t *thread);
+
+/**
+ * Get a notmuch_messages_t iterator for the top-level messages in
+ * 'thread' in oldest-first order.
+ *
+ * This iterator will not necessarily iterate over all of the messages
+ * in the thread. It will only iterate over the messages in the thread
+ * which are not replies to other messages in the thread.
+ *
+ * The returned list will be destroyed when the thread is destroyed.
+ */
+notmuch_messages_t *
+notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
+
+/**
+ * Get a notmuch_thread_t iterator for all messages in 'thread' in
+ * oldest-first order.
+ *
+ * The returned list will be destroyed when the thread is destroyed.
+ */
+notmuch_messages_t *
+notmuch_thread_get_messages (notmuch_thread_t *thread);
+
+/**
+ * Get the number of messages in 'thread' that matched the search.
+ *
+ * This count includes only the messages in this thread that were
+ * matched by the search from which the thread was created and were
+ * not excluded by any exclude tags passed in with the query (see
+ * notmuch_query_add_tag_exclude). Contrast with
+ * notmuch_thread_get_total_messages() .
+ */
+int
+notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
+
+/**
+ * Get the authors of 'thread' as a UTF-8 string.
+ *
+ * The returned string is a comma-separated list of the names of the
+ * authors of mail messages in the query results that belong to this
+ * thread.
+ *
+ * The string contains authors of messages matching the query first, then
+ * non-matched authors (with the two groups separated by '|'). Within
+ * each group, authors are ordered by date.
+ *
+ * The returned string belongs to 'thread' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * thread is valid, (which is until notmuch_thread_destroy or until
+ * the query from which it derived is destroyed).
+ */
+const char *
+notmuch_thread_get_authors (notmuch_thread_t *thread);
+
+/**
+ * Get the subject of 'thread' as a UTF-8 string.
+ *
+ * The subject is taken from the first message (according to the query
+ * order---see notmuch_query_set_sort) in the query results that
+ * belongs to this thread.
+ *
+ * The returned string belongs to 'thread' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * thread is valid, (which is until notmuch_thread_destroy or until
+ * the query from which it derived is destroyed).
+ */
+const char *
+notmuch_thread_get_subject (notmuch_thread_t *thread);
+
+/**
+ * Get the date of the oldest message in 'thread' as a time_t value.
+ */
+time_t
+notmuch_thread_get_oldest_date (notmuch_thread_t *thread);
+
+/**
+ * Get the date of the newest message in 'thread' as a time_t value.
+ */
+time_t
+notmuch_thread_get_newest_date (notmuch_thread_t *thread);
+
+/**
+ * Get the tags for 'thread', returning a notmuch_tags_t object which
+ * can be used to iterate over all tags.
+ *
+ * Note: In the Notmuch database, tags are stored on individual
+ * messages, not on threads. So the tags returned here will be all
+ * tags of the messages which matched the search and which belong to
+ * this thread.
+ *
+ * The tags object is owned by the thread and as such, will only be
+ * valid for as long as the thread is valid, (for example, until
+ * notmuch_thread_destroy or until the query from which it derived is
+ * destroyed).
+ *
+ * Typical usage might be:
+ *
+ *     notmuch_thread_t *thread;
+ *     notmuch_tags_t *tags;
+ *     const char *tag;
+ *
+ *     thread = notmuch_threads_get (threads);
+ *
+ *     for (tags = notmuch_thread_get_tags (thread);
+ *          notmuch_tags_valid (tags);
+ *          notmuch_tags_move_to_next (tags))
+ *     {
+ *         tag = notmuch_tags_get (tags);
+ *         ....
+ *     }
+ *
+ *     notmuch_thread_destroy (thread);
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_tags_t object. (For consistency, we do provide a
+ * notmuch_tags_destroy function, but there's no good reason to call
+ * it if the message is about to be destroyed).
+ */
+notmuch_tags_t *
+notmuch_thread_get_tags (notmuch_thread_t *thread);
+
+/**
+ * Destroy a notmuch_thread_t object.
+ */
+void
+notmuch_thread_destroy (notmuch_thread_t *thread);
+
+/**
+ * Is the given 'messages' iterator pointing at a valid message.
+ *
+ * When this function returns TRUE, notmuch_messages_get will return a
+ * valid object. Whereas when this function returns FALSE,
+ * notmuch_messages_get will return NULL.
+ *
+ * See the documentation of notmuch_query_search_messages for example
+ * code showing how to iterate over a notmuch_messages_t object.
+ */
+notmuch_bool_t
+notmuch_messages_valid (notmuch_messages_t *messages);
+
+/**
+ * Get the current message from 'messages' as a notmuch_message_t.
+ *
+ * Note: The returned message belongs to 'messages' and has a lifetime
+ * identical to it (and the query to which it belongs).
+ *
+ * See the documentation of notmuch_query_search_messages for example
+ * code showing how to iterate over a notmuch_messages_t object.
+ *
+ * If an out-of-memory situation occurs, this function will return
+ * NULL.
+ */
+notmuch_message_t *
+notmuch_messages_get (notmuch_messages_t *messages);
+
+/**
+ * Move the 'messages' iterator to the next message.
+ *
+ * If 'messages' is already pointing at the last message then the
+ * iterator will be moved to a point just beyond that last message,
+ * (where notmuch_messages_valid will return FALSE and
+ * notmuch_messages_get will return NULL).
+ *
+ * See the documentation of notmuch_query_search_messages for example
+ * code showing how to iterate over a notmuch_messages_t object.
+ */
+void
+notmuch_messages_move_to_next (notmuch_messages_t *messages);
+
+/**
+ * Destroy a notmuch_messages_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_messages_t object will be reclaimed when the containing
+ * query object is destroyed.
+ */
+void
+notmuch_messages_destroy (notmuch_messages_t *messages);
+
+/**
+ * Return a list of tags from all messages.
+ *
+ * The resulting list is guaranteed not to contain duplicated tags.
+ *
+ * WARNING: You can no longer iterate over messages after calling this
+ * function, because the iterator will point at the end of the list.
+ * We do not have a function to reset the iterator yet and the only
+ * way how you can iterate over the list again is to recreate the
+ * message list.
+ *
+ * The function returns NULL on error.
+ */
+notmuch_tags_t *
+notmuch_messages_collect_tags (notmuch_messages_t *messages);
+
+/**
+ * Get the database associated with this message.
+ *
+ * @since libnotmuch 5.2 (notmuch 0.27)
+ */
+notmuch_database_t *
+notmuch_message_get_database (const notmuch_message_t *message);
+
+/**
+ * Get the message ID of 'message'.
+ *
+ * The returned string belongs to 'message' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * message is valid, (which is until the query from which it derived
+ * is destroyed).
+ *
+ * This function will return NULL if triggers an unhandled Xapian
+ * exception.
+ */
+const char *
+notmuch_message_get_message_id (notmuch_message_t *message);
+
+/**
+ * Get the thread ID of 'message'.
+ *
+ * The returned string belongs to 'message' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * message is valid, (for example, until the user calls
+ * notmuch_message_destroy on 'message' or until a query from which it
+ * derived is destroyed).
+ *
+ * This function will return NULL if triggers an unhandled Xapian
+ * exception.
+ */
+const char *
+notmuch_message_get_thread_id (notmuch_message_t *message);
+
+/**
+ * Get a notmuch_messages_t iterator for all of the replies to
+ * 'message'.
+ *
+ * Note: This call only makes sense if 'message' was ultimately
+ * obtained from a notmuch_thread_t object, (such as by coming
+ * directly from the result of calling notmuch_thread_get_
+ * toplevel_messages or by any number of subsequent
+ * calls to notmuch_message_get_replies).
+ *
+ * If 'message' was obtained through some non-thread means, (such as
+ * by a call to notmuch_query_search_messages), then this function
+ * will return NULL.
+ *
+ * If there are no replies to 'message', this function will return
+ * NULL. (Note that notmuch_messages_valid will accept that NULL
+ * value as legitimate, and simply return FALSE for it.)
+ *
+ * This function also returns NULL if it triggers a Xapian exception.
+ *
+ * The returned list will be destroyed when the thread is
+ * destroyed.
+ */
+notmuch_messages_t *
+notmuch_message_get_replies (notmuch_message_t *message);
+
+/**
+ * Get the total number of files associated with a message.
+ * @returns Non-negative integer for file count.
+ * @returns Negative integer for error.
+ * @since libnotmuch 5.0 (notmuch 0.25)
+ */
+int
+notmuch_message_count_files (notmuch_message_t *message);
+
+/**
+ * Get a filename for the email corresponding to 'message'.
+ *
+ * The returned filename is an absolute filename, (the initial
+ * component will match notmuch_database_get_path() ).
+ *
+ * The returned string belongs to the message so should not be
+ * modified or freed by the caller (nor should it be referenced after
+ * the message is destroyed).
+ *
+ * Note: If this message corresponds to multiple files in the mail
+ * store, (that is, multiple files contain identical message IDs),
+ * this function will arbitrarily return a single one of those
+ * filenames. See notmuch_message_get_filenames for returning the
+ * complete list of filenames.
+ *
+ * This function returns NULL if it triggers a Xapian exception.
+ */
+const char *
+notmuch_message_get_filename (notmuch_message_t *message);
+
+/**
+ * Get all filenames for the email corresponding to 'message'.
+ *
+ * Returns a notmuch_filenames_t iterator listing all the filenames
+ * associated with 'message'. These files may not have identical
+ * content, but each will have the identical Message-ID.
+ *
+ * Each filename in the iterator is an absolute filename, (the initial
+ * component will match notmuch_database_get_path() ).
+ *
+ * This function returns NULL if it triggers a Xapian exception.
+ */
+notmuch_filenames_t *
+notmuch_message_get_filenames (notmuch_message_t *message);
+
+/**
+ * Re-index the e-mail corresponding to 'message' using the supplied index options
+ *
+ * Returns the status of the re-index operation.  (see the return
+ * codes documented in notmuch_database_index_file)
+ *
+ * After reindexing, the user should discard the message object passed
+ * in here by calling notmuch_message_destroy, since it refers to the
+ * original message, not to the reindexed message.
+ */
+notmuch_status_t
+notmuch_message_reindex (notmuch_message_t *message,
+                        notmuch_indexopts_t *indexopts);
+
+/**
+ * Message flags.
+ */
+typedef enum {
+    NOTMUCH_MESSAGE_FLAG_MATCH,
+    NOTMUCH_MESSAGE_FLAG_EXCLUDED,
+
+    /* This message is a "ghost message", meaning it has no filenames
+     * or content, but we know it exists because it was referenced by
+     * some other message.  A ghost message has only a message ID and
+     * thread ID.
+     */
+    NOTMUCH_MESSAGE_FLAG_GHOST,
+} notmuch_message_flag_t;
+
+/**
+ * Get a value of a flag for the email corresponding to 'message'.
+ *
+ * returns FALSE in case of errors.
+ *
+ * @deprecated Deprecated as of libnotmuch 5.3 (notmuch 0.31). Please
+ * use notmuch_message_get_flag_st instead.
+ */
+NOTMUCH_DEPRECATED (5, 3)
+notmuch_bool_t
+notmuch_message_get_flag (notmuch_message_t *message,
+                         notmuch_message_flag_t flag);
+
+/**
+ * Get a value of a flag for the email corresponding to 'message'.
+ *
+ * @param message a message object
+ * @param flag flag to check
+ * @param is_set pointer to boolean to store flag value.
+ *
+ * @retval #NOTMUCH_STATUS_SUCCESS
+ * @retval #NOTMUCH_STATUS_NULL_POINTER is_set is NULL
+ * @retval #NOTMUCH_STATUS_XAPIAN_EXCEPTION Accessing the database
+ * triggered an exception.
+ *
+ * @since libnotmuch 5.3 (notmuch 0.31)
+ */
+notmuch_status_t
+notmuch_message_get_flag_st (notmuch_message_t *message,
+                            notmuch_message_flag_t flag,
+                            notmuch_bool_t *is_set);
+
+/**
+ * Set a value of a flag for the email corresponding to 'message'.
+ */
+void
+notmuch_message_set_flag (notmuch_message_t *message,
+                         notmuch_message_flag_t flag, notmuch_bool_t value);
+
+/**
+ * Get the date of 'message' as a time_t value.
+ *
+ * For the original textual representation of the Date header from the
+ * message call notmuch_message_get_header() with a header value of
+ * "date".
+ *
+ * Returns 0 in case of error.
+ */
+time_t
+notmuch_message_get_date (notmuch_message_t *message);
+
+/**
+ * Get the value of the specified header from 'message' as a UTF-8 string.
+ *
+ * Common headers are stored in the database when the message is
+ * indexed and will be returned from the database.  Other headers will
+ * be read from the actual message file.
+ *
+ * The header name is case insensitive.
+ *
+ * The returned string belongs to the message so should not be
+ * modified or freed by the caller (nor should it be referenced after
+ * the message is destroyed).
+ *
+ * Returns an empty string ("") if the message does not contain a
+ * header line matching 'header'. Returns NULL if any error occurs.
+ */
+const char *
+notmuch_message_get_header (notmuch_message_t *message, const char *header);
+
+/**
+ * Get the tags for 'message', returning a notmuch_tags_t object which
+ * can be used to iterate over all tags.
+ *
+ * The tags object is owned by the message and as such, will only be
+ * valid for as long as the message is valid, (which is until the
+ * query from which it derived is destroyed).
+ *
+ * Typical usage might be:
+ *
+ *     notmuch_message_t *message;
+ *     notmuch_tags_t *tags;
+ *     const char *tag;
+ *
+ *     message = notmuch_database_find_message (database, message_id);
+ *
+ *     for (tags = notmuch_message_get_tags (message);
+ *          notmuch_tags_valid (tags);
+ *          notmuch_tags_move_to_next (tags))
+ *     {
+ *         tag = notmuch_tags_get (tags);
+ *         ....
+ *     }
+ *
+ *     notmuch_message_destroy (message);
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_tags_t object. (For consistency, we do provide a
+ * notmuch_tags_destroy function, but there's no good reason to call
+ * it if the message is about to be destroyed).
+ */
+notmuch_tags_t *
+notmuch_message_get_tags (notmuch_message_t *message);
+
+/**
+ * The longest possible tag value.
+ */
+#define NOTMUCH_TAG_MAX 200
+
+/**
+ * Add a tag to the given message.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Tag successfully added to message
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The 'tag' argument is NULL
+ *
+ * NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long
+ *     (exceeds NOTMUCH_TAG_MAX)
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so message cannot be modified.
+ */
+notmuch_status_t
+notmuch_message_add_tag (notmuch_message_t *message, const char *tag);
+
+/**
+ * Remove a tag from the given message.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Tag successfully removed from message
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The 'tag' argument is NULL
+ *
+ * NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long
+ *     (exceeds NOTMUCH_TAG_MAX)
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so message cannot be modified.
+ */
+notmuch_status_t
+notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);
+
+/**
+ * Remove all tags from the given message.
+ *
+ * See notmuch_message_freeze for an example showing how to safely
+ * replace tag values.
+ *
+ * @retval #NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in
+ *     read-only mode so message cannot be modified.
+ * @retval #NOTMUCH_STATUS_XAPIAN_EXCEPTION: an exception was thrown
+ *      accessing the database.
+ */
+notmuch_status_t
+notmuch_message_remove_all_tags (notmuch_message_t *message);
+
+/**
+ * Add/remove tags according to maildir flags in the message filename(s).
+ *
+ * This function examines the filenames of 'message' for maildir
+ * flags, and adds or removes tags on 'message' as follows when these
+ * flags are present:
+ *
+ *     Flag    Action if present
+ *     ----    -----------------
+ *     'D'     Adds the "draft" tag to the message
+ *     'F'     Adds the "flagged" tag to the message
+ *     'P'     Adds the "passed" tag to the message
+ *     'R'     Adds the "replied" tag to the message
+ *     'S'     Removes the "unread" tag from the message
+ *
+ * For each flag that is not present, the opposite action (add/remove)
+ * is performed for the corresponding tags.
+ *
+ * Flags are identified as trailing components of the filename after a
+ * sequence of ":2,".
+ *
+ * If there are multiple filenames associated with this message, the
+ * flag is considered present if it appears in one or more
+ * filenames. (That is, the flags from the multiple filenames are
+ * combined with the logical OR operator.)
+ *
+ * A client can ensure that notmuch database tags remain synchronized
+ * with maildir flags by calling this function after each call to
+ * notmuch_database_index_file. See also
+ * notmuch_message_tags_to_maildir_flags for synchronizing tag changes
+ * back to maildir flags.
+ */
+notmuch_status_t
+notmuch_message_maildir_flags_to_tags (notmuch_message_t *message);
+
+/**
+ * return TRUE if any filename of 'message' has maildir flag 'flag',
+ * FALSE otherwise.
+ *
+ * Deprecated wrapper for notmuch_message_has_maildir_flag_st
+ *
+ * @returns FALSE in case of error
+ * @deprecated libnotmuch 5.3 (notmuch 0.31)
+ */
+NOTMUCH_DEPRECATED (5, 3)
+notmuch_bool_t
+notmuch_message_has_maildir_flag (notmuch_message_t *message, char flag);
+
+/**
+ * check message for maildir flag
+ *
+ * @param [in,out]     message message to check
+ * @param [in] flag    flag to check for
+ * @param [out] is_set  pointer to boolean
+ *
+ * @retval #NOTMUCH_STATUS_SUCCESS
+ * @retval #NOTMUCH_STATUS_NULL_POINTER is_set is NULL
+ * @retval #NOTMUCH_STATUS_XAPIAN_EXCEPTION Accessing the database
+ * triggered an exception.
+ */
+notmuch_status_t
+notmuch_message_has_maildir_flag_st (notmuch_message_t *message,
+                                    char flag,
+                                    notmuch_bool_t *is_set);
+
+/**
+ * Rename message filename(s) to encode tags as maildir flags.
+ *
+ * Specifically, for each filename corresponding to this message:
+ *
+ * If the filename is not in a maildir directory, do nothing.  (A
+ * maildir directory is determined as a directory named "new" or
+ * "cur".) Similarly, if the filename has invalid maildir info,
+ * (repeated or outof-ASCII-order flag characters after ":2,"), then
+ * do nothing.
+ *
+ * If the filename is in a maildir directory, rename the file so that
+ * its filename ends with the sequence ":2," followed by zero or more
+ * of the following single-character flags (in ASCII order):
+ *
+ *   * flag 'D' iff the message has the "draft" tag
+ *   * flag 'F' iff the message has the "flagged" tag
+ *   * flag 'P' iff the message has the "passed" tag
+ *   * flag 'R' iff the message has the "replied" tag
+ *   * flag 'S' iff the message does not have the "unread" tag
+ *
+ * Any existing flags unmentioned in the list above will be preserved
+ * in the renaming.
+ *
+ * Also, if this filename is in a directory named "new", rename it to
+ * be within the neighboring directory named "cur".
+ *
+ * A client can ensure that maildir filename flags remain synchronized
+ * with notmuch database tags by calling this function after changing
+ * tags, (after calls to notmuch_message_add_tag,
+ * notmuch_message_remove_tag, or notmuch_message_freeze/
+ * notmuch_message_thaw). See also notmuch_message_maildir_flags_to_tags
+ * for synchronizing maildir flag changes back to tags.
+ */
+notmuch_status_t
+notmuch_message_tags_to_maildir_flags (notmuch_message_t *message);
+
+/**
+ * Freeze the current state of 'message' within the database.
+ *
+ * This means that changes to the message state, (via
+ * notmuch_message_add_tag, notmuch_message_remove_tag, and
+ * notmuch_message_remove_all_tags), will not be committed to the
+ * database until the message is thawed with notmuch_message_thaw.
+ *
+ * Multiple calls to freeze/thaw are valid and these calls will
+ * "stack". That is there must be as many calls to thaw as to freeze
+ * before a message is actually thawed.
+ *
+ * The ability to do freeze/thaw allows for safe transactions to
+ * change tag values. For example, explicitly setting a message to
+ * have a given set of tags might look like this:
+ *
+ *    notmuch_message_freeze (message);
+ *
+ *    notmuch_message_remove_all_tags (message);
+ *
+ *    for (i = 0; i < NUM_TAGS; i++)
+ *        notmuch_message_add_tag (message, tags[i]);
+ *
+ *    notmuch_message_thaw (message);
+ *
+ * With freeze/thaw used like this, the message in the database is
+ * guaranteed to have either the full set of original tag values, or
+ * the full set of new tag values, but nothing in between.
+ *
+ * Imagine the example above without freeze/thaw and the operation
+ * somehow getting interrupted. This could result in the message being
+ * left with no tags if the interruption happened after
+ * notmuch_message_remove_all_tags but before notmuch_message_add_tag.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully frozen.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so message cannot be modified.
+ */
+notmuch_status_t
+notmuch_message_freeze (notmuch_message_t *message);
+
+/**
+ * Thaw the current 'message', synchronizing any changes that may have
+ * occurred while 'message' was frozen into the notmuch database.
+ *
+ * See notmuch_message_freeze for an example of how to use this
+ * function to safely provide tag changes.
+ *
+ * Multiple calls to freeze/thaw are valid and these calls with
+ * "stack". That is there must be as many calls to thaw as to freeze
+ * before a message is actually thawed.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully thawed, (or at least
+ *     its frozen count has successfully been reduced by 1).
+ *
+ * NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: An attempt was made to thaw
+ *     an unfrozen message. That is, there have been an unbalanced
+ *     number of calls to notmuch_message_freeze and
+ *     notmuch_message_thaw.
+ */
+notmuch_status_t
+notmuch_message_thaw (notmuch_message_t *message);
+
+/**
+ * Destroy a notmuch_message_t object.
+ *
+ * It can be useful to call this function in the case of a single
+ * query object with many messages in the result, (such as iterating
+ * over the entire database). Otherwise, it's fine to never call this
+ * function and there will still be no memory leaks. (The memory from
+ * the messages get reclaimed when the containing query is destroyed.)
+ */
+void
+notmuch_message_destroy (notmuch_message_t *message);
+
+/**
+ * @name Message Properties
+ *
+ * This interface provides the ability to attach arbitrary (key,value)
+ * string pairs to a message, to remove such pairs, and to iterate
+ * over them.  The caller should take some care as to what keys they
+ * add or delete values for, as other subsystems or extensions may
+ * depend on these properties.
+ *
+ * Please see notmuch-properties(7) for more details about specific
+ * properties and conventions around their use.
+ *
+ */
+/**@{*/
+/**
+ * Retrieve the value for a single property key
+ *
+ * *value* is set to a string owned by the message or NULL if there is
+ * no such key. In the case of multiple values for the given key, the
+ * first one is retrieved.
+ *
+ * @returns
+ * - NOTMUCH_STATUS_NULL_POINTER: *value* may not be NULL.
+ * - NOTMUCH_STATUS_SUCCESS: No error occurred.
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+notmuch_status_t
+notmuch_message_get_property (notmuch_message_t *message, const char *key, const char **value);
+
+/**
+ * Add a (key,value) pair to a message
+ *
+ * @returns
+ * - NOTMUCH_STATUS_ILLEGAL_ARGUMENT: *key* may not contain an '=' character.
+ * - NOTMUCH_STATUS_NULL_POINTER: Neither *key* nor *value* may be NULL.
+ * - NOTMUCH_STATUS_SUCCESS: No error occurred.
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+notmuch_status_t
+notmuch_message_add_property (notmuch_message_t *message, const char *key, const char *value);
+
+/**
+ * Remove a (key,value) pair from a message.
+ *
+ * It is not an error to remove a non-existent (key,value) pair
+ *
+ * @returns
+ * - NOTMUCH_STATUS_ILLEGAL_ARGUMENT: *key* may not contain an '=' character.
+ * - NOTMUCH_STATUS_NULL_POINTER: Neither *key* nor *value* may be NULL.
+ * - NOTMUCH_STATUS_SUCCESS: No error occurred.
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+notmuch_status_t
+notmuch_message_remove_property (notmuch_message_t *message, const char *key, const char *value);
+
+/**
+ * Remove all (key,value) pairs from the given message.
+ *
+ * @param[in,out] message  message to operate on.
+ * @param[in]     key      key to delete properties for. If NULL, delete
+ *                        properties for all keys
+ * @returns
+ * - NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in
+ *   read-only mode so message cannot be modified.
+ * - NOTMUCH_STATUS_SUCCESS: No error occurred.
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+notmuch_status_t
+notmuch_message_remove_all_properties (notmuch_message_t *message, const char *key);
+
+/**
+ * Remove all (prefix*,value) pairs from the given message
+ *
+ * @param[in,out] message  message to operate on.
+ * @param[in]     prefix   delete properties with keys that start with prefix.
+ *                        If NULL, delete all properties
+ * @returns
+ * - NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in
+ *   read-only mode so message cannot be modified.
+ * - NOTMUCH_STATUS_SUCCESS: No error occurred.
+ *
+ * @since libnotmuch 5.1 (notmuch 0.26)
+ */
+notmuch_status_t
+notmuch_message_remove_all_properties_with_prefix (notmuch_message_t *message, const char *prefix);
+
+/**
+ * Opaque message property iterator
+ */
+typedef struct _notmuch_string_map_iterator notmuch_message_properties_t;
+
+/**
+ * Get the properties for *message*, returning a
+ * notmuch_message_properties_t object which can be used to iterate
+ * over all properties.
+ *
+ * The notmuch_message_properties_t object is owned by the message and
+ * as such, will only be valid for as long as the message is valid,
+ * (which is until the query from which it derived is destroyed).
+ *
+ * @param[in] message  The message to examine
+ * @param[in] key      key or key prefix
+ * @param[in] exact    if TRUE, require exact match with key. Otherwise
+ *                    treat as prefix.
+ *
+ * Typical usage might be:
+ *
+ *     notmuch_message_properties_t *list;
+ *
+ *     for (list = notmuch_message_get_properties (message, "testkey1", TRUE);
+ *          notmuch_message_properties_valid (list); notmuch_message_properties_move_to_next (list)) {
+ *        printf("%s\n", notmuch_message_properties_value(list));
+ *     }
+ *
+ *     notmuch_message_properties_destroy (list);
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_message_properties_t object. (For consistency, we do
+ * provide a notmuch_message_properities_destroy function, but there's
+ * no good reason to call it if the message is about to be destroyed).
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+notmuch_message_properties_t *
+notmuch_message_get_properties (notmuch_message_t *message, const char *key, notmuch_bool_t exact);
+
+/**
+ * Return the number of properties named "key" belonging to the specific message.
+ *
+ * @param[in] message  The message to examine
+ * @param[in] key      key to count
+ * @param[out] count   The number of matching properties associated with this message.
+ *
+ * @returns
+ *
+ * NOTMUCH_STATUS_SUCCESS: successful count, possibly some other error.
+ *
+ * @since libnotmuch 5.2 (notmuch 0.27)
+ */
+notmuch_status_t
+notmuch_message_count_properties (notmuch_message_t *message, const char *key, unsigned int *count);
+
+/**
+ * Is the given *properties* iterator pointing at a valid (key,value)
+ * pair.
+ *
+ * When this function returns TRUE,
+ * notmuch_message_properties_{key,value} will return a valid string,
+ * and notmuch_message_properties_move_to_next will do what it
+ * says. Whereas when this function returns FALSE, calling any of
+ * these functions results in undefined behaviour.
+ *
+ * See the documentation of notmuch_message_get_properties for example
+ * code showing how to iterate over a notmuch_message_properties_t
+ * object.
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+notmuch_bool_t
+notmuch_message_properties_valid (notmuch_message_properties_t *properties);
+
+/**
+ * Move the *properties* iterator to the next (key,value) pair
+ *
+ * If *properties* is already pointing at the last pair then the iterator
+ * will be moved to a point just beyond that last pair, (where
+ * notmuch_message_properties_valid will return FALSE).
+ *
+ * See the documentation of notmuch_message_get_properties for example
+ * code showing how to iterate over a notmuch_message_properties_t object.
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+void
+notmuch_message_properties_move_to_next (notmuch_message_properties_t *properties);
+
+/**
+ * Return the key from the current (key,value) pair.
+ *
+ * this could be useful if iterating for a prefix
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+const char *
+notmuch_message_properties_key (notmuch_message_properties_t *properties);
+
+/**
+ * Return the value from the current (key,value) pair.
+ *
+ * This could be useful if iterating for a prefix.
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+const char *
+notmuch_message_properties_value (notmuch_message_properties_t *properties);
+
+
+/**
+ * Destroy a notmuch_message_properties_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_message_properties_t object will be reclaimed when the
+ * containing message object is destroyed.
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+void
+notmuch_message_properties_destroy (notmuch_message_properties_t *properties);
+
+/**@}*/
+
+/**
+ * Is the given 'tags' iterator pointing at a valid tag.
+ *
+ * When this function returns TRUE, notmuch_tags_get will return a
+ * 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.
+ */
+notmuch_bool_t
+notmuch_tags_valid (notmuch_tags_t *tags);
+
+/**
+ * Get the current tag from 'tags' as a string.
+ *
+ * Note: The returned string belongs to 'tags' and has a lifetime
+ * identical to it (and the query to which it ultimately belongs).
+ *
+ * See the documentation of notmuch_message_get_tags for example code
+ * showing how to iterate over a notmuch_tags_t object.
+ */
+const char *
+notmuch_tags_get (notmuch_tags_t *tags);
+
+/**
+ * Move the 'tags' iterator to the next tag.
+ *
+ * If 'tags' is already pointing at the last tag then the iterator
+ * will be moved to a point just beyond that last tag, (where
+ * notmuch_tags_valid will return FALSE and notmuch_tags_get will
+ * return NULL).
+ *
+ * See the documentation of notmuch_message_get_tags for example code
+ * showing how to iterate over a notmuch_tags_t object.
+ */
+void
+notmuch_tags_move_to_next (notmuch_tags_t *tags);
+
+/**
+ * Destroy a notmuch_tags_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_tags_t object will be reclaimed when the containing
+ * message or query objects are destroyed.
+ */
+void
+notmuch_tags_destroy (notmuch_tags_t *tags);
+
+/**
+ * Store an mtime within the database for 'directory'.
+ *
+ * The 'directory' should be an object retrieved from the database
+ * with notmuch_database_get_directory for a particular path.
+ *
+ * The intention is for the caller to use the mtime to allow efficient
+ * identification of new messages to be added to the database. The
+ * recommended usage is as follows:
+ *
+ *   o Read the mtime of a directory from the filesystem
+ *
+ *   o Call index_file for all mail files in the directory
+ *
+ *   o Call notmuch_directory_set_mtime with the mtime read from the
+ *     filesystem.
+ *
+ * Then, when wanting to check for updates to the directory in the
+ * future, the client can call notmuch_directory_get_mtime and know
+ * that it only needs to add files if the mtime of the directory and
+ * files are newer than the stored timestamp.
+ *
+ * Note: The notmuch_directory_get_mtime function does not allow the
+ * caller to distinguish a timestamp of 0 from a non-existent
+ * timestamp. So don't store a timestamp of 0 unless you are
+ * comfortable with that.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: mtime successfully stored in database.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception
+ *     occurred, mtime not stored.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so directory mtime cannot be modified.
+ */
+notmuch_status_t
+notmuch_directory_set_mtime (notmuch_directory_t *directory,
+                            time_t mtime);
+
+/**
+ * Get the mtime of a directory, (as previously stored with
+ * notmuch_directory_set_mtime).
+ *
+ * Returns 0 if no mtime has previously been stored for this
+ * directory.
+ */
+time_t
+notmuch_directory_get_mtime (notmuch_directory_t *directory);
+
+/**
+ * Get a notmuch_filenames_t iterator listing all the filenames of
+ * messages in the database within the given directory.
+ *
+ * The returned filenames will be the basename-entries only (not
+ * complete paths).
+ *
+ * Returns NULL if it triggers a Xapian exception
+ */
+notmuch_filenames_t *
+notmuch_directory_get_child_files (notmuch_directory_t *directory);
+
+/**
+ * Get a notmuch_filenames_t iterator listing all the filenames of
+ * sub-directories in the database within the given directory.
+ *
+ * The returned filenames will be the basename-entries only (not
+ * complete paths).
+ *
+ * Returns NULL if it triggers a Xapian exception
+ */
+notmuch_filenames_t *
+notmuch_directory_get_child_directories (notmuch_directory_t *directory);
+
+/**
+ * Delete directory document from the database, and destroy the
+ * notmuch_directory_t object. Assumes any child directories and files
+ * have been deleted by the caller.
+ *
+ * @since libnotmuch 4.3 (notmuch 0.21)
+ */
+notmuch_status_t
+notmuch_directory_delete (notmuch_directory_t *directory);
+
+/**
+ * Destroy a notmuch_directory_t object.
+ */
+void
+notmuch_directory_destroy (notmuch_directory_t *directory);
+
+/**
+ * Is the given 'filenames' iterator pointing at a valid filename.
+ *
+ * When this function returns TRUE, notmuch_filenames_get will return
+ * a valid string. Whereas when this function returns FALSE,
+ * notmuch_filenames_get will return NULL.
+ *
+ * It is acceptable to pass NULL for 'filenames', in which case this
+ * function will always return FALSE.
+ */
+notmuch_bool_t
+notmuch_filenames_valid (notmuch_filenames_t *filenames);
+
+/**
+ * Get the current filename from 'filenames' as a string.
+ *
+ * Note: The returned string belongs to 'filenames' and has a lifetime
+ * identical to it (and the directory to which it ultimately belongs).
+ *
+ * It is acceptable to pass NULL for 'filenames', in which case this
+ * function will always return NULL.
+ */
+const char *
+notmuch_filenames_get (notmuch_filenames_t *filenames);
+
+/**
+ * Move the 'filenames' iterator to the next filename.
+ *
+ * If 'filenames' is already pointing at the last filename then the
+ * iterator will be moved to a point just beyond that last filename,
+ * (where notmuch_filenames_valid will return FALSE and
+ * notmuch_filenames_get will return NULL).
+ *
+ * It is acceptable to pass NULL for 'filenames', in which case this
+ * function will do nothing.
+ */
+void
+notmuch_filenames_move_to_next (notmuch_filenames_t *filenames);
+
+/**
+ * Destroy a notmuch_filenames_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_filenames_t object will be reclaimed when the
+ * containing directory object is destroyed.
+ *
+ * It is acceptable to pass NULL for 'filenames', in which case this
+ * function will do nothing.
+ */
+void
+notmuch_filenames_destroy (notmuch_filenames_t *filenames);
+
+
+/**
+ * set config 'key' to 'value'
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ * @retval #NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in
+ *     read-only mode so message cannot be modified.
+ * @retval #NOTMUCH_STATUS_XAPIAN_EXCEPTION: an exception was thrown
+ *      accessing the database.
+ * @retval #NOTMUCH_STATUS_SUCCESS
+ */
+notmuch_status_t
+notmuch_database_set_config (notmuch_database_t *db, const char *key, const char *value);
+
+/**
+ * retrieve config item 'key', assign to  'value'
+ *
+ * keys which have not been previously set with n_d_set_config will
+ * return an empty string.
+ *
+ * return value is allocated by malloc and should be freed by the
+ * caller.
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ *
+ */
+notmuch_status_t
+notmuch_database_get_config (notmuch_database_t *db, const char *key, char **value);
+
+/**
+ * Create an iterator for all config items with keys matching a given prefix
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+notmuch_status_t
+notmuch_database_get_config_list (notmuch_database_t *db, const char *prefix,
+                                 notmuch_config_list_t **out);
+
+/**
+ * Is 'config_list' iterator valid (i.e. _key, _value, _move_to_next can be called).
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+notmuch_bool_t
+notmuch_config_list_valid (notmuch_config_list_t *config_list);
+
+/**
+ * return key for current config pair
+ *
+ * return value is owned by the iterator, and will be destroyed by the
+ * next call to notmuch_config_list_key or notmuch_config_list_destroy.
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+const char *
+notmuch_config_list_key (notmuch_config_list_t *config_list);
+
+/**
+ * return 'value' for current config pair
+ *
+ * return value is owned by the iterator, and will be destroyed by the
+ * next call to notmuch_config_list_value or notmuch config_list_destroy
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ * @retval NULL for errors
+ */
+const char *
+notmuch_config_list_value (notmuch_config_list_t *config_list);
+
+
+/**
+ * move 'config_list' iterator to the next pair
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+void
+notmuch_config_list_move_to_next (notmuch_config_list_t *config_list);
+
+/**
+ * free any resources held by 'config_list'
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+void
+notmuch_config_list_destroy (notmuch_config_list_t *config_list);
+
+/**
+ * Configuration keys known to libnotmuch
+ */
+typedef enum {
+    NOTMUCH_CONFIG_FIRST,
+    NOTMUCH_CONFIG_DATABASE_PATH = NOTMUCH_CONFIG_FIRST,
+    NOTMUCH_CONFIG_MAIL_ROOT,
+    NOTMUCH_CONFIG_HOOK_DIR,
+    NOTMUCH_CONFIG_BACKUP_DIR,
+    NOTMUCH_CONFIG_EXCLUDE_TAGS,
+    NOTMUCH_CONFIG_NEW_TAGS,
+    NOTMUCH_CONFIG_NEW_IGNORE,
+    NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+    NOTMUCH_CONFIG_PRIMARY_EMAIL,
+    NOTMUCH_CONFIG_OTHER_EMAIL,
+    NOTMUCH_CONFIG_USER_NAME,
+    NOTMUCH_CONFIG_AUTOCOMMIT,
+    NOTMUCH_CONFIG_EXTRA_HEADERS,
+    NOTMUCH_CONFIG_INDEX_AS_TEXT,
+    NOTMUCH_CONFIG_LAST
+} notmuch_config_key_t;
+
+/**
+ * get a configuration value from an open database.
+ *
+ * This value reflects all configuration information given at the time
+ * the database was opened.
+ *
+ * @param[in] notmuch database
+ * @param[in] key configuration key
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval NULL if 'key' unknown or if no value is known for
+ *         'key'.  Otherwise returns a string owned by notmuch which should
+ *         not be modified nor freed by the caller.
+ */
+const char *
+notmuch_config_get (notmuch_database_t *notmuch, notmuch_config_key_t key);
+
+/**
+ * set a configuration value from in an open database.
+ *
+ * This value reflects all configuration information given at the time
+ * the database was opened.
+ *
+ * @param[in,out] notmuch database open read/write
+ * @param[in] key configuration key
+ * @param[in] val configuration value
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval returns any return value for notmuch_database_set_config.
+ */
+notmuch_status_t
+notmuch_config_set (notmuch_database_t *notmuch, notmuch_config_key_t key, const char *val);
+
+/**
+ * Returns an iterator for a ';'-delimited list of configuration values
+ *
+ * These values reflect all configuration information given at the
+ * time the database was opened.
+ *
+ * @param[in] notmuch database
+ * @param[in] key configuration key
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval NULL in case of error.
+ */
+notmuch_config_values_t *
+notmuch_config_get_values (notmuch_database_t *notmuch, notmuch_config_key_t key);
+
+/**
+ * Returns an iterator for a ';'-delimited list of configuration values
+ *
+ * These values reflect all configuration information given at the
+ * time the database was opened.
+ *
+ * @param[in] notmuch database
+ * @param[in] key configuration key
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval NULL in case of error.
+ */
+notmuch_config_values_t *
+notmuch_config_get_values_string (notmuch_database_t *notmuch, const char *key);
+
+/**
+ * Is the given 'config_values' iterator pointing at a valid element.
+ *
+ * @param[in] values iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval FALSE if passed a NULL pointer, or the iterator is exhausted.
+ *
+ */
+notmuch_bool_t
+notmuch_config_values_valid (notmuch_config_values_t *values);
+
+/**
+ * Get the current value from the 'values' iterator
+ *
+ * @param[in] values iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval a string with the same lifetime as the iterator
+ */
+const char *
+notmuch_config_values_get (notmuch_config_values_t *values);
+
+/**
+ * Move the 'values' iterator to the next element
+ *
+ * @param[in,out] values iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ */
+void
+notmuch_config_values_move_to_next (notmuch_config_values_t *values);
+
+
+/**
+ * reset the 'values' iterator to the first element
+ *
+ * @param[in,out] values iterator. A NULL value is ignored.
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ */
+void
+notmuch_config_values_start (notmuch_config_values_t *values);
+
+/**
+ * Destroy a config values iterator, along with any associated
+ * resources.
+ *
+ * @param[in,out] values iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ */
+void
+notmuch_config_values_destroy (notmuch_config_values_t *values);
+
+
+/**
+ * Returns an iterator for a (key, value) configuration pairs
+ *
+ * @param[in] notmuch database
+ * @param[in] prefix prefix for keys. Pass "" for all keys.
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval NULL in case of error.
+ */
+notmuch_config_pairs_t *
+notmuch_config_get_pairs (notmuch_database_t *notmuch,
+                         const char *prefix);
+
+/**
+ * Is the given 'config_pairs' iterator pointing at a valid element.
+ *
+ * @param[in] pairs iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval FALSE if passed a NULL pointer, or the iterator is exhausted.
+ *
+ */
+notmuch_bool_t
+notmuch_config_pairs_valid (notmuch_config_pairs_t *pairs);
+
+/**
+ * Move the 'config_pairs' iterator to the next element
+ *
+ * @param[in,out] pairs iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ */
+void
+notmuch_config_pairs_move_to_next (notmuch_config_pairs_t *pairs);
+
+/**
+ * Get the current key from the 'config_pairs' iterator
+ *
+ * @param[in] pairs iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval a string with the same lifetime as the iterator
+ */
+const char *
+notmuch_config_pairs_key (notmuch_config_pairs_t *pairs);
+
+/**
+ * Get the current value from the 'config_pairs' iterator
+ *
+ * @param[in] pairs iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval a string with the same lifetime as the iterator
+ */
+const char *
+notmuch_config_pairs_value (notmuch_config_pairs_t *pairs);
+
+/**
+ * Destroy a config_pairs iterator, along with any associated
+ * resources.
+ *
+ * @param[in,out] pairs iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ */
+void
+notmuch_config_pairs_destroy (notmuch_config_pairs_t *pairs);
+
+/**
+ * get a configuration value from an open database as Boolean
+ *
+ * This value reflects all configuration information given at the time
+ * the database was opened.
+ *
+ * @param[in] notmuch database
+ * @param[in] key configuration key
+ * @param[out] val configuration value, converted to Boolean
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval #NOTMUCH_STATUS_ILLEGAL_ARGUMENT if either key is unknown
+ * or the corresponding value does not convert to Boolean.
+ */
+notmuch_status_t
+notmuch_config_get_bool (notmuch_database_t *notmuch,
+                        notmuch_config_key_t key,
+                        notmuch_bool_t *val);
+
+/**
+ * return the path of the config file loaded, if any
+ *
+ * @retval NULL if no config file was loaded
+ */
+const char *
+notmuch_config_path (notmuch_database_t *notmuch);
+
+/**
+ * get the current default indexing options for a given database.
+ *
+ * This object will survive until the database itself is destroyed,
+ * but the caller may also release it earlier with
+ * notmuch_indexopts_destroy.
+ *
+ * This object represents a set of options on how a message can be
+ * added to the index.  At the moment it is a featureless stub.
+ *
+ * @since libnotmuch 5.1 (notmuch 0.26)
+ * @retval NULL in case of error
+ */
+notmuch_indexopts_t *
+notmuch_database_get_default_indexopts (notmuch_database_t *db);
+
+/**
+ * Stating a policy about how to decrypt messages.
+ *
+ * See index.decrypt in notmuch-config(1) for more details.
+ */
+typedef enum {
+    NOTMUCH_DECRYPT_FALSE,
+    NOTMUCH_DECRYPT_TRUE,
+    NOTMUCH_DECRYPT_AUTO,
+    NOTMUCH_DECRYPT_NOSTASH,
+} notmuch_decryption_policy_t;
+
+/**
+ * Specify whether to decrypt encrypted parts while indexing.
+ *
+ * Be aware that the index is likely sufficient to reconstruct the
+ * cleartext of the message itself, so please ensure that the notmuch
+ * message index is adequately protected. DO NOT SET THIS FLAG TO TRUE
+ * without considering the security of your index.
+ *
+ * @since libnotmuch 5.1 (notmuch 0.26)
+ */
+notmuch_status_t
+notmuch_indexopts_set_decrypt_policy (notmuch_indexopts_t *indexopts,
+                                     notmuch_decryption_policy_t decrypt_policy);
+
+/**
+ * Return whether to decrypt encrypted parts while indexing.
+ * see notmuch_indexopts_set_decrypt_policy.
+ *
+ * @since libnotmuch 5.1 (notmuch 0.26)
+ */
+notmuch_decryption_policy_t
+notmuch_indexopts_get_decrypt_policy (const notmuch_indexopts_t *indexopts);
+
+/**
+ * Destroy a notmuch_indexopts_t object.
+ *
+ * @since libnotmuch 5.1 (notmuch 0.26)
+ */
+void
+notmuch_indexopts_destroy (notmuch_indexopts_t *options);
+
+
+/**
+ * interrogate the library for compile time features
+ *
+ * @since libnotmuch 4.4 (notmuch 0.23)
+ */
+notmuch_bool_t
+notmuch_built_with (const char *name);
+/**@}*/
+
+#pragma GCC visibility pop
+
+NOTMUCH_END_DECLS
+
+#endif
diff --git a/lib/notmuch.sym b/lib/notmuch.sym
new file mode 100644 (file)
index 0000000..7d0c0af
--- /dev/null
@@ -0,0 +1,7 @@
+{
+global:
+       _ZTI*;
+       _ZTS*;
+       notmuch_*;
+local: *;
+};
diff --git a/lib/open.cc b/lib/open.cc
new file mode 100644 (file)
index 0000000..005872d
--- /dev/null
@@ -0,0 +1,990 @@
+#include <unistd.h>
+#include <libgen.h>
+
+#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)
+#else
+#define DB_ACTION Xapian::DB_CREATE_OR_OPEN
+#endif
+
+notmuch_status_t
+notmuch_database_open (const char *path,
+                      notmuch_database_mode_t mode,
+                      notmuch_database_t **database)
+{
+    char *status_string = NULL;
+    notmuch_status_t status;
+
+    status = notmuch_database_open_with_config (path, mode, "", NULL,
+                                               database, &status_string);
+    if (status_string) {
+       fputs (status_string, stderr);
+       free (status_string);
+    }
+
+    return status;
+}
+
+notmuch_status_t
+notmuch_database_open_verbose (const char *path,
+                              notmuch_database_mode_t mode,
+                              notmuch_database_t **database,
+                              char **status_string)
+{
+    return notmuch_database_open_with_config (path, mode, "", NULL,
+                                             database, status_string);
+}
+
+static const char *
+_xdg_dir (void *ctx,
+         const char *xdg_root_variable,
+         const char *xdg_prefix,
+         const char *profile_name)
+{
+    const char *xdg_root = getenv (xdg_root_variable);
+
+    if (! xdg_root) {
+       const char *home = getenv ("HOME");
+
+       if (! home) return NULL;
+
+       xdg_root = talloc_asprintf (ctx,
+                                   "%s/%s",
+                                   home,
+                                   xdg_prefix);
+    }
+
+    if (! profile_name)
+       profile_name = getenv ("NOTMUCH_PROFILE");
+
+    if (! profile_name)
+       profile_name = "default";
+
+    return talloc_asprintf (ctx,
+                           "%s/notmuch/%s",
+                           xdg_root,
+                           profile_name);
+}
+
+static notmuch_status_t
+_choose_dir (notmuch_database_t *notmuch,
+            const char *profile,
+            notmuch_config_key_t key,
+            const char *xdg_var,
+            const char *xdg_subdir,
+            const char *subdir,
+            char **message = NULL)
+{
+    const char *parent;
+    const char *dir;
+    struct stat st;
+    int err;
+
+    dir = notmuch_config_get (notmuch, key);
+
+    if (dir)
+       return NOTMUCH_STATUS_SUCCESS;
+
+    parent = _xdg_dir (notmuch, xdg_var, xdg_subdir, profile);
+    if (! parent)
+       return NOTMUCH_STATUS_PATH_ERROR;
+
+    dir = talloc_asprintf (notmuch, "%s/%s", parent, subdir);
+
+    err = stat (dir, &st);
+    if (err) {
+       if (errno == ENOENT) {
+           char *notmuch_path = dirname (talloc_strdup (notmuch, notmuch->xapian_path));
+           dir = talloc_asprintf (notmuch, "%s/%s", notmuch_path, subdir);
+       } else {
+           IGNORE_RESULT (asprintf (message, "Error: Cannot stat %s: %s.\n",
+                                    dir, strerror (errno)));
+           return NOTMUCH_STATUS_FILE_ERROR;
+       }
+    }
+
+    _notmuch_config_cache (notmuch, key, dir);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_load_key_file (notmuch_database_t *notmuch,
+               const char *path,
+               const char *profile,
+               GKeyFile **key_file)
+{
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+
+    if (path && EMPTY_STRING (path))
+       goto DONE;
+
+    if (! path)
+       path = getenv ("NOTMUCH_CONFIG");
+
+    if (path)
+       path = talloc_strdup (notmuch, path);
+    else {
+       const char *dir = _xdg_dir (notmuch, "XDG_CONFIG_HOME", ".config", profile);
+
+       if (dir) {
+           path = talloc_asprintf (notmuch, "%s/config", dir);
+           if (access (path, R_OK) != 0)
+               path = NULL;
+       }
+    }
+
+    if (! path) {
+       const char *home = getenv ("HOME");
+
+       path = talloc_asprintf (notmuch, "%s/.notmuch-config", home);
+
+       if (! profile)
+           profile = getenv ("NOTMUCH_PROFILE");
+
+       if (profile)
+           path = talloc_asprintf (notmuch, "%s.%s", path, profile);
+    }
+
+    *key_file = g_key_file_new ();
+    if (! g_key_file_load_from_file (*key_file, path, G_KEY_FILE_NONE, NULL)) {
+       status = NOTMUCH_STATUS_NO_CONFIG;
+    }
+
+  DONE:
+    if (path)
+       notmuch->config_path = path;
+
+    return status;
+}
+
+static notmuch_status_t
+_db_dir_exists (const char *database_path, char **message)
+{
+    struct stat st;
+    int err;
+
+    err = stat (database_path, &st);
+    if (err) {
+       IGNORE_RESULT (asprintf (message, "Error: Cannot open database at %s: %s.\n",
+                                database_path, strerror (errno)));
+       return NOTMUCH_STATUS_FILE_ERROR;
+    }
+
+    if (! S_ISDIR (st.st_mode)) {
+       IGNORE_RESULT (asprintf (message, "Error: Cannot open database at %s: "
+                                "Not a directory.\n",
+                                database_path));
+       return NOTMUCH_STATUS_FILE_ERROR;
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_choose_database_path (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);
+       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) {
+       *message = strdup ("Error: could not locate database.\n");
+       return NOTMUCH_STATUS_NO_DATABASE;
+    }
+
+    if (*database_path[0] != '/') {
+       *message = strdup ("Error: Database path must be absolute.\n");
+       return NOTMUCH_STATUS_PATH_ERROR;
+    }
+
+    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;
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_mkdir (const char *path, char **message)
+{
+    if (g_mkdir_with_parents (path, 0755)) {
+       IGNORE_RESULT (asprintf (message, "Error: Cannot create directory %s: %s.\n",
+                                path, strerror (errno)));
+       return NOTMUCH_STATUS_FILE_ERROR;
+    }
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_create_database_path (notmuch_database_t *notmuch,
+                      const char *profile,
+                      GKeyFile *key_file,
+                      const char **database_path,
+                      char **message)
+{
+    notmuch_status_t status;
+
+    if (! *database_path) {
+       *database_path = getenv ("NOTMUCH_DATABASE");
+    }
+
+    if (! *database_path && key_file) {
+       char *path = g_key_file_get_string (key_file, "database", "path", NULL);
+       if (path) {
+           if (path[0] == '/')
+               *database_path = talloc_strdup (notmuch, path);
+           else
+               *database_path = talloc_asprintf (notmuch, "%s/%s", getenv ("HOME"), path);
+           g_free (path);
+       }
+    }
+
+    if (! *database_path) {
+       *database_path = _xdg_dir (notmuch, "XDG_DATA_HOME", ".local/share", profile);
+       notmuch->params |= NOTMUCH_PARAM_SPLIT;
+    }
+
+    if (*database_path[0] != '/') {
+       *message = strdup ("Error: Database path must be absolute.\n");
+       return NOTMUCH_STATUS_PATH_ERROR;
+    }
+
+    if ((status = _mkdir (*database_path, message)))
+       return status;
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_database_t *
+_alloc_notmuch (const char *database_path, const char *config_path, const char *profile)
+{
+    notmuch_database_t *notmuch;
+
+    notmuch = talloc_zero (NULL, notmuch_database_t);
+    if (! notmuch)
+       return NULL;
+
+    notmuch->exception_reported = false;
+    notmuch->status_string = NULL;
+    notmuch->writable_xapian_db = NULL;
+    notmuch->config_path = NULL;
+    notmuch->atomic_nesting = 0;
+    notmuch->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;
+}
+
+static notmuch_status_t
+_trial_open (const char *xapian_path, char **message_ptr)
+{
+    try {
+       Xapian::Database db (xapian_path);
+    } catch (const Xapian::DatabaseOpeningError &error) {
+       IGNORE_RESULT (asprintf (message_ptr,
+                                "Cannot open Xapian database at %s: %s\n",
+                                xapian_path,
+                                error.get_msg ().c_str ()));
+       return NOTMUCH_STATUS_PATH_ERROR;
+    } catch (const Xapian::Error &error) {
+       IGNORE_RESULT (asprintf (message_ptr,
+                                "A Xapian exception occurred opening database: %s\n",
+                                error.get_msg ().c_str ()));
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+_notmuch_choose_xapian_path (void *ctx, const char *database_path,
+                            const char **xapian_path, char **message_ptr)
+{
+    notmuch_status_t status;
+    const char *trial_path, *notmuch_path;
+
+    status = _db_dir_exists (database_path, message_ptr);
+    if (status)
+       goto DONE;
+
+    trial_path = talloc_asprintf (ctx, "%s/xapian", database_path);
+    status = _trial_open (trial_path, message_ptr);
+    if (status != NOTMUCH_STATUS_PATH_ERROR)
+       goto DONE;
+
+    if (*message_ptr)
+       free (*message_ptr);
+
+    notmuch_path = talloc_asprintf (ctx, "%s/.notmuch", database_path);
+    status = _db_dir_exists (notmuch_path, message_ptr);
+    if (status)
+       goto DONE;
+
+    trial_path = talloc_asprintf (ctx, "%s/xapian", notmuch_path);
+    status = _trial_open (trial_path, message_ptr);
+
+  DONE:
+    if (status == NOTMUCH_STATUS_SUCCESS)
+       *xapian_path = trial_path;
+    return status;
+}
+
+static void
+_set_database_path (notmuch_database_t *notmuch,
+                   const char *database_path)
+{
+    char *path = talloc_strdup (notmuch, database_path);
+
+    strip_trailing (path, '/');
+
+    _notmuch_config_cache (notmuch, NOTMUCH_CONFIG_DATABASE_PATH, path);
+}
+
+static void
+_load_database_state (notmuch_database_t *notmuch)
+{
+    std::string last_thread_id;
+    std::string last_mod;
+
+    notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid ();
+    last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
+    if (last_thread_id.empty ()) {
+       notmuch->last_thread_id = 0;
+    } else {
+       const char *str;
+       char *end;
+
+       str = last_thread_id.c_str ();
+       notmuch->last_thread_id = strtoull (str, &end, 16);
+       if (*end != '\0')
+           INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
+    }
+
+    /* Get current highest revision number. */
+    last_mod = notmuch->xapian_db->get_value_upper_bound (
+       NOTMUCH_VALUE_LAST_MOD);
+    if (last_mod.empty ())
+       notmuch->revision = 0;
+    else
+       notmuch->revision = Xapian::sortable_unserialise (last_mod);
+    notmuch->uuid = talloc_strdup (
+       notmuch, notmuch->xapian_db->get_uuid ().c_str ());
+}
+
+/* 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
+_finish_open (notmuch_database_t *notmuch,
+             const char *profile,
+             notmuch_database_mode_t mode,
+             GKeyFile *key_file,
+             char **message_ptr)
+{
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+    char *incompat_features;
+    char *message = NULL;
+    const char *autocommit_str;
+    char *autocommit_end;
+    unsigned int version;
+    const char *database_path = notmuch_database_get_path (notmuch);
+
+    try {
+
+       if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
+           notmuch->writable_xapian_db = new Xapian::WritableDatabase (notmuch->xapian_path,
+                                                                       DB_ACTION);
+           notmuch->xapian_db = notmuch->writable_xapian_db;
+       } else {
+           notmuch->xapian_db = new Xapian::Database (notmuch->xapian_path);
+       }
+
+       /* Check version.  As of database version 3, we represent
+        * changes in terms of features, so assume a version bump
+        * means a dramatically incompatible change. */
+       version = notmuch_database_get_version (notmuch);
+       if (version > NOTMUCH_DATABASE_VERSION) {
+           IGNORE_RESULT (asprintf (&message,
+                                    "Error: Notmuch database at %s\n"
+                                    "       has a newer database format version (%u) than supported by this\n"
+                                    "       version of notmuch (%u).\n",
+                                    database_path, version, NOTMUCH_DATABASE_VERSION));
+           status = NOTMUCH_STATUS_FILE_ERROR;
+           goto DONE;
+       }
+
+       /* Check features. */
+       incompat_features = NULL;
+       notmuch->features = _notmuch_database_parse_features (
+           notmuch, notmuch->xapian_db->get_metadata ("features").c_str (),
+           version, mode == NOTMUCH_DATABASE_MODE_READ_WRITE ? 'w' : 'r',
+           &incompat_features);
+       if (incompat_features) {
+           IGNORE_RESULT (asprintf (&message,
+                                    "Error: Notmuch database at %s\n"
+                                    "       requires features (%s)\n"
+                                    "       not supported by this version of notmuch.\n",
+                                    database_path, incompat_features));
+           status = NOTMUCH_STATUS_FILE_ERROR;
+           goto DONE;
+       }
+
+       _load_database_state (notmuch);
+
+       notmuch->query_parser = new Xapian::QueryParser;
+       notmuch->term_gen = new Xapian::TermGenerator;
+       notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
+       notmuch->value_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
+       notmuch->date_range_processor = new ParseTimeRangeProcessor (NOTMUCH_VALUE_TIMESTAMP,
+                                                                    "date:");
+       notmuch->last_mod_range_processor = new LastModRangeProcessor (notmuch, "lastmod:");
+       notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
+       notmuch->query_parser->set_database (*notmuch->xapian_db);
+       notmuch->stemmer = new Xapian::Stem ("english");
+       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);
+       notmuch->query_parser->add_rangeprocessor (notmuch->last_mod_range_processor);
+
+       /* Configuration information is needed to set up query parser */
+       status = _notmuch_config_load_from_database (notmuch);
+       if (status)
+           goto DONE;
+
+       if (key_file)
+           status = _notmuch_config_load_from_file (notmuch, key_file, &message);
+       if (status)
+           goto DONE;
+
+       status = _choose_dir (notmuch, profile,
+                             NOTMUCH_CONFIG_HOOK_DIR,
+                             "XDG_CONFIG_HOME",
+                             ".config",
+                             "hooks",
+                             &message);
+       if (status)
+           goto DONE;
+
+       status = _choose_dir (notmuch, profile,
+                             NOTMUCH_CONFIG_BACKUP_DIR,
+                             "XDG_DATA_HOME",
+                             ".local/share",
+                             "backups",
+                             &message);
+       if (status)
+           goto DONE;
+       status = _notmuch_config_load_defaults (notmuch);
+       if (status)
+           goto DONE;
+
+       status = _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;
+
+       status = _notmuch_database_setup_user_query_fields (notmuch);
+       if (status)
+           goto DONE;
+
+    } catch (const Xapian::Error &error) {
+       IGNORE_RESULT (asprintf (&message, "A Xapian exception occurred opening database: %s\n",
+                                error.get_msg ().c_str ()));
+       status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+  DONE:
+    if (message_ptr)
+       *message_ptr = message;
+    return status;
+}
+
+notmuch_status_t
+notmuch_database_open_with_config (const char *database_path,
+                                  notmuch_database_mode_t mode,
+                                  const char *config_path,
+                                  const char *profile,
+                                  notmuch_database_t **database,
+                                  char **status_string)
+{
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+    notmuch_database_t *notmuch = NULL;
+    char *message = NULL;
+    GKeyFile *key_file = NULL;
+
+    _notmuch_init ();
+
+    notmuch = _alloc_notmuch (database_path, config_path, profile);
+    if (! notmuch) {
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    status = _load_key_file (notmuch, config_path, profile, &key_file);
+    if (status) {
+       message = strdup ("Error: cannot load config file.\n");
+       goto DONE;
+    }
+
+    if ((status = _choose_database_path (notmuch, profile, key_file,
+                                        &database_path,
+                                        &message)))
+       goto DONE;
+
+    status = _db_dir_exists (database_path, &message);
+    if (status)
+       goto DONE;
+
+    _set_database_path (notmuch, database_path);
+
+    status = _notmuch_choose_xapian_path (notmuch, database_path,
+                                         &notmuch->xapian_path, &message);
+    if (status)
+       goto DONE;
+
+    status = _finish_open (notmuch, profile, mode, key_file, &message);
+
+  DONE:
+    if (key_file)
+       g_key_file_free (key_file);
+
+    if (message) {
+       if (status_string)
+           *status_string = message;
+       else
+           free (message);
+    }
+
+    if (status && notmuch) {
+       notmuch_database_destroy (notmuch);
+       notmuch = NULL;
+    }
+
+    if (database)
+       *database = notmuch;
+
+    if (notmuch)
+       notmuch->open = true;
+
+    return status;
+}
+
+notmuch_status_t
+notmuch_database_create (const char *path, notmuch_database_t **database)
+{
+    char *status_string = NULL;
+    notmuch_status_t status;
+
+    status = notmuch_database_create_verbose (path, database,
+                                             &status_string);
+
+    if (status_string) {
+       fputs (status_string, stderr);
+       free (status_string);
+    }
+
+    return status;
+}
+
+notmuch_status_t
+notmuch_database_create_verbose (const char *path,
+                                notmuch_database_t **database,
+                                char **status_string)
+{
+    return notmuch_database_create_with_config (path, "", NULL, database, status_string);
+}
+
+notmuch_status_t
+notmuch_database_create_with_config (const char *database_path,
+                                    const char *config_path,
+                                    const char *profile,
+                                    notmuch_database_t **database,
+                                    char **status_string)
+{
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+    notmuch_database_t *notmuch = NULL;
+    const char *notmuch_path = NULL;
+    char *message = NULL;
+    GKeyFile *key_file = NULL;
+
+    _notmuch_init ();
+
+    notmuch = _alloc_notmuch (database_path, config_path, profile);
+    if (! notmuch) {
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    status = _load_key_file (notmuch, config_path, profile, &key_file);
+    if (status) {
+       message = strdup ("Error: cannot load config file.\n");
+       goto DONE;
+    }
+
+    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 && ! (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);
+
+       if (mail_root && (0 != strcmp (mail_root, db_path)))
+           notmuch->params |= NOTMUCH_PARAM_SPLIT;
+
+       free (mail_root);
+       free (db_path);
+    }
+
+    if (notmuch->params & NOTMUCH_PARAM_SPLIT) {
+       notmuch_path = database_path;
+    } else {
+       if (! (notmuch_path = talloc_asprintf (notmuch, "%s/%s", database_path, ".notmuch"))) {
+           status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+           goto DONE;
+       }
+
+       status = _mkdir (notmuch_path, &message);
+       if (status)
+           goto DONE;
+    }
+
+    if (! (notmuch->xapian_path = talloc_asprintf (notmuch, "%s/%s", notmuch_path, "xapian"))) {
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    status = _trial_open (notmuch->xapian_path, &message);
+    if (status == NOTMUCH_STATUS_SUCCESS) {
+       notmuch_database_destroy (notmuch);
+       notmuch = NULL;
+       status = NOTMUCH_STATUS_DATABASE_EXISTS;
+       goto DONE;
+    }
+
+    if (message)
+       free (message);
+
+    status = _finish_open (notmuch,
+                          profile,
+                          NOTMUCH_DATABASE_MODE_READ_WRITE,
+                          key_file,
+                          &message);
+    if (status)
+       goto DONE;
+
+    /* Upgrade doesn't add these feature to existing databases, but
+     * new databases have them. */
+    notmuch->features |= NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES;
+    notmuch->features |= NOTMUCH_FEATURE_INDEXED_MIMETYPES;
+    notmuch->features |= NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY;
+
+    status = notmuch_database_upgrade (notmuch, NULL, NULL);
+    if (status) {
+       notmuch_database_close (notmuch);
+       notmuch = NULL;
+    }
+
+  DONE:
+    if (key_file)
+       g_key_file_free (key_file);
+
+    if (message) {
+       if (status_string)
+           *status_string = message;
+       else
+           free (message);
+    }
+    if (status && notmuch) {
+       notmuch_database_destroy (notmuch);
+       notmuch = NULL;
+    }
+
+    if (database)
+       *database = notmuch;
+
+    if (notmuch)
+       notmuch->open = true;
+    return status;
+}
+
+notmuch_status_t
+notmuch_database_reopen (notmuch_database_t *notmuch,
+                        notmuch_database_mode_t new_mode)
+{
+    notmuch_database_mode_t cur_mode = _notmuch_database_mode (notmuch);
+
+    if (notmuch->xapian_db == NULL) {
+       _notmuch_database_log (notmuch, "Cannot reopen closed or nonexistent database\n");
+       return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
+    }
+
+    try {
+       if (cur_mode == new_mode &&
+           new_mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
+           notmuch->xapian_db->reopen ();
+       } else {
+           notmuch->xapian_db->close ();
+
+           delete notmuch->xapian_db;
+           notmuch->xapian_db = NULL;
+           /* no need to free the same object twice */
+           notmuch->writable_xapian_db = NULL;
+
+           if (new_mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
+               notmuch->writable_xapian_db = new Xapian::WritableDatabase (notmuch->xapian_path,
+                                                                           DB_ACTION);
+               notmuch->xapian_db = notmuch->writable_xapian_db;
+           } else {
+               notmuch->xapian_db = new Xapian::Database (notmuch->xapian_path,
+                                                          DB_ACTION);
+           }
+       }
+
+       _load_database_state (notmuch);
+    } catch (const Xapian::Error &error) {
+       if (! notmuch->exception_reported) {
+           _notmuch_database_log (notmuch, "Error: A Xapian exception reopening database: %s\n",
+                                  error.get_msg ().c_str ());
+           notmuch->exception_reported = true;
+       }
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    notmuch->view++;
+    notmuch->open = true;
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_maybe_load_config_from_database (notmuch_database_t *notmuch,
+                                 GKeyFile *key_file,
+                                 const char *database_path,
+                                 const char *profile)
+{
+    char *message; /* ignored */
+
+    if (_db_dir_exists (database_path, &message))
+       return NOTMUCH_STATUS_NO_DATABASE;
+
+    _set_database_path (notmuch, database_path);
+
+    if (_notmuch_choose_xapian_path (notmuch, database_path, &notmuch->xapian_path, &message))
+       return NOTMUCH_STATUS_NO_DATABASE;
+
+    (void) _finish_open (notmuch, profile, NOTMUCH_DATABASE_MODE_READ_ONLY, key_file, &message);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_database_load_config (const char *database_path,
+                             const char *config_path,
+                             const char *profile,
+                             notmuch_database_t **database,
+                             char **status_string)
+{
+    notmuch_status_t status = NOTMUCH_STATUS_SUCCESS, warning = NOTMUCH_STATUS_SUCCESS;
+    notmuch_database_t *notmuch = NULL;
+    char *message = NULL;
+    GKeyFile *key_file = NULL;
+
+    _notmuch_init ();
+
+    notmuch = _alloc_notmuch (database_path, config_path, profile);
+    if (! notmuch) {
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    status = _load_key_file (notmuch, config_path, profile, &key_file);
+    switch (status) {
+    case NOTMUCH_STATUS_SUCCESS:
+       break;
+    case NOTMUCH_STATUS_NO_CONFIG:
+       warning = status;
+       break;
+    default:
+       message = strdup ("Error: cannot load config file.\n");
+       goto DONE;
+    }
+
+    status = _choose_database_path (notmuch, profile, key_file,
+                                   &database_path, &message);
+    switch (status) {
+    case NOTMUCH_STATUS_NO_DATABASE:
+    case NOTMUCH_STATUS_SUCCESS:
+       if (! warning)
+           warning = status;
+       break;
+    default:
+       goto DONE;
+    }
+
+
+    if (database_path) {
+       status = _maybe_load_config_from_database (notmuch, key_file, database_path, profile);
+       switch (status) {
+       case NOTMUCH_STATUS_NO_DATABASE:
+       case NOTMUCH_STATUS_SUCCESS:
+           if (! warning)
+               warning = status;
+           break;
+       default:
+           goto DONE;
+       }
+    }
+
+    if (key_file) {
+       status = _notmuch_config_load_from_file (notmuch, key_file, &message);
+       if (status)
+           goto DONE;
+    }
+    status = _notmuch_config_load_defaults (notmuch);
+    if (status)
+       goto DONE;
+
+  DONE:
+    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;
+
+    if (status)
+       return status;
+    else
+       return warning;
+}
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
diff --git a/lib/parse-time-vrp.cc b/lib/parse-time-vrp.cc
new file mode 100644 (file)
index 0000000..6b07970
--- /dev/null
@@ -0,0 +1,107 @@
+/* parse-time-vrp.cc - date range query glue
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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: Jani Nikula <jani@nikula.org>
+ */
+
+#include "database-private.h"
+#include "parse-time-vrp.h"
+#include "parse-time-string.h"
+
+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) {
+       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)) {
+           msg = "Didn't understand date specification '" + begin + "'";
+           return NOTMUCH_STATUS_BAD_QUERY_SYNTAX;
+       }
+
+       from = (double) parsed_time;
+    }
+
+    if (! end.empty ()) {
+       if (end == "!" && ! begin.empty ())
+           str = begin;
+       else
+           str = end;
+
+       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;
+    }
+
+    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? */
+Xapian::Query
+DateFieldProcessor::operator() (const std::string & str)
+{
+    double from = DBL_MIN, to = DBL_MAX;
+    time_t parsed_time, now;
+
+    /* Use the same 'now' for begin and end. */
+    if (time (&now) == (time_t) -1)
+       throw Xapian::QueryParserError ("Unable to get current time");
+
+    if (parse_time_string (str.c_str (), &parsed_time, &now, PARSE_TIME_ROUND_DOWN))
+       throw Xapian::QueryParserError ("Didn't understand date specification '" + str + "'");
+    else
+       from = (double) parsed_time;
+
+    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;
+
+    return Xapian::Query (Xapian::Query::OP_VALUE_RANGE, slot,
+                         Xapian::sortable_serialise (from),
+                         Xapian::sortable_serialise (to));
+}
diff --git a/lib/parse-time-vrp.h b/lib/parse-time-vrp.h
new file mode 100644 (file)
index 0000000..f495e71
--- /dev/null
@@ -0,0 +1,46 @@
+/* parse-time-vrp.h - date range query glue
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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: Jani Nikula <jani@nikula.org>
+ */
+
+#ifndef NOTMUCH_PARSE_TIME_VRP_H
+#define NOTMUCH_PARSE_TIME_VRP_H
+
+#include <xapian.h>
+
+/* see *ValueRangeProcessor in xapian-core/include/xapian/queryparser.h */
+class ParseTimeRangeProcessor : public Xapian::RangeProcessor {
+
+public:
+    ParseTimeRangeProcessor (Xapian::valueno slot_, const std::string prefix_)
+       :  Xapian::RangeProcessor(slot_, prefix_, 0) { }
+
+    Xapian::Query operator() (const std::string &begin, const std::string &end);
+};
+
+class DateFieldProcessor : public Xapian::FieldProcessor {
+private:
+    Xapian::valueno slot;
+public:
+    DateFieldProcessor(Xapian::valueno slot_) : slot(slot_) { };
+    Xapian::Query operator()(const std::string & str);
+};
+
+#endif /* NOTMUCH_PARSE_TIME_VRP_H */
diff --git a/lib/prefix.cc b/lib/prefix.cc
new file mode 100644 (file)
index 0000000..06e2333
--- /dev/null
@@ -0,0 +1,215 @@
+#include "database-private.h"
+#include "query-fp.h"
+#include "thread-fp.h"
+#include "regexp-fields.h"
+#include "parse-time-vrp.h"
+#include "sexp-fp.h"
+
+typedef struct {
+    const char *name;
+    const char *prefix;
+    notmuch_field_flag_t flags;
+} prefix_t;
+
+/* With these prefix values we follow the conventions published here:
+ *
+ * https://xapian.org/docs/omega/termprefixes.html
+ *
+ * as much as makes sense. Note that I took some liberty in matching
+ * the reserved prefix values to notmuch concepts, (for example, 'G'
+ * is documented as "newsGroup (or similar entity - e.g. a web forum
+ * name)", for which I think the thread is the closest analogue in
+ * notmuch. This in spite of the fact that we will eventually be
+ * storing mailing-list messages where 'G' for "mailing list name"
+ * might be even a closer analogue. I'm treating the single-character
+ * prefixes preferentially for core notmuch concepts (which will be
+ * nearly universal to all mail messages).
+ */
+
+static const
+prefix_t prefix_table[] = {
+    /* name                    term prefix     flags */
+    { "type",                   "T",            NOTMUCH_FIELD_NO_FLAGS },
+    { "reference",              "XREFERENCE",   NOTMUCH_FIELD_NO_FLAGS },
+    { "replyto",                "XREPLYTO",     NOTMUCH_FIELD_NO_FLAGS },
+    { "directory",              "XDIRECTORY",   NOTMUCH_FIELD_NO_FLAGS },
+    { "file-direntry",          "XFDIRENTRY",   NOTMUCH_FIELD_NO_FLAGS },
+    { "directory-direntry",     "XDDIRENTRY",   NOTMUCH_FIELD_NO_FLAGS },
+    { "body",                   "",             NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROBABILISTIC },
+    { "thread",                 "G",            NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROCESSOR },
+    { "tag",                    "K",            NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROCESSOR },
+    { "is",                     "K",            NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROCESSOR },
+    { "id",                     "Q",            NOTMUCH_FIELD_EXTERNAL },
+    { "mid",                    "Q",            NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROCESSOR },
+    { "path",                   "P",            NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROCESSOR | NOTMUCH_FIELD_STRIP_TRAILING_SLASH },
+    { "property",               "XPROPERTY",    NOTMUCH_FIELD_EXTERNAL },
+    /*
+     * Unconditionally add ':' to reduce potential ambiguity with
+     * overlapping prefixes and/or terms that start with capital
+     * letters. See Xapian document termprefixes.html for related
+     * discussion.
+     */
+    { "folder",                 "XFOLDER:",     NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROCESSOR | 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 },
+    { "to",                     "XTO",          NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROBABILISTIC },
+    { "attachment",             "XATTACHMENT",  NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROBABILISTIC },
+    { "mimetype",               "XMIMETYPE",    NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROBABILISTIC },
+    { "subject",                "XSUBJECT",     NOTMUCH_FIELD_EXTERNAL |
+      NOTMUCH_FIELD_PROBABILISTIC |
+      NOTMUCH_FIELD_PROCESSOR },
+};
+
+static const char *
+_user_prefix (void *ctx, const char *name)
+{
+    return talloc_asprintf (ctx, "XU%s:", name);
+}
+
+const char *
+_find_prefix (const char *name)
+{
+    unsigned int i;
+
+    for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
+       if (strcmp (name, prefix_table[i].name) == 0)
+           return prefix_table[i].prefix;
+    }
+
+    INTERNAL_ERROR ("No prefix exists for '%s'\n", name);
+
+    return "";
+}
+
+/* Like find prefix, but include the possibility of user defined
+ * prefixes specific to this database */
+
+const char *
+_notmuch_database_prefix (notmuch_database_t *notmuch, const char *name)
+{
+    unsigned int i;
+
+    /*XXX TODO: reduce code duplication */
+    for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
+       if (strcmp (name, prefix_table[i].name) == 0)
+           return prefix_table[i].prefix;
+    }
+
+    if (notmuch->user_prefix)
+       return _notmuch_string_map_get (notmuch->user_prefix, name);
+
+    return NULL;
+}
+
+static void
+_setup_query_field_default (const prefix_t *prefix, notmuch_database_t *notmuch)
+{
+    if (prefix->prefix)
+       notmuch->query_parser->add_prefix ("", prefix->prefix);
+    if (prefix->flags & NOTMUCH_FIELD_PROBABILISTIC)
+       notmuch->query_parser->add_prefix (prefix->name, prefix->prefix);
+    else
+       notmuch->query_parser->add_boolean_prefix (prefix->name, prefix->prefix);
+}
+
+static void
+_setup_query_field (const prefix_t *prefix, notmuch_database_t *notmuch)
+{
+    if (prefix->flags & NOTMUCH_FIELD_PROCESSOR) {
+       Xapian::FieldProcessor *fp;
+
+       if (STRNCMP_LITERAL (prefix->name, "date") == 0)
+           fp = (new DateFieldProcessor (NOTMUCH_VALUE_TIMESTAMP))->release ();
+       else if (STRNCMP_LITERAL (prefix->name, "query") == 0)
+           fp = (new QueryFieldProcessor (*notmuch->query_parser, notmuch))->release ();
+       else if (STRNCMP_LITERAL (prefix->name, "thread") == 0)
+           fp = (new ThreadFieldProcessor (*notmuch->query_parser, notmuch))->release ();
+       else 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 ();
+
+       /* we treat all field-processor fields as boolean in order to get the raw input */
+       if (prefix->prefix)
+           notmuch->query_parser->add_prefix ("", prefix->prefix);
+       notmuch->query_parser->add_boolean_prefix (prefix->name, fp);
+    } else {
+       _setup_query_field_default (prefix, notmuch);
+    }
+}
+
+notmuch_status_t
+_notmuch_database_setup_standard_query_fields (notmuch_database_t *notmuch)
+{
+    for (unsigned int i = 0; i < ARRAY_SIZE (prefix_table); i++) {
+       const prefix_t *prefix = &prefix_table[i];
+       if (prefix->flags & NOTMUCH_FIELD_EXTERNAL) {
+           _setup_query_field (prefix, notmuch);
+       }
+    }
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+_notmuch_database_setup_user_query_fields (notmuch_database_t *notmuch)
+{
+    notmuch_string_map_iterator_t *list;
+
+    notmuch->user_prefix = _notmuch_string_map_create (notmuch);
+    if (notmuch->user_prefix == NULL)
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+    notmuch->user_header = _notmuch_string_map_create (notmuch);
+    if (notmuch->user_header == NULL)
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+    list = _notmuch_string_map_iterator_create (notmuch->config, CONFIG_HEADER_PREFIX, FALSE);
+    if (! list)
+       INTERNAL_ERROR ("unable to read headers from configuration");
+
+    for (; _notmuch_string_map_iterator_valid (list);
+        _notmuch_string_map_iterator_move_to_next (list)) {
+
+       prefix_t query_field;
+
+       const char *key = _notmuch_string_map_iterator_key (list)
+                         + sizeof (CONFIG_HEADER_PREFIX) - 1;
+
+       _notmuch_string_map_append (notmuch->user_prefix,
+                                   key,
+                                   _user_prefix (notmuch, key));
+
+       _notmuch_string_map_append (notmuch->user_header,
+                                   key,
+                                   _notmuch_string_map_iterator_value (list));
+
+       query_field.name = talloc_strdup (notmuch, key);
+       query_field.prefix = _user_prefix (notmuch, key);
+       query_field.flags = NOTMUCH_FIELD_PROBABILISTIC
+                           | NOTMUCH_FIELD_EXTERNAL;
+
+       _setup_query_field_default (&query_field, notmuch);
+    }
+
+    _notmuch_string_map_iterator_destroy (list);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
diff --git a/lib/query-fp.cc b/lib/query-fp.cc
new file mode 100644 (file)
index 0000000..75b1d87
--- /dev/null
@@ -0,0 +1,56 @@
+/* query-fp.cc - "query:" field processor glue
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2016 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 "query-fp.h"
+#include <iostream>
+
+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 output;
+
+}
diff --git a/lib/query-fp.h b/lib/query-fp.h
new file mode 100644 (file)
index 0000000..beaaf40
--- /dev/null
@@ -0,0 +1,43 @@
+/* query-fp.h - query field processor glue
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2016 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_QUERY_FP_H
+#define NOTMUCH_QUERY_FP_H
+
+#include <xapian.h>
+#include "notmuch.h"
+
+class QueryFieldProcessor : public Xapian::FieldProcessor {
+protected:
+    Xapian::QueryParser &parser;
+    notmuch_database_t *notmuch;
+
+public:
+    QueryFieldProcessor (Xapian::QueryParser &parser_, notmuch_database_t *notmuch_)
+       : parser (parser_), notmuch (notmuch_)
+    {
+    };
+
+    Xapian::Query operator() (const std::string & str);
+};
+
+#endif /* NOTMUCH_QUERY_FP_H */
diff --git a/lib/query.cc b/lib/query.cc
new file mode 100644 (file)
index 0000000..1c60c12
--- /dev/null
@@ -0,0 +1,874 @@
+/* query.cc - Support for searching a notmuch database
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+#include "database-private.h"
+#include "xapian-extra.h"
+
+#include <glib.h> /* GHashTable, GPtrArray */
+
+struct _notmuch_query {
+    notmuch_database_t *notmuch;
+    const char *query_string;
+    notmuch_sort_t sort;
+    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;
+};
+
+typedef struct _notmuch_mset_messages {
+    notmuch_messages_t base;
+    notmuch_database_t *notmuch;
+    Xapian::MSetIterator iterator;
+    Xapian::MSetIterator iterator_end;
+} notmuch_mset_messages_t;
+
+struct _notmuch_doc_id_set {
+    unsigned char *bitmap;
+    unsigned int bound;
+};
+
+#define DOCIDSET_WORD(bit) ((bit) / CHAR_BIT)
+#define DOCIDSET_BIT(bit) ((bit) % CHAR_BIT)
+
+struct _notmuch_threads {
+    notmuch_query_t *query;
+
+    /* The ordered list of doc ids matched by the query. */
+    GArray *doc_ids;
+    /* Our iterator's current position in doc_ids. */
+    unsigned int doc_id_pos;
+    /* The set of matched docid's that have not been assigned to a
+     * thread. Initially, this contains every docid in doc_ids. */
+    notmuch_doc_id_set_t match_set;
+};
+
+/* We need this in the message functions so forward declare. */
+static bool
+_notmuch_doc_id_set_init (void *ctx,
+                         notmuch_doc_id_set_t *doc_ids,
+                         GArray *arr);
+
+static bool
+_debug_query (void)
+{
+    char *env = getenv ("NOTMUCH_DEBUG_QUERY");
+
+    return (env && strcmp (env, "") != 0);
+}
+
+/* Explicit destructor call for placement new */
+static int
+_notmuch_query_destructor (notmuch_query_t *query)
+{
+    query->xapian_query.~Query();
+    query->terms.~set<std::string>();
+    return 0;
+}
+
+static notmuch_query_t *
+_notmuch_query_constructor (notmuch_database_t *notmuch,
+                           const char *query_string)
+{
+    notmuch_query_t *query;
+
+    if (_debug_query ())
+       fprintf (stderr, "Query string is:\n%s\n", query_string);
+
+    query = talloc (notmuch, notmuch_query_t);
+    if (unlikely (query == NULL))
+       return NULL;
+
+    new (&query->xapian_query) Xapian::Query ();
+    new (&query->terms) std::set<std::string> ();
+    query->parsed = false;
+
+    talloc_set_destructor (query, _notmuch_query_destructor);
+
+    query->notmuch = notmuch;
+
+    if (query_string)
+       query->query_string = talloc_strdup (query, query_string);
+    else
+       query->query_string = NULL;
+
+    query->sort = NOTMUCH_SORT_NEWEST_FIRST;
+
+    query->exclude_terms = _notmuch_string_list_create (query);
+
+    query->omit_excluded = NOTMUCH_EXCLUDE_TRUE;
+
+    return query;
+}
+
+notmuch_query_t *
+notmuch_query_create (notmuch_database_t *notmuch,
+                     const char *query_string)
+{
+
+    notmuch_query_t *query;
+    notmuch_status_t status;
+
+    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;
+
+    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->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 (! 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",
+                                         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)
+{
+    return query->query_string;
+}
+
+void
+notmuch_query_set_omit_excluded (notmuch_query_t *query,
+                                notmuch_exclude_t omit_excluded)
+{
+    query->omit_excluded = omit_excluded;
+}
+
+void
+notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort)
+{
+    query->sort = sort;
+}
+
+notmuch_sort_t
+notmuch_query_get_sort (const notmuch_query_t *query)
+{
+    return query->sort;
+}
+
+notmuch_status_t
+notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag)
+{
+    notmuch_status_t status;
+    char *term;
+
+    status = _notmuch_query_ensure_parsed (query);
+    if (status)
+       return status;
+
+    term = talloc_asprintf (query, "%s%s", _find_prefix ("tag"), tag);
+    if (query->terms.count (term) != 0)
+       return NOTMUCH_STATUS_IGNORED;
+
+    _notmuch_string_list_append (query->exclude_terms, term);
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* We end up having to call the destructors explicitly because we had
+ * to use "placement new" in order to initialize C++ objects within a
+ * block that we allocated with talloc. So C++ is making talloc
+ * slightly less simple to use, (we wouldn't need
+ * talloc_set_destructor at all otherwise).
+ */
+static int
+_notmuch_messages_destructor (notmuch_mset_messages_t *messages)
+{
+    messages->iterator.~MSetIterator ();
+    messages->iterator_end.~MSetIterator ();
+
+    return 0;
+}
+
+/* Return a query that matches messages with the excluded tags
+ * registered with query. The caller of this function has to combine the returned
+ * query appropriately.*/
+static Xapian::Query
+_notmuch_exclude_tags (notmuch_query_t *query)
+{
+    Xapian::Query exclude_query = Xapian::Query::MatchNothing;
+
+    for (notmuch_string_node_t *term = query->exclude_terms->head; term;
+        term = term->next) {
+       exclude_query = Xapian::Query (Xapian::Query::OP_OR,
+                                      exclude_query, Xapian::Query (term->string));
+    }
+    return exclude_query;
+}
+
+
+notmuch_status_t
+notmuch_query_search_messages_st (notmuch_query_t *query,
+                                 notmuch_messages_t **out)
+{
+    return notmuch_query_search_messages (query, out);
+}
+
+notmuch_status_t
+notmuch_query_search_messages (notmuch_query_t *query,
+                              notmuch_messages_t **out)
+{
+    return _notmuch_query_search_documents (query, "mail", out);
+}
+
+notmuch_status_t
+_notmuch_query_search_documents (notmuch_query_t *query,
+                                const char *type,
+                                notmuch_messages_t **out)
+{
+    notmuch_database_t *notmuch = query->notmuch;
+    notmuch_mset_messages_t *messages;
+    notmuch_status_t status;
+
+    status = _notmuch_query_ensure_parsed (query);
+    if (status)
+       return status;
+
+    messages = talloc (query, notmuch_mset_messages_t);
+    if (unlikely (messages == NULL))
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+    try {
+
+       messages->base.is_of_list_type = false;
+       messages->base.iterator = NULL;
+       messages->notmuch = notmuch;
+       new (&messages->iterator) Xapian::MSetIterator ();
+       new (&messages->iterator_end) Xapian::MSetIterator ();
+
+       talloc_set_destructor (messages, _notmuch_messages_destructor);
+
+       Xapian::Enquire enquire (*notmuch->xapian_db);
+       Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
+                                                  _find_prefix ("type"),
+                                                  type));
+       Xapian::Query final_query, exclude_query;
+       Xapian::MSet mset;
+       Xapian::MSetIterator iterator;
+
+       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)) {
+           exclude_query = _notmuch_exclude_tags (query);
+
+           if (query->omit_excluded == NOTMUCH_EXCLUDE_TRUE ||
+               query->omit_excluded == NOTMUCH_EXCLUDE_ALL) {
+               final_query = Xapian::Query (Xapian::Query::OP_AND_NOT,
+                                            final_query, exclude_query);
+           } else { /* NOTMUCH_EXCLUDE_FLAG */
+               exclude_query = Xapian::Query (Xapian::Query::OP_AND,
+                                              exclude_query, final_query);
+
+               enquire.set_weighting_scheme (Xapian::BoolWeight ());
+               enquire.set_query (exclude_query);
+
+               mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
+
+               GArray *excluded_doc_ids = g_array_new (false, false, sizeof (unsigned int));
+
+               for (iterator = mset.begin (); iterator != mset.end (); iterator++) {
+                   unsigned int doc_id = *iterator;
+                   g_array_append_val (excluded_doc_ids, doc_id);
+               }
+               messages->base.excluded_doc_ids = talloc (messages, _notmuch_doc_id_set);
+               _notmuch_doc_id_set_init (query, messages->base.excluded_doc_ids,
+                                         excluded_doc_ids);
+               g_array_unref (excluded_doc_ids);
+           }
+       }
+
+
+       enquire.set_weighting_scheme (Xapian::BoolWeight ());
+
+       switch (query->sort) {
+       case NOTMUCH_SORT_OLDEST_FIRST:
+           enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, false);
+           break;
+       case NOTMUCH_SORT_NEWEST_FIRST:
+           enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, true);
+           break;
+       case NOTMUCH_SORT_MESSAGE_ID:
+           enquire.set_sort_by_value (NOTMUCH_VALUE_MESSAGE_ID, false);
+           break;
+       case NOTMUCH_SORT_UNSORTED:
+           break;
+       }
+
+       if (_debug_query ()) {
+           fprintf (stderr, "Exclude query is:\n%s\n",
+                    exclude_query.get_description ().c_str ());
+           fprintf (stderr, "Final query is:\n%s\n",
+                    final_query.get_description ().c_str ());
+       }
+
+       enquire.set_query (final_query);
+
+       mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
+
+       messages->iterator = mset.begin ();
+       messages->iterator_end = mset.end ();
+
+       *out = &messages->base;
+       return NOTMUCH_STATUS_SUCCESS;
+
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch,
+                              "A Xapian exception occurred performing query: %s\n",
+                              error.get_msg ().c_str ());
+       _notmuch_database_log_append (notmuch,
+                                     "Query string was: %s\n",
+                                     query->query_string);
+
+       notmuch->exception_reported = true;
+       talloc_free (messages);
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+}
+
+bool
+_notmuch_mset_messages_valid (notmuch_messages_t *messages)
+{
+    notmuch_mset_messages_t *mset_messages;
+
+    mset_messages = (notmuch_mset_messages_t *) messages;
+
+    return (mset_messages->iterator != mset_messages->iterator_end);
+}
+
+static Xapian::docid
+_notmuch_mset_messages_get_doc_id (notmuch_messages_t *messages)
+{
+    notmuch_mset_messages_t *mset_messages;
+
+    mset_messages = (notmuch_mset_messages_t *) messages;
+
+    if (! _notmuch_mset_messages_valid (&mset_messages->base))
+       return 0;
+
+    return *mset_messages->iterator;
+}
+
+notmuch_message_t *
+_notmuch_mset_messages_get (notmuch_messages_t *messages)
+{
+    notmuch_message_t *message;
+    Xapian::docid doc_id;
+    notmuch_private_status_t status;
+    notmuch_mset_messages_t *mset_messages;
+
+    mset_messages = (notmuch_mset_messages_t *) messages;
+
+    if (! _notmuch_mset_messages_valid (&mset_messages->base))
+       return NULL;
+
+    doc_id = *mset_messages->iterator;
+
+    message = _notmuch_message_create (mset_messages,
+                                      mset_messages->notmuch, doc_id,
+                                      &status);
+
+    if (message == NULL &&
+       status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+       INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n");
+    }
+
+    if (messages->excluded_doc_ids &&
+       _notmuch_doc_id_set_contains (messages->excluded_doc_ids, doc_id))
+       notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, true);
+
+    return message;
+}
+
+void
+_notmuch_mset_messages_move_to_next (notmuch_messages_t *messages)
+{
+    notmuch_mset_messages_t *mset_messages;
+
+    mset_messages = (notmuch_mset_messages_t *) messages;
+
+    mset_messages->iterator++;
+}
+
+static bool
+_notmuch_doc_id_set_init (void *ctx,
+                         notmuch_doc_id_set_t *doc_ids,
+                         GArray *arr)
+{
+    unsigned int max = 0;
+    unsigned char *bitmap;
+
+    for (unsigned int i = 0; i < arr->len; i++)
+       max = MAX (max, g_array_index (arr, unsigned int, i));
+    bitmap = talloc_zero_array (ctx, unsigned char, DOCIDSET_WORD (max) + 1);
+
+    if (bitmap == NULL)
+       return false;
+
+    doc_ids->bitmap = bitmap;
+    doc_ids->bound = max + 1;
+
+    for (unsigned int i = 0; i < arr->len; i++) {
+       unsigned int doc_id = g_array_index (arr, unsigned int, i);
+       bitmap[DOCIDSET_WORD (doc_id)] |= 1 << DOCIDSET_BIT (doc_id);
+    }
+
+    return true;
+}
+
+bool
+_notmuch_doc_id_set_contains (notmuch_doc_id_set_t *doc_ids,
+                             unsigned int doc_id)
+{
+    if (doc_id >= doc_ids->bound)
+       return false;
+    return doc_ids->bitmap[DOCIDSET_WORD (doc_id)] & (1 << DOCIDSET_BIT (doc_id));
+}
+
+void
+_notmuch_doc_id_set_remove (notmuch_doc_id_set_t *doc_ids,
+                           unsigned int doc_id)
+{
+    if (doc_id < doc_ids->bound)
+       doc_ids->bitmap[DOCIDSET_WORD (doc_id)] &= ~(1 << DOCIDSET_BIT (doc_id));
+}
+
+/* Glib objects force use to use a talloc destructor as well, (but not
+ * nearly as ugly as the for messages due to C++ objects). At
+ * this point, I'd really like to have some talloc-friendly
+ * equivalents for the few pieces of glib that I'm using. */
+static int
+_notmuch_threads_destructor (notmuch_threads_t *threads)
+{
+    if (threads->doc_ids)
+       g_array_unref (threads->doc_ids);
+
+    return 0;
+}
+
+notmuch_status_t
+notmuch_query_search_threads_st (notmuch_query_t *query, notmuch_threads_t **out)
+{
+    return notmuch_query_search_threads (query, out);
+}
+
+notmuch_status_t
+notmuch_query_search_threads (notmuch_query_t *query,
+                             notmuch_threads_t **out)
+{
+    notmuch_threads_t *threads;
+    notmuch_messages_t *messages;
+    notmuch_status_t status;
+
+    threads = talloc (query, notmuch_threads_t);
+    if (threads == NULL)
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+    threads->doc_ids = NULL;
+    talloc_set_destructor (threads, _notmuch_threads_destructor);
+
+    threads->query = query;
+
+    status = notmuch_query_search_messages (query, &messages);
+    if (status) {
+       talloc_free (threads);
+       return status;
+    }
+
+    threads->doc_ids = g_array_new (false, false, sizeof (unsigned int));
+    while (notmuch_messages_valid (messages)) {
+       unsigned int doc_id = _notmuch_mset_messages_get_doc_id (messages);
+       g_array_append_val (threads->doc_ids, doc_id);
+       notmuch_messages_move_to_next (messages);
+    }
+    threads->doc_id_pos = 0;
+
+    talloc_free (messages);
+
+    if (! _notmuch_doc_id_set_init (threads, &threads->match_set,
+                                   threads->doc_ids)) {
+       talloc_free (threads);
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+    }
+
+    *out = threads;
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+void
+notmuch_query_destroy (notmuch_query_t *query)
+{
+    talloc_free (query);
+}
+
+notmuch_bool_t
+notmuch_threads_valid (notmuch_threads_t *threads)
+{
+    unsigned int doc_id;
+
+    if (! threads)
+       return false;
+
+    while (threads->doc_id_pos < threads->doc_ids->len) {
+       doc_id = g_array_index (threads->doc_ids, unsigned int,
+                               threads->doc_id_pos);
+       if (_notmuch_doc_id_set_contains (&threads->match_set, doc_id))
+           break;
+
+       threads->doc_id_pos++;
+    }
+
+    return threads->doc_id_pos < threads->doc_ids->len;
+}
+
+notmuch_thread_t *
+notmuch_threads_get (notmuch_threads_t *threads)
+{
+    unsigned int doc_id;
+
+    if (! notmuch_threads_valid (threads))
+       return NULL;
+
+    doc_id = g_array_index (threads->doc_ids, unsigned int,
+                           threads->doc_id_pos);
+    return _notmuch_thread_create (threads->query,
+                                  threads->query->notmuch,
+                                  doc_id,
+                                  &threads->match_set,
+                                  threads->query->exclude_terms,
+                                  threads->query->omit_excluded,
+                                  threads->query->sort);
+}
+
+void
+notmuch_threads_move_to_next (notmuch_threads_t *threads)
+{
+    threads->doc_id_pos++;
+}
+
+void
+notmuch_threads_destroy (notmuch_threads_t *threads)
+{
+    talloc_free (threads);
+}
+
+notmuch_status_t
+notmuch_query_count_messages_st (notmuch_query_t *query, unsigned *count_out)
+{
+    return notmuch_query_count_messages (query, count_out);
+}
+
+notmuch_status_t
+notmuch_query_count_messages (notmuch_query_t *query, unsigned *count_out)
+{
+    return _notmuch_query_count_documents (query, "mail", count_out);
+}
+
+notmuch_status_t
+_notmuch_query_count_documents (notmuch_query_t *query, const char *type, unsigned *count_out)
+{
+    notmuch_database_t *notmuch = query->notmuch;
+    Xapian::doccount count = 0;
+    notmuch_status_t status;
+
+    status = _notmuch_query_ensure_parsed (query);
+    if (status)
+       return status;
+
+    try {
+       Xapian::Enquire enquire (*notmuch->xapian_db);
+       Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
+                                                  _find_prefix ("type"),
+                                                  type));
+       Xapian::Query final_query, exclude_query;
+       Xapian::MSet mset;
+
+       final_query = Xapian::Query (Xapian::Query::OP_AND,
+                                    mail_query, query->xapian_query);
+
+       exclude_query = _notmuch_exclude_tags (query);
+
+       final_query = Xapian::Query (Xapian::Query::OP_AND_NOT,
+                                    final_query, exclude_query);
+
+       enquire.set_weighting_scheme (Xapian::BoolWeight ());
+       enquire.set_docid_order (Xapian::Enquire::ASCENDING);
+
+       if (_debug_query ()) {
+           fprintf (stderr, "Exclude query is:\n%s\n",
+                    exclude_query.get_description ().c_str ());
+           fprintf (stderr, "Final query is:\n%s\n",
+                    final_query.get_description ().c_str ());
+       }
+
+       enquire.set_query (final_query);
+
+       /*
+        * Set the checkatleast parameter to the number of documents
+        * in the database to make get_matches_estimated() exact.
+        * Set the max parameter to 1 to avoid fetching documents we will discard.
+        */
+       mset = enquire.get_mset (0, 1,
+                                notmuch->xapian_db->get_doccount ());
+
+       count = mset.get_matches_estimated ();
+
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch,
+                              "A Xapian exception occurred performing query: %s\n",
+                              error.get_msg ().c_str ());
+       _notmuch_database_log_append (notmuch,
+                                     "Query string was: %s\n",
+                                     query->query_string);
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    *count_out = count;
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_query_count_threads_st (notmuch_query_t *query, unsigned *count)
+{
+    return notmuch_query_count_threads (query, count);
+}
+
+notmuch_status_t
+notmuch_query_count_threads (notmuch_query_t *query, unsigned *count)
+{
+    notmuch_messages_t *messages;
+    GHashTable *hash;
+    notmuch_sort_t sort;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+    sort = query->sort;
+    query->sort = NOTMUCH_SORT_UNSORTED;
+    ret = notmuch_query_search_messages (query, &messages);
+    if (ret)
+       return ret;
+    query->sort = sort;
+    if (messages == NULL)
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+
+    hash = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL);
+    if (hash == NULL) {
+       talloc_free (messages);
+       return NOTMUCH_STATUS_OUT_OF_MEMORY;
+    }
+
+    while (notmuch_messages_valid (messages)) {
+       notmuch_message_t *message = notmuch_messages_get (messages);
+       const char *thread_id = notmuch_message_get_thread_id (message);
+       char *thread_id_copy = talloc_strdup (messages, thread_id);
+       if (unlikely (thread_id_copy == NULL)) {
+           notmuch_message_destroy (message);
+           ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
+           goto DONE;
+       }
+       g_hash_table_insert (hash, thread_id_copy, NULL);
+       notmuch_message_destroy (message);
+       notmuch_messages_move_to_next (messages);
+    }
+
+    *count = g_hash_table_size (hash);
+
+  DONE:
+    g_hash_table_unref (hash);
+    talloc_free (messages);
+
+    return ret;
+}
+
+notmuch_database_t *
+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;
+}
diff --git a/lib/regexp-fields.cc b/lib/regexp-fields.cc
new file mode 100644 (file)
index 0000000..3a77526
--- /dev/null
@@ -0,0 +1,252 @@
+/* regexp-fields.cc - field processor glue for regex supporting fields
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2015 Austin Clements
+ * Copyright © 2016 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: Austin Clements <aclements@csail.mit.edu>
+ *                David Bremner <david@tethera.net>
+ */
+
+#include "regexp-fields.h"
+#include "notmuch-private.h"
+#include "database-private.h"
+#include "xapian-extra.h"
+
+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];
+       msg = "Regexp error: ";
+       (void) regerror (err, &regexp, buffer, len);
+       msg.append (buffer, len);
+       delete[] buffer;
+
+       return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
+    }
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+RegexpPostingSource::RegexpPostingSource (Xapian::valueno slot, const std::string &regexp)
+    : slot_ (slot)
+{
+    std::string msg;
+    notmuch_status_t status = compile_regex (regexp_, regexp.c_str (), msg);
+
+    if (status)
+       throw Xapian::QueryParserError (msg);
+}
+
+RegexpPostingSource::~RegexpPostingSource ()
+{
+    regfree (&regexp_);
+}
+
+void
+RegexpPostingSource::init (const Xapian::Database &db)
+{
+    db_ = db;
+    it_ = db_.valuestream_begin (slot_);
+    end_ = db.valuestream_end (slot_);
+    started_ = false;
+}
+
+Xapian::doccount
+RegexpPostingSource::get_termfreq_min () const
+{
+    return 0;
+}
+
+Xapian::doccount
+RegexpPostingSource::get_termfreq_est () const
+{
+    return get_termfreq_max () / 2;
+}
+
+Xapian::doccount
+RegexpPostingSource::get_termfreq_max () const
+{
+    return db_.get_value_freq (slot_);
+}
+
+Xapian::docid
+RegexpPostingSource::get_docid () const
+{
+    return it_.get_docid ();
+}
+
+bool
+RegexpPostingSource::at_end () const
+{
+    return it_ == end_;
+}
+
+void
+RegexpPostingSource::next (unused (double min_wt))
+{
+    if (started_ && ! at_end ())
+       ++it_;
+    started_ = true;
+
+    for (; ! at_end (); ++it_) {
+       std::string value = *it_;
+       if (regexec (&regexp_, value.c_str (), 0, NULL, 0) == 0)
+           break;
+    }
+}
+
+void
+RegexpPostingSource::skip_to (Xapian::docid did, unused (double min_wt))
+{
+    started_ = true;
+    it_.skip_to (did);
+    for (; ! at_end (); ++it_) {
+       std::string value = *it_;
+       if (regexec (&regexp_, value.c_str (), 0, NULL, 0) == 0)
+           break;
+    }
+}
+
+bool
+RegexpPostingSource::check (Xapian::docid did, unused (double min_wt))
+{
+    started_ = true;
+    if (! it_.check (did) || at_end ())
+       return false;
+    return (regexec (&regexp_, (*it_).c_str (), 0, NULL, 0) == 0);
+}
+
+static inline Xapian::valueno
+_find_slot (std::string prefix)
+{
+    if (prefix == "from")
+       return NOTMUCH_VALUE_FROM;
+    else if (prefix == "subject")
+       return NOTMUCH_VALUE_SUBJECT;
+    else if (prefix == "mid")
+       return NOTMUCH_VALUE_MESSAGE_ID;
+    else
+       return Xapian::BAD_VALUENO;
+}
+
+RegexpFieldProcessor::RegexpFieldProcessor (std::string field_,
+                                           notmuch_field_flag_t options_,
+                                           Xapian::QueryParser &parser_,
+                                           notmuch_database_t *notmuch_)
+    : 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_match_all (),
+                                 Xapian::Query (Xapian::Query::OP_WILDCARD, term_prefix));
+       } else {
+           return Xapian::Query (term_prefix);
+       }
+    }
+
+    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);
+           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 + "'");
+       }
+    } else {
+       if (options & NOTMUCH_FIELD_PROBABILISTIC) {
+           /* TODO replace this with a nicer API level triggering of
+            * phrase parsing, when possible */
+           std::string query_str;
+
+           if ((str.at (0) != '(' || *str.rbegin () != ')') &&
+               (*str.rbegin () != '*' || str.find (' ') != std::string::npos))
+               query_str = '"' + str + '"';
+           else
+               query_str = str;
+
+           return parser.parse_query (query_str, NOTMUCH_QUERY_PARSER_FLAGS, term_prefix);
+       } else {
+           /* Boolean prefix */
+           std::string query_str;
+           std::string term;
+
+           if (str.length () > 1 && str.at (str.size () - 1) == '/')
+               query_str = str.substr (0, str.size () - 1);
+           else
+               query_str = str;
+
+           term = term_prefix + query_str;
+           return Xapian::Query (term);
+       }
+    }
+}
diff --git a/lib/regexp-fields.h b/lib/regexp-fields.h
new file mode 100644 (file)
index 0000000..9c871de
--- /dev/null
@@ -0,0 +1,89 @@
+/* regex-fields.h - xapian glue for semi-bruteforce regexp search
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2015 Austin Clements
+ * Copyright © 2016 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: Austin Clements <aclements@csail.mit.edu>
+ *                David Bremner <david@tethera.net>
+ */
+
+#ifndef NOTMUCH_REGEXP_FIELDS_H
+#define NOTMUCH_REGEXP_FIELDS_H
+
+#include <sys/types.h>
+#include <regex.h>
+#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.
+ */
+class RegexpPostingSource : public Xapian::PostingSource
+{
+protected:
+    const Xapian::valueno slot_;
+    regex_t regexp_;
+    Xapian::Database db_;
+    bool started_;
+    Xapian::ValueIterator it_, end_;
+
+/* No copying */
+    RegexpPostingSource (const RegexpPostingSource &);
+    RegexpPostingSource &operator= (const RegexpPostingSource &);
+
+public:
+    RegexpPostingSource (Xapian::valueno slot, const std::string &regexp);
+    ~RegexpPostingSource ();
+    void init (const Xapian::Database &db);
+    Xapian::doccount get_termfreq_min () const;
+    Xapian::doccount get_termfreq_est () const;
+    Xapian::doccount get_termfreq_max () const;
+    Xapian::docid get_docid () const;
+    bool at_end () const;
+    void next (unused (double min_wt));
+    void skip_to (Xapian::docid did, unused (double min_wt));
+    bool check (Xapian::docid did, unused (double min_wt));
+};
+
+
+class RegexpFieldProcessor : public Xapian::FieldProcessor {
+protected:
+    Xapian::valueno slot;
+    std::string field;
+    std::string term_prefix;
+    notmuch_field_flag_t options;
+    Xapian::QueryParser &parser;
+    notmuch_database_t *notmuch;
+
+public:
+    RegexpFieldProcessor (std::string prefix, notmuch_field_flag_t options,
+                         Xapian::QueryParser &parser_, notmuch_database_t *notmuch_);
+
+    ~RegexpFieldProcessor ()
+    {
+    };
+
+    Xapian::Query operator() (const std::string & str);
+};
+
+#endif /* NOTMUCH_REGEXP_FIELDS_H */
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 */
diff --git a/lib/sha1.c b/lib/sha1.c
new file mode 100644 (file)
index 0000000..d1a76ee
--- /dev/null
@@ -0,0 +1,94 @@
+/* sha1.c - Interfaces to SHA-1 hash for the notmuch mail system
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+
+#include <glib.h>
+
+/* Create a hexadecimal string version of the SHA-1 digest of 'str'
+ * (including its null terminating character).
+ *
+ * This function returns a newly allocated string which the caller
+ * should free() when finished.
+ */
+char *
+_notmuch_sha1_of_string (const char *str)
+{
+    GChecksum *sha1;
+    char *digest;
+
+    sha1 = g_checksum_new (G_CHECKSUM_SHA1);
+    g_checksum_update (sha1, (const guchar *) str, strlen (str) + 1);
+    digest = xstrdup (g_checksum_get_string (sha1));
+    g_checksum_free (sha1);
+
+    return digest;
+}
+
+/* Create a hexadecimal string version of the SHA-1 digest of the
+ * contents of the named file.
+ *
+ * This function returns a newly allocated string which the caller
+ * should free() when finished.
+ *
+ * If any error occurs while reading the file, (permission denied,
+ * file not found, etc.), this function returns NULL.
+ */
+char *
+_notmuch_sha1_of_file (const char *filename)
+{
+    FILE *file;
+
+#define BLOCK_SIZE 4096
+    unsigned char block[BLOCK_SIZE];
+    size_t bytes_read;
+    GChecksum *sha1;
+    char *digest = NULL;
+
+    file = fopen (filename, "r");
+    if (file == NULL)
+       return NULL;
+
+    sha1 = g_checksum_new (G_CHECKSUM_SHA1);
+    if (sha1 == NULL)
+       goto DONE;
+
+    while (1) {
+       bytes_read = fread (block, 1, 4096, file);
+       if (bytes_read == 0) {
+           if (feof (file))
+               break;
+           else if (ferror (file))
+               goto DONE;
+       } else {
+           g_checksum_update (sha1, block, bytes_read);
+       }
+    }
+
+    digest = xstrdup (g_checksum_get_string (sha1));
+
+  DONE:
+    if (sha1)
+       g_checksum_free (sha1);
+    if (file)
+       fclose (file);
+
+    return digest;
+}
diff --git a/lib/string-list.c b/lib/string-list.c
new file mode 100644 (file)
index 0000000..f3dac67
--- /dev/null
@@ -0,0 +1,101 @@
+/* strings.c - Iterator for a list of strings
+ *
+ * Copyright © 2010 Intel Corporation
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ *         Austin Clements <aclements@csail.mit.edu>
+ */
+
+#include "notmuch-private.h"
+
+/* Create a new notmuch_string_list_t object, with 'ctx' as its
+ * talloc owner.
+ *
+ * This function can return NULL in case of out-of-memory.
+ */
+notmuch_string_list_t *
+_notmuch_string_list_create (const void *ctx)
+{
+    notmuch_string_list_t *list;
+
+    list = talloc (ctx, notmuch_string_list_t);
+    if (unlikely (list == NULL))
+       return NULL;
+
+    list->length = 0;
+    list->head = NULL;
+    list->tail = &list->head;
+
+    return list;
+}
+
+int
+_notmuch_string_list_length (notmuch_string_list_t *list)
+{
+    return list->length;
+}
+
+void
+_notmuch_string_list_append (notmuch_string_list_t *list,
+                            const char *string)
+{
+    /* Create and initialize new node. */
+    notmuch_string_node_t *node = talloc (list, notmuch_string_node_t);
+
+    node->string = talloc_strdup (node, string);
+    node->next = NULL;
+
+    /* Append the node to the list. */
+    *(list->tail) = node;
+    list->tail = &node->next;
+    list->length++;
+}
+
+static int
+cmpnode (const void *pa, const void *pb)
+{
+    notmuch_string_node_t *a = *(notmuch_string_node_t *const *) pa;
+    notmuch_string_node_t *b = *(notmuch_string_node_t *const *) pb;
+
+    return strcmp (a->string, b->string);
+}
+
+void
+_notmuch_string_list_sort (notmuch_string_list_t *list)
+{
+    notmuch_string_node_t **nodes, *node;
+    int i;
+
+    if (list->length == 0)
+       return;
+
+    nodes = talloc_array (list, notmuch_string_node_t *, list->length);
+    if (unlikely (nodes == NULL))
+       INTERNAL_ERROR ("Could not allocate memory for list sort");
+
+    for (i = 0, node = list->head; node; i++, node = node->next)
+       nodes[i] = node;
+
+    qsort (nodes, list->length, sizeof (*nodes), cmpnode);
+
+    for (i = 0; i < list->length - 1; ++i)
+       nodes[i]->next = nodes[i + 1];
+    nodes[i]->next = NULL;
+    list->head = nodes[0];
+    list->tail = &nodes[i]->next;
+
+    talloc_free (nodes);
+}
diff --git a/lib/string-map.c b/lib/string-map.c
new file mode 100644 (file)
index 0000000..99bc2ea
--- /dev/null
@@ -0,0 +1,250 @@
+/* string-map.c - associative arrays of strings
+ *
+ *
+ * Copyright © 2016 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 "notmuch-private.h"
+
+/* Create a new notmuch_string_map_t object, with 'ctx' as its
+ * talloc owner.
+ *
+ * This function can return NULL in case of out-of-memory.
+ */
+
+typedef struct _notmuch_string_pair_t {
+    char *key;
+    char *value;
+} notmuch_string_pair_t;
+
+struct _notmuch_string_map {
+    bool sorted;
+    size_t length;
+    notmuch_string_pair_t *pairs;
+};
+
+struct _notmuch_string_map_iterator {
+    notmuch_string_pair_t *current;
+    bool exact;
+    const char *key;
+};
+
+notmuch_string_map_t *
+_notmuch_string_map_create (const void *ctx)
+{
+    notmuch_string_map_t *map;
+
+    map = talloc (ctx, notmuch_string_map_t);
+    if (unlikely (map == NULL))
+       return NULL;
+
+    map->length = 0;
+    map->pairs = NULL;
+    map->sorted = true;
+
+    return map;
+}
+
+void
+_notmuch_string_map_append (notmuch_string_map_t *map,
+                           const char *key,
+                           const char *value)
+{
+
+    map->length++;
+    map->sorted = false;
+
+    if (map->pairs)
+       map->pairs = talloc_realloc (map, map->pairs, notmuch_string_pair_t, map->length + 1);
+    else
+       map->pairs = talloc_array (map, notmuch_string_pair_t, map->length + 1);
+
+    map->pairs[map->length - 1].key = talloc_strdup (map, key);
+    map->pairs[map->length - 1].value = talloc_strdup (map, value);
+
+    /* Add sentinel */
+    map->pairs[map->length].key = NULL;
+    map->pairs[map->length].value = NULL;
+
+}
+
+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;
+
+    cmp = strcmp (a->key, b->key);
+    if (cmp == 0)
+       cmp = strcmp (a->value, b->value);
+    return cmp;
+}
+
+static void
+_notmuch_string_map_sort (notmuch_string_map_t *map)
+{
+    if (map->length == 0)
+       return;
+
+    if (map->sorted)
+       return;
+
+    qsort (map->pairs, map->length, sizeof (notmuch_string_pair_t), cmppair);
+
+    map->sorted = true;
+}
+
+static int
+string_cmp (const char *a, const char *b, bool exact)
+{
+    if (exact)
+       return (strcmp (a, b));
+    else
+       return (strncmp (a, b, strlen (a)));
+}
+
+static notmuch_string_pair_t *
+bsearch_first (notmuch_string_pair_t *array, size_t len, const char *key, bool exact)
+{
+    size_t first = 0;
+    size_t last = len - 1;
+    size_t mid;
+
+    if (len <= 0)
+       return NULL;
+
+    while (last > first) {
+       mid = (first + last) / 2;
+       int sign = string_cmp (key, array[mid].key, exact);
+
+       if (sign <= 0)
+           last = mid;
+       else
+           first = mid + 1;
+    }
+
+
+    if (string_cmp (key, array[first].key, exact) == 0)
+       return array + first;
+    else
+       return NULL;
+
+}
+
+void
+_notmuch_string_map_set (notmuch_string_map_t *map,
+                        const char *key,
+                        const char *val)
+{
+    notmuch_string_pair_t *pair;
+
+    /* this means that calling string_map_set invalidates iterators */
+    _notmuch_string_map_sort (map);
+    pair = bsearch_first (map->pairs, map->length, key, true);
+    if (! pair)
+       _notmuch_string_map_append (map, key, val);
+    else {
+       talloc_free (pair->value);
+       pair->value = talloc_strdup (map->pairs, val);
+    }
+}
+
+const char *
+_notmuch_string_map_get (notmuch_string_map_t *map, const char *key)
+{
+    notmuch_string_pair_t *pair;
+
+    /* this means that calling append invalidates iterators */
+    _notmuch_string_map_sort (map);
+
+    pair = bsearch_first (map->pairs, map->length, key, true);
+    if (! pair)
+       return NULL;
+
+    return pair->value;
+}
+
+notmuch_string_map_iterator_t *
+_notmuch_string_map_iterator_create (notmuch_string_map_t *map, const char *key,
+                                    bool exact)
+{
+    notmuch_string_map_iterator_t *iter;
+
+    _notmuch_string_map_sort (map);
+
+    iter = talloc (map, notmuch_string_map_iterator_t);
+    if (unlikely (iter == NULL))
+       return NULL;
+
+    if (unlikely (talloc_reference (iter, map) == NULL))
+       return NULL;
+
+    iter->key = talloc_strdup (iter, key);
+    iter->exact = exact;
+    iter->current = bsearch_first (map->pairs, map->length, key, exact);
+    return iter;
+}
+
+bool
+_notmuch_string_map_iterator_valid (notmuch_string_map_iterator_t *iterator)
+{
+    if (iterator->current == NULL)
+       return false;
+
+    /* sentinel */
+    if (iterator->current->key == NULL)
+       return false;
+
+    return (0 == string_cmp (iterator->key, iterator->current->key, iterator->exact));
+
+}
+
+void
+_notmuch_string_map_iterator_move_to_next (notmuch_string_map_iterator_t *iterator)
+{
+
+    if (! _notmuch_string_map_iterator_valid (iterator))
+       return;
+
+    (iterator->current)++;
+}
+
+const char *
+_notmuch_string_map_iterator_key (notmuch_string_map_iterator_t *iterator)
+{
+    if (! _notmuch_string_map_iterator_valid (iterator))
+       return NULL;
+
+    return iterator->current->key;
+}
+
+const char *
+_notmuch_string_map_iterator_value (notmuch_string_map_iterator_t *iterator)
+{
+    if (! _notmuch_string_map_iterator_valid (iterator))
+       return NULL;
+
+    return iterator->current->value;
+}
+
+void
+_notmuch_string_map_iterator_destroy (notmuch_string_map_iterator_t *iterator)
+{
+    talloc_free (iterator);
+}
diff --git a/lib/tags.c b/lib/tags.c
new file mode 100644 (file)
index 0000000..ec5366f
--- /dev/null
@@ -0,0 +1,76 @@
+/* tags.c - Iterator for tags returned from message or thread
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+
+struct _notmuch_tags {
+    notmuch_string_node_t *iterator;
+};
+
+/* Create a new notmuch_tags_t object, with 'ctx' as its talloc owner.
+ * The returned iterator will talloc_steal the 'list', since the list
+ * is almost always transient.
+ *
+ * This function can return NULL in case of out-of-memory.
+ */
+notmuch_tags_t *
+_notmuch_tags_create (const void *ctx, notmuch_string_list_t *list)
+{
+    notmuch_tags_t *tags;
+
+    tags = talloc (ctx, notmuch_tags_t);
+    if (unlikely (tags == NULL))
+       return NULL;
+
+    tags->iterator = list->head;
+    (void) talloc_steal (tags, list);
+
+    return tags;
+}
+
+notmuch_bool_t
+notmuch_tags_valid (notmuch_tags_t *tags)
+{
+    return tags && (tags->iterator != NULL);
+}
+
+const char *
+notmuch_tags_get (notmuch_tags_t *tags)
+{
+    if (tags->iterator == NULL)
+       return NULL;
+
+    return (char *) tags->iterator->string;
+}
+
+void
+notmuch_tags_move_to_next (notmuch_tags_t *tags)
+{
+    if (tags->iterator == NULL)
+       return;
+
+    tags->iterator = tags->iterator->next;
+}
+
+void
+notmuch_tags_destroy (notmuch_tags_t *tags)
+{
+    talloc_free (tags);
+}
diff --git a/lib/thread-fp.cc b/lib/thread-fp.cc
new file mode 100644 (file)
index 0000000..3aa9c42
--- /dev/null
@@ -0,0 +1,58 @@
+/* thread-fp.cc - "thread:" field processor glue
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2018 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 "thread-fp.h"
+#include <iostream>
+
+Xapian::Query
+ThreadFieldProcessor::operator() (const std::string & str)
+{
+    notmuch_status_t status;
+    const char *thread_prefix = _find_prefix ("thread");
+
+    if (str.at (0) == '{') {
+       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);
+
+           status = _notmuch_query_string_to_xapian_query (notmuch, subquery_str, subquery, msg);
+           if (status)
+               throw Xapian::QueryParserError (msg);
+
+           status = _notmuch_query_expand (notmuch, "thread", subquery, query, msg);
+           if (status)
+               throw Xapian::QueryParserError (msg);
+
+           return query;
+       }
+    } else {
+       /* literal thread id */
+       std::string term = thread_prefix + str;
+       return Xapian::Query (term);
+    }
+
+}
diff --git a/lib/thread-fp.h b/lib/thread-fp.h
new file mode 100644 (file)
index 0000000..00bf1aa
--- /dev/null
@@ -0,0 +1,43 @@
+/* thread-fp.h - thread field processor glue
+ *
+ * This file is part of notmuch.
+ *
+ * Copyright © 2018 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_THREAD_FP_H
+#define NOTMUCH_THREAD_FP_H
+
+#include <xapian.h>
+#include "notmuch.h"
+
+class ThreadFieldProcessor : public Xapian::FieldProcessor {
+protected:
+    Xapian::QueryParser &parser;
+    notmuch_database_t *notmuch;
+
+public:
+    ThreadFieldProcessor (Xapian::QueryParser &parser_, notmuch_database_t *notmuch_)
+       : parser (parser_), notmuch (notmuch_)
+    {
+    };
+
+    Xapian::Query operator() (const std::string & str);
+};
+
+#endif /* NOTMUCH_THREAD_FP_H */
diff --git a/lib/thread.cc b/lib/thread.cc
new file mode 100644 (file)
index 0000000..60e9a66
--- /dev/null
@@ -0,0 +1,742 @@
+/* thread.cc - Results of thread-based searches from a notmuch database
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+#include "database-private.h"
+
+#include <gmime/gmime.h>
+#include <glib.h>                                       /* GHashTable */
+
+#ifdef DEBUG_THREADING
+#define THREAD_DEBUG(format, ...) fprintf (stderr, "DT: " format " (%s).\n", ##__VA_ARGS__, \
+                                          __location__)
+#else
+#define THREAD_DEBUG(format, ...) do {} while (0)       /* ignored */
+#endif
+
+struct _notmuch_thread {
+    notmuch_database_t *notmuch;
+    char *thread_id;
+    char *subject;
+    GHashTable *authors_hash;
+    GPtrArray *authors_array;
+    GHashTable *matched_authors_hash;
+    GPtrArray *matched_authors_array;
+    char *authors;
+    GHashTable *tags;
+
+    /* All messages, oldest first. */
+    notmuch_message_list_t *message_list;
+    /* Top-level messages, oldest first. */
+    notmuch_message_list_t *toplevel_list;
+
+    GHashTable *message_hash;
+    int total_messages;
+    int total_files;
+    int matched_messages;
+    time_t oldest;
+    time_t newest;
+};
+
+static int
+_notmuch_thread_destructor (notmuch_thread_t *thread)
+{
+    g_hash_table_unref (thread->authors_hash);
+    g_hash_table_unref (thread->matched_authors_hash);
+    g_hash_table_unref (thread->tags);
+    g_hash_table_unref (thread->message_hash);
+
+    if (thread->authors_array) {
+       g_ptr_array_free (thread->authors_array, true);
+       thread->authors_array = NULL;
+    }
+
+    if (thread->matched_authors_array) {
+       g_ptr_array_free (thread->matched_authors_array, true);
+       thread->matched_authors_array = NULL;
+    }
+
+    return 0;
+}
+
+/* Add each author of the thread to the thread's authors_hash and to
+ * the thread's authors_array. */
+static void
+_thread_add_author (notmuch_thread_t *thread,
+                   const char *author)
+{
+    char *author_copy;
+
+    if (author == NULL)
+       return;
+
+    if (g_hash_table_lookup_extended (thread->authors_hash,
+                                     author, NULL, NULL))
+       return;
+
+    author_copy = talloc_strdup (thread, author);
+
+    g_hash_table_insert (thread->authors_hash, author_copy, NULL);
+
+    g_ptr_array_add (thread->authors_array, author_copy);
+}
+
+/* Add each matched author of the thread to the thread's
+ * matched_authors_hash and to the thread's matched_authors_array. */
+static void
+_thread_add_matched_author (notmuch_thread_t *thread,
+                           const char *author)
+{
+    char *author_copy;
+
+    if (author == NULL)
+       return;
+
+    if (g_hash_table_lookup_extended (thread->matched_authors_hash,
+                                     author, NULL, NULL))
+       return;
+
+    author_copy = talloc_strdup (thread, author);
+
+    g_hash_table_insert (thread->matched_authors_hash, author_copy, NULL);
+
+    g_ptr_array_add (thread->matched_authors_array, author_copy);
+}
+
+/* Construct an authors string from matched_authors_array and
+ * authors_array. The string contains matched authors first, then
+ * non-matched authors (with the two groups separated by '|'). Within
+ * each group, authors are listed in date order. */
+static void
+_resolve_thread_authors_string (notmuch_thread_t *thread)
+{
+    unsigned int i;
+    char *author;
+    int first_non_matched_author = 1;
+
+    /* First, list all matched authors in date order. */
+    for (i = 0; i < thread->matched_authors_array->len; i++) {
+       author = (char *) g_ptr_array_index (thread->matched_authors_array, i);
+       if (thread->authors)
+           thread->authors = talloc_asprintf (thread, "%s, %s",
+                                              thread->authors,
+                                              author);
+       else
+           thread->authors = author;
+    }
+
+    /* Next, append any non-matched authors that haven't already appeared. */
+    for (i = 0; i < thread->authors_array->len; i++) {
+       author = (char *) g_ptr_array_index (thread->authors_array, i);
+       if (g_hash_table_lookup_extended (thread->matched_authors_hash,
+                                         author, NULL, NULL))
+           continue;
+       if (first_non_matched_author) {
+           thread->authors = talloc_asprintf (thread, "%s| %s",
+                                              thread->authors,
+                                              author);
+       } else {
+           thread->authors = talloc_asprintf (thread, "%s, %s",
+                                              thread->authors,
+                                              author);
+       }
+
+       first_non_matched_author = 0;
+    }
+
+    g_ptr_array_free (thread->authors_array, true);
+    thread->authors_array = NULL;
+    g_ptr_array_free (thread->matched_authors_array, true);
+    thread->matched_authors_array = NULL;
+
+    if (! thread->authors)
+       thread->authors = talloc_strdup (thread, "");
+}
+
+/* clean up the ugly "Lastname, Firstname" format that some mail systems
+ * (most notably, Exchange) are creating to be "Firstname Lastname"
+ * To make sure that we don't change other potential situations where a
+ * comma is in the name, we check that we match one of these patterns
+ * "Last, First" <first.last@company.com>
+ * "Last, First MI" <first.mi.last@company.com>
+ */
+static char *
+_thread_cleanup_author (notmuch_thread_t *thread,
+                       const char *author, const char *from)
+{
+    char *clean_author, *test_author;
+    const char *comma;
+    char *blank;
+    int fname, lname;
+
+    if (author == NULL)
+       return NULL;
+    clean_author = talloc_strdup (thread, author);
+    if (clean_author == NULL)
+       return NULL;
+    /* check if there's a comma in the name and that there's a
+     * component of the name behind it (so the name doesn't end with
+     * the comma - in which case the string that strchr finds is just
+     * one character long ",\0").
+     * Otherwise just return the copy of the original author name that
+     * we just made*/
+    comma = strchr (author, ',');
+    if (comma && strlen (comma) > 1) {
+       /* let's assemble what we think is the correct name */
+       lname = comma - author;
+
+       /* Skip all the spaces after the comma */
+       fname = strlen (author) - lname - 1;
+       comma += 1;
+       while (*comma == ' ') {
+           fname -= 1;
+           comma += 1;
+       }
+       strncpy (clean_author, comma, fname);
+
+       *(clean_author + fname) = ' ';
+       strncpy (clean_author + fname + 1, author, lname);
+       *(clean_author + fname + 1 + lname) = '\0';
+       /* make a temporary copy and see if it matches the email */
+       test_author = talloc_strdup (thread, clean_author);
+
+       blank = strchr (test_author, ' ');
+       while (blank != NULL) {
+           *blank = '.';
+           blank = strchr (test_author, ' ');
+       }
+       if (strcasestr (from, test_author) == NULL)
+           /* we didn't identify this as part of the email address
+            * so let's punt and return the original author */
+           strcpy (clean_author, author);
+    }
+    return clean_author;
+}
+
+/* Add 'message' as a message that belongs to 'thread'.
+ *
+ * The 'thread' will talloc_steal the 'message' and hold onto a
+ * reference to it.
+ */
+static void
+_thread_add_message (notmuch_thread_t *thread,
+                    notmuch_message_t *message,
+                    notmuch_string_list_t *exclude_terms,
+                    notmuch_exclude_t omit_exclude)
+{
+    notmuch_tags_t *tags;
+    const char *tag;
+    InternetAddressList *list = NULL;
+    InternetAddress *address;
+    const char *from, *author;
+    char *clean_author;
+    bool message_excluded = false;
+
+    if (omit_exclude != NOTMUCH_EXCLUDE_FALSE) {
+       for (tags = notmuch_message_get_tags (message);
+            notmuch_tags_valid (tags);
+            notmuch_tags_move_to_next (tags)) {
+           tag = notmuch_tags_get (tags);
+           /* Is message excluded? */
+           for (notmuch_string_node_t *term = exclude_terms->head;
+                term != NULL;
+                term = term->next) {
+               /* Check for an empty string, and then ignore initial 'K'. */
+               if (*(term->string) && strcmp (tag, (term->string + 1)) == 0) {
+                   message_excluded = true;
+                   break;
+               }
+           }
+       }
+    }
+
+    if (message_excluded && omit_exclude == NOTMUCH_EXCLUDE_ALL)
+       return;
+
+    _notmuch_message_list_add_message (thread->message_list,
+                                      talloc_steal (thread, message));
+    thread->total_messages++;
+    thread->total_files += notmuch_message_count_files (message);
+
+    g_hash_table_insert (thread->message_hash,
+                        xstrdup (notmuch_message_get_message_id (message)),
+                        message);
+
+    from = notmuch_message_get_header (message, "from");
+    if (from)
+       list = internet_address_list_parse (NULL, from);
+
+    if (list) {
+       address = internet_address_list_get_address (list, 0);
+       if (address) {
+           author = internet_address_get_name (address);
+           /* We treat quoted empty names as if they were empty. */
+           if (author == NULL || author[0] == '\0') {
+               InternetAddressMailbox *mailbox;
+               mailbox = INTERNET_ADDRESS_MAILBOX (address);
+               author = internet_address_mailbox_get_addr (mailbox);
+           }
+           clean_author = _thread_cleanup_author (thread, author, from);
+           _thread_add_author (thread, clean_author);
+           _notmuch_message_set_author (message, clean_author);
+       }
+       g_object_unref (G_OBJECT (list));
+    }
+
+    if (! thread->subject) {
+       const char *subject;
+       subject = notmuch_message_get_header (message, "subject");
+       thread->subject = talloc_strdup (thread, subject ? subject : "");
+    }
+
+    for (tags = notmuch_message_get_tags (message);
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags)) {
+       tag = notmuch_tags_get (tags);
+       g_hash_table_insert (thread->tags, xstrdup (tag), NULL);
+    }
+
+    /* Mark excluded messages. */
+    if (message_excluded)
+       notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, true);
+}
+
+static void
+_thread_set_subject_from_message (notmuch_thread_t *thread,
+                                 notmuch_message_t *message)
+{
+    const char *subject;
+    const char *cleaned_subject;
+
+    subject = notmuch_message_get_header (message, "subject");
+    if (! subject)
+       return;
+
+    if ((strncasecmp (subject, "Re: ", 4) == 0) ||
+       (strncasecmp (subject, "Aw: ", 4) == 0) ||
+       (strncasecmp (subject, "Vs: ", 4) == 0) ||
+       (strncasecmp (subject, "Sv: ", 4) == 0)) {
+
+       cleaned_subject = talloc_strndup (thread,
+                                         subject + 4,
+                                         strlen (subject) - 4);
+    } else {
+       cleaned_subject = talloc_strdup (thread, subject);
+    }
+
+    if (! EMPTY_STRING (cleaned_subject)) {
+       if (thread->subject)
+           talloc_free (thread->subject);
+
+       thread->subject = talloc_strdup (thread, cleaned_subject);
+    }
+}
+
+/* Add a message to this thread which is known to match the original
+ * search specification. The 'sort' parameter controls whether the
+ * oldest or newest matching subject is applied to the thread as a
+ * whole. Returns 0 on success.
+ */
+static int
+_thread_add_matched_message (notmuch_thread_t *thread,
+                            notmuch_message_t *message,
+                            notmuch_sort_t sort)
+{
+    time_t date;
+    notmuch_message_t *hashed_message;
+    notmuch_bool_t is_set;
+
+    date = notmuch_message_get_date (message);
+
+    if (date < thread->oldest || ! thread->matched_messages) {
+       thread->oldest = date;
+       if (sort == NOTMUCH_SORT_OLDEST_FIRST)
+           _thread_set_subject_from_message (thread, message);
+    }
+
+    if (date > thread->newest || ! thread->matched_messages) {
+       thread->newest = date;
+       const char *cur_subject = notmuch_thread_get_subject (thread);
+       if (sort != NOTMUCH_SORT_OLDEST_FIRST || EMPTY_STRING (cur_subject))
+           _thread_set_subject_from_message (thread, message);
+    }
+
+    if (notmuch_message_get_flag_st (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, &is_set))
+       return -1;
+    if (! is_set)
+       thread->matched_messages++;
+
+    if (g_hash_table_lookup_extended (thread->message_hash,
+                                     notmuch_message_get_message_id (message), NULL,
+                                     (void **) &hashed_message)) {
+       notmuch_message_set_flag (hashed_message,
+                                 NOTMUCH_MESSAGE_FLAG_MATCH, 1);
+    }
+
+    _thread_add_matched_author (thread, _notmuch_message_get_author (hashed_message));
+    return 0;
+}
+
+static bool
+_parent_via_in_reply_to (notmuch_thread_t *thread, notmuch_message_t *message)
+{
+    notmuch_message_t *parent;
+    const char *in_reply_to;
+
+    in_reply_to = _notmuch_message_get_in_reply_to (message);
+    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)) &&
+       g_hash_table_lookup_extended (thread->message_hash,
+                                     in_reply_to, NULL,
+                                     (void **) &parent)) {
+       _notmuch_message_add_reply (parent, message);
+       return true;
+    } else {
+       return false;
+    }
+}
+
+static void
+_parent_or_toplevel (notmuch_thread_t *thread, notmuch_message_t *message)
+{
+    size_t max_depth = 0;
+    notmuch_message_t *new_parent;
+    notmuch_message_t *parent = NULL;
+    const notmuch_string_list_t *references =
+       _notmuch_message_get_references (message);
+
+    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", 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", new_depth);
+           if (new_depth > max_depth || ! parent) {
+               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",
+                     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",
+                     notmuch_message_get_message_id (message));
+       _notmuch_message_list_add_message (thread->toplevel_list, message);
+    }
+}
+
+static void
+_resolve_thread_relationships (notmuch_thread_t *thread)
+{
+    notmuch_message_node_t *node, *first_node;
+    notmuch_message_t *message;
+    void *local;
+    notmuch_message_list_t *maybe_toplevel_list;
+
+    first_node = thread->message_list->head;
+    if (! first_node)
+       return;
+
+    local = talloc_new (thread);
+    maybe_toplevel_list = _notmuch_message_list_create (local);
+
+    for (node = first_node->next; node; node = node->next) {
+       message = node->message;
+       if (! _parent_via_in_reply_to (thread, message))
+           _notmuch_message_list_add_message (maybe_toplevel_list, message);
+    }
+
+    /*
+     * if we reach the end of the list without finding a top-level
+     * message, that means the thread is a cycle (or set of cycles)
+     * and any message can be considered top-level.  Choose the oldest
+     * message, which happens to be first in our list.
+     */
+    if (first_node) {
+       message = first_node->message;
+       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",
+                         notmuch_message_get_message_id (message));
+           _notmuch_message_list_add_message (maybe_toplevel_list, message);
+       }
+    }
+
+    for (notmuch_messages_t *messages = _notmuch_messages_create (maybe_toplevel_list);
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages)) {
+       notmuch_message_t *message = notmuch_messages_get (messages);
+       _notmuch_message_label_depths (message, 0);
+    }
+
+    for (notmuch_messages_t *roots = _notmuch_messages_create (maybe_toplevel_list);
+        notmuch_messages_valid (roots);
+        notmuch_messages_move_to_next (roots)) {
+       notmuch_message_t *message = notmuch_messages_get (roots);
+       if (_notmuch_messages_has_next (roots) || ! _notmuch_message_list_empty (
+               thread->toplevel_list))
+           _parent_or_toplevel (thread, message);
+       else
+           _notmuch_message_list_add_message (thread->toplevel_list, message);
+    }
+
+    /* XXX this could be made conditional on messages being inserted
+     * (out of order) in later passes
+     */
+    thread->toplevel_list = _notmuch_message_sort_subtrees (thread, thread->toplevel_list);
+
+    talloc_free (local);
+}
+
+/* Create a new notmuch_thread_t object by finding the thread
+ * containing the message with the given doc ID, treating any messages
+ * contained in match_set as "matched".  Remove all messages in the
+ * thread from match_set.
+ *
+ * Creating the thread will perform a database search to get all
+ * messages belonging to the thread and will get the first subject
+ * line, the total count of messages, and all authors in the thread.
+ * Each message in the thread is checked against match_set to allow
+ * for a separate count of matched messages, and to allow a viewer to
+ * display these messages differently.
+ *
+ * Here, 'ctx' is talloc context for the resulting thread object.
+ *
+ * This function returns NULL in the case of any error.
+ */
+notmuch_thread_t *
+_notmuch_thread_create (void *ctx,
+                       notmuch_database_t *notmuch,
+                       unsigned int seed_doc_id,
+                       notmuch_doc_id_set_t *match_set,
+                       notmuch_string_list_t *exclude_terms,
+                       notmuch_exclude_t omit_excluded,
+                       notmuch_sort_t sort)
+{
+    void *local = talloc_new (ctx);
+    notmuch_thread_t *thread = NULL;
+    notmuch_message_t *seed_message;
+    const char *thread_id;
+    char *thread_id_query_string;
+    notmuch_query_t *thread_id_query;
+
+    notmuch_messages_t *messages;
+    notmuch_message_t *message;
+    notmuch_status_t status;
+
+    seed_message = _notmuch_message_create (local, notmuch, seed_doc_id, NULL);
+    if (! seed_message)
+       INTERNAL_ERROR ("Thread seed message %u does not exist", seed_doc_id);
+
+    thread_id = notmuch_message_get_thread_id (seed_message);
+    thread_id_query_string = talloc_asprintf (local, "thread:%s", thread_id);
+    if (unlikely (thread_id_query_string == NULL))
+       goto DONE;
+
+    thread_id_query = talloc_steal (
+       local, notmuch_query_create (notmuch, thread_id_query_string));
+    if (unlikely (thread_id_query == NULL))
+       goto DONE;
+
+    thread = talloc (local, notmuch_thread_t);
+    if (unlikely (thread == NULL))
+       goto DONE;
+
+    talloc_set_destructor (thread, _notmuch_thread_destructor);
+
+    thread->notmuch = notmuch;
+    thread->thread_id = talloc_strdup (thread, thread_id);
+    thread->subject = NULL;
+    thread->authors_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                 NULL, NULL);
+    thread->authors_array = g_ptr_array_new ();
+    thread->matched_authors_hash = g_hash_table_new_full (g_str_hash,
+                                                         g_str_equal,
+                                                         NULL, NULL);
+    thread->matched_authors_array = g_ptr_array_new ();
+    thread->authors = NULL;
+    thread->tags = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                         free, NULL);
+
+    thread->message_list = _notmuch_message_list_create (thread);
+    thread->toplevel_list = _notmuch_message_list_create (thread);
+    if (unlikely (thread->message_list == NULL ||
+                 thread->toplevel_list == NULL)) {
+       thread = NULL;
+       goto DONE;
+    }
+
+    thread->message_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                 free, NULL);
+
+    thread->total_messages = 0;
+    thread->total_files = 0;
+    thread->matched_messages = 0;
+    thread->oldest = 0;
+    thread->newest = 0;
+
+    /* We use oldest-first order unconditionally here to obtain the
+     * proper author ordering for the thread. The 'sort' parameter
+     * passed to this function is used only to indicate whether the
+     * oldest or newest subject is desired. */
+    notmuch_query_set_sort (thread_id_query, NOTMUCH_SORT_OLDEST_FIRST);
+
+    status = notmuch_query_search_messages (thread_id_query, &messages);
+    if (status)
+       goto DONE;
+
+    for (;
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages)) {
+       unsigned int doc_id;
+
+       message = notmuch_messages_get (messages);
+       doc_id = _notmuch_message_get_doc_id (message);
+       if (doc_id == seed_doc_id)
+           message = seed_message;
+
+       _thread_add_message (thread, message, exclude_terms, omit_excluded);
+
+       if ( _notmuch_doc_id_set_contains (match_set, doc_id)) {
+           _notmuch_doc_id_set_remove (match_set, doc_id);
+           if (_thread_add_matched_message (thread, message, sort)) {
+               thread = NULL;
+               goto DONE;
+           }
+       }
+
+       _notmuch_message_close (message);
+    }
+
+    _resolve_thread_authors_string (thread);
+
+    _resolve_thread_relationships (thread);
+
+    /* Commit to returning thread. */
+    (void) talloc_steal (ctx, thread);
+
+  DONE:
+    talloc_free (local);
+    return thread;
+}
+
+notmuch_messages_t *
+notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread)
+{
+    return _notmuch_messages_create (thread->toplevel_list);
+}
+
+notmuch_messages_t *
+notmuch_thread_get_messages (notmuch_thread_t *thread)
+{
+    return _notmuch_messages_create (thread->message_list);
+}
+
+const char *
+notmuch_thread_get_thread_id (notmuch_thread_t *thread)
+{
+    return thread->thread_id;
+}
+
+int
+notmuch_thread_get_total_messages (notmuch_thread_t *thread)
+{
+    return thread->total_messages;
+}
+
+int
+notmuch_thread_get_total_files (notmuch_thread_t *thread)
+{
+    return thread->total_files;
+}
+
+int
+notmuch_thread_get_matched_messages (notmuch_thread_t *thread)
+{
+    return thread->matched_messages;
+}
+
+const char *
+notmuch_thread_get_authors (notmuch_thread_t *thread)
+{
+    return thread->authors;
+}
+
+const char *
+notmuch_thread_get_subject (notmuch_thread_t *thread)
+{
+    return thread->subject;
+}
+
+time_t
+notmuch_thread_get_oldest_date (notmuch_thread_t *thread)
+{
+    return thread->oldest;
+}
+
+time_t
+notmuch_thread_get_newest_date (notmuch_thread_t *thread)
+{
+    return thread->newest;
+}
+
+notmuch_tags_t *
+notmuch_thread_get_tags (notmuch_thread_t *thread)
+{
+    notmuch_string_list_t *tags;
+    GList *keys, *l;
+
+    tags = _notmuch_string_list_create (thread);
+    if (unlikely (tags == NULL))
+       return NULL;
+
+    keys = g_hash_table_get_keys (thread->tags);
+
+    for (l = keys; l; l = l->next)
+       _notmuch_string_list_append (tags, (char *) l->data);
+
+    g_list_free (keys);
+
+    _notmuch_string_list_sort (tags);
+
+    return _notmuch_tags_create (thread, tags);
+}
+
+void
+notmuch_thread_destroy (notmuch_thread_t *thread)
+{
+    talloc_free (thread);
+}
diff --git a/mime-node.c b/mime-node.c
new file mode 100644 (file)
index 0000000..1c5d619
--- /dev/null
@@ -0,0 +1,515 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ * Copyright © 2009 Keith Packard
+ *
+ * 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/ .
+ *
+ * Authors: Carl Worth <cworth@cworth.org>
+ *          Keith Packard <keithp@keithp.com>
+ *          Austin Clements <aclements@csail.mit.edu>
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include "notmuch-client.h"
+
+/* Context that gets inherited from the root node. */
+typedef struct mime_node_context {
+    /* Per-message resources.  These are allocated internally and must
+     * be destroyed. */
+    GMimeStream *stream;
+    GMimeParser *parser;
+    GMimeMessage *mime_message;
+    _notmuch_message_crypto_t *msg_crypto;
+
+    /* repaired/unmangled parts that will need to be cleaned up */
+    GSList *repaired_parts;
+
+    /* Context provided by the caller. */
+    _notmuch_crypto_t *crypto;
+} mime_node_context_t;
+
+static int
+_mime_node_context_free (mime_node_context_t *res)
+{
+    if (res->mime_message)
+       g_object_unref (res->mime_message);
+
+    if (res->parser)
+       g_object_unref (res->parser);
+
+    if (res->stream)
+       g_object_unref (res->stream);
+
+    if (res->repaired_parts)
+       g_slist_free_full (res->repaired_parts, g_object_unref);
+
+    return 0;
+}
+
+/* keep track of objects that need to be destroyed when the mime node
+ * context goes away. */
+static void
+_mime_node_context_track_repaired_part (mime_node_context_t *ctx, GMimeObject *part)
+{
+    if (part)
+       ctx->repaired_parts = g_slist_prepend (ctx->repaired_parts, part);
+}
+
+const _notmuch_message_crypto_t *
+mime_node_get_message_crypto_status (mime_node_t *node)
+{
+    return node->ctx->msg_crypto;
+}
+
+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 = -1;
+
+    root = talloc_zero (ctx, mime_node_t);
+    if (root == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    /* Create the tree-wide context */
+    mctx = talloc_zero (root, mime_node_context_t);
+    if (mctx == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+    talloc_set_destructor (mctx, _mime_node_context_free);
+
+    /* Fast path */
+    if (duplicate <= 0)
+       fd = open (filename, O_RDONLY);
+    if (fd == -1) {
+       /* 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), 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);
+       if (fd == -1) {
+           /* Give up */
+           fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
+           status = NOTMUCH_STATUS_FILE_ERROR;
+           goto DONE;
+       }
+    }
+
+    mctx->stream = g_mime_stream_gzfile_new (fd);
+    if (! mctx->stream) {
+       fprintf (stderr, "Out of memory.\n");
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    mctx->parser = g_mime_parser_new_with_stream (mctx->stream);
+    if (! mctx->parser) {
+       fprintf (stderr, "Out of memory.\n");
+       status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+       goto DONE;
+    }
+
+    mctx->mime_message = g_mime_parser_construct_message (mctx->parser, NULL);
+    if (! mctx->mime_message) {
+       fprintf (stderr, "Failed to parse %s\n", filename);
+       status = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
+    }
+
+    mctx->msg_crypto = _notmuch_message_crypto_new (mctx);
+
+    mctx->crypto = crypto;
+
+    /* Create the root node */
+    root->part = GMIME_OBJECT (mctx->mime_message);
+    root->envelope_file = message;
+    root->nchildren = 1;
+    root->ctx = mctx;
+
+    root->parent = NULL;
+    root->part_num = 0;
+    root->next_child = 0;
+    root->next_part_num = 1;
+
+    *root_out = root;
+    return NOTMUCH_STATUS_SUCCESS;
+
+  DONE:
+    talloc_free (root);
+    return status;
+}
+
+/* Signature list destructor */
+static int
+_signature_list_free (GMimeSignatureList **proxy)
+{
+    g_object_unref (*proxy);
+    return 0;
+}
+
+/* Set up signature list destructor */
+static void
+set_signature_list_destructor (mime_node_t *node)
+{
+    GMimeSignatureList **proxy = talloc (node, GMimeSignatureList *);
+
+    if (proxy) {
+       *proxy = node->sig_list;
+       talloc_set_destructor (proxy, _signature_list_free);
+    }
+}
+
+/* Unwrapped MIME part destructor */
+static int
+_unwrapped_child_free (GMimeObject **proxy)
+{
+    g_object_unref (*proxy);
+    return 0;
+}
+
+/* Set up unwrapped MIME part destructor */
+static void
+set_unwrapped_child_destructor (mime_node_t *node)
+{
+    GMimeObject **proxy = talloc (node, GMimeObject *);
+
+    if (proxy) {
+       *proxy = node->unwrapped_child;
+       talloc_set_destructor (proxy, _unwrapped_child_free);
+    }
+}
+
+/* Verify a signed mime node */
+static void
+node_verify (mime_node_t *node, GMimeObject *part)
+{
+    GError *err = NULL;
+    notmuch_status_t status;
+
+    node->verify_attempted = true;
+    if (GMIME_IS_APPLICATION_PKCS7_MIME (part))
+       node->sig_list = g_mime_application_pkcs7_mime_verify (
+           GMIME_APPLICATION_PKCS7_MIME (part), GMIME_VERIFY_NONE, &node->unwrapped_child, &err);
+    else
+       node->sig_list = g_mime_multipart_signed_verify (
+           GMIME_MULTIPART_SIGNED (part), GMIME_VERIFY_NONE, &err);
+
+    if (node->unwrapped_child) {
+       node->nchildren = 1;
+       set_unwrapped_child_destructor (node);
+    }
+
+    if (node->sig_list)
+       set_signature_list_destructor (node);
+    else
+       fprintf (stderr, "Failed to verify signed part: %s\n",
+                err ? err->message : "no error explanation given");
+
+    if (err)
+       g_error_free (err);
+
+    status = _notmuch_message_crypto_potential_sig_list (node->ctx->msg_crypto, node->sig_list);
+    if (status) /* this is a warning, not an error */
+       fprintf (stderr, "Warning: failed to note signature status: %s.\n", notmuch_status_to_string (
+                    status));
+}
+
+/* Decrypt and optionally verify an encrypted mime node */
+static void
+node_decrypt_and_verify (mime_node_t *node, GMimeObject *part)
+{
+    GError *err = NULL;
+    GMimeDecryptResult *decrypt_result = NULL;
+    notmuch_status_t status;
+    notmuch_message_t *message = NULL;
+
+    if (! node->unwrapped_child) {
+       for (mime_node_t *parent = node; parent; parent = parent->parent)
+           if (parent->envelope_file) {
+               message = parent->envelope_file;
+               break;
+           }
+
+       node->unwrapped_child = _notmuch_crypto_decrypt (&node->decrypt_attempted,
+                                                        node->ctx->crypto->decrypt,
+                                                        message,
+                                                        part, &decrypt_result, &err);
+       if (node->unwrapped_child)
+           set_unwrapped_child_destructor (node);
+    }
+    if (! node->unwrapped_child) {
+       fprintf (stderr, "Failed to decrypt part: %s\n",
+                err ? err->message : "no error explanation given");
+       goto DONE;
+    }
+
+    node->decrypt_success = true;
+    status = _notmuch_message_crypto_successful_decryption (node->ctx->msg_crypto);
+    if (status) /* this is a warning, not an error */
+       fprintf (stderr, "Warning: failed to note decryption status: %s.\n",
+                notmuch_status_to_string (status));
+
+    if (decrypt_result) {
+       /* This may be NULL if the part is not signed. */
+       node->sig_list = g_mime_decrypt_result_get_signatures (decrypt_result);
+       if (node->sig_list) {
+           node->verify_attempted = true;
+           g_object_ref (node->sig_list);
+           set_signature_list_destructor (node);
+           status = _notmuch_message_crypto_potential_sig_list (node->ctx->msg_crypto,
+                                                                node->sig_list);
+           if (status) /* this is a warning, not an error */
+               fprintf (stderr, "Warning: failed to note signature status: %s.\n",
+                        notmuch_status_to_string (status));
+       }
+
+       if (node->ctx->crypto->decrypt == NOTMUCH_DECRYPT_TRUE && message) {
+           notmuch_database_t *db = notmuch_message_get_database (message);
+           const char *session_key = g_mime_decrypt_result_get_session_key (decrypt_result);
+           if (db && session_key)
+               print_status_message ("Failed to stash session key in the database",
+                                     message,
+                                     notmuch_message_add_property (message, "session-key",
+                                                                   session_key));
+       }
+       g_object_unref (decrypt_result);
+    }
+
+  DONE:
+    if (err)
+       g_error_free (err);
+}
+
+static bool
+_mime_node_set_up_part (mime_node_t *node, GMimeObject *part, int numchild);
+
+static mime_node_t *
+_mime_node_create (mime_node_t *parent, GMimeObject *part, int numchild)
+{
+    mime_node_t *node = talloc_zero (parent, mime_node_t);
+
+    /* Set basic node properties */
+    node->ctx = parent->ctx;
+    if (! talloc_reference (node, node->ctx)) {
+       fprintf (stderr, "Out of memory.\n");
+       talloc_free (node);
+       return NULL;
+    }
+    node->parent = parent;
+    node->part_num = node->next_part_num = -1;
+    node->next_child = 0;
+
+    if (_mime_node_set_up_part (node, part, numchild))
+       return node;
+    talloc_free (node);
+    return NULL;
+}
+
+/* associate a MIME part with a node. */
+static bool
+_mime_node_set_up_part (mime_node_t *node, GMimeObject *part, int numchild)
+{
+    /* Deal with the different types of parts */
+    if (GMIME_IS_PART (part)) {
+       node->part = part;
+       node->nchildren = 0;
+    } else if (GMIME_IS_MULTIPART (part)) {
+       GMimeObject *repaired_part = _notmuch_repair_mixed_up_mangled (part);
+       if (repaired_part) {
+           /* This was likely "Mixed Up" in transit!  We replace it
+            * with the more likely-to-be-correct variant. */
+           _mime_node_context_track_repaired_part (node->ctx, repaired_part);
+           part = repaired_part;
+       }
+       node->part = part;
+       node->nchildren = g_mime_multipart_get_count (GMIME_MULTIPART (part));
+    } else if (GMIME_IS_MESSAGE_PART (part)) {
+       /* Promote part to an envelope and open it */
+       GMimeMessagePart *message_part = GMIME_MESSAGE_PART (part);
+       GMimeMessage *message = g_mime_message_part_get_message (message_part);
+       node->envelope_part = message_part;
+       node->part = GMIME_OBJECT (message);
+       node->nchildren = 1;
+    } else {
+       fprintf (stderr, "Warning: Unknown mime part type: %s.\n",
+                g_type_name (G_OBJECT_TYPE (part)));
+       return false;
+    }
+
+    /* Handle PGP/MIME parts (by definition not cryptographic payload parts) */
+    if (GMIME_IS_MULTIPART_ENCRYPTED (part) && (node->ctx->crypto->decrypt !=
+                                               NOTMUCH_DECRYPT_FALSE)) {
+       if (node->nchildren != 2) {
+           /* this violates RFC 3156 section 4, so we won't bother with it. */
+           fprintf (stderr, "Error: %d part(s) for a multipart/encrypted "
+                    "message (must be exactly 2)\n",
+                    node->nchildren);
+       } else {
+           node_decrypt_and_verify (node, part);
+       }
+    } else if (GMIME_IS_MULTIPART_SIGNED (part) && node->ctx->crypto->verify) {
+       if (node->nchildren != 2) {
+           /* this violates RFC 3156 section 5, so we won't bother with it. */
+           fprintf (stderr, "Error: %d part(s) for a multipart/signed message "
+                    "(must be exactly 2)\n",
+                    node->nchildren);
+       } else {
+           node_verify (node, part);
+       }
+    } else if (GMIME_IS_APPLICATION_PKCS7_MIME (part) &&
+              GMIME_SECURE_MIME_TYPE_SIGNED_DATA == g_mime_application_pkcs7_mime_get_smime_type (
+                  GMIME_APPLICATION_PKCS7_MIME (part))) {
+       /* If node->ctx->crypto->verify is false, it would be better
+        * to just unwrap (instead of verifying), but
+        * https://github.com/jstedfast/gmime/issues/67 */
+       node_verify (node, part);
+    } else if (GMIME_IS_APPLICATION_PKCS7_MIME (part) &&
+              GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA == g_mime_application_pkcs7_mime_get_smime_type (
+                  GMIME_APPLICATION_PKCS7_MIME (part)) &&
+              (node->ctx->crypto->decrypt != NOTMUCH_DECRYPT_FALSE)) {
+       node_decrypt_and_verify (node, part);
+       if (node->unwrapped_child && node->nchildren == 0)
+           node->nchildren = 1;
+    } else {
+       if (_notmuch_message_crypto_potential_payload (node->ctx->msg_crypto, part, node->parent ?
+                                                      node->parent->part : NULL, numchild) &&
+           node->ctx->msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL) {
+           GMimeObject *clean_payload = _notmuch_repair_crypto_payload_skip_legacy_display (part);
+           if (clean_payload != part) {
+               /* only one layer of recursion is possible here
+                * because there can be only a single cryptographic
+                * payload: */
+               return _mime_node_set_up_part (node, clean_payload, numchild);
+           }
+       }
+    }
+
+    return true;
+}
+
+mime_node_t *
+mime_node_child (mime_node_t *parent, int child)
+{
+    GMimeObject *sub;
+    mime_node_t *node;
+
+    if (! parent || ! parent->part || child < 0 || child >= parent->nchildren)
+       return NULL;
+
+    if (GMIME_IS_MULTIPART (parent->part)) {
+       if (child == GMIME_MULTIPART_ENCRYPTED_CONTENT && parent->unwrapped_child)
+           sub = parent->unwrapped_child;
+       else
+           sub = g_mime_multipart_get_part (
+               GMIME_MULTIPART (parent->part), child);
+    } else if (GMIME_IS_MESSAGE (parent->part)) {
+       sub = g_mime_message_get_mime_part (GMIME_MESSAGE (parent->part));
+    } else if (GMIME_IS_APPLICATION_PKCS7_MIME (parent->part) &&
+              parent->unwrapped_child &&
+              child == 0) {
+       sub = parent->unwrapped_child;
+    } else {
+       /* This should have been caught by _mime_node_set_up_part */
+       INTERNAL_ERROR ("Unexpected GMimeObject type: %s",
+                       g_type_name (G_OBJECT_TYPE (parent->part)));
+    }
+    node = _mime_node_create (parent, sub, child);
+
+    if (child == parent->next_child && parent->next_part_num != -1) {
+       /* We're traversing in depth-first order.  Record the child's
+        * depth-first numbering. */
+       node->part_num = parent->next_part_num;
+       node->next_part_num = node->part_num + 1;
+
+       /* Prepare the parent for its next depth-first child. */
+       parent->next_child++;
+       parent->next_part_num = -1;
+
+       if (node->nchildren == 0) {
+           /* We've reached a leaf, so find the parent that has more
+            * children and set it up to number its next child. */
+           mime_node_t *iter = node->parent;
+           while (iter && iter->next_child == iter->nchildren)
+               iter = iter->parent;
+           if (iter)
+               iter->next_part_num = node->part_num + 1;
+       }
+    }
+
+    return node;
+}
+
+static mime_node_t *
+_mime_node_seek_dfs_walk (mime_node_t *node, int *n)
+{
+    int i;
+
+    if (*n == 0)
+       return node;
+
+    *n -= 1;
+    for (i = 0; i < node->nchildren; i++) {
+       mime_node_t *child = mime_node_child (node, i);
+       mime_node_t *ret = _mime_node_seek_dfs_walk (child, n);
+       if (ret)
+           return ret;
+
+       talloc_free (child);
+    }
+    return NULL;
+}
+
+mime_node_t *
+mime_node_seek_dfs (mime_node_t *node, int n)
+{
+    if (n < 0)
+       return NULL;
+    return _mime_node_seek_dfs_walk (node, &n);
+}
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 ();
+}
diff --git a/notmuch-client.h b/notmuch-client.h
new file mode 100644 (file)
index 0000000..1a87240
--- /dev/null
@@ -0,0 +1,514 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#ifndef NOTMUCH_CLIENT_H
+#define NOTMUCH_CLIENT_H
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* for getline */
+#endif
+#include <stdbool.h>
+#include <stdio.h>
+#include <sysexits.h>
+
+#include "compat.h"
+
+#include "gmime-extra.h"
+
+#include "notmuch.h"
+
+/* This is separate from notmuch-private.h because we're trying to
+ * keep notmuch.c from looking into any internals, (which helps us
+ * develop notmuch.h into a plausible library interface).
+ */
+#include "xutil.h"
+
+#include <stddef.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <errno.h>
+#include <signal.h>
+#include <ctype.h>
+#include <zlib.h>
+
+#include "talloc-extra.h"
+#include "crypto.h"
+#include "repair.h"
+
+#define unused(x) x ## _unused __attribute__ ((unused))
+
+#define STRINGIFY(s) STRINGIFY_ (s)
+#define STRINGIFY_(s) #s
+
+typedef struct mime_node mime_node_t;
+struct sprinter;
+struct notmuch_show_params;
+
+typedef struct notmuch_show_format {
+    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);
+} notmuch_show_format_t;
+
+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;
+} notmuch_show_params_t;
+
+/* There's no point in continuing when we've detected that we've done
+ * something wrong internally (as opposed to the user passing in a
+ * bogus value).
+ *
+ * Note that __location__ comes from talloc.h.
+ */
+#define INTERNAL_ERROR(format, ...)                     \
+    do {                                                \
+       fprintf (stderr,                                 \
+                "Internal error: " format " (%s)\n",    \
+                ##__VA_ARGS__, __location__);           \
+       exit (1);                                       \
+    } while (0)
+
+#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
+
+#define STRNCMP_LITERAL(var, literal) \
+    strncmp ((var), (literal), sizeof (literal) - 1)
+
+static inline void
+chomp_newline (char *str)
+{
+    if (str && str[strlen (str) - 1] == '\n')
+       str[strlen (str) - 1] = '\0';
+}
+
+/* Exit status code indicating temporary failure; user is invited to
+ * retry.
+ *
+ * For example, file(s) in the mail store were removed or renamed
+ * after notmuch new scanned the directories but before indexing the
+ * file(s). If the file was renamed, the indexing might not be
+ * complete, and the user is advised to re-run notmuch new.
+ */
+#define NOTMUCH_EXIT_TEMPFAIL EX_TEMPFAIL
+
+/* Exit status code indicating the requested format version is too old
+ * (support for that version has been dropped).  CLI code should use
+ * notmuch_exit_if_unsupported_format rather than directly exiting
+ * with this code.
+ */
+#define NOTMUCH_EXIT_FORMAT_TOO_OLD 20
+/* Exit status code indicating the requested format version is newer
+ * than the version supported by the CLI.  CLI code should use
+ * notmuch_exit_if_unsupported_format rather than directly exiting
+ * with this code.
+ */
+#define NOTMUCH_EXIT_FORMAT_TOO_NEW 21
+
+/* The current structured output format version.  Requests for format
+ * versions above this will return an error.  Backwards-incompatible
+ * changes such as removing map fields, changing the meaning of map
+ * fields, or changing the meanings of list elements should increase
+ * this.  New (required) map fields can be added without increasing
+ * this.
+ */
+#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
+/* The minimum non-deprecated structured output format version.
+ * Requests for format versions below this will print a stern warning.
+ * Must be between NOTMUCH_FORMAT_MIN and NOTMUCH_FORMAT_CUR,
+ * inclusive.
+ */
+#define NOTMUCH_FORMAT_MIN_ACTIVE 1
+
+/* The output format version requested by the caller on the command
+ * line.  If no format version is requested, this will be set to
+ * NOTMUCH_FORMAT_CUR.  Even though the command-line option is
+ * per-command, this is global because commands can share structured
+ * output code.
+ */
+extern int notmuch_format_version;
+
+typedef struct _notmuch_conffile notmuch_conffile_t;
+
+/* Commands that support structured output should support the
+ * following argument
+ *  { NOTMUCH_OPT_INT, &notmuch_format_version, "format-version", 0, 0 }
+ * and should invoke notmuch_exit_if_unsupported_format to check the
+ * requested version.  If notmuch_format_version is outside the
+ * supported range, this will print a detailed diagnostic message for
+ * the user and exit with NOTMUCH_EXIT_FORMAT_TOO_{OLD,NEW} to inform
+ * the invoking program of the problem.
+ */
+void
+notmuch_exit_if_unsupported_format (void);
+
+int
+notmuch_count_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_dump_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_new_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_reindex_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_restore_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_search_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_address_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_setup_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_tag_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_config_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+int
+notmuch_compact_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+const char *
+notmuch_time_relative_date (const void *ctx, time_t then);
+
+void
+notmuch_time_print_formatted_seconds (double seconds);
+
+double
+notmuch_time_elapsed (struct timeval start, struct timeval end);
+
+char *
+query_string_from_args (void *ctx, int argc, char *argv[]);
+
+notmuch_status_t
+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);
+
+void
+format_headers_sprinter (struct sprinter *sp, GMimeMessage *message,
+                        bool reply, const _notmuch_message_crypto_t *msg_crypto);
+
+typedef enum {
+    NOTMUCH_SHOW_TEXT_PART_REPLY = 1 << 0,
+} notmuch_show_text_part_flags;
+
+void
+show_text_part_content (GMimeObject *part, GMimeStream *stream_out,
+                       notmuch_show_text_part_flags flags);
+
+char *
+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 {
+    NOTMUCH_COMMAND_CONFIG_CREATE      = 1 << 1,
+    NOTMUCH_COMMAND_DATABASE_EARLY     = 1 << 2,
+    NOTMUCH_COMMAND_DATABASE_WRITE     = 1 << 3,
+    NOTMUCH_COMMAND_DATABASE_CREATE    = 1 << 4,
+    NOTMUCH_COMMAND_CONFIG_LOAD                = 1 << 5,
+} notmuch_command_mode_t;
+
+notmuch_conffile_t *
+notmuch_conffile_open (notmuch_database_t *notmuch,
+                      const char *filename,
+                      bool create);
+
+void
+notmuch_conffile_close (notmuch_conffile_t *config);
+
+int
+notmuch_conffile_save (notmuch_conffile_t *config);
+
+bool
+notmuch_conffile_is_new (notmuch_conffile_t *config);
+
+void
+notmuch_conffile_set_database_path (notmuch_conffile_t *config,
+                                   const char *database_path);
+
+void
+notmuch_conffile_set_user_name (notmuch_conffile_t *config,
+                               const char *user_name);
+
+void
+notmuch_conffile_set_user_primary_email (notmuch_conffile_t *config,
+                                        const char *primary_email);
+
+void
+notmuch_conffile_set_user_other_email (notmuch_conffile_t *config,
+                                      const char *other_email[],
+                                      size_t length);
+
+void
+notmuch_conffile_set_new_tags (notmuch_conffile_t *config,
+                              const char *new_tags[],
+                              size_t length);
+
+void
+notmuch_conffile_set_new_ignore (notmuch_conffile_t *config,
+                                const char *new_ignore[],
+                                size_t length);
+
+void
+notmuch_conffile_set_maildir_synchronize_flags (notmuch_conffile_t *config,
+                                               bool synchronize_flags);
+
+void
+notmuch_conffile_set_search_exclude_tags (notmuch_conffile_t *config,
+                                         const char *list[],
+                                         size_t length);
+int
+notmuch_run_hook (notmuch_database_t *notmuch, const char *hook);
+
+bool
+debugger_is_active (void);
+
+/* mime-node.c */
+
+/* mime_node_t represents a single node in a MIME tree.  A MIME tree
+ * abstracts the different ways of traversing different types of MIME
+ * parts, allowing a MIME message to be viewed as a generic tree of
+ * parts.  Message-type parts have one child, multipart-type parts
+ * have multiple children, and leaf parts have zero children.
+ */
+struct mime_node {
+    /* The MIME object of this part.  This will be a GMimeMessage,
+     * GMimePart, GMimeMultipart, or a subclass of one of these.
+     *
+     * This will never be a GMimeMessagePart because GMimeMessagePart
+     * is structurally redundant with GMimeMessage.  If this part is a
+     * message (that is, 'part' is a GMimeMessage), then either
+     * envelope_file will be set to a notmuch_message_t (for top-level
+     * messages) or envelope_part will be set to a GMimeMessagePart
+     * (for embedded message parts).
+     */
+    GMimeObject *part;
+
+    /* If part is a GMimeMessage, these record the envelope of the
+     * message: either a notmuch_message_t representing a top-level
+     * message, or a GMimeMessagePart representing a MIME part
+     * containing a message.
+     */
+    notmuch_message_t *envelope_file;
+    GMimeMessagePart *envelope_part;
+
+    /* The number of children of this part. */
+    int nchildren;
+
+    /* The parent of this node or NULL if this is the root node. */
+    struct mime_node *parent;
+
+    /* The depth-first part number of this child if the MIME tree is
+     * being traversed in depth-first order, or -1 otherwise. */
+    int part_num;
+
+    /* True if decryption of this part was attempted. */
+    bool decrypt_attempted;
+    /* True if decryption of this part's child succeeded.  In this
+     * case, the decrypted part is substituted for the second child of
+     * this part (which would usually be the encrypted data). */
+    bool decrypt_success;
+
+    /* True if signature verification on this part was attempted. */
+    bool verify_attempted;
+
+    /* The list of signatures for signed or encrypted containers. If
+     * there are no signatures, this will be NULL. */
+    GMimeSignatureList *sig_list;
+
+    /* Internal: Context inherited from the root iterator. */
+    struct mime_node_context *ctx;
+
+    /* Internal: For successfully decrypted multipart parts, the
+     * decrypted part to substitute for the second child; or, for
+     * PKCS#7 parts, the part returned after removing/processing the
+     * PKCS#7 transformation */
+    GMimeObject *unwrapped_child;
+
+    /* Internal: The next child for depth-first traversal and the part
+     * number to assign it (or -1 if unknown). */
+    int next_child;
+    int next_part_num;
+};
+
+/* Construct a new MIME node pointing to the root message part of
+ * 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,
+ * only session keys will be tried.  If the crypto contexts
+ * (crypto->gpgctx or crypto->pkcs7) are NULL, they will be lazily
+ * initialized.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Root node is returned in *node_out.
+ *
+ * NOTMUCH_STATUS_FILE_ERROR: Failed to open message file.
+ *
+ * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory.
+ */
+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.
+ * parent will be used as the talloc context for the returned child
+ * node.
+ *
+ * In case of any failure, this function returns NULL, (after printing
+ * an error message on stderr).
+ */
+mime_node_t *
+mime_node_child (mime_node_t *parent, int child);
+
+/* Return the nth child of node in a depth-first traversal.  If n is
+* 0, returns node itself.  Returns NULL if there is no such part. */
+mime_node_t *
+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_FORMAT_AUTO,
+    DUMP_FORMAT_BATCH_TAG,
+    DUMP_FORMAT_SUP
+} dump_format_t;
+
+typedef enum {
+    DUMP_INCLUDE_TAGS          = 1,
+    DUMP_INCLUDE_CONFIG                = 2,
+    DUMP_INCLUDE_PROPERTIES    = 4
+} dump_include_t;
+
+#define DUMP_INCLUDE_DEFAULT (DUMP_INCLUDE_TAGS | DUMP_INCLUDE_CONFIG | DUMP_INCLUDE_PROPERTIES)
+
+#define NOTMUCH_DUMP_VERSION 3
+
+int
+notmuch_database_dump (notmuch_database_t *notmuch,
+                      const char *output_file_name,
+                      const char *query_str,
+                      dump_format_t output_format,
+                      dump_include_t include,
+                      bool gzip_output);
+
+/* If status indicates error print appropriate
+ * messages to stderr.
+ */
+
+notmuch_status_t
+print_status_query (const char *loc,
+                   const notmuch_query_t *query,
+                   notmuch_status_t status);
+
+notmuch_status_t
+print_status_message (const char *loc,
+                     const notmuch_message_t *message,
+                     notmuch_status_t status);
+
+notmuch_status_t
+print_status_database (const char *loc,
+                      const notmuch_database_t *database,
+                      notmuch_status_t status);
+
+int
+status_to_exit (notmuch_status_t status);
+
+notmuch_status_t
+print_status_gzbytes (const char *loc,
+                     gzFile file,
+                     int bytes);
+
+/* the __location__ macro is defined in talloc.h */
+#define ASSERT_GZBYTES(file, bytes) ((print_status_gzbytes (__location__, file, bytes)) ? exit (1) : \
+                                    0)
+#define GZPRINTF(file, fmt, ...) ASSERT_GZBYTES (file, gzprintf (file, fmt, ##__VA_ARGS__));
+#define GZPUTS(file, str) ASSERT_GZBYTES (file, gzputs (file, str));
+
+#include "command-line-arguments.h"
+
+extern const notmuch_opt_desc_t notmuch_shared_options [];
+
+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);
+
+
+/* the state chosen by the user invoking one of the notmuch
+ * subcommands that does indexing */
+struct _notmuch_client_indexing_cli_choices {
+    int decrypt_policy;
+    bool decrypt_policy_set;
+};
+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_indexopts_t *opts);
+
+#endif
diff --git a/notmuch-compact.c b/notmuch-compact.c
new file mode 100644 (file)
index 0000000..40ffb42
--- /dev/null
@@ -0,0 +1,67 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2013 Ben Gamari
+ *
+ * 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: Ben Gamari <bgamari.foss@gmail.com>
+ */
+
+#include "notmuch-client.h"
+
+static void
+status_update_cb (const char *msg, unused (void *closure))
+{
+    printf ("%s\n", msg);
+}
+
+int
+notmuch_compact_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    const char *backup_path = NULL;
+    notmuch_status_t ret;
+    bool quiet = false;
+    int opt_index;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_string = &backup_path, .name = "backup" },
+       { .opt_bool =  &quiet, .name = "quiet" },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (NULL, argv[0]);
+
+    if (! quiet)
+       printf ("Compacting database...\n");
+    ret = notmuch_database_compact_db (notmuch, backup_path,
+                                      quiet ? NULL : status_update_cb, NULL);
+    if (ret) {
+       fprintf (stderr, "Compaction failed: %s\n", notmuch_status_to_string (ret));
+       return EXIT_FAILURE;
+    }
+
+    if (! quiet) {
+       if (backup_path)
+           printf ("The old database has been moved to %s.\n", backup_path);
+
+       printf ("Done.\n");
+    }
+
+    return EXIT_SUCCESS;
+}
diff --git a/notmuch-config.c b/notmuch-config.c
new file mode 100644 (file)
index 0000000..8123e43
--- /dev/null
@@ -0,0 +1,760 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+
+#include <pwd.h>
+#include <netdb.h>
+#include <assert.h>
+
+#include "path-util.h"
+#include "unicode-util.h"
+
+static const char toplevel_config_comment[] =
+    " .notmuch-config - Configuration file for the notmuch mail system\n"
+    "\n"
+    " For more information about notmuch, see https://notmuchmail.org";
+
+static const struct config_group {
+    const char *group_name;
+    const char *comment;
+} group_comment_table [] = {
+    {
+       "database",
+       " Database configuration\n"
+       "\n"
+       " The only value supported here is 'path' which should be the top-level\n"
+       " directory where your mail currently exists and to where mail will be\n"
+       " delivered in the future. Files should be individual email messages.\n"
+       " Notmuch will store its database within a sub-directory of the path\n"
+       " configured here named \".notmuch\".\n"
+    },
+    {
+       "user",
+       " User configuration\n"
+       "\n"
+       " Here is where you can let notmuch know how you would like to be\n"
+       " addressed. Valid settings are\n"
+       "\n"
+       "\tname         Your full name.\n"
+       "\tprimary_email        Your primary email address.\n"
+       "\tother_email  A list (separated by ';') of other email addresses\n"
+       "\t             at which you receive email.\n"
+       "\n"
+       " Notmuch will use the various email addresses configured here when\n"
+       " formatting replies. It will avoid including your own addresses in the\n"
+       " recipient list of replies, and will set the From address based on the\n"
+       " address to which the original email was addressed.\n"
+    },
+    {
+       "new",
+       " Configuration for \"notmuch new\"\n"
+       "\n"
+       " The following options are supported here:\n"
+       "\n"
+       "\ttags A list (separated by ';') of the tags that will be\n"
+       "\t     added to all messages incorporated by \"notmuch new\".\n"
+       "\n"
+       "\tignore       A list (separated by ';') of file and directory names\n"
+       "\t     that will not be searched for messages by \"notmuch new\".\n"
+       "\n"
+       "\t     NOTE: *Every* file/directory that goes by one of those\n"
+       "\t     names will be ignored, independent of its depth/location\n"
+       "\t     in the mail store.\n"
+    },
+    {
+       "search",
+       " Search configuration\n"
+       "\n"
+       " The following option is supported here:\n"
+       "\n"
+       "\texclude_tags\n"
+       "\t\tA ;-separated list of tags that will be excluded from\n"
+       "\t\tsearch results by default.  Using an excluded tag in a\n"
+       "\t\tquery will override that exclusion.\n"
+    },
+    {
+       "maildir",
+       " Maildir compatibility configuration\n"
+       "\n"
+       " The following option is supported here:\n"
+       "\n"
+       "\tsynchronize_flags      Valid values are true and false.\n"
+       "\n"
+       "\tIf true, then the following maildir flags (in message filenames)\n"
+       "\twill be synchronized with the corresponding notmuch tags:\n"
+       "\n"
+       "\t\tFlag       Tag\n"
+       "\t\t----       -------\n"
+       "\t\tD  draft\n"
+       "\t\tF  flagged\n"
+       "\t\tP  passed\n"
+       "\t\tR  replied\n"
+       "\t\tS  unread (added when 'S' flag is not present)\n"
+       "\n"
+       "\tThe \"notmuch new\" command will notice flag changes in filenames\n"
+       "\tand update tags, while the \"notmuch tag\" and \"notmuch restore\"\n"
+       "\tcommands will notice tag changes and update flags in filenames\n"
+    },
+};
+
+struct _notmuch_conffile {
+    char *filename;
+    GKeyFile *key_file;
+    bool is_new;
+};
+
+static int
+notmuch_conffile_destructor (notmuch_conffile_t *config)
+{
+    if (config->key_file)
+       g_key_file_free (config->key_file);
+
+    return 0;
+}
+
+static bool
+get_config_from_file (notmuch_conffile_t *config, bool create_new)
+{
+    #define BUF_SIZE 4096
+    char *config_str = NULL;
+    int config_len = 0;
+    int config_bufsize = BUF_SIZE;
+    size_t len;
+    GError *error = NULL;
+    bool ret = false;
+
+    FILE *fp = fopen (config->filename, "r");
+    if (fp == NULL) {
+       if (errno == ENOENT) {
+           /* If create_new is true, then the caller is prepared for a
+            * default configuration file in the case of FILE NOT FOUND.
+            */
+           if (create_new) {
+               config->is_new = true;
+               ret = true;
+           } else {
+               fprintf (stderr, "Configuration file %s not found.\n"
+                        "Try running 'notmuch setup' to create a configuration.\n",
+                        config->filename);
+           }
+       } else {
+           fprintf (stderr, "Error opening config file '%s': %s\n",
+                    config->filename, strerror (errno));
+       }
+       goto out;
+    }
+
+    config_str = talloc_zero_array (config, char, config_bufsize);
+    if (config_str == NULL) {
+       fprintf (stderr, "Error reading '%s': Out of memory\n", config->filename);
+       goto out;
+    }
+
+    while ((len = fread (config_str + config_len, 1,
+                        config_bufsize - config_len, fp)) > 0) {
+       config_len += len;
+       if (config_len == config_bufsize) {
+           config_bufsize += BUF_SIZE;
+           config_str = talloc_realloc (config, config_str, char, config_bufsize);
+           if (config_str == NULL) {
+               fprintf (stderr, "Error reading '%s': Failed to reallocate memory\n",
+                        config->filename);
+               goto out;
+           }
+       }
+    }
+
+    if (ferror (fp)) {
+       fprintf (stderr, "Error reading '%s': I/O error\n", config->filename);
+       goto out;
+    }
+
+    if (g_key_file_load_from_data (config->key_file, config_str, config_len,
+                                  G_KEY_FILE_KEEP_COMMENTS, &error)) {
+       ret = true;
+       goto out;
+    }
+
+    fprintf (stderr, "Error parsing config file '%s': %s\n",
+            config->filename, error->message);
+
+    g_error_free (error);
+
+  out:
+    if (fp)
+       fclose (fp);
+
+    if (config_str)
+       talloc_free (config_str);
+
+    return ret;
+}
+
+/* Open the named notmuch configuration file. If the filename is NULL,
+ * the value of the environment variable $NOTMUCH_CONFIG will be used.
+ * If $NOTMUCH_CONFIG is unset, the default configuration file
+ * ($HOME/.notmuch-config) will be used.
+ *
+ * If any error occurs, (out of memory, or a permission-denied error,
+ * etc.), this function will print a message to stderr and return
+ * NULL.
+ *
+ * FILE NOT FOUND: When the specified configuration file (whether from
+ * 'filename' or the $NOTMUCH_CONFIG environment variable) does not
+ * exist, the behavior of this function depends on the 'is_new_ret'
+ * variable.
+ *
+ *     If is_new_ret is NULL, then a "file not found" message will be
+ *     printed to stderr and NULL will be returned.
+ *
+ *     If is_new_ret is non-NULL then a default configuration will be
+ *     returned and *is_new_ret will be set to 1 on return so that
+ *     the caller can recognize this case.
+ *
+ *     These default configuration settings are determined as
+ *     follows:
+ *
+ *             database_path:          $MAILDIR, otherwise $HOME/mail
+ *
+ *             user_name:              $NAME variable if set, otherwise
+ *                                     read from /etc/passwd
+ *
+ *             user_primary_mail:      $EMAIL variable if set, otherwise
+ *                                     constructed from the username and
+ *                                     hostname of the current machine.
+ *
+ *             user_other_email:       Not set.
+ *
+ *     The default configuration also contains comments to guide the
+ *     user in editing the file directly.
+ */
+notmuch_conffile_t *
+notmuch_conffile_open (notmuch_database_t *notmuch,
+                      const char *filename,
+                      bool create)
+{
+    char *notmuch_config_env = NULL;
+
+    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_conffile_destructor);
+
+    if (filename) {
+       config->filename = talloc_strdup (config, filename);
+    } else if ((notmuch_config_env = getenv ("NOTMUCH_CONFIG"))) {
+       config->filename = talloc_strdup (config, notmuch_config_env);
+    } else {
+       config->filename = talloc_asprintf (config, "%s/.notmuch-config",
+                                           getenv ("HOME"));
+    }
+
+    config->key_file = g_key_file_new ();
+
+    if (! get_config_from_file (config, create)) {
+       talloc_free (config);
+       return 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);
+           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_conffile_t object, freeing all resources.
+ *
+ * Note: Any changes made to the configuration are *not* saved by this
+ * function. To save changes, call notmuch_conffile_save before
+ * notmuch_conffile_close.
+ */
+void
+notmuch_conffile_close (notmuch_conffile_t *config)
+{
+    talloc_free (config);
+}
+
+/* Save any changes made to the notmuch configuration.
+ *
+ * Any comments originally in the file will be preserved.
+ *
+ * Returns 0 if successful, and 1 in case of any error, (after
+ * printing a description of the error to stderr).
+ */
+int
+notmuch_conffile_save (notmuch_conffile_t *config)
+{
+    size_t length;
+    char *data, *filename;
+    GError *error = NULL;
+
+    data = g_key_file_to_data (config->key_file, &length, NULL);
+    if (data == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return 1;
+    }
+
+    /* Try not to overwrite symlinks. */
+    filename = notmuch_canonicalize_file_name (config->filename);
+    if (! filename) {
+       if (errno == ENOENT) {
+           filename = strdup (config->filename);
+           if (! filename) {
+               fprintf (stderr, "Out of memory.\n");
+               g_free (data);
+               return 1;
+           }
+       } else {
+           fprintf (stderr, "Error canonicalizing %s: %s\n", config->filename,
+                    strerror (errno));
+           g_free (data);
+           return 1;
+       }
+    }
+
+    if (! g_file_set_contents (filename, data, length, &error)) {
+       if (strcmp (filename, config->filename) != 0) {
+           fprintf (stderr, "Error saving configuration to %s (-> %s): %s\n",
+                    config->filename, filename, error->message);
+       } else {
+           fprintf (stderr, "Error saving configuration to %s: %s\n",
+                    filename, error->message);
+       }
+       g_error_free (error);
+       free (filename);
+       g_free (data);
+       return 1;
+    }
+
+    free (filename);
+    g_free (data);
+    return 0;
+}
+
+bool
+notmuch_conffile_is_new (notmuch_conffile_t *config)
+{
+    return config->is_new;
+}
+
+static void
+_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_conffile_t *config,
+                 const char *group, const char *key,
+                 const char *list[],
+                 size_t 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_conffile_set_database_path (notmuch_conffile_t *config,
+                                   const char *database_path)
+{
+    _config_set (config, "database", "path", database_path);
+}
+
+void
+notmuch_conffile_set_user_name (notmuch_conffile_t *config,
+                               const char *user_name)
+{
+    _config_set (config, "user", "name", user_name);
+}
+
+void
+notmuch_conffile_set_user_primary_email (notmuch_conffile_t *config,
+                                        const char *primary_email)
+{
+    _config_set (config, "user", "primary_email", primary_email);
+}
+
+void
+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_conffile_set_new_tags (notmuch_conffile_t *config,
+                              const char *list[],
+                              size_t length)
+{
+    _config_set_list (config, "new", "tags", list, length);
+}
+
+void
+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_conffile_set_search_exclude_tags (notmuch_conffile_t *config,
+                                         const char *list[],
+                                         size_t length)
+{
+    _config_set_list (config, "search", "exclude_tags", list, length);
+}
+
+
+/* Given a configuration item of the form <group>.<key> return the
+ * component group and key. If any error occurs, print a message on
+ * stderr and return 1. Otherwise, return 0.
+ *
+ * Note: This function modifies the original 'item' string.
+ */
+static int
+_item_split (char *item, char **group, char **key)
+{
+    char *period;
+
+    *group = item;
+
+    period = strchr (item, '.');
+    if (period == NULL || *(period + 1) == '\0') {
+       fprintf (stderr,
+                "Invalid configuration name: %s\n"
+                "(Should be of the form <section>.<item>)\n", item);
+       return 1;
+    }
+
+    *period = '\0';
+    *key = period + 1;
+
+    return 0;
+}
+
+/* These are more properly called Xapian fields, but the user facing
+ * docs call them prefixes, so make the error message match */
+static bool
+validate_field_name (const char *str)
+{
+    const char *key;
+
+    if (! g_utf8_validate (str, -1, NULL)) {
+       fprintf (stderr, "Invalid utf8: %s\n", str);
+       return false;
+    }
+
+    key = g_utf8_strrchr (str, -1, '.');
+    if (! key ) {
+       INTERNAL_ERROR ("Impossible code path on input: %s\n", str);
+    }
+
+    key++;
+
+    if (! *key) {
+       fprintf (stderr, "Empty prefix name: %s\n", str);
+       return false;
+    }
+
+    if (! unicode_word_utf8 (key)) {
+       fprintf (stderr, "Non-word character in prefix name: %s\n", key);
+       return false;
+    }
+
+    if (key[0] >= 'a' && key[0] <= 'z') {
+       fprintf (stderr, "Prefix names starting with lower case letters are reserved: %s\n", key);
+       return false;
+    }
+
+    return true;
+}
+
+#define BUILT_WITH_PREFIX "built_with."
+
+typedef struct config_key {
+    const char *name;
+    bool prefix;
+    bool (*validate)(const char *);
+} config_key_info_t;
+
+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 const config_key_info_t *
+_config_key_info (const char *item)
+{
+    for (size_t i = 0; i < ARRAY_SIZE (config_key_table); i++) {
+       if (config_key_table[i].prefix &&
+           strncmp (item, config_key_table[i].name,
+                    strlen (config_key_table[i].name)) == 0)
+           return config_key_table + i;
+       if (strcmp (item, config_key_table[i].name) == 0)
+           return config_key_table + i;
+    }
+    return NULL;
+}
+
+static int
+notmuch_config_command_get (notmuch_database_t *notmuch, char *item)
+{
+    notmuch_config_values_t *list;
+
+    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;
+}
+
+static int
+_set_db_config (notmuch_database_t *notmuch, const char *key, int argc, char **argv)
+{
+    const char *val = "";
+
+    if (argc > 1) {
+       /* XXX handle lists? */
+       fprintf (stderr, "notmuch config set: at most one value expected for %s\n", key);
+       return EXIT_FAILURE;
+    }
+
+    if (argc > 0) {
+       val = argv[0];
+    }
+
+    if (print_status_database ("notmuch config", notmuch,
+                              notmuch_database_reopen (notmuch,
+                                                       NOTMUCH_DATABASE_MODE_READ_WRITE)))
+       return EXIT_FAILURE;
+
+    if (print_status_database ("notmuch config", notmuch,
+                              notmuch_database_set_config (notmuch, key, val)))
+       return EXIT_FAILURE;
+
+    if (print_status_database ("notmuch config", notmuch,
+                              notmuch_database_close (notmuch)))
+       return EXIT_FAILURE;
+
+    return EXIT_SUCCESS;
+}
+
+static int
+notmuch_config_command_set (notmuch_database_t *notmuch,
+                           int argc, char *argv[])
+{
+    char *group, *key;
+    const config_key_info_t *key_info;
+    notmuch_conffile_t *config;
+    bool update_database = false;
+    int opt_index, ret;
+    char *item;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_bool = &update_database, .name = "database" },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    argc -= opt_index;
+    argv += opt_index;
+
+    if (argc < 1) {
+       fprintf (stderr, "Error: notmuch config set requires at least "
+                "one argument.\n");
+       return EXIT_FAILURE;
+    }
+
+    item = argv[0];
+    argv++;
+    argc--;
+
+    if (STRNCMP_LITERAL (item, BUILT_WITH_PREFIX) == 0) {
+       fprintf (stderr, "Error: read only option: %s\n", item);
+       return 1;
+    }
+
+    key_info = _config_key_info (item);
+    if (key_info && key_info->validate && (! key_info->validate (item)))
+       return 1;
+
+    if (update_database) {
+       return _set_db_config (notmuch, item, argc, argv);
+    }
+
+    if (_item_split (item, &group, &key))
+       return 1;
+
+    config = notmuch_conffile_open (notmuch,
+                                   notmuch_config_path (notmuch), false);
+    if (! config)
+       return 1;
+
+    /* With only the name of an item, we clear it from the
+     * configuration file.
+     *
+     * With a single value, we set it as a string.
+     *
+     * With multiple values, we set them as a string list.
+     */
+    switch (argc) {
+    case 0:
+       g_key_file_remove_key (config->key_file, group, key, NULL);
+       break;
+    case 1:
+       g_key_file_set_string (config->key_file, group, key, argv[0]);
+       break;
+    default:
+       g_key_file_set_string_list (config->key_file, group, key,
+                                   (const gchar **) argv, argc);
+       break;
+    }
+
+    ret = notmuch_conffile_save (config);
+
+    notmuch_conffile_close (config);
+
+    return ret;
+}
+
+static
+void
+_notmuch_config_list_built_with ()
+{
+    printf ("%scompact=%s\n",
+           BUILT_WITH_PREFIX,
+           notmuch_built_with ("compact") ? "true" : "false");
+    printf ("%sfield_processor=%s\n",
+           BUILT_WITH_PREFIX,
+           notmuch_built_with ("field_processor") ? "true" : "false");
+    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
+notmuch_config_command_list (notmuch_database_t *notmuch)
+{
+    notmuch_config_pairs_t *list;
+
+    _notmuch_config_list_built_with ();
+    for (list = notmuch_config_get_pairs (notmuch, "");
+        notmuch_config_pairs_valid (list);
+        notmuch_config_pairs_move_to_next (list)) {
+       const char *value = notmuch_config_pairs_value (list);
+       if (value)
+           printf ("%s=%s\n", notmuch_config_pairs_key (list), value);
+    }
+    notmuch_config_pairs_destroy (list);
+    return EXIT_SUCCESS;
+}
+
+int
+notmuch_config_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    int ret;
+    int opt_index;
+
+    opt_index = notmuch_minimal_options ("config", argc, argv);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    /* skip at least subcommand argument */
+    argc -= opt_index;
+    argv += opt_index;
+
+    if (argc < 1) {
+       fprintf (stderr, "Error: notmuch config requires at least one argument.\n");
+       return EXIT_FAILURE;
+    }
+
+    if (strcmp (argv[0], "get") == 0) {
+       if (argc != 2) {
+           fprintf (stderr, "Error: notmuch config get requires exactly "
+                    "one argument.\n");
+           return EXIT_FAILURE;
+       }
+       ret = notmuch_config_command_get (notmuch, argv[1]);
+    } else if (strcmp (argv[0], "set") == 0) {
+       ret = notmuch_config_command_set (notmuch, argc, argv);
+    } else if (strcmp (argv[0], "list") == 0) {
+       ret = notmuch_config_command_list (notmuch);
+    } else {
+       fprintf (stderr, "Unrecognized argument for notmuch config: %s\n",
+                argv[0]);
+       return EXIT_FAILURE;
+    }
+
+    return ret ? EXIT_FAILURE : EXIT_SUCCESS;
+
+}
+
+void
+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);
+}
diff --git a/notmuch-count.c b/notmuch-count.c
new file mode 100644 (file)
index 0000000..0d9046a
--- /dev/null
@@ -0,0 +1,227 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ * Copyright © 2009 Keith Packard
+ *
+ * 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: Keith Packard <keithp@keithp.com>
+ */
+
+#include "notmuch-client.h"
+
+enum {
+    OUTPUT_THREADS,
+    OUTPUT_MESSAGES,
+    OUTPUT_FILES,
+};
+
+/* Return the number of files matching the query, or -1 for an error */
+static int
+count_files (notmuch_query_t *query)
+{
+    notmuch_messages_t *messages;
+    notmuch_message_t *message;
+    notmuch_filenames_t *filenames;
+    notmuch_status_t status;
+    int count = 0;
+
+    status = notmuch_query_search_messages (query, &messages);
+    if (print_status_query ("notmuch count", query, status))
+       return -1;
+
+    for (;
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages)) {
+       message = notmuch_messages_get (messages);
+       filenames = notmuch_message_get_filenames (message);
+
+       for (;
+            notmuch_filenames_valid (filenames);
+            notmuch_filenames_move_to_next (filenames))
+           count++;
+
+       notmuch_filenames_destroy (filenames);
+       notmuch_message_destroy (message);
+    }
+
+    notmuch_messages_destroy (messages);
+
+    return count;
+}
+
+/* return 0 on success, -1 on failure */
+static int
+print_count (notmuch_database_t *notmuch, const char *query_str,
+            notmuch_config_values_t *exclude_tags, int output, int print_lastmod)
+{
+    notmuch_query_t *query;
+    int count;
+    unsigned int ucount;
+    unsigned long revision;
+    const char *uuid;
+    int ret = 0;
+    notmuch_status_t status;
+
+    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);
+        notmuch_config_values_valid (exclude_tags);
+        notmuch_config_values_move_to_next (exclude_tags)) {
+
+       status = notmuch_query_add_tag_exclude (query,
+                                               notmuch_config_values_get (exclude_tags));
+       if (status && status != NOTMUCH_STATUS_IGNORED) {
+           print_status_query ("notmuch count", query, status);
+           ret = -1;
+           goto DONE;
+       }
+    }
+
+    switch (output) {
+    case OUTPUT_MESSAGES:
+       status = notmuch_query_count_messages (query, &ucount);
+       if (print_status_query ("notmuch count", query, status))
+           return -1;
+       printf ("%u", ucount);
+       break;
+    case OUTPUT_THREADS:
+       status = notmuch_query_count_threads (query, &ucount);
+       if (print_status_query ("notmuch count", query, status))
+           return -1;
+       printf ("%u", ucount);
+       break;
+    case OUTPUT_FILES:
+       count = count_files (query);
+       if (count >= 0) {
+           printf ("%d", count);
+       } else {
+           ret = -1;
+           goto DONE;
+       }
+       break;
+    }
+
+    if (print_lastmod) {
+       revision = notmuch_database_get_revision (notmuch, &uuid);
+       printf ("\t%s\t%lu\n", uuid, revision);
+    } else {
+       fputs ("\n", stdout);
+    }
+
+  DONE:
+    notmuch_query_destroy (query);
+
+    return ret;
+}
+
+static int
+count_file (notmuch_database_t *notmuch, FILE *input, notmuch_config_values_t *exclude_tags,
+           int output, int print_lastmod)
+{
+    char *line = NULL;
+    ssize_t line_len;
+    size_t line_size;
+    int ret = 0;
+
+    while (! ret && (line_len = getline (&line, &line_size, input)) != -1) {
+       chomp_newline (line);
+       ret = print_count (notmuch, line, exclude_tags, output, print_lastmod);
+    }
+
+    if (line)
+       free (line);
+
+    return ret;
+}
+
+int
+notmuch_count_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    char *query_str;
+    int opt_index;
+    int output = OUTPUT_MESSAGES;
+    bool exclude = true;
+    notmuch_config_values_t *exclude_tags = NULL;
+    bool batch = false;
+    bool print_lastmod = false;
+    FILE *input = stdin;
+    const char *input_file_name = NULL;
+    int ret;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_keyword = &output, .name = "output", .keywords =
+             (notmuch_keyword_t []){ { "threads", OUTPUT_THREADS },
+                                     { "messages", OUTPUT_MESSAGES },
+                                     { "files", OUTPUT_FILES },
+                                     { 0, 0 } } },
+       { .opt_bool = &exclude, .name = "exclude" },
+       { .opt_bool = &print_lastmod, .name = "lastmod" },
+       { .opt_bool = &batch, .name = "batch" },
+       { .opt_string = &input_file_name, .name = "input" },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    if (input_file_name) {
+       batch = true;
+       input = fopen (input_file_name, "r");
+       if (input == NULL) {
+           fprintf (stderr, "Error opening %s for reading: %s\n",
+                    input_file_name, strerror (errno));
+           return EXIT_FAILURE;
+       }
+    }
+
+    if (batch && opt_index != argc) {
+       fprintf (stderr, "--batch and query string are not compatible\n");
+       if (input)
+           fclose (input);
+       return EXIT_FAILURE;
+    }
+
+    query_str = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
+    if (query_str == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return EXIT_FAILURE;
+    }
+
+    if (exclude) {
+       exclude_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS);
+    }
+
+    if (batch)
+       ret = count_file (notmuch, input, exclude_tags, output, print_lastmod);
+    else
+       ret = print_count (notmuch, query_str, exclude_tags, output, print_lastmod);
+
+    notmuch_database_destroy (notmuch);
+
+    if (input != stdin)
+       fclose (input);
+
+    return ret ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/notmuch-dump.c b/notmuch-dump.c
new file mode 100644 (file)
index 0000000..cb82d61
--- /dev/null
@@ -0,0 +1,415 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+#include "hex-escape.h"
+#include "string-util.h"
+#include "zlib-extra.h"
+
+static int
+database_dump_config (notmuch_database_t *notmuch, gzFile output)
+{
+    notmuch_config_list_t *list;
+    int ret = EXIT_FAILURE;
+    char *buffer = NULL;
+    size_t buffer_size = 0;
+
+    if (print_status_database ("notmuch dump", notmuch,
+                              notmuch_database_get_config_list (notmuch, NULL, &list)))
+       goto DONE;
+
+    for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+       if (hex_encode (notmuch, notmuch_config_list_key (list),
+                       &buffer, &buffer_size) != HEX_SUCCESS) {
+           fprintf (stderr, "Error: failed to hex-encode config key %s\n",
+                    notmuch_config_list_key (list));
+           goto DONE;
+       }
+       GZPRINTF (output, "#@ %s", buffer);
+
+       if (hex_encode (notmuch, notmuch_config_list_value (list),
+                       &buffer, &buffer_size) != HEX_SUCCESS) {
+           fprintf (stderr, "Error: failed to hex-encode config value %s\n",
+                    notmuch_config_list_value (list) );
+           goto DONE;
+       }
+
+       GZPUTS (output, " ");
+       GZPUTS (output, buffer);
+       GZPUTS (output, "\n");
+    }
+
+    ret = EXIT_SUCCESS;
+
+  DONE:
+    if (list)
+       notmuch_config_list_destroy (list);
+
+    if (buffer)
+       talloc_free (buffer);
+
+    return ret;
+}
+
+static void
+print_dump_header (gzFile output, int output_format, int include)
+{
+    const char *sep = "";
+
+    GZPRINTF (output, "#notmuch-dump %s:%d ",
+             (output_format == DUMP_FORMAT_SUP) ? "sup" : "batch-tag",
+             NOTMUCH_DUMP_VERSION);
+
+    if (include & DUMP_INCLUDE_CONFIG) {
+       GZPUTS (output, "config");
+       sep = ",";
+    }
+    if (include & DUMP_INCLUDE_PROPERTIES) {
+       GZPRINTF (output, "%sproperties", sep);
+       sep = ",";
+    }
+    if (include & DUMP_INCLUDE_TAGS) {
+       GZPRINTF (output, "%stags", sep);
+    }
+    GZPUTS (output, "\n");
+}
+
+static int
+dump_properties_message (void *ctx,
+                        notmuch_message_t *message,
+                        gzFile output,
+                        char **buffer_p, size_t *size_p)
+{
+    const char *message_id;
+    notmuch_message_properties_t *list;
+    bool first = true;
+
+    message_id = notmuch_message_get_message_id (message);
+
+    if (strchr (message_id, '\n')) {
+       fprintf (stderr, "Warning: skipping message id containing line break: \"%s\"\n", message_id);
+       return 0;
+    }
+
+    for (list = notmuch_message_get_properties (message, "", false);
+        notmuch_message_properties_valid (list); notmuch_message_properties_move_to_next (list)) {
+       const char *key, *val;
+
+       if (first) {
+           if (hex_encode (ctx, message_id, buffer_p, size_p) != HEX_SUCCESS) {
+               fprintf (stderr, "Error: failed to hex-encode message-id %s\n", message_id);
+               return 1;
+           }
+           GZPRINTF (output, "#= %s", *buffer_p);
+           first = false;
+       }
+
+       key = notmuch_message_properties_key (list);
+       val = notmuch_message_properties_value (list);
+
+       if (hex_encode (ctx, key, buffer_p, size_p) != HEX_SUCCESS) {
+           fprintf (stderr, "Error: failed to hex-encode key %s\n", key);
+           return 1;
+       }
+       GZPRINTF (output, " %s", *buffer_p);
+
+       if (hex_encode (ctx, val, buffer_p, size_p) != HEX_SUCCESS) {
+           fprintf (stderr, "Error: failed to hex-encode value %s\n", val);
+           return 1;
+       }
+       GZPRINTF (output, "=%s", *buffer_p);
+    }
+    notmuch_message_properties_destroy (list);
+
+    if (! first)
+       GZPRINTF (output, "\n", *buffer_p);
+
+    return 0;
+}
+
+static int
+dump_tags_message (void *ctx,
+                  notmuch_message_t *message, int output_format,
+                  gzFile output,
+                  char **buffer_p, size_t *size_p)
+{
+    int first = 1;
+    const char *message_id;
+
+    message_id = notmuch_message_get_message_id (message);
+
+    if (output_format == DUMP_FORMAT_BATCH_TAG &&
+       strchr (message_id, '\n')) {
+       /* This will produce a line break in the output, which
+        * would be difficult to handle in tools.  However, it's
+        * also impossible to produce an email containing a line
+        * break in a message ID because of unfolding, so we can
+        * safely disallow it. */
+       fprintf (stderr, "Warning: skipping message id containing line break: \"%s\"\n", message_id);
+       return EXIT_SUCCESS;
+    }
+
+    if (output_format == DUMP_FORMAT_SUP) {
+       GZPRINTF (output, "%s (", message_id);
+    }
+
+    for (notmuch_tags_t *tags = notmuch_message_get_tags (message);
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags)) {
+       const char *tag_str = notmuch_tags_get (tags);
+
+       if (! first)
+           GZPUTS (output, " ");
+
+       first = 0;
+
+       if (output_format == DUMP_FORMAT_SUP) {
+           GZPUTS (output, tag_str);
+       } else {
+           if (hex_encode (ctx, tag_str,
+                           buffer_p, size_p) != HEX_SUCCESS) {
+               fprintf (stderr, "Error: failed to hex-encode tag %s\n",
+                        tag_str);
+               return EXIT_FAILURE;
+           }
+           GZPRINTF (output, "+%s", *buffer_p);
+       }
+    }
+
+    if (output_format == DUMP_FORMAT_SUP) {
+       GZPUTS (output, ")\n");
+    } else {
+       if (make_boolean_term (ctx, "id", message_id,
+                              buffer_p, size_p)) {
+           fprintf (stderr, "Error quoting message id %s: %s\n",
+                    message_id, strerror (errno));
+           return EXIT_FAILURE;
+       }
+       GZPRINTF (output, " -- %s\n", *buffer_p);
+    }
+    return EXIT_SUCCESS;
+}
+
+static int
+database_dump_file (notmuch_database_t *notmuch, gzFile output,
+                   const char *query_str, int output_format, int include)
+{
+    notmuch_query_t *query;
+    notmuch_messages_t *messages;
+    notmuch_message_t *message;
+    notmuch_status_t status;
+    char *buffer = NULL;
+    size_t buffer_size = 0;
+
+    print_dump_header (output, output_format, include);
+
+    if (include & DUMP_INCLUDE_CONFIG) {
+       if (print_status_database ("notmuch dump", notmuch,
+                                  database_dump_config (notmuch, output)))
+           return EXIT_FAILURE;
+    }
+
+    if (! (include & (DUMP_INCLUDE_TAGS | DUMP_INCLUDE_PROPERTIES)))
+       return EXIT_SUCCESS;
+
+    if (! query_str)
+       query_str = "";
+
+    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.
+     */
+    notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);
+
+    status = notmuch_query_search_messages (query, &messages);
+    if (print_status_query ("notmuch dump", query, status))
+       return EXIT_FAILURE;
+
+    for (;
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages)) {
+
+       message = notmuch_messages_get (messages);
+
+       if ((include & DUMP_INCLUDE_TAGS) &&
+           dump_tags_message (notmuch, message, output_format, output,
+                              &buffer, &buffer_size))
+           return EXIT_FAILURE;
+
+       if ((include & DUMP_INCLUDE_PROPERTIES) &&
+           dump_properties_message (notmuch, message, output,
+                                    &buffer, &buffer_size))
+           return EXIT_FAILURE;
+
+       notmuch_message_destroy (message);
+    }
+
+    notmuch_query_destroy (query);
+
+    return EXIT_SUCCESS;
+}
+
+/* Dump database into output_file_name if it's non-NULL, stdout
+ * otherwise.
+ */
+int
+notmuch_database_dump (notmuch_database_t *notmuch,
+                      const char *output_file_name,
+                      const char *query_str,
+                      dump_format_t output_format,
+                      dump_include_t include,
+                      bool gzip_output)
+{
+    gzFile output = NULL;
+    const char *mode = gzip_output ? "w9" : "wT";
+    const char *name_for_error = output_file_name ? output_file_name : "stdout";
+
+    char *tempname = NULL;
+    int outfd = -1;
+
+    int ret = -1;
+
+    if (output_file_name) {
+       tempname = talloc_asprintf (notmuch, "%s.XXXXXX", output_file_name);
+       outfd = mkstemp (tempname);
+    } else {
+       outfd = dup (STDOUT_FILENO);
+    }
+
+    if (outfd < 0) {
+       fprintf (stderr, "Bad output file %s\n", name_for_error);
+       goto DONE;
+    }
+
+    output = gzdopen (outfd, mode);
+
+    if (output == NULL) {
+       fprintf (stderr, "Error opening %s for (gzip) writing: %s\n",
+                name_for_error, strerror (errno));
+       if (close (outfd))
+           fprintf (stderr, "Error closing %s during shutdown: %s\n",
+                    name_for_error, strerror (errno));
+       goto DONE;
+    }
+
+    ret = database_dump_file (notmuch, output, query_str, output_format, include);
+    if (ret) goto DONE;
+
+    ret = gzflush (output, Z_FINISH);
+    if (ret) {
+       fprintf (stderr, "Error flushing output: %s\n", gzerror_str (output));
+       goto DONE;
+    }
+
+    if (output_file_name) {
+       ret = fsync (outfd);
+       if (ret) {
+           fprintf (stderr, "Error syncing %s to disk: %s\n",
+                    name_for_error, strerror (errno));
+           goto DONE;
+       }
+    }
+
+    ret = gzclose_w (output);
+    if (ret) {
+       fprintf (stderr, "Error closing %s: %s\n", name_for_error,
+                gzerror_str (output));
+       ret = EXIT_FAILURE;
+       output = NULL;
+       goto DONE;
+    } else
+       output = NULL;
+
+    if (output_file_name) {
+       ret = rename (tempname, output_file_name);
+       if (ret) {
+           fprintf (stderr, "Error renaming %s to %s: %s\n",
+                    tempname, output_file_name, strerror (errno));
+           goto DONE;
+       }
+
+    }
+  DONE:
+    if (ret != EXIT_SUCCESS && output)
+       (void) gzclose_w (output);
+
+    if (ret != EXIT_SUCCESS && output_file_name)
+       (void) unlink (tempname);
+
+    return ret;
+}
+
+int
+notmuch_dump_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    const char *query_str = NULL;
+    int ret;
+
+    const char *output_file_name = NULL;
+    int opt_index;
+
+    int output_format = DUMP_FORMAT_BATCH_TAG;
+    int include = 0;
+    bool gzip_output = 0;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_keyword = &output_format, .name = "format", .keywords =
+             (notmuch_keyword_t []){ { "sup", DUMP_FORMAT_SUP },
+                                     { "batch-tag", DUMP_FORMAT_BATCH_TAG },
+                                     { 0, 0 } } },
+       { .opt_flags = &include, .name = "include", .keywords =
+             (notmuch_keyword_t []){ { "config", DUMP_INCLUDE_CONFIG },
+                                     { "properties", DUMP_INCLUDE_PROPERTIES },
+                                     { "tags", DUMP_INCLUDE_TAGS } } },
+       { .opt_string = &output_file_name, .name = "output" },
+       { .opt_bool = &gzip_output, .name = "gzip" },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    if (include == 0)
+       include = DUMP_INCLUDE_CONFIG | DUMP_INCLUDE_TAGS | DUMP_INCLUDE_PROPERTIES;
+
+    if (opt_index < argc) {
+       query_str = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
+       if (query_str == NULL) {
+           fprintf (stderr, "Out of memory.\n");
+           return EXIT_FAILURE;
+       }
+    }
+
+    ret = notmuch_database_dump (notmuch, output_file_name, query_str,
+                                output_format, include, gzip_output);
+
+    notmuch_database_destroy (notmuch);
+
+    return ret;
+}
diff --git a/notmuch-git.py b/notmuch-git.py
new file mode 100644 (file)
index 0000000..57098aa
--- /dev/null
@@ -0,0 +1,1218 @@
+#!/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 shutil as _shutil
+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.'.format(safe))
+        _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)
diff --git a/notmuch-insert.c b/notmuch-insert.c
new file mode 100644 (file)
index 0000000..e44607a
--- /dev/null
@@ -0,0 +1,619 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2013 Peter Wang
+ *
+ * Based in part on notmuch-deliver
+ * 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: Peter Wang <novalazy@gmail.com>
+ */
+
+#include "notmuch-client.h"
+#include "tag-util.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include "string-util.h"
+
+static volatile sig_atomic_t interrupted;
+
+static void
+handle_sigint (unused (int sig))
+{
+    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
+     * fail or produce a short write, we want to get out of the signal
+     * handler as quickly as possible, not retry it. */
+    IGNORE_RESULT (write (2, msg, sizeof (msg) - 1));
+    interrupted = 1;
+}
+
+/* Like gethostname but guarantees that a null-terminated hostname is
+ * returned, even if it has to make one up. Invalid characters are
+ * substituted such that the hostname can be used within a filename.
+ */
+static void
+safe_gethostname (char *hostname, size_t len)
+{
+    char *p;
+
+    if (gethostname (hostname, len) == -1) {
+       strncpy (hostname, "unknown", len);
+    }
+    hostname[len - 1] = '\0';
+
+    for (p = hostname; *p != '\0'; p++) {
+       if (*p == '/' || *p == ':')
+           *p = '_';
+    }
+}
+
+/* Call fsync() on a directory path. */
+static bool
+sync_dir (const char *dir)
+{
+    int fd, r;
+
+    fd = open (dir, O_RDONLY);
+    if (fd == -1) {
+       fprintf (stderr, "Error: open %s: %s\n", dir, strerror (errno));
+       return false;
+    }
+
+    r = fsync (fd);
+    if (r)
+       fprintf (stderr, "Error: fsync %s: %s\n", dir, strerror (errno));
+
+    close (fd);
+
+    return r == 0;
+}
+
+/*
+ * Check the specified folder name does not contain a directory
+ * component ".." to prevent writes outside of the Maildir
+ * hierarchy. Return true on valid folder name, false otherwise.
+ */
+static bool
+is_valid_folder_name (const char *folder)
+{
+    const char *p = folder;
+
+    for (;;) {
+       if ((p[0] == '.') && (p[1] == '.') && (p[2] == '\0' || p[2] == '/'))
+           return false;
+       p = strchr (p, '/');
+       if (! p)
+           return true;
+       p++;
+    }
+}
+
+/*
+ * Make the given directory and its parents as necessary, using the
+ * given mode. Return true on success, false otherwise. Partial
+ * results are not cleaned up on errors.
+ */
+static bool
+mkdir_recursive (const void *ctx, const char *path, int mode)
+{
+    struct stat st;
+    int r;
+    char *parent = NULL, *slash;
+
+    /* First check the common case: directory already exists. */
+    r = stat (path, &st);
+    if (r == 0) {
+       if (! S_ISDIR (st.st_mode)) {
+           fprintf (stderr, "Error: '%s' is not a directory: %s\n",
+                    path, strerror (EEXIST));
+           return false;
+       }
+
+       return true;
+    } else if (errno != ENOENT) {
+       fprintf (stderr, "Error: stat '%s': %s\n", path, strerror (errno));
+       return false;
+    }
+
+    /* mkdir parents, if any */
+    slash = strrchr (path, '/');
+    if (slash && slash != path) {
+       parent = talloc_strndup (ctx, path, slash - path);
+       if (! parent) {
+           fprintf (stderr, "Error: %s\n", strerror (ENOMEM));
+           return false;
+       }
+
+       if (! mkdir_recursive (ctx, parent, mode))
+           return false;
+    }
+
+    if (mkdir (path, mode)) {
+       fprintf (stderr, "Error: mkdir '%s': %s\n", path, strerror (errno));
+       return false;
+    }
+
+    return parent ? sync_dir (parent) : true;
+}
+
+/*
+ * Create the given maildir folder, i.e. maildir and its
+ * subdirectories cur/new/tmp. Return true on success, false
+ * otherwise. Partial results are not cleaned up on errors.
+ */
+static bool
+maildir_create_folder (const void *ctx, const char *maildir, bool world_readable)
+{
+    const char *subdirs[] = { "cur", "new", "tmp" };
+    const int mode = (world_readable ? 0755 : 0700);
+    char *subdir;
+    unsigned int i;
+
+    for (i = 0; i < ARRAY_SIZE (subdirs); i++) {
+       subdir = talloc_asprintf (ctx, "%s/%s", maildir, subdirs[i]);
+       if (! subdir) {
+           fprintf (stderr, "Error: %s\n", strerror (ENOMEM));
+           return false;
+       }
+
+       if (! mkdir_recursive (ctx, subdir, mode))
+           return false;
+    }
+
+    return true;
+}
+
+/*
+ * Generate a temporary file basename, no path, do not create an
+ * actual file. Return the basename, or NULL on errors.
+ */
+static char *
+tempfilename (const void *ctx)
+{
+    char *filename;
+    char hostname[256];
+    struct timeval tv;
+    pid_t pid;
+
+    /* We follow the Dovecot file name generation algorithm. */
+    pid = getpid ();
+    safe_gethostname (hostname, sizeof (hostname));
+    gettimeofday (&tv, NULL);
+
+    filename = talloc_asprintf (ctx, "%ld.M%ldP%d.%s",
+                               (long) tv.tv_sec, (long) tv.tv_usec, pid, hostname);
+    if (! filename)
+       fprintf (stderr, "Error: %s\n", strerror (ENOMEM));
+
+    return filename;
+}
+
+/*
+ * Create a unique temporary file in maildir/tmp, return fd and full
+ * path to file in *path_out, or -1 on errors (in which case *path_out
+ * is not touched).
+ */
+static int
+maildir_mktemp (const void *ctx, const char *maildir, bool world_readable, char **path_out)
+{
+    char *filename, *path;
+    int fd;
+    const int mode = (world_readable ? 0644 : 0600);
+
+    do {
+       filename = tempfilename (ctx);
+       if (! filename)
+           return -1;
+
+       path = talloc_asprintf (ctx, "%s/tmp/%s", maildir, filename);
+       if (! path) {
+           fprintf (stderr, "Error: %s\n", strerror (ENOMEM));
+           return -1;
+       }
+
+       fd = open (path, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode);
+    } while (fd == -1 && errno == EEXIST);
+
+    if (fd == -1) {
+       fprintf (stderr, "Error: open '%s': %s\n", path, strerror (errno));
+       return -1;
+    }
+
+    *path_out = path;
+
+    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.
+ */
+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];
+       const char *p = buf;
+
+       remain = read (fdin, buf, sizeof (buf));
+       if (remain == 0)
+           break;
+       if (remain < 0) {
+           if (errno == EINTR)
+               continue;
+           fprintf (stderr, "Error: reading from standard input: %s\n",
+                    strerror (errno));
+           return false;
+       }
+
+       if (first && remain >= 5 && 0 == strncmp (buf, "From ", 5)) {
+           if (! write_buf (header, fdout, strlen (header)))
+               return false;
+           p += 5;
+           remain -= 5;
+       }
+
+       first = false;
+
+       if (! write_buf (p, fdout, remain))
+           return false;
+       empty = false;
+    }
+
+    return (! interrupted && ! empty);
+}
+
+/*
+ * Write fdin to a new temp file in maildir/tmp, return full path to
+ * the file, or NULL on errors.
+ */
+static char *
+maildir_write_tmp (const void *ctx, int fdin, const char *maildir, bool world_readable)
+{
+    char *path;
+    int fdout;
+
+    fdout = maildir_mktemp (ctx, maildir, world_readable, &path);
+    if (fdout < 0)
+       return NULL;
+
+    if (! copy_fd (fdout, fdin))
+       goto FAIL;
+
+    if (fsync (fdout)) {
+       fprintf (stderr, "Error: fsync '%s': %s\n", path, strerror (errno));
+       goto FAIL;
+    }
+
+    close (fdout);
+
+    return path;
+
+  FAIL:
+    close (fdout);
+    unlink (path);
+
+    return NULL;
+}
+
+/*
+ * Write fdin to a new file in maildir/new, using an intermediate temp
+ * file in maildir/tmp, return full path to the new file, or NULL on
+ * errors.
+ */
+static char *
+maildir_write_new (const void *ctx, int fdin, const char *maildir, bool world_readable)
+{
+    char *cleanpath, *tmppath, *newpath, *newdir;
+
+    tmppath = maildir_write_tmp (ctx, fdin, maildir, world_readable);
+    if (! tmppath)
+       return NULL;
+    cleanpath = tmppath;
+
+    newpath = talloc_strdup (ctx, tmppath);
+    if (! newpath) {
+       fprintf (stderr, "Error: %s\n", strerror (ENOMEM));
+       goto FAIL;
+    }
+
+    /* sanity checks needed? */
+    memcpy (newpath + strlen (maildir) + 1, "new", 3);
+
+    if (rename (tmppath, newpath)) {
+       fprintf (stderr, "Error: rename '%s' '%s': %s\n",
+                tmppath, newpath, strerror (errno));
+       goto FAIL;
+    }
+    cleanpath = newpath;
+
+    newdir = talloc_asprintf (ctx, "%s/%s", maildir, "new");
+    if (! newdir) {
+       fprintf (stderr, "Error: %s\n", strerror (ENOMEM));
+       goto FAIL;
+    }
+
+    if (! sync_dir (newdir))
+       goto FAIL;
+
+    return newpath;
+
+  FAIL:
+    unlink (cleanpath);
+
+    return NULL;
+}
+
+/*
+ * Add the specified message file to the notmuch database, applying
+ * tags in tag_ops. If synchronize_flags is true, the tags are
+ * synchronized to maildir flags (which may result in message file
+ * rename).
+ *
+ * Return NOTMUCH_STATUS_SUCCESS on success, errors otherwise. If keep
+ * is true, errors in tag changes and flag syncing are ignored and
+ * success status is returned; otherwise such errors cause the message
+ * to be removed from the database. Failure to add the message to the
+ * database results in error status regardless of keep.
+ */
+static notmuch_status_t
+add_file (notmuch_database_t *notmuch, const char *path, tag_op_list_t *tag_ops,
+         bool synchronize_flags, bool keep,
+         notmuch_indexopts_t *indexopts)
+{
+    notmuch_message_t *message;
+    notmuch_status_t status;
+
+    status = notmuch_database_index_file (notmuch, path, indexopts, &message);
+    if (status == NOTMUCH_STATUS_SUCCESS) {
+       status = tag_op_list_apply (message, tag_ops, 0);
+       if (status) {
+           fprintf (stderr, "%s: failed to apply tags to file '%s': %s\n",
+                    keep ? "Warning" : "Error",
+                    path, notmuch_status_to_string (status));
+           goto DONE;
+       }
+    } else if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
+       status = NOTMUCH_STATUS_SUCCESS;
+    } else if (status == NOTMUCH_STATUS_FILE_NOT_EMAIL) {
+       fprintf (stderr, "Error: delivery of non-mail file: '%s'\n", path);
+       goto FAIL;
+    } else {
+       fprintf (stderr, "Error: failed to add '%s' to notmuch database: %s\n",
+                path, notmuch_status_to_string (status));
+       goto FAIL;
+    }
+
+    if (synchronize_flags) {
+       status = notmuch_message_tags_to_maildir_flags (message);
+       if (status != NOTMUCH_STATUS_SUCCESS)
+           fprintf (stderr, "%s: failed to sync tags to maildir flags for '%s': %s\n",
+                    keep ? "Warning" : "Error",
+                    path, notmuch_status_to_string (status));
+
+       /*
+        * Note: Unfortunately a failed maildir flag sync might
+        * already have renamed the file, in which case the cleanup
+        * path may fail.
+        */
+    }
+
+  DONE:
+    notmuch_message_destroy (message);
+
+    if (status) {
+       if (keep) {
+           status = NOTMUCH_STATUS_SUCCESS;
+       } else {
+           notmuch_status_t cleanup_status;
+
+           cleanup_status = notmuch_database_remove_message (notmuch, path);
+           if (cleanup_status != NOTMUCH_STATUS_SUCCESS &&
+               cleanup_status != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
+               fprintf (stderr, "Warning: failed to remove '%s' from database "
+                        "after errors: %s. Please run 'notmuch new' to fix.\n",
+                        path, notmuch_status_to_string (cleanup_status));
+           }
+       }
+    }
+
+  FAIL:
+    return status;
+}
+
+int
+notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    notmuch_status_t status, close_status;
+    struct sigaction action;
+    const char *mail_root;
+    notmuch_config_values_t *new_tags = NULL;
+    tag_op_list_t *tag_ops;
+    char *query_string = NULL;
+    const char *folder = "";
+    bool create_folder = false;
+    bool keep = false;
+    bool hooks = true;
+    bool world_readable = false;
+    notmuch_bool_t synchronize_flags;
+    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[] = {
+       { .opt_string = &folder, .name = "folder", .allow_empty = true },
+       { .opt_bool = &create_folder, .name = "create-folder" },
+       { .opt_bool = &keep, .name = "keep" },
+       { .opt_bool = &hooks, .name = "hooks" },
+       { .opt_bool = &world_readable, .name = "world-readable" },
+       { .opt_inherit = notmuch_shared_indexing_options },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    mail_root = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT);
+
+    new_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_TAGS);
+
+    if (print_status_database (
+           "notmuch insert",
+           notmuch,
+           notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+                                    &synchronize_flags)))
+       return EXIT_FAILURE;
+
+    tag_ops = tag_op_list_create (local);
+    if (tag_ops == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return EXIT_FAILURE;
+    }
+    for (;
+        notmuch_config_values_valid (new_tags);
+        notmuch_config_values_move_to_next (new_tags)) {
+       const char *error_msg;
+       const char *tag = notmuch_config_values_get (new_tags);
+       error_msg = illegal_tag (tag, false);
+       if (error_msg) {
+           fprintf (stderr, "Error: tag '%s' in new.tags: %s\n",
+                    tag,  error_msg);
+           return EXIT_FAILURE;
+       }
+
+       if (tag_op_list_append (tag_ops, tag, false))
+           return EXIT_FAILURE;
+    }
+
+    if (parse_tag_command_line (local, argc - opt_index, argv + opt_index,
+                               &query_string, tag_ops))
+       return EXIT_FAILURE;
+
+    if (*query_string != '\0') {
+       fprintf (stderr, "Error: unexpected query string: %s\n", query_string);
+       return EXIT_FAILURE;
+    }
+
+    if (! is_valid_folder_name (folder)) {
+       fprintf (stderr, "Error: invalid folder name: '%s'\n", folder);
+       return EXIT_FAILURE;
+    }
+
+    maildir = talloc_asprintf (local, "%s/%s", mail_root, folder);
+    if (! maildir) {
+       fprintf (stderr, "Out of memory\n");
+       return EXIT_FAILURE;
+    }
+
+    strip_trailing (maildir, '/');
+    if (create_folder && ! maildir_create_folder (local, maildir, world_readable))
+       return EXIT_FAILURE;
+
+    /* Set up our handler for SIGINT. We do not set SA_RESTART so that copying
+     * from standard input may be interrupted. */
+    memset (&action, 0, sizeof (struct sigaction));
+    action.sa_handler = handle_sigint;
+    sigemptyset (&action.sa_mask);
+    action.sa_flags = 0;
+    sigaction (SIGINT, &action, NULL);
+
+    /* Write the message to the Maildir new directory. */
+    newpath = maildir_write_new (local, STDIN_FILENO, maildir, world_readable);
+    if (! newpath) {
+       return EXIT_FAILURE;
+    }
+
+    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));
+       return EXIT_FAILURE;
+    }
+
+    /* Index the message. */
+    status = add_file (notmuch, newpath, tag_ops, synchronize_flags, keep, indexopts);
+
+    /* Commit changes. */
+    close_status = notmuch_database_close (notmuch);
+    if (close_status) {
+       /* Hold on to the first error, if any. */
+       if (! status)
+           status = close_status;
+       fprintf (stderr, "%s: failed to commit database changes: %s\n",
+                keep ? "Warning" : "Error",
+                notmuch_status_to_string (close_status));
+    }
+
+    if (status) {
+       if (keep) {
+           status = NOTMUCH_STATUS_SUCCESS;
+       } else {
+           /* If maildir flag sync failed, this might fail. */
+           if (unlink (newpath)) {
+               fprintf (stderr, "Warning: failed to remove '%s' from maildir "
+                        "after errors: %s. Please run 'notmuch new' to fix.\n",
+                        newpath, strerror (errno));
+           }
+       }
+    }
+
+    if (hooks && status == NOTMUCH_STATUS_SUCCESS) {
+       /* Ignore hook failures. */
+       notmuch_run_hook (notmuch, "post-insert");
+    }
+
+    notmuch_database_destroy (notmuch);
+
+    talloc_free (local);
+
+    return status_to_exit (status);
+}
diff --git a/notmuch-new.c b/notmuch-new.c
new file mode 100644 (file)
index 0000000..4a53e3e
--- /dev/null
@@ -0,0 +1,1325 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+#include "tag-util.h"
+
+#include <unistd.h>
+
+typedef struct _filename_node {
+    char *filename;
+    time_t mtime;
+    struct _filename_node *next;
+} _filename_node_t;
+
+typedef struct _filename_list {
+    unsigned count;
+    _filename_node_t *head;
+    _filename_node_t **tail;
+} _filename_list_t;
+
+enum verbosity {
+    VERBOSITY_QUIET,
+    VERBOSITY_NORMAL,
+    VERBOSITY_VERBOSE,
+};
+
+typedef struct {
+    const char *db_path;
+    const char *mail_root;
+
+    notmuch_indexopts_t *indexopts;
+    int output_is_a_tty;
+    enum verbosity verbosity;
+    bool debug;
+    bool full_scan;
+    notmuch_config_values_t *new_tags;
+    const char **ignore_verbatim;
+    size_t ignore_verbatim_length;
+    regex_t *ignore_regex;
+    size_t ignore_regex_length;
+
+    int total_files;
+    int processed_files;
+    int added_messages, removed_messages, renamed_messages;
+    int vanished_files;
+    struct timeval tv_start;
+
+    _filename_list_t *removed_files;
+    _filename_list_t *removed_directories;
+    _filename_list_t *directory_mtimes;
+
+    notmuch_bool_t synchronize_flags;
+} add_files_state_t;
+
+static volatile sig_atomic_t do_print_progress = 0;
+
+static void
+handle_sigalrm (unused (int signal))
+{
+    do_print_progress = 1;
+}
+
+static volatile sig_atomic_t interrupted;
+
+static void
+handle_sigint (unused (int sig))
+{
+    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
+     * fail or produce a short write, we want to get out of the signal
+     * handler as quickly as possible, not retry it. */
+    IGNORE_RESULT (write (2, msg, sizeof (msg) - 1));
+    interrupted = 1;
+}
+
+static _filename_list_t *
+_filename_list_create (const void *ctx)
+{
+    _filename_list_t *list;
+
+    list = talloc (ctx, _filename_list_t);
+    if (list == NULL)
+       return NULL;
+
+    list->head = NULL;
+    list->tail = &list->head;
+    list->count = 0;
+
+    return list;
+}
+
+static _filename_node_t *
+_filename_list_add (_filename_list_t *list,
+                   const char *filename)
+{
+    _filename_node_t *node = talloc (list, _filename_node_t);
+
+    list->count++;
+
+    node->filename = talloc_strdup (list, filename);
+    node->next = NULL;
+
+    *(list->tail) = node;
+    list->tail = &node->next;
+
+    return node;
+}
+
+static void
+generic_print_progress (const char *action, const char *object,
+                       struct timeval tv_start, unsigned processed, unsigned total)
+{
+    struct timeval tv_now;
+    double elapsed_overall, rate_overall;
+
+    gettimeofday (&tv_now, NULL);
+
+    elapsed_overall = notmuch_time_elapsed (tv_start, tv_now);
+    rate_overall = processed / elapsed_overall;
+
+    printf ("%s %u ", action, processed);
+
+    if (total) {
+       printf ("of %u %s", total, object);
+       if (processed > 0 && elapsed_overall > 0.5) {
+           double time_remaining = ((total - processed) / rate_overall);
+           printf (" (");
+           notmuch_time_print_formatted_seconds (time_remaining);
+           printf (" remaining)");
+       }
+    } else {
+       printf ("%s", object);
+       if (elapsed_overall > 0.5)
+           printf (" (%d %s/sec.)", (int) rate_overall, object);
+    }
+    printf (".\033[K\r");
+
+    fflush (stdout);
+}
+
+static int
+dirent_sort_inode (const struct dirent **a, const struct dirent **b)
+{
+    return ((*a)->d_ino < (*b)->d_ino) ? -1 : 1;
+}
+
+static int
+dirent_sort_strcmp_name (const struct dirent **a, const struct dirent **b)
+{
+    return strcmp ((*a)->d_name, (*b)->d_name);
+}
+
+/* Return the type of a directory entry relative to path as a stat(2)
+ * mode.  Like stat, this follows symlinks.  Returns -1 and sets errno
+ * if the file's type cannot be determined (which includes dangling
+ * symlinks).
+ */
+static int
+dirent_type (const char *path, const struct dirent *entry)
+{
+    struct stat statbuf;
+    char *abspath;
+    int err, saved_errno;
+
+#if HAVE_D_TYPE
+    /* Mapping from d_type to stat mode_t.  We omit DT_LNK so that
+     * we'll fall through to stat and get the real file type. */
+    static const mode_t modes[] = {
+       [DT_BLK] = S_IFBLK,
+       [DT_CHR] = S_IFCHR,
+       [DT_DIR] = S_IFDIR,
+       [DT_FIFO] = S_IFIFO,
+       [DT_REG] = S_IFREG,
+       [DT_SOCK] = S_IFSOCK
+    };
+    if (entry->d_type < ARRAY_SIZE (modes) && modes[entry->d_type])
+       return modes[entry->d_type];
+#endif
+
+    abspath = talloc_asprintf (NULL, "%s/%s", path, entry->d_name);
+    if (! abspath) {
+       errno = ENOMEM;
+       return -1;
+    }
+    err = stat (abspath, &statbuf);
+    saved_errno = errno;
+    talloc_free (abspath);
+    if (err < 0) {
+       errno = saved_errno;
+       return -1;
+    }
+    return statbuf.st_mode & S_IFMT;
+}
+
+/* Test if the directory looks like a Maildir directory.
+ *
+ * Search through the array of directory entries to see if we can find all
+ * three subdirectories typical for Maildir, that is "new", "cur", and "tmp".
+ *
+ * Return 1 if the directory looks like a Maildir and 0 otherwise.
+ */
+static int
+_entries_resemble_maildir (const char *path, struct dirent **entries, int count)
+{
+    int i, found = 0;
+
+    for (i = 0; i < count; i++) {
+       if (dirent_type (path, entries[i]) != S_IFDIR)
+           continue;
+
+       if (strcmp (entries[i]->d_name, "new") == 0 ||
+           strcmp (entries[i]->d_name, "cur") == 0 ||
+           strcmp (entries[i]->d_name, "tmp") == 0) {
+           found++;
+           if (found == 3)
+               return 1;
+       }
+    }
+
+    return 0;
+}
+
+static bool
+_special_directory (const char *entry)
+{
+    return strcmp (entry, ".") == 0 || strcmp (entry, "..") == 0;
+}
+
+static bool
+_setup_ignore (notmuch_database_t *notmuch, add_files_state_t *state)
+{
+    notmuch_config_values_t *ignore_list;
+    int nregex = 0, nverbatim = 0;
+    const char **verbatim = NULL;
+    regex_t *regex = NULL;
+
+    for (ignore_list = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_IGNORE);
+        notmuch_config_values_valid (ignore_list);
+        notmuch_config_values_move_to_next (ignore_list)) {
+       const char *s = notmuch_config_values_get (ignore_list);
+       size_t len = strlen (s);
+
+       if (len == 0) {
+           fprintf (stderr, "Error: Empty string in new.ignore list\n");
+           return false;
+       }
+
+       if (s[0] == '/') {
+           regex_t *preg;
+           char *r;
+           int rerr;
+
+           if (len < 3 || s[len - 1] != '/') {
+               fprintf (stderr, "Error: Malformed pattern '%s' in new.ignore\n",
+                        s);
+               return false;
+           }
+
+           r = talloc_strndup (notmuch, s + 1, len - 2);
+           regex = talloc_realloc (notmuch, regex, regex_t, nregex + 1);
+           preg = &regex[nregex];
+
+           rerr = regcomp (preg, r, REG_EXTENDED | REG_NOSUB);
+           if (rerr) {
+               size_t error_size = regerror (rerr, preg, NULL, 0);
+               char *error = talloc_size (r, error_size);
+
+               regerror (rerr, preg, error, error_size);
+
+               fprintf (stderr, "Error: Invalid regex '%s' in new.ignore: %s\n",
+                        r, error);
+               return false;
+           }
+           nregex++;
+
+           talloc_free (r);
+       } else {
+           verbatim = talloc_realloc (notmuch, verbatim, const char *,
+                                      nverbatim + 1);
+           verbatim[nverbatim++] = s;
+       }
+    }
+
+    state->ignore_regex = regex;
+    state->ignore_regex_length = nregex;
+    state->ignore_verbatim = verbatim;
+    state->ignore_verbatim_length = nverbatim;
+
+    return true;
+}
+
+static char *
+_get_relative_path (const char *mail_root, const char *dirpath, const char *entry)
+{
+    size_t mail_root_len = strlen (mail_root);
+
+    /* paranoia? */
+    if (strncmp (dirpath, mail_root, mail_root_len) != 0) {
+       fprintf (stderr, "Warning: '%s' is not a subdirectory of '%s'\n",
+                dirpath, mail_root);
+       return NULL;
+    }
+
+    dirpath += mail_root_len;
+    while (*dirpath == '/')
+       dirpath++;
+
+    if (*dirpath)
+       return talloc_asprintf (NULL, "%s/%s", dirpath, entry);
+    else
+       return talloc_strdup (NULL, entry);
+}
+
+/* Test if the file/directory is to be ignored.
+ */
+static bool
+_entry_in_ignore_list (add_files_state_t *state, const char *dirpath,
+                      const char *entry)
+{
+    bool ret = false;
+    size_t i;
+    char *path;
+
+    for (i = 0; i < state->ignore_verbatim_length; i++) {
+       if (strcmp (entry, state->ignore_verbatim[i]) == 0)
+           return true;
+    }
+
+    if (state->ignore_regex_length == 0)
+       return false;
+
+    path = _get_relative_path (state->mail_root, dirpath, entry);
+    if (! path)
+       return false;
+
+    for (i = 0; i < state->ignore_regex_length; i++) {
+       if (regexec (&state->ignore_regex[i], path, 0, NULL, 0) == 0) {
+           ret = true;
+           break;
+       }
+    }
+
+    talloc_free (path);
+
+    return ret;
+}
+
+/* Add a single file to the database. */
+static notmuch_status_t
+add_file (notmuch_database_t *notmuch, const char *filename,
+         add_files_state_t *state)
+{
+    notmuch_message_t *message = NULL;
+    const char *tag;
+    notmuch_status_t status;
+
+    status = notmuch_database_begin_atomic (notmuch);
+    if (status)
+       goto DONE;
+
+    status = notmuch_database_index_file (notmuch, filename, state->indexopts, &message);
+    switch (status) {
+    /* Success. */
+    case NOTMUCH_STATUS_SUCCESS:
+       state->added_messages++;
+       notmuch_message_freeze (message);
+       if (state->synchronize_flags)
+           notmuch_message_maildir_flags_to_tags (message);
+
+       for (notmuch_config_values_start (state->new_tags);
+            notmuch_config_values_valid (state->new_tags);
+            notmuch_config_values_move_to_next (state->new_tags)) {
+           notmuch_bool_t is_set;
+
+           tag = notmuch_config_values_get (state->new_tags);
+           /* Currently all errors from has_maildir_flag are fatal */
+           if ((status = notmuch_message_has_maildir_flag_st (message, 'S', &is_set)))
+               goto DONE;
+           if (strcmp ("unread", tag) != 0 || ! is_set) {
+               notmuch_message_add_tag (message, tag);
+           }
+       }
+
+       notmuch_message_thaw (message);
+       break;
+    /* Non-fatal issues (go on to next file). */
+    case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+       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++;
+       fprintf (stderr, "Unexpected error with file %s\n", filename);
+       (void) print_status_database ("add_file", notmuch, status);
+       break;
+    /* Fatal issues. Don't process anymore. */
+    case NOTMUCH_STATUS_READ_ONLY_DATABASE:
+    case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
+    case NOTMUCH_STATUS_OUT_OF_MEMORY:
+       (void) print_status_database ("add_file", notmuch, status);
+       goto DONE;
+    default:
+       INTERNAL_ERROR ("add_message returned unexpected value: %d", status);
+       goto DONE;
+    }
+
+    status = notmuch_database_end_atomic (notmuch);
+
+  DONE:
+    if (message)
+       notmuch_message_destroy (message);
+
+    return status;
+}
+
+/* Examine 'path' recursively as follows:
+ *
+ *   o Ask the filesystem for the mtime of 'path' (fs_mtime)
+ *   o Ask the database for its timestamp of 'path' (db_mtime)
+ *
+ *   o Ask the filesystem for files and directories within 'path'
+ *     (via scandir and stored in fs_entries)
+ *
+ *   o Pass 1: For each directory in fs_entries, recursively call into
+ *     this same function.
+ *
+ *   o Compare fs_mtime to db_mtime. If they are equivalent, terminate
+ *     the algorithm at this point, (this directory has not been
+ *     updated in the filesystem since the last database scan of PASS
+ *     2).
+ *
+ *   o Ask the database for files and directories within 'path'
+ *     (db_files and db_subdirs)
+ *
+ *   o Pass 2: Walk fs_entries simultaneously with db_files and
+ *     db_subdirs. Look for one of three interesting cases:
+ *
+ *        1. Regular file in fs_entries and not in db_files
+ *            This is a new file to add_message into the database.
+ *
+ *         2. Filename in db_files not in fs_entries.
+ *            This is a file that has been removed from the mail store.
+ *
+ *         3. Directory in db_subdirs not in fs_entries
+ *            This is a directory that has been removed from the mail store.
+ *
+ *     Note that the addition of a directory is not interesting here,
+ *     since that will have been taken care of in pass 1. Also, we
+ *     don't immediately act on file/directory removal since we must
+ *     ensure that in the case of a rename that the new filename is
+ *     added before the old filename is removed, (so that no
+ *     information is lost from the database).
+ *
+ *   o Tell the database to update its time of 'path' to 'fs_mtime'
+ *     if fs_mtime isn't the current wall-clock time.
+ */
+static notmuch_status_t
+add_files (notmuch_database_t *notmuch,
+          const char *path,
+          add_files_state_t *state)
+{
+    struct dirent *entry = NULL;
+    char *next = NULL;
+    time_t fs_mtime, db_mtime;
+    notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
+    struct dirent **fs_entries = NULL;
+    int i, num_fs_entries = 0, entry_type;
+    notmuch_directory_t *directory;
+    notmuch_filenames_t *db_files = NULL;
+    notmuch_filenames_t *db_subdirs = NULL;
+    time_t stat_time;
+    struct stat st;
+    bool is_maildir;
+
+    if (stat (path, &st)) {
+       fprintf (stderr, "Error reading directory %s: %s\n",
+                path, strerror (errno));
+       return NOTMUCH_STATUS_FILE_ERROR;
+    }
+    stat_time = time (NULL);
+
+    if (! S_ISDIR (st.st_mode)) {
+       fprintf (stderr, "Error: %s is not a directory.\n", path);
+       return NOTMUCH_STATUS_FILE_ERROR;
+    }
+
+    fs_mtime = st.st_mtime;
+
+    status = notmuch_database_get_directory (notmuch, path, &directory);
+    if (status) {
+       ret = status;
+       goto DONE;
+    }
+    db_mtime = directory ? notmuch_directory_get_mtime (directory) : 0;
+
+    /* If the directory is unchanged from our last scan and has no
+     * sub-directories, then return without scanning it at all.  In
+     * some situations, skipping the scan can substantially reduce the
+     * cost of notmuch new, especially since the huge numbers of files
+     * in Maildirs make scans expensive, but all files live in leaf
+     * directories.
+     *
+     * To check for sub-directories, we borrow a trick from find,
+     * kpathsea, and many other UNIX tools: since a directory's link
+     * count is the number of sub-directories (specifically, their
+     * '..' entries) plus 2 (the link from the parent and the link for
+     * '.').  This check is safe even on weird file systems, since
+     * file systems that can't compute this will return 0 or 1.  This
+     * is safe even on *really* weird file systems like HFS+ that
+     * mistakenly return the total number of directory entries, since
+     * that only inflates the count beyond 2.
+     */
+    if (directory && (! state->full_scan) && fs_mtime == db_mtime && st.st_nlink == 2) {
+       /* There's one catch: pass 1 below considers symlinks to
+        * directories to be directories, but these don't increase the
+        * file system link count.  So, only bail early if the
+        * database agrees that there are no sub-directories. */
+       db_subdirs = notmuch_directory_get_child_directories (directory);
+       if (! notmuch_filenames_valid (db_subdirs))
+           goto DONE;
+       notmuch_filenames_destroy (db_subdirs);
+       db_subdirs = NULL;
+    }
+
+    /* If the database knows about this directory, then we sort based
+     * on strcmp to match the database sorting. Otherwise, we can do
+     * inode-based sorting for faster filesystem operation. */
+    num_fs_entries = scandir (path, &fs_entries, 0,
+                             directory ?
+                             dirent_sort_strcmp_name : dirent_sort_inode);
+
+    if (num_fs_entries == -1) {
+       fprintf (stderr, "Error opening directory %s: %s\n",
+                path, strerror (errno));
+       /* We consider this a fatal error because, if a user moved a
+        * message from another directory that we were able to scan
+        * into this directory, skipping this directory will cause
+        * that message to be lost. */
+       ret = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
+    }
+
+    /* Pass 1: Recurse into all sub-directories. */
+    is_maildir = _entries_resemble_maildir (path, fs_entries, num_fs_entries);
+
+    for (i = 0; i < num_fs_entries && ! interrupted; i++) {
+       entry = fs_entries[i];
+
+       /* Ignore special directories to avoid infinite recursion. */
+       if (_special_directory (entry->d_name))
+           continue;
+
+       /* Ignore any files/directories the user has configured to
+        * ignore.  We do this before dirent_type both for performance
+        * and because we don't care if dirent_type fails on entries
+        * that are explicitly ignored.
+        */
+       if (_entry_in_ignore_list (state, path, entry->d_name)) {
+           if (state->debug)
+               printf ("(D) add_files, pass 1: explicitly ignoring %s/%s\n",
+                       path, entry->d_name);
+           continue;
+       }
+
+       /* We only want to descend into directories (and symlinks to
+        * directories). */
+       entry_type = dirent_type (path, entry);
+       if (entry_type == -1) {
+           /* Be pessimistic, e.g. so we don't lose lots of mail just
+            * because a user broke a symlink. */
+           fprintf (stderr, "Error reading file %s/%s: %s\n",
+                    path, entry->d_name, strerror (errno));
+           return NOTMUCH_STATUS_FILE_ERROR;
+       } else if (entry_type != S_IFDIR) {
+           continue;
+       }
+
+       /* 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 (path, state->mail_root)) == 0))
+           continue;
+
+       next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
+       status = add_files (notmuch, next, state);
+       if (status) {
+           ret = status;
+           goto DONE;
+       }
+       talloc_free (next);
+       next = NULL;
+    }
+
+    /* If the directory's modification time in the filesystem is the
+     * same as what we recorded in the database the last time we
+     * scanned it, then we can skip the second pass entirely.
+     *
+     * We test for strict equality here to avoid a bug that can happen
+     * if the system clock jumps backward, (preventing new mail from
+     * being discovered until the clock catches up and the directory
+     * is modified again).
+     */
+    if (directory && (! state->full_scan) && fs_mtime == db_mtime)
+       goto DONE;
+
+    /* If the database has never seen this directory before, we can
+     * simply leave db_files and db_subdirs NULL. */
+    if (directory) {
+       db_files = notmuch_directory_get_child_files (directory);
+       db_subdirs = notmuch_directory_get_child_directories (directory);
+    }
+
+    /* Pass 2: Scan for new files, removed files, and removed directories. */
+    for (i = 0; i < num_fs_entries && ! interrupted; i++) {
+       entry = fs_entries[i];
+
+       /* Ignore special directories early. */
+       if (_special_directory (entry->d_name))
+           continue;
+
+       /* Ignore files & directories user has configured to be ignored */
+       if (_entry_in_ignore_list (state, path, entry->d_name)) {
+           if (state->debug)
+               printf ("(D) add_files, pass 2: explicitly ignoring %s/%s\n",
+                       path, entry->d_name);
+           continue;
+       }
+
+       /* Check if we've walked past any names in db_files or
+        * db_subdirs. If so, these have been deleted. */
+       while (notmuch_filenames_valid (db_files) &&
+              strcmp (notmuch_filenames_get (db_files), entry->d_name) < 0) {
+           char *absolute = talloc_asprintf (state->removed_files,
+                                             "%s/%s", path,
+                                             notmuch_filenames_get (db_files));
+
+           if (state->debug)
+               printf ("(D) add_files, pass 2: queuing passed file %s for deletion from database\n",
+                       absolute);
+
+           _filename_list_add (state->removed_files, absolute);
+
+           notmuch_filenames_move_to_next (db_files);
+       }
+
+       while (notmuch_filenames_valid (db_subdirs) &&
+              strcmp (notmuch_filenames_get (db_subdirs), entry->d_name) <= 0) {
+           const char *filename = notmuch_filenames_get (db_subdirs);
+
+           if (strcmp (filename, entry->d_name) < 0) {
+               char *absolute = talloc_asprintf (state->removed_directories,
+                                                 "%s/%s", path, filename);
+               if (state->debug)
+                   printf (
+                       "(D) add_files, pass 2: queuing passed directory %s for deletion from database\n",
+                       absolute);
+
+               _filename_list_add (state->removed_directories, absolute);
+           }
+
+           notmuch_filenames_move_to_next (db_subdirs);
+       }
+
+       /* Only add regular files (and symlinks to regular files). */
+       entry_type = dirent_type (path, entry);
+       if (entry_type == -1) {
+           fprintf (stderr, "Error reading file %s/%s: %s\n",
+                    path, entry->d_name, strerror (errno));
+           return NOTMUCH_STATUS_FILE_ERROR;
+       } else if (entry_type != S_IFREG) {
+           continue;
+       }
+
+       /* Don't add a file that we've added before. */
+       if (notmuch_filenames_valid (db_files) &&
+           strcmp (notmuch_filenames_get (db_files), entry->d_name) == 0) {
+           notmuch_filenames_move_to_next (db_files);
+           continue;
+       }
+
+       /* We're now looking at a regular file that doesn't yet exist
+        * in the database, so add it. */
+       next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
+
+       state->processed_files++;
+
+       if (state->verbosity >= VERBOSITY_VERBOSE) {
+           if (state->output_is_a_tty)
+               printf ("\r\033[K");
+
+           printf ("%i/%i: %s", state->processed_files, state->total_files,
+                   next);
+
+           putchar ((state->output_is_a_tty) ? '\r' : '\n');
+           fflush (stdout);
+       }
+
+       status = add_file (notmuch, next, state);
+       if (status) {
+           ret = status;
+           goto DONE;
+       }
+
+       if (do_print_progress) {
+           do_print_progress = 0;
+           generic_print_progress ("Processed", "files", state->tv_start,
+                                   state->processed_files, state->total_files);
+       }
+
+       talloc_free (next);
+       next = NULL;
+    }
+
+    if (interrupted)
+       goto DONE;
+
+    /* Now that we've walked the whole filesystem list, anything left
+     * over in the database lists has been deleted. */
+    while (notmuch_filenames_valid (db_files)) {
+       char *absolute = talloc_asprintf (state->removed_files,
+                                         "%s/%s", path,
+                                         notmuch_filenames_get (db_files));
+       if (state->debug)
+           printf ("(D) add_files, pass 3: queuing leftover file %s for deletion from database\n",
+                   absolute);
+
+       _filename_list_add (state->removed_files, absolute);
+
+       notmuch_filenames_move_to_next (db_files);
+    }
+
+    while (notmuch_filenames_valid (db_subdirs)) {
+       char *absolute = talloc_asprintf (state->removed_directories,
+                                         "%s/%s", path,
+                                         notmuch_filenames_get (db_subdirs));
+
+       if (state->debug)
+           printf (
+               "(D) add_files, pass 3: queuing leftover directory %s for deletion from database\n",
+               absolute);
+
+       _filename_list_add (state->removed_directories, absolute);
+
+       notmuch_filenames_move_to_next (db_subdirs);
+    }
+
+    /* If the directory's mtime is the same as the wall-clock time
+     * when we stat'ed the directory, we skip updating the mtime in
+     * the database because a message could be delivered later in this
+     * same second.  This may lead to unnecessary re-scans, but it
+     * avoids overlooking messages. */
+    if (fs_mtime != stat_time)
+       _filename_list_add (state->directory_mtimes, path)->mtime = fs_mtime;
+
+  DONE:
+    if (next)
+       talloc_free (next);
+    if (fs_entries) {
+       for (i = 0; i < num_fs_entries; i++)
+           free (fs_entries[i]);
+
+       free (fs_entries);
+    }
+    if (db_subdirs)
+       notmuch_filenames_destroy (db_subdirs);
+    if (db_files)
+       notmuch_filenames_destroy (db_files);
+    if (directory)
+       notmuch_directory_destroy (directory);
+
+    return ret;
+}
+
+static void
+setup_progress_printing_timer (void)
+{
+    struct sigaction action;
+    struct itimerval timerval;
+
+    /* Set up our handler for SIGALRM */
+    memset (&action, 0, sizeof (struct sigaction));
+    action.sa_handler = handle_sigalrm;
+    sigemptyset (&action.sa_mask);
+    action.sa_flags = SA_RESTART;
+    sigaction (SIGALRM, &action, NULL);
+
+    /* Then start a timer to send SIGALRM once per second. */
+    timerval.it_interval.tv_sec = 1;
+    timerval.it_interval.tv_usec = 0;
+    timerval.it_value.tv_sec = 1;
+    timerval.it_value.tv_usec = 0;
+    setitimer (ITIMER_REAL, &timerval, NULL);
+}
+
+static void
+stop_progress_printing_timer (void)
+{
+    struct sigaction action;
+    struct itimerval timerval;
+
+    /* Now stop the timer. */
+    timerval.it_interval.tv_sec = 0;
+    timerval.it_interval.tv_usec = 0;
+    timerval.it_value.tv_sec = 0;
+    timerval.it_value.tv_usec = 0;
+    setitimer (ITIMER_REAL, &timerval, NULL);
+
+    /* And disable the signal handler. */
+    action.sa_handler = SIG_IGN;
+    sigaction (SIGALRM, &action, NULL);
+}
+
+
+/* XXX: This should be merged with the add_files function since it
+ * shares a lot of logic with it. */
+/* Recursively count all regular files in path and all sub-directories
+ * of path.  The result is added to *count (which should be
+ * initialized to zero by the top-level caller before calling
+ * count_files). */
+static void
+count_files (const char *path, int *count, add_files_state_t *state)
+{
+    struct dirent *entry = NULL;
+    char *next;
+    struct dirent **fs_entries = NULL;
+    int num_fs_entries = scandir (path, &fs_entries, 0, dirent_sort_inode);
+    int entry_type, i;
+
+    if (num_fs_entries == -1) {
+       fprintf (stderr, "Warning: failed to open directory %s: %s\n",
+                path, strerror (errno));
+       goto DONE;
+    }
+
+    for (i = 0; i < num_fs_entries && ! interrupted; i++) {
+       entry = fs_entries[i];
+
+       /* Ignore special directories to avoid infinite recursion.
+        * Also ignore the .notmuch directory.
+        */
+       if (_special_directory (entry->d_name) ||
+           strcmp (entry->d_name, ".notmuch") == 0)
+           continue;
+
+       /* Ignore any files/directories the user has configured to be
+        * ignored
+        */
+       if (_entry_in_ignore_list (state, path, entry->d_name)) {
+           if (state->debug)
+               printf ("(D) count_files: explicitly ignoring %s/%s\n",
+                       path, entry->d_name);
+           continue;
+       }
+
+       if (asprintf (&next, "%s/%s", path, entry->d_name) == -1) {
+           next = NULL;
+           fprintf (stderr, "Error descending from %s to %s: Out of memory\n",
+                    path, entry->d_name);
+           continue;
+       }
+
+       entry_type = dirent_type (path, entry);
+       if (entry_type == S_IFREG) {
+           *count = *count + 1;
+           if (*count % 1000 == 0 && state->verbosity >= VERBOSITY_NORMAL) {
+               printf ("Found %d files so far.\r", *count);
+               fflush (stdout);
+           }
+       } else if (entry_type == S_IFDIR) {
+           count_files (next, count, state);
+       }
+
+       free (next);
+    }
+
+  DONE:
+    if (fs_entries) {
+       for (i = 0; i < num_fs_entries; i++)
+           free (fs_entries[i]);
+
+       free (fs_entries);
+    }
+}
+
+static void
+upgrade_print_progress (void *closure,
+                       double progress)
+{
+    add_files_state_t *state = closure;
+
+    printf ("Upgrading database: %.2f%% complete", progress * 100.0);
+
+    if (progress > 0) {
+       struct timeval tv_now;
+       double elapsed, time_remaining;
+
+       gettimeofday (&tv_now, NULL);
+
+       elapsed = notmuch_time_elapsed (state->tv_start, tv_now);
+       time_remaining = (elapsed / progress) * (1.0 - progress);
+       printf (" (");
+       notmuch_time_print_formatted_seconds (time_remaining);
+       printf (" remaining)");
+    }
+
+    printf (".      \r");
+
+    fflush (stdout);
+}
+
+/* Remove one message filename from the database. */
+static notmuch_status_t
+remove_filename (notmuch_database_t *notmuch,
+                const char *path,
+                add_files_state_t *add_files_state)
+{
+    notmuch_status_t status;
+    notmuch_message_t *message;
+
+    status = notmuch_database_begin_atomic (notmuch);
+    if (status)
+       return status;
+    status = notmuch_database_find_message_by_filename (notmuch, path, &message);
+    if (status || message == NULL)
+       goto DONE;
+
+    status = notmuch_database_remove_message (notmuch, path);
+    if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
+       add_files_state->renamed_messages++;
+       if (add_files_state->synchronize_flags == true)
+           notmuch_message_maildir_flags_to_tags (message);
+       status = NOTMUCH_STATUS_SUCCESS;
+    } else if (status == NOTMUCH_STATUS_SUCCESS) {
+       add_files_state->removed_messages++;
+    }
+    notmuch_message_destroy (message);
+
+  DONE:
+    notmuch_database_end_atomic (notmuch);
+    return status;
+}
+
+/* Recursively remove all filenames from the database referring to
+ * 'path' (or to any of its children). */
+static notmuch_status_t
+_remove_directory (notmuch_database_t *notmuch,
+                  const char *path,
+                  add_files_state_t *add_files_state)
+{
+    notmuch_status_t status;
+    notmuch_directory_t *directory;
+    notmuch_filenames_t *files, *subdirs;
+    char *absolute;
+
+    status = notmuch_database_get_directory (notmuch, path, &directory);
+    if (status || ! directory)
+       return status;
+
+    for (files = notmuch_directory_get_child_files (directory);
+        notmuch_filenames_valid (files);
+        notmuch_filenames_move_to_next (files)) {
+       absolute = talloc_asprintf (notmuch, "%s/%s", path,
+                                   notmuch_filenames_get (files));
+       status = remove_filename (notmuch, absolute, add_files_state);
+       talloc_free (absolute);
+       if (status)
+           goto DONE;
+    }
+
+    for (subdirs = notmuch_directory_get_child_directories (directory);
+        notmuch_filenames_valid (subdirs);
+        notmuch_filenames_move_to_next (subdirs)) {
+       absolute = talloc_asprintf (notmuch, "%s/%s", path,
+                                   notmuch_filenames_get (subdirs));
+       status = _remove_directory (notmuch, absolute, add_files_state);
+       talloc_free (absolute);
+       if (status)
+           goto DONE;
+    }
+
+    status = notmuch_directory_delete (directory);
+
+  DONE:
+    if (status)
+       notmuch_directory_destroy (directory);
+    return status;
+}
+
+static void
+print_results (const add_files_state_t *state)
+{
+    double elapsed;
+    struct timeval tv_now;
+
+    gettimeofday (&tv_now, NULL);
+    elapsed = notmuch_time_elapsed (state->tv_start, tv_now);
+
+    if (state->processed_files) {
+       printf ("Processed %d %s in ", state->processed_files,
+               state->processed_files == 1 ? "file" : "total files");
+       notmuch_time_print_formatted_seconds (elapsed);
+       if (elapsed > 1)
+           printf (" (%d files/sec.)",
+                   (int) (state->processed_files / elapsed));
+       printf (".%s\n", (state->output_is_a_tty) ? "\033[K" : "");
+    }
+
+    if (state->added_messages)
+       printf ("Added %d new %s to the database.", state->added_messages,
+               state->added_messages == 1 ? "message" : "messages");
+    else
+       printf ("No new mail.");
+
+    if (state->removed_messages)
+       printf (" Removed %d %s.", state->removed_messages,
+               state->removed_messages == 1 ? "message" : "messages");
+
+    if (state->renamed_messages)
+       printf (" Detected %d file %s.", state->renamed_messages,
+               state->renamed_messages == 1 ? "rename" : "renames");
+
+    printf ("\n");
+}
+
+static int
+_maybe_upgrade (notmuch_database_t *notmuch, add_files_state_t *state)
+{
+    if (notmuch_database_needs_upgrade (notmuch)) {
+       time_t now = time (NULL);
+       struct tm *gm_time = gmtime (&now);
+       int err;
+       notmuch_status_t status;
+       const char *backup_dir = notmuch_config_get (notmuch, NOTMUCH_CONFIG_BACKUP_DIR);
+       const char *backup_name;
+
+       err = mkdir (backup_dir, 0755);
+       if (err && errno != EEXIST) {
+           fprintf (stderr, "Failed to create %s: %s\n", backup_dir, strerror (errno));
+           return EXIT_FAILURE;
+       }
+
+       /* since dump files are written atomically, the amount of
+        * harm from overwriting one within a second seems
+        * relatively small. */
+       backup_name = talloc_asprintf (notmuch, "%s/dump-%04d%02d%02dT%02d%02d%02d.gz",
+                                      backup_dir,
+                                      gm_time->tm_year + 1900,
+                                      gm_time->tm_mon + 1,
+                                      gm_time->tm_mday,
+                                      gm_time->tm_hour,
+                                      gm_time->tm_min,
+                                      gm_time->tm_sec);
+
+       if (state->verbosity >= VERBOSITY_NORMAL) {
+           printf ("Welcome to a new version of notmuch! Your database will now be upgraded.\n");
+           printf ("This process is safe to interrupt.\n");
+           printf ("Backing up tags to %s...\n", backup_name);
+       }
+
+       if (notmuch_database_dump (notmuch, backup_name, "",
+                                  DUMP_FORMAT_BATCH_TAG, DUMP_INCLUDE_DEFAULT, true)) {
+           fprintf (stderr, "Backup failed. Aborting upgrade.");
+           return EXIT_FAILURE;
+       }
+
+       gettimeofday (&state->tv_start, NULL);
+       status = notmuch_database_upgrade (
+           notmuch,
+           state->verbosity >= VERBOSITY_NORMAL ? upgrade_print_progress : NULL,
+           state);
+       if (status) {
+           printf ("Upgrade failed: %s\n",
+                   notmuch_status_to_string (status));
+           notmuch_database_destroy (notmuch);
+           return EXIT_FAILURE;
+       }
+       if (state->verbosity >= VERBOSITY_NORMAL)
+           printf ("Your notmuch database has now been upgraded.\n");
+    }
+    return EXIT_SUCCESS;
+}
+
+int
+notmuch_new_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    add_files_state_t add_files_state = {
+       .verbosity = VERBOSITY_NORMAL,
+       .debug = false,
+       .full_scan = false,
+       .output_is_a_tty = isatty (fileno (stdout)),
+    };
+    struct timeval tv_start;
+    int ret = 0;
+    const char *db_path, *mail_root;
+    struct sigaction action;
+    _filename_node_t *f;
+    int opt_index;
+    unsigned int i;
+    bool timer_is_active = false;
+    bool hooks = true;
+    bool quiet = false, verbose = false;
+    notmuch_status_t status;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_bool = &quiet, .name = "quiet" },
+       { .opt_bool = &verbose, .name = "verbose" },
+       { .opt_bool = &add_files_state.debug, .name = "debug" },
+       { .opt_bool = &add_files_state.full_scan, .name = "full-scan" },
+       { .opt_bool = &hooks, .name = "hooks" },
+       { .opt_inherit = notmuch_shared_indexing_options },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    /* quiet trumps verbose */
+    if (quiet)
+       add_files_state.verbosity = VERBOSITY_QUIET;
+    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 (
+           "notmuch new",
+           notmuch,
+           notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+                                    &add_files_state.synchronize_flags)))
+       return EXIT_FAILURE;
+
+    db_path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH);
+    add_files_state.db_path = db_path;
+
+    mail_root = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT);
+    add_files_state.mail_root = mail_root;
+
+    if (! _setup_ignore (notmuch, &add_files_state))
+       return EXIT_FAILURE;
+
+    for (notmuch_config_values_start (add_files_state.new_tags);
+        notmuch_config_values_valid (add_files_state.new_tags);
+        notmuch_config_values_move_to_next (add_files_state.new_tags)) {
+       const char *tag, *error_msg;
+
+       tag = notmuch_config_values_get (add_files_state.new_tags);
+       error_msg = illegal_tag (tag, false);
+       if (error_msg) {
+           fprintf (stderr, "Error: tag '%s' in new.tags: %s\n", tag, error_msg);
+           return EXIT_FAILURE;
+       }
+    }
+
+    if (hooks) {
+       /* Drop write lock to run hook */
+       status = notmuch_database_reopen (notmuch, NOTMUCH_DATABASE_MODE_READ_ONLY);
+       if (print_status_database ("notmuch new", notmuch, status))
+           return EXIT_FAILURE;
+
+       ret = notmuch_run_hook (notmuch, "pre-new");
+       if (ret)
+           return EXIT_FAILURE;
+
+       /* acquire write lock again */
+       status = notmuch_database_reopen (notmuch, NOTMUCH_DATABASE_MODE_READ_WRITE);
+       if (print_status_database ("notmuch new", notmuch, status))
+           return EXIT_FAILURE;
+    }
+
+    if (notmuch_database_get_revision (notmuch, NULL) == 0) {
+       int count = 0;
+       count_files (mail_root, &count, &add_files_state);
+       if (interrupted)
+           return EXIT_FAILURE;
+
+       if (add_files_state.verbosity >= VERBOSITY_NORMAL)
+           printf ("Found %d total files (that's not much mail).\n", count);
+
+       add_files_state.total_files = count;
+    } else {
+       if (_maybe_upgrade (notmuch, &add_files_state))
+           return EXIT_FAILURE;
+
+       add_files_state.total_files = 0;
+    }
+
+    if (notmuch == NULL)
+       return EXIT_FAILURE;
+
+    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));
+       return EXIT_FAILURE;
+    }
+
+    /* Set up our handler for SIGINT. We do this after having
+     * potentially done a database upgrade we this interrupt handler
+     * won't support. */
+    memset (&action, 0, sizeof (struct sigaction));
+    action.sa_handler = handle_sigint;
+    sigemptyset (&action.sa_mask);
+    action.sa_flags = SA_RESTART;
+    sigaction (SIGINT, &action, NULL);
+
+    gettimeofday (&add_files_state.tv_start, NULL);
+
+    add_files_state.removed_files = _filename_list_create (notmuch);
+    add_files_state.removed_directories = _filename_list_create (notmuch);
+    add_files_state.directory_mtimes = _filename_list_create (notmuch);
+
+    if (add_files_state.verbosity == VERBOSITY_NORMAL &&
+       add_files_state.output_is_a_tty && ! debugger_is_active ()) {
+       setup_progress_printing_timer ();
+       timer_is_active = true;
+    }
+
+    ret = add_files (notmuch, mail_root, &add_files_state);
+    if (ret)
+       goto DONE;
+
+    gettimeofday (&tv_start, NULL);
+    for (f = add_files_state.removed_files->head; f && ! interrupted; f = f->next) {
+       ret = remove_filename (notmuch, f->filename, &add_files_state);
+       if (ret)
+           goto DONE;
+       if (do_print_progress) {
+           do_print_progress = 0;
+           generic_print_progress ("Cleaned up", "messages",
+                                   tv_start, add_files_state.removed_messages +
+                                   add_files_state.renamed_messages,
+                                   add_files_state.removed_files->count);
+       }
+    }
+
+    gettimeofday (&tv_start, NULL);
+    for (f = add_files_state.removed_directories->head, i = 0; f && ! interrupted; f = f->next, i++) {
+       ret = _remove_directory (notmuch, f->filename, &add_files_state);
+       if (ret)
+           goto DONE;
+       if (do_print_progress) {
+           do_print_progress = 0;
+           generic_print_progress ("Cleaned up", "directories",
+                                   tv_start, i,
+                                   add_files_state.removed_directories->count);
+       }
+    }
+
+    for (f = add_files_state.directory_mtimes->head; f && ! interrupted; f = f->next) {
+       notmuch_directory_t *directory;
+       status = notmuch_database_get_directory (notmuch, f->filename, &directory);
+       if (status == NOTMUCH_STATUS_SUCCESS && directory) {
+           notmuch_directory_set_mtime (directory, f->mtime);
+           notmuch_directory_destroy (directory);
+       }
+    }
+
+  DONE:
+    talloc_free (add_files_state.removed_files);
+    talloc_free (add_files_state.removed_directories);
+    talloc_free (add_files_state.directory_mtimes);
+
+    if (timer_is_active)
+       stop_progress_printing_timer ();
+
+    if (add_files_state.verbosity >= VERBOSITY_NORMAL)
+       print_results (&add_files_state);
+
+    if (ret)
+       fprintf (stderr, "Note: A fatal error was encountered: %s\n",
+                notmuch_status_to_string (ret));
+
+    notmuch_database_close (notmuch);
+
+    if (hooks && ! ret && ! interrupted)
+       ret = notmuch_run_hook (notmuch, "post-new");
+
+    notmuch_database_destroy (notmuch);
+
+    if (ret || interrupted)
+       return EXIT_FAILURE;
+
+    if (add_files_state.vanished_files)
+       return NOTMUCH_EXIT_TEMPFAIL;
+
+    return EXIT_SUCCESS;
+}
diff --git a/notmuch-reindex.c b/notmuch-reindex.c
new file mode 100644 (file)
index 0000000..e9a6545
--- /dev/null
@@ -0,0 +1,137 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2016 Daniel Kahn Gillmor
+ *
+ * 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: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+ */
+
+#include "notmuch-client.h"
+#include "string-util.h"
+
+static volatile sig_atomic_t interrupted;
+
+static void
+handle_sigint (unused (int sig))
+{
+    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
+     * fail or produce a short write, we want to get out of the signal
+     * handler as quickly as possible, not retry it. */
+    IGNORE_RESULT (write (2, msg, sizeof (msg) - 1));
+    interrupted = 1;
+}
+
+/* reindex all messages matching 'query_string' using the passed-in indexopts
+ */
+static int
+reindex_query (notmuch_database_t *notmuch, const char *query_string,
+              notmuch_indexopts_t *indexopts)
+{
+    notmuch_query_t *query;
+    notmuch_messages_t *messages;
+    notmuch_message_t *message;
+    notmuch_status_t status;
+
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+    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);
+
+    status = notmuch_query_search_messages (query, &messages);
+    if (print_status_query ("notmuch reindex", query, status))
+       return status;
+
+    ret = notmuch_database_begin_atomic (notmuch);
+    for (;
+        notmuch_messages_valid (messages) && ! interrupted;
+        notmuch_messages_move_to_next (messages)) {
+       message = notmuch_messages_get (messages);
+
+       ret = notmuch_message_reindex (message, indexopts);
+       if (ret != NOTMUCH_STATUS_SUCCESS)
+           break;
+       notmuch_message_destroy (message);
+    }
+
+    if (! ret)
+       ret = notmuch_database_end_atomic (notmuch);
+
+    notmuch_query_destroy (query);
+
+    return ret || interrupted;
+}
+
+int
+notmuch_reindex_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    char *query_string = NULL;
+    struct sigaction action;
+    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));
+    action.sa_handler = handle_sigint;
+    sigemptyset (&action.sa_mask);
+    action.sa_flags = SA_RESTART;
+    sigaction (SIGINT, &action, NULL);
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_inherit = notmuch_shared_indexing_options },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    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));
+       return EXIT_FAILURE;
+    }
+
+    query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
+    if (query_string == NULL) {
+       fprintf (stderr, "Out of memory\n");
+       return EXIT_FAILURE;
+    }
+
+    if (*query_string == '\0') {
+       fprintf (stderr, "Error: notmuch reindex requires at least one search term.\n");
+       return EXIT_FAILURE;
+    }
+
+    ret = reindex_query (notmuch, query_string, indexopts);
+
+    notmuch_database_destroy (notmuch);
+
+    return ret || interrupted ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/notmuch-reply.c b/notmuch-reply.c
new file mode 100644 (file)
index 0000000..4429725
--- /dev/null
@@ -0,0 +1,781 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ * Copyright © 2009 Keith Packard
+ *
+ * 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/ .
+ *
+ * Authors: Carl Worth <cworth@cworth.org>
+ *         Keith Packard <keithp@keithp.com>
+ */
+
+#include "notmuch-client.h"
+#include "string-util.h"
+#include "sprinter.h"
+
+static void
+show_reply_headers (GMimeStream *stream, GMimeMessage *message)
+{
+    /* Output RFC 2822 formatted (and RFC 2047 encoded) headers. */
+    if (g_mime_object_write_to_stream (GMIME_OBJECT (message), NULL, stream) < 0) {
+       INTERNAL_ERROR ("failed to write headers to stdout\n");
+    }
+}
+
+static void
+format_part_reply (GMimeStream *stream, mime_node_t *node)
+{
+    int i;
+
+    if (node->envelope_file) {
+       g_mime_stream_printf (stream, "On %s, %s wrote:\n",
+                             notmuch_message_get_header (node->envelope_file, "date"),
+                             notmuch_message_get_header (node->envelope_file, "from"));
+    } else if (GMIME_IS_MESSAGE (node->part)) {
+       GMimeMessage *message = GMIME_MESSAGE (node->part);
+       char *recipients_string;
+
+       g_mime_stream_printf (stream, "> From: %s\n", g_mime_message_get_from_string (message));
+       recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_TO);
+       if (recipients_string)
+           g_mime_stream_printf (stream, "> To: %s\n",
+                                 recipients_string);
+       g_free (recipients_string);
+       recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_CC);
+       if (recipients_string)
+           g_mime_stream_printf (stream, "> Cc: %s\n",
+                                 recipients_string);
+       g_free (recipients_string);
+       g_mime_stream_printf (stream, "> Subject: %s\n", g_mime_message_get_subject (message));
+       g_mime_stream_printf (stream, "> Date: %s\n", g_mime_message_get_date_string (node, message));
+       g_mime_stream_printf (stream, ">\n");
+    } else if (GMIME_IS_PART (node->part)) {
+       GMimeContentType *content_type = g_mime_object_get_content_type (node->part);
+       GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (node->part);
+
+       if (g_mime_content_type_is_type (content_type, "application", "pgp-encrypted") ||
+           g_mime_content_type_is_type (content_type, "application", "pgp-signature") ||
+           g_mime_content_type_is_type (content_type, "application", "pkcs7-mime")) {
+           /* Ignore PGP/MIME and S/MIME cruft parts */
+       } else if (g_mime_content_type_is_type (content_type, "text", "*") &&
+                  ! g_mime_content_type_is_type (content_type, "text", "html")) {
+           show_text_part_content (node->part, stream, NOTMUCH_SHOW_TEXT_PART_REPLY);
+       } else if (disposition &&
+                  strcasecmp (g_mime_content_disposition_get_disposition (disposition),
+                              GMIME_DISPOSITION_ATTACHMENT) == 0) {
+           const char *filename = g_mime_part_get_filename (GMIME_PART (node->part));
+           g_mime_stream_printf (stream, "Attachment: %s (%s)\n", filename,
+                                 g_mime_content_type_get_mime_type (content_type));
+       } else {
+           g_mime_stream_printf (stream, "Non-text part: %s\n",
+                                 g_mime_content_type_get_mime_type (content_type));
+       }
+    }
+
+    for (i = 0; i < node->nchildren; i++)
+       format_part_reply (stream, mime_node_child (node, i));
+}
+
+typedef enum {
+    USER_ADDRESS_IN_STRING,
+    STRING_IN_USER_ADDRESS,
+    STRING_IS_USER_ADDRESS,
+} address_match_t;
+
+/* Match given string against given address according to mode. */
+static bool
+match_address (const char *str, const char *address, address_match_t mode)
+{
+    switch (mode) {
+    case USER_ADDRESS_IN_STRING:
+       return strcasestr (str, address) != NULL;
+    case STRING_IN_USER_ADDRESS:
+       return strcasestr (address, str) != NULL;
+    case STRING_IS_USER_ADDRESS:
+       return strcasecmp (address, str) == 0;
+    }
+
+    return false;
+}
+
+/* Match given string against user's configured "primary" and "other"
+ * addresses according to mode. */
+static const char *
+address_match (const char *str, notmuch_database_t *notmuch, address_match_t mode)
+{
+    const char *primary;
+    notmuch_config_values_t *other = NULL;
+
+    if (! str || *str == '\0')
+       return NULL;
+
+    primary = notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL);
+    if (match_address (str, primary, mode))
+       return primary;
+
+    for (other = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_OTHER_EMAIL);
+        notmuch_config_values_valid (other);
+        notmuch_config_values_move_to_next (other)) {
+       const char *addr = notmuch_config_values_get (other);
+
+       if (match_address (str, addr, mode))
+           return addr;
+    }
+    return NULL;
+}
+
+/* Does the given string contain an address configured as one of the
+ * user's "primary" or "other" addresses. If so, return the matching
+ * address, NULL otherwise. */
+static const char *
+user_address_in_string (const char *str, notmuch_database_t *notmuch)
+{
+    return address_match (str, notmuch, USER_ADDRESS_IN_STRING);
+}
+
+/* Do any of the addresses configured as one of the user's "primary"
+ * or "other" addresses contain the given string. If so, return the
+ * matching address, NULL otherwise. */
+static const char *
+string_in_user_address (const char *str, notmuch_database_t *notmuch)
+{
+    return address_match (str, notmuch, STRING_IN_USER_ADDRESS);
+}
+
+/* Is the given address configured as one of the user's "primary" or
+ * "other" addresses. */
+static bool
+address_is_users (const char *address, notmuch_database_t *notmuch)
+{
+    return address_match (address, notmuch, STRING_IS_USER_ADDRESS) != NULL;
+}
+
+/* Scan addresses in 'list'.
+ *
+ * If 'message' is non-NULL, then for each address in 'list' that is
+ * not configured as one of the user's addresses in 'config', add that
+ * address to 'message' as an address of 'type'.
+ *
+ * If 'user_from' is non-NULL and *user_from is NULL, *user_from will
+ * be set to the first address encountered in 'list' that is the
+ * user's address.
+ *
+ * Return the number of addresses added to 'message'. (If 'message' is
+ * NULL, the function returns 0 by definition.)
+ */
+static unsigned int
+scan_address_list (InternetAddressList *list,
+                  notmuch_database_t *notmuch,
+                  GMimeMessage *message,
+                  GMimeAddressType type,
+                  const char **user_from)
+{
+    InternetAddress *address;
+    int i;
+    unsigned int n = 0;
+
+    if (list == NULL)
+       return 0;
+
+    for (i = 0; i < internet_address_list_length (list); i++) {
+       address = internet_address_list_get_address (list, i);
+       if (INTERNET_ADDRESS_IS_GROUP (address)) {
+           InternetAddressGroup *group;
+           InternetAddressList *group_list;
+
+           group = INTERNET_ADDRESS_GROUP (address);
+           group_list = internet_address_group_get_members (group);
+           n += scan_address_list (group_list, notmuch, message, type, user_from);
+       } else {
+           InternetAddressMailbox *mailbox;
+           const char *name;
+           const char *addr;
+
+           mailbox = INTERNET_ADDRESS_MAILBOX (address);
+
+           name = internet_address_get_name (address);
+           addr = internet_address_mailbox_get_addr (mailbox);
+
+           if (address_is_users (addr, notmuch)) {
+               if (user_from && *user_from == NULL)
+                   *user_from = addr;
+           } else if (message) {
+               g_mime_message_add_mailbox (message, type, name, addr);
+               n++;
+           }
+       }
+    }
+
+    return n;
+}
+
+/* Does the address in the Reply-To header of 'message' already appear
+ * in either the 'To' or 'Cc' header of the message?
+ */
+static bool
+reply_to_header_is_redundant (GMimeMessage *message,
+                             InternetAddressList *reply_to_list)
+{
+    const char *addr, *reply_to;
+    InternetAddress *address;
+    InternetAddressMailbox *mailbox;
+    InternetAddressList *recipients;
+    bool ret = false;
+    int i;
+
+    if (reply_to_list == NULL ||
+       internet_address_list_length (reply_to_list) != 1)
+       return 0;
+
+    address = internet_address_list_get_address (reply_to_list, 0);
+    if (INTERNET_ADDRESS_IS_GROUP (address))
+       return 0;
+
+    mailbox = INTERNET_ADDRESS_MAILBOX (address);
+    reply_to = internet_address_mailbox_get_addr (mailbox);
+
+    recipients = g_mime_message_get_all_recipients (message);
+
+    for (i = 0; i < internet_address_list_length (recipients); i++) {
+       address = internet_address_list_get_address (recipients, i);
+       if (INTERNET_ADDRESS_IS_GROUP (address))
+           continue;
+
+       mailbox = INTERNET_ADDRESS_MAILBOX (address);
+       addr = internet_address_mailbox_get_addr (mailbox);
+       if (strcmp (addr, reply_to) == 0) {
+           ret = true;
+           break;
+       }
+    }
+
+    g_object_unref (G_OBJECT (recipients));
+
+    return ret;
+}
+
+static InternetAddressList *
+get_sender (GMimeMessage *message)
+{
+    InternetAddressList *reply_to_list;
+
+    reply_to_list = g_mime_message_get_reply_to_list (message);
+    if (reply_to_list &&
+       internet_address_list_length (reply_to_list) > 0) {
+       /*
+        * Some mailing lists munge the Reply-To header despite it
+        * being A Bad Thing, see
+        * http://marc.merlins.org/netrants/reply-to-harmful.html
+        *
+        * The munging is easy to detect, because it results in a
+        * redundant reply-to header, (with an address that already
+        * exists in either To or Cc). So in this case, we ignore the
+        * Reply-To field and use the From header. This ensures the
+        * original sender will get the reply even if not subscribed
+        * to the list. Note that the address in the Reply-To header
+        * will always appear in the reply if reply_all is true.
+        */
+       if (! reply_to_header_is_redundant (message, reply_to_list))
+           return reply_to_list;
+    }
+
+    return g_mime_message_get_from (message);
+}
+
+static InternetAddressList *
+get_to (GMimeMessage *message)
+{
+    return g_mime_message_get_addresses (message, GMIME_ADDRESS_TYPE_TO);
+}
+
+static InternetAddressList *
+get_cc (GMimeMessage *message)
+{
+    return g_mime_message_get_addresses (message, GMIME_ADDRESS_TYPE_CC);
+}
+
+static InternetAddressList *
+get_bcc (GMimeMessage *message)
+{
+    return g_mime_message_get_addresses (message, GMIME_ADDRESS_TYPE_BCC);
+}
+
+/* Augment the recipients of 'reply' from the "Reply-to:", "From:",
+ * "To:", "Cc:", and "Bcc:" headers of 'message'.
+ *
+ * If 'reply_all' is true, use sender and all recipients, otherwise
+ * scan the headers for the first that contains something other than
+ * the user's addresses and add the recipients from this header
+ * (typically this would be reply-to-sender, but also handles reply to
+ * user's own message in a sensible way).
+ *
+ * If any of the user's addresses were found in these headers, the
+ * first of these returned, otherwise NULL is returned.
+ */
+static const char *
+add_recipients_from_message (GMimeMessage *reply,
+                            notmuch_database_t *notmuch,
+                            GMimeMessage *message,
+                            bool reply_all)
+{
+    struct {
+       InternetAddressList * (*get_header)(GMimeMessage *message);
+       GMimeAddressType recipient_type;
+    } reply_to_map[] = {
+       { get_sender,   GMIME_ADDRESS_TYPE_TO },
+       { get_to,       GMIME_ADDRESS_TYPE_TO },
+       { get_cc,       GMIME_ADDRESS_TYPE_CC },
+       { get_bcc,      GMIME_ADDRESS_TYPE_BCC },
+    };
+    const char *from_addr = NULL;
+    unsigned int i;
+    unsigned int n = 0;
+
+    for (i = 0; i < ARRAY_SIZE (reply_to_map); i++) {
+       InternetAddressList *recipients;
+
+       recipients = reply_to_map[i].get_header (message);
+
+       n += scan_address_list (recipients, notmuch, reply,
+                               reply_to_map[i].recipient_type, &from_addr);
+
+       if (! reply_all && n) {
+           /* Stop adding new recipients in reply-to-sender mode if
+            * we have added some recipient(s) above.
+            *
+            * This also handles the case of user replying to his own
+            * message, where reply-to/from is not a recipient. In
+            * this case there may be more than one recipient even if
+            * not replying to all.
+            */
+           reply = NULL;
+
+           /* From address and some recipients are enough, bail out. */
+           if (from_addr)
+               break;
+       }
+    }
+
+    /* If no recipients were added but we found one of the user's
+     * addresses to use as a from address then the message is from the
+     * user to the user - add the discovered from address to the list
+     * of recipients so that the reply goes back to the user.
+     */
+    if (n == 0 && from_addr)
+       g_mime_message_add_mailbox (reply, GMIME_ADDRESS_TYPE_TO, NULL, from_addr);
+
+    return from_addr;
+}
+
+/*
+ * Look for the user's address in " for <email@add.res>" in the
+ * received headers.
+ *
+ * Return the address that was found, if any, and NULL otherwise.
+ */
+static const char *
+guess_from_in_received_for (notmuch_database_t *notmuch, const char *received)
+{
+    const char *ptr;
+
+    ptr = strstr (received, " for ");
+    if (! ptr)
+       return NULL;
+
+    return user_address_in_string (ptr, notmuch);
+}
+
+/*
+ * Parse all the " by MTA ..." parts in received headers to guess the
+ * email address that this was originally delivered to.
+ *
+ * Extract just the MTA here by removing leading whitespace and
+ * assuming that the MTA name ends at the next whitespace. Test for
+ * *(by+4) to be non-'\0' to make sure there's something there at all
+ * - and then assume that the first whitespace delimited token that
+ * follows is the receiving system in this step of the receive chain.
+ *
+ * Return the address that was found, if any, and NULL otherwise.
+ */
+static const char *
+guess_from_in_received_by (notmuch_database_t *notmuch, const char *received)
+{
+    const char *addr;
+    const char *by = received;
+    char *domain, *tld, *mta, *ptr, *token;
+
+    while ((by = strstr (by, " by ")) != NULL) {
+       by += 4;
+       if (*by == '\0')
+           break;
+       mta = xstrdup (by);
+       token = strtok (mta, " \t");
+       if (token == NULL) {
+           free (mta);
+           break;
+       }
+       /*
+        * Now extract the last two components of the MTA host name as
+        * domain and tld.
+        */
+       domain = tld = NULL;
+       while ((ptr = strsep (&token, ". \t")) != NULL) {
+           if (*ptr == '\0')
+               continue;
+           domain = tld;
+           tld = ptr;
+       }
+
+       if (domain) {
+           /*
+            * Recombine domain and tld and look for it among the
+            * configured email addresses. This time we have a known
+            * domain name and nothing else - so the test is the other
+            * way around: we check if this is a substring of one of
+            * the email addresses.
+            */
+           *(tld - 1) = '.';
+
+           addr = string_in_user_address (domain, notmuch);
+           if (addr) {
+               free (mta);
+               return addr;
+           }
+       }
+       free (mta);
+    }
+
+    return NULL;
+}
+
+/*
+ * Get the concatenated Received: headers and search from the front
+ * (last Received: header added) and try to extract from them
+ * indications to which email address this message was delivered.
+ *
+ * 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.
+ */
+static const char *
+guess_from_in_received_headers (notmuch_message_t *message)
+{
+    const char *received, *addr;
+    char *sanitized;
+
+    notmuch_database_t *notmuch = notmuch_message_get_database (message);
+
+    received = notmuch_message_get_header (message, "received");
+    if (! received)
+       return NULL;
+
+    sanitized = sanitize_string (NULL, received);
+    if (! sanitized)
+       return NULL;
+
+    addr = guess_from_in_received_for (notmuch, sanitized);
+    if (! addr)
+       addr = guess_from_in_received_by (notmuch, sanitized);
+
+    talloc_free (sanitized);
+
+    return addr;
+}
+
+/*
+ * Try to find user's email address in one of the extra To-like
+ * 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 *
+get_from_in_to_headers (notmuch_message_t *message)
+{
+    size_t i;
+    const char *tohdr, *addr;
+    const char *to_headers[] = {
+       "Envelope-to",
+       "X-Original-To",
+       "Delivered-To",
+    };
+
+    notmuch_database_t *notmuch = notmuch_message_get_database (message);
+
+    for (i = 0; i < ARRAY_SIZE (to_headers); i++) {
+       tohdr = notmuch_message_get_header (message, to_headers[i]);
+
+       /* Note: tohdr potentially contains a list of email addresses. */
+       addr = user_address_in_string (tohdr, notmuch);
+       if (addr)
+           return addr;
+    }
+
+    return NULL;
+}
+
+static GMimeMessage *
+create_reply_message (void *ctx,
+                     notmuch_message_t *message,
+                     GMimeMessage *mime_message,
+                     bool reply_all,
+                     bool limited)
+{
+    const char *subject, *from_addr = NULL;
+    const char *in_reply_to, *orig_references, *references;
+    notmuch_database_t *notmuch = notmuch_message_get_database (message);
+    /*
+     * Use the below header order for limited headers, "pretty" order
+     * otherwise.
+     */
+    GMimeMessage *reply = g_mime_message_new (limited ? 0 : 1);
+
+    if (reply == NULL) {
+       fprintf (stderr, "Out of memory\n");
+       return NULL;
+    }
+
+    in_reply_to = talloc_asprintf (ctx, "<%s>",
+                                  notmuch_message_get_message_id (message));
+
+    g_mime_object_set_header (GMIME_OBJECT (reply), "In-Reply-To", in_reply_to, NULL);
+
+    orig_references = notmuch_message_get_header (message, "references");
+    if (orig_references && *orig_references)
+       references = talloc_asprintf (ctx, "%s %s", orig_references,
+                                     in_reply_to);
+    else
+       references = talloc_strdup (ctx, in_reply_to);
+
+    g_mime_object_set_header (GMIME_OBJECT (reply), "References", references, NULL);
+
+    from_addr = add_recipients_from_message (reply, notmuch,
+                                            mime_message, reply_all);
+
+    /* The above is all that is needed for limited headers. */
+    if (limited)
+       return reply;
+
+    /*
+     * Sadly, there is no standard way to find out to which email
+     * address a mail was delivered - what is in the headers depends
+     * on the MTAs used along the way.
+     *
+     * If none of the user's email addresses are in the To: or Cc:
+     * headers, we try a number of heuristics which hopefully will
+     * answer this question.
+     *
+     * First, check for Envelope-To:, X-Original-To:, and
+     * Delivered-To: headers.
+     */
+    if (from_addr == NULL)
+       from_addr = get_from_in_to_headers (message);
+
+    /*
+     * Check for a (for <email@add.res>) clause in Received: headers,
+     * and the domain part of known email addresses in the 'by' part
+     * of Received: headers
+     */
+    if (from_addr == NULL)
+       from_addr = guess_from_in_received_headers (message);
+
+    /* Default to user's primary address. */
+    if (from_addr == NULL)
+       from_addr = notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL);
+
+    from_addr = talloc_asprintf (ctx, "%s <%s>",
+                                notmuch_config_get (notmuch, NOTMUCH_CONFIG_USER_NAME),
+                                from_addr);
+    g_mime_object_set_header (GMIME_OBJECT (reply), "From", from_addr, NULL);
+
+    subject = g_mime_message_get_subject (mime_message);
+    if (subject) {
+       if (strncasecmp (subject, "Re:", 3))
+           subject = talloc_asprintf (ctx, "Re: %s", subject);
+       g_mime_message_set_subject (reply, subject, NULL);
+    }
+
+    return reply;
+}
+
+enum {
+    FORMAT_DEFAULT,
+    FORMAT_JSON,
+    FORMAT_SEXP,
+    FORMAT_HEADERS_ONLY,
+};
+
+static int
+do_reply (notmuch_database_t *notmuch,
+         notmuch_query_t *query,
+         notmuch_show_params_t *params,
+         int format,
+         bool reply_all)
+{
+    GMimeMessage *reply;
+    mime_node_t *node;
+    notmuch_messages_t *messages;
+    notmuch_message_t *message;
+    notmuch_status_t status;
+    struct sprinter *sp = NULL;
+
+    if (format == FORMAT_JSON || format == FORMAT_SEXP) {
+       unsigned count;
+
+       status = notmuch_query_count_messages (query, &count);
+       if (print_status_query ("notmuch reply", query, status))
+           return 1;
+
+       if (count != 1) {
+           fprintf (stderr,
+                    "Error: search term did not match precisely one message (matched %u messages).\n",
+                    count);
+           return 1;
+       }
+
+       if (format == FORMAT_JSON)
+           sp = sprinter_json_create (notmuch, stdout);
+       else
+           sp = sprinter_sexp_create (notmuch, stdout);
+    }
+
+    status = notmuch_query_search_messages (query, &messages);
+    if (print_status_query ("notmuch reply", query, status))
+       return 1;
+
+    for (;
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages)) {
+       message = notmuch_messages_get (messages);
+
+       if (mime_node_open (notmuch, message, params->duplicate, &params->crypto, &node))
+           return 1;
+
+       reply = create_reply_message (notmuch, message,
+                                     GMIME_MESSAGE (node->part), reply_all,
+                                     format == FORMAT_HEADERS_ONLY);
+       if (! reply)
+           return 1;
+
+       if (format == FORMAT_JSON || format == FORMAT_SEXP) {
+           sp->begin_map (sp);
+
+           /* The headers of the reply message we've created */
+           sp->map_key (sp, "reply-headers");
+           /* FIXME: send msg_crypto here to avoid killing the
+            * subject line on reply to encrypted messages! */
+           format_headers_sprinter (sp, reply, true, NULL);
+
+           /* Start the original */
+           sp->map_key (sp, "original");
+           format_part_sprinter (notmuch, sp, node, params->duplicate, true, false);
+
+           /* End */
+           sp->end (sp);
+       } else {
+           GMimeStream *stream_stdout = stream_stdout = g_mime_stream_stdout_new ();
+           if (stream_stdout) {
+               show_reply_headers (stream_stdout, reply);
+               if (format == FORMAT_DEFAULT)
+                   format_part_reply (stream_stdout, node);
+           }
+           g_mime_stream_flush (stream_stdout);
+           g_object_unref (stream_stdout);
+       }
+
+       g_object_unref (G_OBJECT (reply));
+       talloc_free (node);
+
+       notmuch_message_destroy (message);
+    }
+
+    return 0;
+}
+
+int
+notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    notmuch_query_t *query;
+    char *query_string;
+    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 =
+             (notmuch_keyword_t []){ { "default", FORMAT_DEFAULT },
+                                     { "json", FORMAT_JSON },
+                                     { "sexp", FORMAT_SEXP },
+                                     { "headers-only", FORMAT_HEADERS_ONLY },
+                                     { 0, 0 } } },
+       { .opt_int = &notmuch_format_version, .name = "format-version" },
+       { .opt_keyword = &reply_all, .name = "reply-to", .keywords =
+             (notmuch_keyword_t []){ { "all", true },
+                                     { "sender", false },
+                                     { 0, 0 } } },
+       { .opt_keyword = (int *) (&params.crypto.decrypt), .name = "decrypt",
+         .keyword_no_arg_value = "true", .keywords =
+             (notmuch_keyword_t []){ { "false", NOTMUCH_DECRYPT_FALSE },
+                                     { "auto", NOTMUCH_DECRYPT_AUTO },
+                                     { "true", NOTMUCH_DECRYPT_NOSTASH },
+                                     { 0, 0 } } },
+       { .opt_int = &params.duplicate, .name = "duplicate" },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    notmuch_exit_if_unsupported_format ();
+
+    query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
+    if (query_string == NULL) {
+       fprintf (stderr, "Out of memory\n");
+       return EXIT_FAILURE;
+    }
+
+    if (*query_string == '\0') {
+       fprintf (stderr, "Error: notmuch reply requires at least one search term.\n");
+       return EXIT_FAILURE;
+    }
+
+    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;
+
+    _notmuch_crypto_cleanup (&params.crypto);
+    notmuch_query_destroy (query);
+    notmuch_database_destroy (notmuch);
+
+    return EXIT_SUCCESS;
+}
diff --git a/notmuch-restore.c b/notmuch-restore.c
new file mode 100644 (file)
index 0000000..1cce004
--- /dev/null
@@ -0,0 +1,467 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+#include "hex-escape.h"
+#include "tag-util.h"
+#include "string-util.h"
+#include "zlib-extra.h"
+
+static int
+process_config_line (notmuch_database_t *notmuch, const char *line)
+{
+    const char *key_p, *val_p;
+    char *key, *val;
+    size_t key_len, val_len;
+    const char *delim = " \t\n";
+    int ret = EXIT_FAILURE;
+
+    void *local = talloc_new (NULL);
+
+    key_p = strtok_len_c (line, delim, &key_len);
+    val_p = strtok_len_c (key_p + key_len, delim, &val_len);
+
+    key = talloc_strndup (local, key_p, key_len);
+    val = talloc_strndup (local, val_p, val_len);
+    if (hex_decode_inplace (key) != HEX_SUCCESS ||
+       hex_decode_inplace (val) != HEX_SUCCESS ) {
+       fprintf (stderr, "hex decoding failure on line %s\n", line);
+       goto DONE;
+    }
+
+    if (print_status_database ("notmuch restore", notmuch,
+                              notmuch_database_set_config (notmuch, key, val)))
+       goto DONE;
+
+    ret = EXIT_SUCCESS;
+
+  DONE:
+    talloc_free (local);
+    return ret;
+}
+
+static int
+process_properties_line (notmuch_database_t *notmuch, const char *line)
+{
+    const char *id_p, *tok;
+    size_t id_len = 0, tok_len = 0;
+    char *id;
+
+    notmuch_message_t *message = NULL;
+    const char *delim = " \t\n";
+    int ret = EXIT_FAILURE;
+
+    void *local = talloc_new (NULL);
+
+    id_p = strtok_len_c (line, delim, &id_len);
+    id = talloc_strndup (local, id_p, id_len);
+    if (hex_decode_inplace (id) != HEX_SUCCESS) {
+       fprintf (stderr, "hex decoding failure on line %s\n", line);
+       goto DONE;
+    }
+
+    if (print_status_database ("notmuch restore", notmuch,
+                              notmuch_database_find_message (notmuch, id, &message)))
+       goto DONE;
+
+    if (print_status_database ("notmuch restore", notmuch,
+                              notmuch_message_remove_all_properties (message, NULL)))
+       goto DONE;
+
+    tok = id_p + id_len;
+
+    while ((tok = strtok_len_c (tok + tok_len, delim, &tok_len)) != NULL) {
+       char *key, *value;
+       size_t off = strcspn (tok, "=");
+       if (off > tok_len) {
+           fprintf (stderr, "unparsable token %s\n", tok);
+           goto DONE;
+       }
+
+       key = talloc_strndup (local, tok, off);
+       value = talloc_strndup (local, tok + off + 1, tok_len - off - 1);
+
+       if (hex_decode_inplace (key) != HEX_SUCCESS) {
+           fprintf (stderr, "hex decoding failure on key %s\n", key);
+           goto DONE;
+       }
+
+       if (hex_decode_inplace (value) != HEX_SUCCESS) {
+           fprintf (stderr, "hex decoding failure on value %s\n", value);
+           goto DONE;
+       }
+
+       if (print_status_database ("notmuch restore", notmuch,
+                                  notmuch_message_add_property (message, key, value)))
+           goto DONE;
+
+    }
+
+    ret = EXIT_SUCCESS;
+
+  DONE:
+    talloc_free (local);
+    return ret;
+}
+
+
+static regex_t regex;
+
+/* Non-zero return indicates an error in retrieving the message,
+ * or in applying the tags.  Missing messages are reported, but not
+ * considered errors.
+ */
+static int
+tag_message (unused (void *ctx),
+            notmuch_database_t *notmuch,
+            const char *message_id,
+            tag_op_list_t *tag_ops,
+            tag_op_flag_t flags)
+{
+    notmuch_status_t status;
+    notmuch_message_t *message = NULL;
+    int ret = 0;
+
+    status = notmuch_database_find_message (notmuch, message_id, &message);
+    if (status) {
+       fprintf (stderr, "Error applying tags to message %s: %s\n",
+                message_id, notmuch_status_to_string (status));
+       return 1;
+    }
+    if (message == NULL) {
+       fprintf (stderr, "Warning: cannot apply tags to missing message: %s\n",
+                message_id);
+       /* We consider this a non-fatal error. */
+       return 0;
+    }
+
+    /* In order to detect missing messages, this check/optimization is
+     * intentionally done *after* first finding the message. */
+    if ((flags & TAG_FLAG_REMOVE_ALL) || tag_op_list_size (tag_ops))
+       ret = tag_op_list_apply (message, tag_ops, flags);
+
+    notmuch_message_destroy (message);
+
+    return ret;
+}
+
+/* Sup dump output is one line per message. We match a sequence of
+ * non-space characters for the message-id, then one or more
+ * spaces, then a list of space-separated tags as a sequence of
+ * characters within literal '(' and ')'. */
+
+static int
+parse_sup_line (void *ctx, char *line,
+               char **query_str, tag_op_list_t *tag_ops)
+{
+
+    regmatch_t match[3];
+    char *file_tags;
+    int rerr;
+
+    tag_op_list_reset (tag_ops);
+
+    chomp_newline (line);
+
+    /* Silently ignore blank lines */
+    if (line[0] == '\0') {
+       return 1;
+    }
+
+    rerr = xregexec (&regex, line, 3, match, 0);
+    if (rerr == REG_NOMATCH) {
+       fprintf (stderr, "Warning: Ignoring invalid sup format line: %s\n",
+                line);
+       return 1;
+    }
+
+    *query_str = talloc_strndup_debug (ctx, line + match[1].rm_so,
+                                      match[1].rm_eo - match[1].rm_so);
+
+    file_tags = talloc_strndup_debug (ctx, line + match[2].rm_so,
+                                     match[2].rm_eo - match[2].rm_so);
+
+    char *tok = file_tags;
+    size_t tok_len = 0;
+
+    tag_op_list_reset (tag_ops);
+
+    while ((tok = strtok_len (tok + tok_len, " ", &tok_len)) != NULL) {
+
+       if (*(tok + tok_len) != '\0') {
+           *(tok + tok_len) = '\0';
+           tok_len++;
+       }
+
+       if (tag_op_list_append (tag_ops, tok, false))
+           return -1;
+    }
+
+    return 0;
+
+}
+
+int
+notmuch_restore_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    bool accumulate = false;
+    tag_op_flag_t flags = 0;
+    tag_op_list_t *tag_ops;
+
+    const char *input_file_name = NULL;
+    const char *name_for_error = NULL;
+    gzFile input = NULL;
+    char *line = NULL;
+    void *line_ctx = NULL;
+    ssize_t line_len;
+
+    int ret = 0;
+    int opt_index;
+    int include = 0;
+    int input_format = DUMP_FORMAT_AUTO;
+    int errnum;
+    notmuch_bool_t synchronize_flags;
+
+    if (print_status_database (
+           "notmuch restore",
+           notmuch,
+           notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+                                    &synchronize_flags)))
+       return EXIT_FAILURE;
+
+    if (synchronize_flags)
+       flags |= TAG_FLAG_MAILDIR_SYNC;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_keyword = &input_format, .name = "format", .keywords =
+             (notmuch_keyword_t []){ { "auto", DUMP_FORMAT_AUTO },
+                                     { "batch-tag", DUMP_FORMAT_BATCH_TAG },
+                                     { "sup", DUMP_FORMAT_SUP },
+                                     { 0, 0 } } },
+       { .opt_flags = &include, .name = "include", .keywords =
+             (notmuch_keyword_t []){ { "config", DUMP_INCLUDE_CONFIG },
+                                     { "properties", DUMP_INCLUDE_PROPERTIES },
+                                     { "tags", DUMP_INCLUDE_TAGS } } },
+
+       { .opt_string = &input_file_name, .name = "input" },
+       { .opt_bool = &accumulate, .name = "accumulate" },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0) {
+       ret = EXIT_FAILURE;
+       goto DONE;
+    }
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    if (include == 0) {
+       include = DUMP_INCLUDE_CONFIG | DUMP_INCLUDE_PROPERTIES | DUMP_INCLUDE_TAGS;
+    }
+
+    name_for_error = input_file_name ? input_file_name : "stdin";
+
+    if (! accumulate)
+       flags |= TAG_FLAG_REMOVE_ALL;
+
+    errno = 0;
+    if (input_file_name)
+       input = gzopen (input_file_name, "r");
+    else {
+       int infd = dup (STDIN_FILENO);
+       if (infd < 0) {
+           fprintf (stderr, "Error duping stdin: %s\n",
+                    strerror (errno));
+           ret = EXIT_FAILURE;
+           goto DONE;
+       }
+       input = gzdopen (infd, "r");
+       if (! input)
+           close (infd);
+    }
+
+    if (input == NULL) {
+       fprintf (stderr, "Error opening %s for (gzip) reading: %s\n",
+                name_for_error, strerror (errno));
+       ret = EXIT_FAILURE;
+       goto DONE;
+    }
+
+    if (opt_index < argc) {
+       fprintf (stderr, "Unused positional parameter: %s\n", argv[opt_index]);
+       ret = EXIT_FAILURE;
+       goto DONE;
+    }
+
+    tag_ops = tag_op_list_create (notmuch);
+    if (tag_ops == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       ret = EXIT_FAILURE;
+       goto DONE;
+    }
+
+    do {
+       util_status_t status;
+
+       status = gz_getline (line_ctx, &line, &line_len, input);
+
+       /* empty input file not considered an error */
+       if (status == UTIL_EOF) {
+           ret = EXIT_SUCCESS;
+           goto DONE;
+       }
+
+       if (status) {
+           fprintf (stderr, "Error reading (gzipped) input: %s\n",
+                    gz_error_string (status, input));
+           ret = EXIT_FAILURE;
+           goto DONE;
+       }
+
+       if ((include & DUMP_INCLUDE_CONFIG) && line_len >= 2 && line[0] == '#' && line[1] == '@') {
+           ret = process_config_line (notmuch, line + 2);
+           if (ret)
+               goto DONE;
+       }
+       if ((include & DUMP_INCLUDE_PROPERTIES) && line_len >= 2 && line[0] == '#' && line[1] ==
+           '=') {
+           ret = process_properties_line (notmuch, line + 2);
+           if (ret)
+               goto DONE;
+       }
+
+    } while ((line_len == 0) ||
+            (line[0] == '#') ||
+             /* the cast is safe because we checked about for line_len < 0 */
+            (strspn (line, " \t\n") == (unsigned) line_len));
+
+    if (! ((include & DUMP_INCLUDE_TAGS) || (include & DUMP_INCLUDE_PROPERTIES))) {
+       ret = EXIT_SUCCESS;
+       goto DONE;
+    }
+
+    char *p;
+
+    for (p = line; (input_format == DUMP_FORMAT_AUTO) && *p; p++) {
+       if (*p == '(')
+           input_format = DUMP_FORMAT_SUP;
+    }
+
+    if (input_format == DUMP_FORMAT_AUTO)
+       input_format = DUMP_FORMAT_BATCH_TAG;
+
+    if (input_format == DUMP_FORMAT_SUP)
+       if ( xregcomp (&regex,
+                      "^([^ ]+) \\(([^)]*)\\)$",
+                      REG_EXTENDED) )
+           INTERNAL_ERROR ("compile time constant regex failed.");
+
+    do {
+       char *query_string, *prefix, *term;
+
+       if (line_ctx != NULL)
+           talloc_free (line_ctx);
+
+       line_ctx = talloc_new (notmuch);
+
+       if ((include & DUMP_INCLUDE_PROPERTIES) && line_len >= 2 && line[0] == '#' && line[1] ==
+           '=') {
+           ret = process_properties_line (notmuch, line + 2);
+           if (ret)
+               goto DONE;
+       }
+
+       if (input_format == DUMP_FORMAT_SUP) {
+           ret = parse_sup_line (line_ctx, line, &query_string, tag_ops);
+       } else {
+           ret = parse_tag_line (line_ctx, line, TAG_FLAG_BE_GENEROUS,
+                                 &query_string, tag_ops);
+
+           if (ret == 0) {
+               ret = parse_boolean_term (line_ctx, query_string,
+                                         &prefix, &term);
+               if (ret && errno == EINVAL) {
+                   fprintf (stderr, "Warning: cannot parse query: %s (skipping)\n", query_string);
+                   continue;
+               } else if (ret) {
+                   /* This is more fatal (e.g., out of memory) */
+                   fprintf (stderr, "Error parsing query: %s\n",
+                            strerror (errno));
+                   ret = 1;
+                   break;
+               } else if (strcmp ("id", prefix) != 0) {
+                   fprintf (stderr, "Warning: not an id query: %s (skipping)\n", query_string);
+                   continue;
+               }
+               query_string = term;
+           }
+       }
+
+       if (ret > 0)
+           continue;
+
+       if (ret < 0)
+           break;
+
+       ret = tag_message (line_ctx, notmuch, query_string,
+                          tag_ops, flags);
+       if (ret)
+           break;
+
+    }  while (! (ret = gz_getline (line_ctx, &line, &line_len, input)));
+
+
+    /* EOF is normal loop termination condition, UTIL_SUCCESS is
+     * impossible here */
+    if (ret == UTIL_EOF) {
+       ret = EXIT_SUCCESS;
+    } else {
+       fprintf (stderr, "Error reading (gzipped) input: %s\n",
+                gz_error_string (ret, input));
+       ret = EXIT_FAILURE;
+    }
+
+    /* currently this should not be after DONE: since we don't
+     * know if the xregcomp was reached
+     */
+
+    if (input_format == DUMP_FORMAT_SUP)
+       regfree (&regex);
+
+  DONE:
+    if (line_ctx != NULL)
+       talloc_free (line_ctx);
+
+    if (notmuch)
+       notmuch_database_destroy (notmuch);
+
+    if (input) {
+       errnum = gzclose_r (input);
+       if (errnum) {
+           fprintf (stderr, "Error closing %s: %d\n",
+                    name_for_error, errnum);
+           ret = EXIT_FAILURE;
+       }
+    }
+
+    return ret ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/notmuch-search.c b/notmuch-search.c
new file mode 100644 (file)
index 0000000..327e144
--- /dev/null
@@ -0,0 +1,926 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+#include "sprinter.h"
+#include "string-util.h"
+
+typedef enum {
+    /* Search command */
+    OUTPUT_SUMMARY     = 1 << 0,
+    OUTPUT_THREADS     = 1 << 1,
+    OUTPUT_MESSAGES    = 1 << 2,
+    OUTPUT_FILES       = 1 << 3,
+    OUTPUT_TAGS                = 1 << 4,
+
+    /* Address command */
+    OUTPUT_SENDER      = 1 << 5,
+    OUTPUT_RECIPIENTS  = 1 << 6,
+    OUTPUT_COUNT       = 1 << 7,
+    OUTPUT_ADDRESS     = 1 << 8,
+} output_t;
+
+typedef enum {
+    DEDUP_NONE,
+    DEDUP_MAILBOX,
+    DEDUP_ADDRESS,
+} dedup_t;
+
+typedef enum {
+    NOTMUCH_FORMAT_JSON,
+    NOTMUCH_FORMAT_TEXT,
+    NOTMUCH_FORMAT_TEXT0,
+    NOTMUCH_FORMAT_SEXP
+} format_sel_t;
+
+typedef struct {
+    notmuch_database_t *notmuch;
+    void *talloc_ctx;
+    int format_sel;
+    sprinter_t *format;
+    int exclude;
+    int query_syntax;
+    notmuch_query_t *query;
+    int sort;
+    int output;
+    int offset;
+    int limit;
+    int dupe;
+    GHashTable *addresses;
+    int dedup;
+} search_context_t;
+
+typedef struct {
+    const char *name;
+    const char *addr;
+    int count;
+} mailbox_t;
+
+/* Return two stable query strings that identify exactly the matched
+ * and unmatched messages currently in thread.  If there are no
+ * matched or unmatched messages, the returned buffers will be
+ * NULL. */
+static int
+get_thread_query (notmuch_thread_t *thread,
+                 char **matched_out, char **unmatched_out)
+{
+    notmuch_messages_t *messages;
+    char *escaped = NULL;
+    size_t escaped_len = 0;
+
+    *matched_out = *unmatched_out = NULL;
+
+    for (messages = notmuch_thread_get_messages (thread);
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages)) {
+       notmuch_message_t *message = notmuch_messages_get (messages);
+       const char *mid = notmuch_message_get_message_id (message);
+       notmuch_bool_t is_set;
+       char **buf;
+
+       if (notmuch_message_get_flag_st (message, NOTMUCH_MESSAGE_FLAG_MATCH, &is_set))
+           return -1;
+       /* Determine which query buffer to extend */
+       buf = is_set ? matched_out : unmatched_out;
+       /* Add this message's id: query.  Since "id" is an exclusive
+        * prefix, it is implicitly 'or'd together, so we only need to
+        * join queries with a space. */
+       if (make_boolean_term (thread, "id", mid, &escaped, &escaped_len) < 0)
+           return -1;
+       if (*buf)
+           *buf = talloc_asprintf_append_buffer (*buf, " %s", escaped);
+       else
+           *buf = talloc_strdup (thread, escaped);
+       if (! *buf)
+           return -1;
+    }
+    talloc_free (escaped);
+    return 0;
+}
+
+static int
+do_search_threads (search_context_t *ctx)
+{
+    notmuch_thread_t *thread;
+    notmuch_threads_t *threads;
+    notmuch_tags_t *tags;
+    sprinter_t *format = ctx->format;
+    time_t date;
+    int i;
+    notmuch_status_t status;
+
+    if (ctx->offset < 0) {
+       unsigned count;
+       notmuch_status_t status;
+       status = notmuch_query_count_threads (ctx->query, &count);
+       if (print_status_query ("notmuch search", ctx->query, status))
+           return 1;
+
+       ctx->offset += count;
+       if (ctx->offset < 0)
+           ctx->offset = 0;
+    }
+
+    status = notmuch_query_search_threads (ctx->query, &threads);
+    if (print_status_query ("notmuch search", ctx->query, status))
+       return 1;
+
+    format->begin_list (format);
+
+    for (i = 0;
+        notmuch_threads_valid (threads) && (ctx->limit < 0 || i < ctx->offset + ctx->limit);
+        notmuch_threads_move_to_next (threads), i++) {
+       thread = notmuch_threads_get (threads);
+
+       if (i < ctx->offset) {
+           notmuch_thread_destroy (thread);
+           continue;
+       }
+
+       if (ctx->output == OUTPUT_THREADS) {
+           format->set_prefix (format, "thread");
+           format->string (format,
+                           notmuch_thread_get_thread_id (thread));
+           format->separator (format);
+       } else { /* output == OUTPUT_SUMMARY */
+           void *ctx_quote = talloc_new (thread);
+           const char *authors = notmuch_thread_get_authors (thread);
+           const char *subject = notmuch_thread_get_subject (thread);
+           const char *thread_id = notmuch_thread_get_thread_id (thread);
+           int matched = notmuch_thread_get_matched_messages (thread);
+           int files = notmuch_thread_get_total_files (thread);
+           int total = notmuch_thread_get_total_messages (thread);
+           const char *relative_date = NULL;
+           bool first_tag = true;
+
+           format->begin_map (format);
+
+           if (ctx->sort == NOTMUCH_SORT_OLDEST_FIRST)
+               date = notmuch_thread_get_oldest_date (thread);
+           else
+               date = notmuch_thread_get_newest_date (thread);
+
+           relative_date = notmuch_time_relative_date (ctx_quote, date);
+
+           if (format->is_text_printer) {
+               /* Special case for the text formatter */
+               printf ("thread:%s %12s ",
+                       thread_id,
+                       relative_date);
+               if (total == files)
+                   printf ("[%d/%d] %s; %s (",
+                           matched,
+                           total,
+                           sanitize_string (ctx_quote, authors),
+                           sanitize_string (ctx_quote, subject));
+               else
+                   printf ("[%d/%d(%d)] %s; %s (",
+                           matched,
+                           total,
+                           files,
+                           sanitize_string (ctx_quote, authors),
+                           sanitize_string (ctx_quote, subject));
+
+           } else { /* Structured Output */
+               format->map_key (format, "thread");
+               format->string (format, thread_id);
+               format->map_key (format, "timestamp");
+               format->integer (format, date);
+               format->map_key (format, "date_relative");
+               format->string (format, relative_date);
+               format->map_key (format, "matched");
+               format->integer (format, matched);
+               format->map_key (format, "total");
+               format->integer (format, total);
+               format->map_key (format, "authors");
+               format->string (format, authors);
+               format->map_key (format, "subject");
+               format->string (format, subject);
+               if (notmuch_format_version >= 2) {
+                   char *matched_query, *unmatched_query;
+                   if (get_thread_query (thread, &matched_query,
+                                         &unmatched_query) < 0) {
+                       fprintf (stderr, "Out of memory\n");
+                       return 1;
+                   }
+                   format->map_key (format, "query");
+                   format->begin_list (format);
+                   if (matched_query)
+                       format->string (format, matched_query);
+                   else
+                       format->null (format);
+                   if (unmatched_query)
+                       format->string (format, unmatched_query);
+                   else
+                       format->null (format);
+                   format->end (format);
+               }
+           }
+
+           talloc_free (ctx_quote);
+
+           format->map_key (format, "tags");
+           format->begin_list (format);
+
+           for (tags = notmuch_thread_get_tags (thread);
+                notmuch_tags_valid (tags);
+                notmuch_tags_move_to_next (tags)) {
+               const char *tag = notmuch_tags_get (tags);
+
+               if (format->is_text_printer) {
+                   /* Special case for the text formatter */
+                   if (first_tag)
+                       first_tag = false;
+                   else
+                       fputc (' ', stdout);
+                   fputs (tag, stdout);
+               } else { /* Structured Output */
+                   format->string (format, tag);
+               }
+           }
+
+           if (format->is_text_printer)
+               printf (")");
+
+           format->end (format);
+           format->end (format);
+           format->separator (format);
+       }
+
+       notmuch_thread_destroy (thread);
+    }
+
+    format->end (format);
+
+    return 0;
+}
+
+static mailbox_t *
+new_mailbox (void *ctx, const char *name, const char *addr)
+{
+    mailbox_t *mailbox;
+
+    mailbox = talloc (ctx, mailbox_t);
+    if (! mailbox)
+       return NULL;
+
+    mailbox->name = talloc_strdup (mailbox, name);
+    mailbox->addr = talloc_strdup (mailbox, addr);
+    mailbox->count = 1;
+
+    return mailbox;
+}
+
+static int
+mailbox_compare (const void *v1, const void *v2)
+{
+    const mailbox_t *m1 = v1, *m2 = v2;
+    int ret;
+
+    ret = strcmp_null (m1->name, m2->name);
+    if (! ret)
+       ret = strcmp (m1->addr, m2->addr);
+
+    return ret;
+}
+
+/* Returns true iff name and addr is duplicate. If not, stores the
+ * name/addr pair in order to detect subsequent duplicates. */
+static bool
+is_duplicate (const search_context_t *ctx, const char *name, const char *addr)
+{
+    char *key;
+    GList *list, *l;
+    mailbox_t *mailbox;
+
+    list = g_hash_table_lookup (ctx->addresses, addr);
+    if (list) {
+       mailbox_t find = {
+           .name = name,
+           .addr = addr,
+       };
+
+       l = g_list_find_custom (list, &find, mailbox_compare);
+       if (l) {
+           mailbox = l->data;
+           mailbox->count++;
+           return true;
+       }
+
+       mailbox = new_mailbox (ctx->format, name, addr);
+       if (! mailbox)
+           return false;
+
+       /*
+        * XXX: It would be more efficient to prepend to the list, but
+        * then we'd have to store the changed list head back to the
+        * hash table. This check is here just to avoid the compiler
+        * warning for unused result.
+        */
+       if (list != g_list_append (list, mailbox))
+           INTERNAL_ERROR ("appending to list changed list head\n");
+
+       return false;
+    }
+
+    key = talloc_strdup (ctx->format, addr);
+    if (! key)
+       return false;
+
+    mailbox = new_mailbox (ctx->format, name, addr);
+    if (! mailbox)
+       return false;
+
+    list = g_list_append (NULL, mailbox);
+    if (! list)
+       return false;
+
+    g_hash_table_insert (ctx->addresses, key, list);
+
+    return false;
+}
+
+static void
+print_mailbox (const search_context_t *ctx, const mailbox_t *mailbox)
+{
+    const char *name = mailbox->name;
+    const char *addr = mailbox->addr;
+    int count = mailbox->count;
+    sprinter_t *format = ctx->format;
+    InternetAddress *ia = internet_address_mailbox_new (name, addr);
+    char *name_addr;
+
+    /* name_addr has the name part quoted if necessary. Compare
+     * 'John Doe <john@doe.com>' vs. '"Doe, John" <john@doe.com>' */
+    name_addr = internet_address_to_string (ia, NULL, false);
+
+    if (format->is_text_printer) {
+       if (ctx->output & OUTPUT_COUNT) {
+           format->integer (format, count);
+           format->string (format, "\t");
+       }
+       if (ctx->output & OUTPUT_ADDRESS)
+           format->string (format, addr);
+       else
+           format->string (format, name_addr);
+       format->separator (format);
+    } else {
+       format->begin_map (format);
+       format->map_key (format, "name");
+       format->string (format, name);
+       format->map_key (format, "address");
+       format->string (format, addr);
+       format->map_key (format, "name-addr");
+       format->string (format, name_addr);
+       if (ctx->output & OUTPUT_COUNT) {
+           format->map_key (format, "count");
+           format->integer (format, count);
+       }
+       format->end (format);
+       format->separator (format);
+    }
+
+    g_object_unref (ia);
+    g_free (name_addr);
+}
+
+/* Print or prepare for printing addresses from InternetAddressList. */
+static void
+process_address_list (const search_context_t *ctx,
+                     InternetAddressList *list)
+{
+    InternetAddress *address;
+    int i;
+
+    for (i = 0; i < internet_address_list_length (list); i++) {
+       address = internet_address_list_get_address (list, i);
+       if (INTERNET_ADDRESS_IS_GROUP (address)) {
+           InternetAddressGroup *group;
+           InternetAddressList *group_list;
+
+           group = INTERNET_ADDRESS_GROUP (address);
+           group_list = internet_address_group_get_members (group);
+           if (group_list == NULL)
+               continue;
+
+           process_address_list (ctx, group_list);
+       } else {
+           InternetAddressMailbox *mailbox = INTERNET_ADDRESS_MAILBOX (address);
+           mailbox_t mbx = {
+               .name = internet_address_get_name (address),
+               .addr = internet_address_mailbox_get_addr (mailbox),
+           };
+
+           /* OUTPUT_COUNT only works with deduplication */
+           if (ctx->dedup != DEDUP_NONE &&
+               is_duplicate (ctx, mbx.name, mbx.addr))
+               continue;
+
+           /* OUTPUT_COUNT and DEDUP_ADDRESS require a full pass. */
+           if (ctx->output & OUTPUT_COUNT || ctx->dedup == DEDUP_ADDRESS)
+               continue;
+
+           print_mailbox (ctx, &mbx);
+       }
+    }
+}
+
+/* Print or prepare for printing addresses from a message header. */
+static void
+process_address_header (const search_context_t *ctx, const char *value)
+{
+    InternetAddressList *list;
+
+    if (value == NULL)
+       return;
+
+    list = internet_address_list_parse (NULL, value);
+    if (list == NULL)
+       return;
+
+    process_address_list (ctx, list);
+
+    g_object_unref (list);
+}
+
+/* Destructor for talloc-allocated GHashTable keys and values. */
+static void
+_talloc_free_for_g_hash (void *ptr)
+{
+    talloc_free (ptr);
+}
+
+static void
+_list_free_for_g_hash (void *ptr)
+{
+    g_list_free_full (ptr, _talloc_free_for_g_hash);
+}
+
+/* Print the most common variant of a list of unique mailboxes, and
+ * conflate the counts. */
+static void
+print_popular (const search_context_t *ctx, GList *list)
+{
+    GList *l;
+    mailbox_t *mailbox = NULL, *m;
+    int max = 0;
+    int total = 0;
+
+    for (l = list; l; l = l->next) {
+       m = l->data;
+       total += m->count;
+       if (m->count > max) {
+           mailbox = m;
+           max = m->count;
+       }
+    }
+
+    if (! mailbox)
+       INTERNAL_ERROR ("Empty list in address hash table\n");
+
+    /* The original count is no longer needed, so overwrite. */
+    mailbox->count = total;
+
+    print_mailbox (ctx, mailbox);
+}
+
+static void
+print_list_value (void *mailbox, void *context)
+{
+    print_mailbox (context, mailbox);
+}
+
+static void
+print_hash_value (unused (void *key), void *list, void *context)
+{
+    const search_context_t *ctx = context;
+
+    if (ctx->dedup == DEDUP_ADDRESS)
+       print_popular (ctx, list);
+    else
+       g_list_foreach (list, print_list_value, context);
+}
+
+static int
+_count_filenames (notmuch_message_t *message)
+{
+    notmuch_filenames_t *filenames;
+    int i = 0;
+
+    filenames = notmuch_message_get_filenames (message);
+
+    while (notmuch_filenames_valid (filenames)) {
+       notmuch_filenames_move_to_next (filenames);
+       i++;
+    }
+
+    notmuch_filenames_destroy (filenames);
+
+    return i;
+}
+
+static int
+do_search_messages (search_context_t *ctx)
+{
+    notmuch_message_t *message;
+    notmuch_messages_t *messages;
+    notmuch_filenames_t *filenames;
+    sprinter_t *format = ctx->format;
+    int i;
+    notmuch_status_t status;
+
+    if (ctx->offset < 0) {
+       unsigned count;
+       notmuch_status_t status;
+       status = notmuch_query_count_messages (ctx->query, &count);
+       if (print_status_query ("notmuch search", ctx->query, status))
+           return 1;
+
+       ctx->offset += count;
+       if (ctx->offset < 0)
+           ctx->offset = 0;
+    }
+
+    status = notmuch_query_search_messages (ctx->query, &messages);
+    if (print_status_query ("notmuch search", ctx->query, status))
+       return 1;
+
+    format->begin_list (format);
+
+    for (i = 0;
+        notmuch_messages_valid (messages) && (ctx->limit < 0 || i < ctx->offset + ctx->limit);
+        notmuch_messages_move_to_next (messages), i++) {
+       if (i < ctx->offset)
+           continue;
+
+       message = notmuch_messages_get (messages);
+
+       if (ctx->output == OUTPUT_FILES) {
+           int j;
+           filenames = notmuch_message_get_filenames (message);
+
+           for (j = 1;
+                notmuch_filenames_valid (filenames);
+                notmuch_filenames_move_to_next (filenames), j++) {
+               if (ctx->dupe < 0 || ctx->dupe == j) {
+                   format->string (format, notmuch_filenames_get (filenames));
+                   format->separator (format);
+               }
+           }
+
+           notmuch_filenames_destroy ( filenames );
+
+       } else if (ctx->output == OUTPUT_MESSAGES) {
+           /* special case 1 for speed */
+           if (ctx->dupe <= 1 || ctx->dupe <= _count_filenames (message)) {
+               format->set_prefix (format, "id");
+               format->string (format,
+                               notmuch_message_get_message_id (message));
+               format->separator (format);
+           }
+       } else {
+           if (ctx->output & OUTPUT_SENDER) {
+               const char *addrs;
+
+               addrs = notmuch_message_get_header (message, "from");
+               process_address_header (ctx, addrs);
+           }
+
+           if (ctx->output & OUTPUT_RECIPIENTS) {
+               const char *hdrs[] = { "to", "cc", "bcc" };
+               const char *addrs;
+               size_t j;
+
+               for (j = 0; j < ARRAY_SIZE (hdrs); j++) {
+                   addrs = notmuch_message_get_header (message, hdrs[j]);
+                   process_address_header (ctx, addrs);
+               }
+           }
+       }
+
+       notmuch_message_destroy (message);
+    }
+
+    if (ctx->addresses &&
+       (ctx->output & OUTPUT_COUNT || ctx->dedup == DEDUP_ADDRESS))
+       g_hash_table_foreach (ctx->addresses, print_hash_value, ctx);
+
+    notmuch_messages_destroy (messages);
+
+    format->end (format);
+
+    return 0;
+}
+
+static int
+do_search_tags (const search_context_t *ctx)
+{
+    notmuch_messages_t *messages = NULL;
+    notmuch_tags_t *tags;
+    const char *tag;
+    sprinter_t *format = ctx->format;
+    notmuch_query_t *query = ctx->query;
+    notmuch_database_t *notmuch = ctx->notmuch;
+
+    /* should the following only special case if no excluded terms
+     * specified? */
+
+    /* Special-case query of "*" for better performance. */
+    if (strcmp (notmuch_query_get_query_string (query), "*") == 0) {
+       tags = notmuch_database_get_all_tags (notmuch);
+    } else {
+       notmuch_status_t status;
+       status = notmuch_query_search_messages (query, &messages);
+       if (print_status_query ("notmuch search", query, status))
+           return 1;
+
+       tags = notmuch_messages_collect_tags (messages);
+    }
+    if (tags == NULL)
+       return 1;
+
+    format->begin_list (format);
+
+    for (;
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags)) {
+       tag = notmuch_tags_get (tags);
+
+       format->string (format, tag);
+       format->separator (format);
+
+    }
+
+    notmuch_tags_destroy (tags);
+
+    if (messages)
+       notmuch_messages_destroy (messages);
+
+    format->end (format);
+
+    return 0;
+}
+
+static int
+_notmuch_search_prepare (search_context_t *ctx, int argc, char *argv[])
+{
+    char *query_str;
+
+    if (! ctx->talloc_ctx)
+       ctx->talloc_ctx = talloc_new (NULL);
+
+    switch (ctx->format_sel) {
+    case NOTMUCH_FORMAT_TEXT:
+       ctx->format = sprinter_text_create (ctx->talloc_ctx, stdout);
+       break;
+    case NOTMUCH_FORMAT_TEXT0:
+       if (ctx->output == OUTPUT_SUMMARY) {
+           fprintf (stderr, "Error: --format=text0 is not compatible with --output=summary.\n");
+           return EXIT_FAILURE;
+       }
+       ctx->format = sprinter_text0_create (ctx->talloc_ctx, stdout);
+       break;
+    case NOTMUCH_FORMAT_JSON:
+       ctx->format = sprinter_json_create (ctx->talloc_ctx, stdout);
+       break;
+    case NOTMUCH_FORMAT_SEXP:
+       ctx->format = sprinter_sexp_create (ctx->talloc_ctx, stdout);
+       break;
+    default:
+       /* this should never happen */
+       INTERNAL_ERROR ("no output format selected");
+    }
+
+    notmuch_exit_if_unsupported_format ();
+
+    query_str = query_string_from_args (ctx->notmuch, argc, argv);
+    if (query_str == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return EXIT_FAILURE;
+    }
+    if (*query_str == '\0') {
+       fprintf (stderr, "Error: notmuch search requires at least one search term.\n");
+       return EXIT_FAILURE;
+    }
+
+    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);
+
+    if (ctx->exclude == NOTMUCH_EXCLUDE_FLAG && ctx->output != OUTPUT_SUMMARY) {
+       /* If we are not doing summary output there is nowhere to
+        * print the excluded flag so fall back on including the
+        * excluded messages. */
+       fprintf (stderr, "Warning: this output format cannot flag excluded messages.\n");
+       ctx->exclude = NOTMUCH_EXCLUDE_FALSE;
+    }
+
+    if (ctx->exclude != NOTMUCH_EXCLUDE_FALSE) {
+       notmuch_config_values_t *exclude_tags;
+       notmuch_status_t status;
+
+       for (exclude_tags = notmuch_config_get_values (ctx->notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS);
+            notmuch_config_values_valid (exclude_tags);
+            notmuch_config_values_move_to_next (exclude_tags)) {
+
+           status = notmuch_query_add_tag_exclude (ctx->query,
+                                                   notmuch_config_values_get (exclude_tags));
+           if (status && status != NOTMUCH_STATUS_IGNORED) {
+               print_status_query ("notmuch search", ctx->query, status);
+               return EXIT_FAILURE;
+           }
+       }
+       notmuch_query_set_omit_excluded (ctx->query, ctx->exclude);
+    }
+
+    return 0;
+}
+
+static void
+_notmuch_search_cleanup (search_context_t *ctx)
+{
+    notmuch_query_destroy (ctx->query);
+    notmuch_database_destroy (ctx->notmuch);
+
+    talloc_free (ctx->format);
+}
+
+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 */
+    .dupe = -1,
+    .dedup = DEDUP_MAILBOX,
+};
+
+static const notmuch_opt_desc_t common_options[] = {
+    { .opt_keyword = &search_context.sort, .name = "sort", .keywords =
+         (notmuch_keyword_t []){ { "oldest-first", NOTMUCH_SORT_OLDEST_FIRST },
+                                 { "newest-first", NOTMUCH_SORT_NEWEST_FIRST },
+                                 { 0, 0 } } },
+    { .opt_keyword = &search_context.format_sel, .name = "format", .keywords =
+         (notmuch_keyword_t []){ { "json", NOTMUCH_FORMAT_JSON },
+                                 { "sexp", NOTMUCH_FORMAT_SEXP },
+                                 { "text", NOTMUCH_FORMAT_TEXT },
+                                 { "text0", NOTMUCH_FORMAT_TEXT0 },
+                                 { 0, 0 } } },
+    { .opt_int = &notmuch_format_version, .name = "format-version" },
+    { }
+};
+
+int
+notmuch_search_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    search_context_t *ctx = &search_context;
+    int opt_index, ret;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_keyword = &ctx->output, .name = "output", .keywords =
+             (notmuch_keyword_t []){ { "summary", OUTPUT_SUMMARY },
+                                     { "threads", OUTPUT_THREADS },
+                                     { "messages", OUTPUT_MESSAGES },
+                                     { "files", OUTPUT_FILES },
+                                     { "tags", OUTPUT_TAGS },
+                                     { 0, 0 } } },
+       { .opt_keyword = &ctx->exclude, .name = "exclude", .keywords =
+             (notmuch_keyword_t []){ { "true", NOTMUCH_EXCLUDE_TRUE },
+                                     { "false", NOTMUCH_EXCLUDE_FALSE },
+                                     { "flag", NOTMUCH_EXCLUDE_FLAG },
+                                     { "all", NOTMUCH_EXCLUDE_ALL },
+                                     { 0, 0 } } },
+       { .opt_int = &ctx->offset, .name = "offset" },
+       { .opt_int = &ctx->limit, .name = "limit" },
+       { .opt_int = &ctx->dupe, .name = "duplicate" },
+       { .opt_inherit = common_options },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    ctx->notmuch = notmuch;
+    ctx->output = OUTPUT_SUMMARY;
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    if (ctx->output != OUTPUT_FILES && ctx->output != OUTPUT_MESSAGES &&
+       ctx->dupe != -1) {
+       fprintf (stderr,
+                "Error: --duplicate=N is only supported with --output=files and --output=messages.\n");
+       return EXIT_FAILURE;
+    }
+
+    if (_notmuch_search_prepare (ctx, argc - opt_index, argv + opt_index))
+       return EXIT_FAILURE;
+
+    switch (ctx->output) {
+    case OUTPUT_SUMMARY:
+    case OUTPUT_THREADS:
+       ret = do_search_threads (ctx);
+       break;
+    case OUTPUT_MESSAGES:
+    case OUTPUT_FILES:
+       ret = do_search_messages (ctx);
+       break;
+    case OUTPUT_TAGS:
+       ret = do_search_tags (ctx);
+       break;
+    default:
+       INTERNAL_ERROR ("Unexpected output");
+    }
+
+    _notmuch_search_cleanup (ctx);
+
+    return ret ? EXIT_FAILURE : EXIT_SUCCESS;
+}
+
+int
+notmuch_address_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    search_context_t *ctx = &search_context;
+    int opt_index, ret;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_flags = &ctx->output, .name = "output", .keywords =
+             (notmuch_keyword_t []){ { "sender", OUTPUT_SENDER },
+                                     { "recipients", OUTPUT_RECIPIENTS },
+                                     { "count", OUTPUT_COUNT },
+                                     { "address", OUTPUT_ADDRESS },
+                                     { 0, 0 } } },
+       { .opt_keyword = &ctx->exclude, .name = "exclude", .keywords =
+             (notmuch_keyword_t []){ { "true", NOTMUCH_EXCLUDE_TRUE },
+                                     { "false", NOTMUCH_EXCLUDE_FALSE },
+                                     { 0, 0 } } },
+       { .opt_keyword = &ctx->dedup, .name = "deduplicate", .keywords =
+             (notmuch_keyword_t []){ { "no", DEDUP_NONE },
+                                     { "mailbox", DEDUP_MAILBOX },
+                                     { "address", DEDUP_ADDRESS },
+                                     { 0, 0 } } },
+       { .opt_inherit = common_options },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    ctx->notmuch = notmuch;
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    if (! (ctx->output & (OUTPUT_SENDER | OUTPUT_RECIPIENTS)))
+       ctx->output |= OUTPUT_SENDER;
+
+    if (ctx->output & OUTPUT_COUNT && ctx->dedup == DEDUP_NONE) {
+       fprintf (stderr, "--output=count is not applicable with --deduplicate=no\n");
+       return EXIT_FAILURE;
+    }
+
+    if (_notmuch_search_prepare (ctx, argc - opt_index, argv + opt_index))
+       return EXIT_FAILURE;
+
+    ctx->addresses = g_hash_table_new_full (strcase_hash, strcase_equal,
+                                           _talloc_free_for_g_hash,
+                                           _list_free_for_g_hash);
+
+    /* The order is not guaranteed if a full pass is required, so go
+     * for fastest. */
+    if (ctx->output & OUTPUT_COUNT || ctx->dedup == DEDUP_ADDRESS)
+       notmuch_query_set_sort (ctx->query, NOTMUCH_SORT_UNSORTED);
+
+    ret = do_search_messages (ctx);
+
+    g_hash_table_unref (ctx->addresses);
+
+
+    _notmuch_search_cleanup (ctx);
+
+    return ret ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/notmuch-setup.c b/notmuch-setup.c
new file mode 100644 (file)
index 0000000..9382e27
--- /dev/null
@@ -0,0 +1,243 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+
+static const char *
+make_path_absolute (void *ctx, const char *path)
+{
+    char *cwd;
+
+    if (*path == '/')
+       return path;
+
+    cwd = getcwd (NULL, 0);
+    if (cwd == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return NULL;
+    }
+
+    path = talloc_asprintf (ctx, "%s/%s", cwd, path);
+    if (path == NULL)
+       fprintf (stderr, "Out of memory.\n");
+
+    free (cwd);
+
+    return path;
+}
+
+static void
+welcome_message_pre_setup (void)
+{
+    printf (
+       "Welcome to notmuch!\n\n"
+
+       "The goal of notmuch is to help you manage and search your collection of\n"
+       "email, and to efficiently keep up with the flow of email as it comes in.\n\n"
+
+       "Notmuch needs to know a few things about you such as your name and email\n"
+       "address, as well as the directory that contains your email. This is where\n"
+       "you already have mail stored and where messages will be delivered in the\n"
+       "future. This directory can contain any number of sub-directories. Regular\n"
+       "files in these directories should be individual email messages. If there\n"
+       "are other, non-email files (such as indexes maintained by other email\n"
+       "programs) then notmuch will do its best to detect those and ignore them.\n\n"
+
+       "If you already have your email being delivered to directories in either\n"
+       "maildir or mh format, then that's perfect. Mail storage that uses mbox\n"
+       "format, (where one mbox file contains many messages), will not work with\n"
+       "notmuch. If that's how your mail is currently stored, we recommend you\n"
+       "first convert it to maildir format with a utility such as mb2md. You can\n"
+       "continue configuring notmuch now, but be sure to complete the conversion\n"
+       "before you run \"notmuch new\" for the first time.\n\n");
+}
+
+static void
+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"
+           "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"
+
+           "The next step is to run \"notmuch new\" which will create a database\n"
+           "that indexes all of your mail. Depending on the amount of mail you have\n"
+           "the initial indexing process can take a long time, so expect that.\n"
+           "Also, the resulting database will require roughly the same amount of\n"
+           "storage space as your current collection of email. So please ensure you\n"
+           "have sufficient storage space available now.\n\n");
+}
+
+static void
+print_tag_list (notmuch_config_values_t *tags)
+{
+    bool first = false;
+
+    for (;
+        notmuch_config_values_valid (tags);
+        notmuch_config_values_move_to_next (tags)) {
+       if (! first)
+           printf (" ");
+       first = false;
+       printf ("%s", notmuch_config_values_get (tags));
+    }
+}
+
+static GPtrArray *
+parse_tag_list (void *ctx, char *response)
+{
+    GPtrArray *tags = g_ptr_array_new ();
+    char *tag = response;
+    char *space;
+
+    while (tag && *tag) {
+       space = strchr (tag, ' ');
+       if (space)
+           g_ptr_array_add (tags, talloc_strndup (ctx, tag, space - tag));
+       else
+           g_ptr_array_add (tags, talloc_strdup (ctx, tag));
+       tag = space;
+       while (tag && *tag == ' ')
+           tag++;
+    }
+
+    return tags;
+}
+
+int
+notmuch_setup_command (notmuch_database_t *notmuch,
+                      int argc, char *argv[])
+{
+    char *response = NULL;
+    size_t response_size = 0;
+    GPtrArray *other_emails;
+    notmuch_config_values_t *new_tags, *search_exclude_tags, *emails;
+    notmuch_conffile_t *config;
+
+#define prompt(format, ...)                                     \
+    do {                                                        \
+       printf (format, ##__VA_ARGS__);                         \
+       fflush (stdout);                                        \
+       if (getline (&response, &response_size, stdin) < 0) {   \
+           printf ("Exiting.\n");                              \
+           exit (EXIT_FAILURE);                                \
+       }                                                       \
+       chomp_newline (response);                               \
+    } while (0)
+
+    if (notmuch_minimal_options ("setup", argc, argv) < 0)
+       return EXIT_FAILURE;
+
+    config = notmuch_conffile_open (notmuch,
+                                   notmuch_config_path (notmuch), true);
+    if (! config)
+       return EXIT_FAILURE;
+
+    if (notmuch_conffile_is_new (config))
+       welcome_message_pre_setup ();
+
+    prompt ("Your full name [%s]: ", notmuch_config_get (notmuch, NOTMUCH_CONFIG_USER_NAME));
+    if (strlen (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_conffile_set_user_primary_email (config, response);
+
+    other_emails = g_ptr_array_new ();
+
+    for (emails = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_OTHER_EMAIL);
+        notmuch_config_values_valid (emails);
+        notmuch_config_values_move_to_next (emails)) {
+       const char *email = notmuch_config_values_get (emails);
+
+       prompt ("Additional email address [%s]: ", email);
+       if (strlen (response))
+           g_ptr_array_add (other_emails, talloc_strdup (config, response));
+       else
+           g_ptr_array_add (other_emails, talloc_strdup (config, email));
+    }
+
+    do {
+       prompt ("Additional email address [Press 'Enter' if none]: ");
+       if (strlen (response))
+           g_ptr_array_add (other_emails, talloc_strdup (config, response));
+    } while (strlen (response));
+    if (other_emails->len)
+       notmuch_conffile_set_user_other_email (config,
+                                              (const char **)
+                                              other_emails->pdata,
+                                              other_emails->len);
+    g_ptr_array_free (other_emails, true);
+
+    prompt ("Top-level directory of your email archive [%s]: ",
+           notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH));
+    if (strlen (response)) {
+       const char *absolute_path;
+
+       absolute_path = make_path_absolute (config, response);
+       notmuch_conffile_set_database_path (config, absolute_path);
+    }
+
+    new_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_TAGS);
+
+    printf ("Tags to apply to all new messages (separated by spaces) [");
+    print_tag_list (new_tags);
+    prompt ("]: ");
+
+    if (strlen (response)) {
+       GPtrArray *tags = parse_tag_list (config, response);
+
+       notmuch_conffile_set_new_tags (config, (const char **) tags->pdata,
+                                      tags->len);
+
+       g_ptr_array_free (tags, true);
+    }
+
+    search_exclude_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS);
+
+    printf ("Tags to exclude when searching messages (separated by spaces) [");
+    print_tag_list (search_exclude_tags);
+    prompt ("]: ");
+
+    if (strlen (response)) {
+       GPtrArray *tags = parse_tag_list (config, response);
+
+       notmuch_conffile_set_search_exclude_tags (config,
+                                                 (const char **) tags->pdata,
+                                                 tags->len);
+
+       g_ptr_array_free (tags, true);
+    }
+
+    if (notmuch_conffile_save (config))
+       return EXIT_FAILURE;
+
+    if (config)
+       notmuch_conffile_close (config);
+
+    if (notmuch_conffile_is_new (config))
+       welcome_message_post_setup ();
+
+    return EXIT_SUCCESS;
+}
diff --git a/notmuch-show.c b/notmuch-show.c
new file mode 100644 (file)
index 0000000..7fb40ce
--- /dev/null
@@ -0,0 +1,1513 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+#include "gmime-filter-reply.h"
+#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)
+{
+    notmuch_tags_t *tags;
+    int first = 1;
+    const char *tag;
+    char *result;
+
+    result = talloc_strdup (ctx, "");
+    if (result == NULL)
+       return NULL;
+
+    for (tags = notmuch_message_get_tags (message);
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags)) {
+       tag = notmuch_tags_get (tags);
+
+       result = talloc_asprintf_append (result, "%s%s",
+                                        first ? "" : " ", tag);
+       first = 0;
+    }
+
+    return result;
+}
+
+/* Get a nice, single-line summary of message. */
+static const char *
+_get_one_line_summary (const void *ctx, notmuch_message_t *message)
+{
+    const char *from;
+    time_t date;
+    const char *relative_date;
+    const char *tags;
+
+    from = notmuch_message_get_header (message, "from");
+
+    date = notmuch_message_get_date (message);
+    relative_date = notmuch_time_relative_date (ctx, date);
+
+    tags = _get_tags_as_string (ctx, message);
+
+    return talloc_asprintf (ctx, "%s (%s) (%s)",
+                           from, relative_date, tags);
+}
+
+static const char *
+_get_disposition (GMimeObject *meta)
+{
+    GMimeContentDisposition *disposition;
+
+    disposition = g_mime_object_get_content_disposition (meta);
+    if (! disposition)
+       return NULL;
+
+    return g_mime_content_disposition_get_disposition (disposition);
+}
+
+static bool
+_get_message_flag (notmuch_message_t *message, notmuch_message_flag_t flag)
+{
+    notmuch_bool_t is_set;
+    notmuch_status_t status;
+
+    status = notmuch_message_get_flag_st (message, flag, &is_set);
+
+    if (print_status_message ("notmuch show", message, status))
+       INTERNAL_ERROR ("unexpected error getting message flag\n");
+
+    return is_set;
+}
+
+/* Emit a sequence of key/value pairs for the metadata of message.
+ * The caller should begin a map before calling this. */
+static void
+format_message_sprinter (sprinter_t *sp, notmuch_message_t *message)
+{
+    /* Any changes to the JSON or S-Expression format should be
+     * reflected in the file devel/schemata. */
+
+    void *local = talloc_new (NULL);
+    notmuch_tags_t *tags;
+    time_t date;
+    const char *relative_date;
+
+    sp->map_key (sp, "id");
+    sp->string (sp, notmuch_message_get_message_id (message));
+
+    sp->map_key (sp, "match");
+    sp->boolean (sp, _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH));
+
+    sp->map_key (sp, "excluded");
+    sp->boolean (sp, _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED));
+
+    sp->map_key (sp, "filename");
+    if (notmuch_format_version >= 3) {
+       notmuch_filenames_t *filenames;
+
+       sp->begin_list (sp);
+       for (filenames = notmuch_message_get_filenames (message);
+            notmuch_filenames_valid (filenames);
+            notmuch_filenames_move_to_next (filenames)) {
+           sp->string (sp, notmuch_filenames_get (filenames));
+       }
+       notmuch_filenames_destroy (filenames);
+       sp->end (sp);
+    } else {
+       sp->string (sp, notmuch_message_get_filename (message));
+    }
+
+    sp->map_key (sp, "timestamp");
+    date = notmuch_message_get_date (message);
+    sp->integer (sp, date);
+
+    sp->map_key (sp, "date_relative");
+    relative_date = notmuch_time_relative_date (local, date);
+    sp->string (sp, relative_date);
+
+    sp->map_key (sp, "tags");
+    sp->begin_list (sp);
+    for (tags = notmuch_message_get_tags (message);
+        notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags))
+       sp->string (sp, notmuch_tags_get (tags));
+    sp->end (sp);
+
+    talloc_free (local);
+}
+
+/* Extract just the email address from the contents of a From:
+ * header. */
+static const char *
+_extract_email_address (const void *ctx, const char *from)
+{
+    InternetAddressList *addresses;
+    InternetAddress *address;
+    InternetAddressMailbox *mailbox;
+    const char *email = "MAILER-DAEMON";
+
+    addresses = internet_address_list_parse (NULL, from);
+
+    /* Bail if there is no address here. */
+    if (addresses == NULL || internet_address_list_length (addresses) < 1)
+       goto DONE;
+
+    /* Otherwise, just use the first address. */
+    address = internet_address_list_get_address (addresses, 0);
+
+    /* The From header should never contain an address group rather
+     * than a mailbox. So bail if it does. */
+    if (! INTERNET_ADDRESS_IS_MAILBOX (address))
+       goto DONE;
+
+    mailbox = INTERNET_ADDRESS_MAILBOX (address);
+    email = internet_address_mailbox_get_addr (mailbox);
+    email = talloc_strdup (ctx, email);
+
+  DONE:
+    if (addresses)
+       g_object_unref (addresses);
+
+    return email;
+}
+
+/* Return 1 if 'line' is an mbox From_ line---that is, a line
+ * beginning with zero or more '>' characters followed by the
+ * characters 'F', 'r', 'o', 'm', and space.
+ *
+ * Any characters at all may appear after that in the line.
+ */
+static int
+_is_from_line (const char *line)
+{
+    const char *s = line;
+
+    if (line == NULL)
+       return 0;
+
+    while (*s == '>')
+       s++;
+
+    if (STRNCMP_LITERAL (s, "From ") == 0)
+       return 1;
+    else
+       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)
+{
+    /* Any changes to the JSON or S-Expression format should be
+     * reflected in the file devel/schemata. */
+
+    char *recipients_string;
+    const char *reply_to_string;
+    void *local = talloc_new (sp);
+
+    sp->begin_map (sp);
+
+    sp->map_key (sp, "Subject");
+    if (msg_crypto && msg_crypto->payload_subject) {
+       sp->string (sp, msg_crypto->payload_subject);
+    } else
+       sp->string (sp, g_mime_message_get_subject (message));
+
+    sp->map_key (sp, "From");
+    sp->string (sp, g_mime_message_get_from_string (message));
+
+    recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_TO);
+    if (recipients_string) {
+       sp->map_key (sp, "To");
+       sp->string (sp, recipients_string);
+       g_free (recipients_string);
+    }
+
+    recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_CC);
+    if (recipients_string) {
+       sp->map_key (sp, "Cc");
+       sp->string (sp, recipients_string);
+       g_free (recipients_string);
+    }
+
+    recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_BCC);
+    if (recipients_string) {
+       sp->map_key (sp, "Bcc");
+       sp->string (sp, recipients_string);
+       g_free (recipients_string);
+    }
+
+    reply_to_string = g_mime_message_get_reply_to_string (local, message);
+    if (reply_to_string) {
+       sp->map_key (sp, "Reply-To");
+       sp->string (sp, reply_to_string);
+    }
+
+    if (reply) {
+       sp->map_key (sp, "In-reply-to");
+       sp->string (sp, g_mime_object_get_header (GMIME_OBJECT (message), "In-reply-to"));
+
+       sp->map_key (sp, "References");
+       sp->string (sp, g_mime_object_get_header (GMIME_OBJECT (message), "References"));
+    } else {
+       sp->map_key (sp, "Date");
+       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);
+}
+
+/* Write a MIME text part out to the given stream.
+ *
+ * If (flags & NOTMUCH_SHOW_TEXT_PART_REPLY), this prepends "> " to
+ * each output line.
+ *
+ * Both line-ending conversion (CRLF->LF) and charset conversion ( ->
+ * UTF-8) will be performed, so it is inappropriate to call this
+ * function with a non-text part. Doing so will trigger an internal
+ * error.
+ */
+void
+show_text_part_content (GMimeObject *part, GMimeStream *stream_out,
+                       notmuch_show_text_part_flags flags)
+{
+    GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
+    GMimeStream *stream_filter = NULL;
+    GMimeFilter *crlf_filter = NULL;
+    GMimeFilter *windows_filter = NULL;
+    GMimeDataWrapper *wrapper;
+    const char *charset;
+
+    if (! g_mime_content_type_is_type (content_type, "text", "*"))
+       INTERNAL_ERROR ("Illegal request to format non-text part (%s) as text.",
+                       g_mime_content_type_get_mime_type (content_type));
+
+    if (stream_out == NULL)
+       return;
+
+    charset = g_mime_object_get_content_type_parameter (part, "charset");
+    charset = charset ? g_mime_charset_canon_name (charset) : NULL;
+    wrapper = g_mime_part_get_content (GMIME_PART (part));
+    if (wrapper && charset && ! g_ascii_strncasecmp (charset, "iso-8859-", 9)) {
+       GMimeStream *null_stream = NULL;
+       GMimeStream *null_stream_filter = NULL;
+
+       /* Check for mislabeled Windows encoding */
+       null_stream = g_mime_stream_null_new ();
+       null_stream_filter = g_mime_stream_filter_new (null_stream);
+       windows_filter = g_mime_filter_windows_new (charset);
+       g_mime_stream_filter_add (GMIME_STREAM_FILTER (null_stream_filter),
+                                 windows_filter);
+       g_mime_data_wrapper_write_to_stream (wrapper, null_stream_filter);
+       charset = g_mime_filter_windows_real_charset (
+           (GMimeFilterWindows *) windows_filter);
+
+       if (null_stream_filter)
+           g_object_unref (null_stream_filter);
+       if (null_stream)
+           g_object_unref (null_stream);
+       /* Keep a reference to windows_filter in order to prevent the
+        * charset string from deallocation. */
+    }
+
+    stream_filter = g_mime_stream_filter_new (stream_out);
+    crlf_filter = g_mime_filter_dos2unix_new (false);
+    g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
+                             crlf_filter);
+    g_object_unref (crlf_filter);
+
+    if (charset) {
+       GMimeFilter *charset_filter;
+       charset_filter = g_mime_filter_charset_new (charset, "UTF-8");
+       /* This result can be NULL for things like "unknown-8bit".
+        * Don't set a NULL filter as that makes GMime print
+        * annoying assertion-failure messages on stderr. */
+       if (charset_filter) {
+           g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
+                                     charset_filter);
+           g_object_unref (charset_filter);
+       }
+
+    }
+
+    if (flags & NOTMUCH_SHOW_TEXT_PART_REPLY) {
+       GMimeFilter *reply_filter;
+       reply_filter = g_mime_filter_reply_new (true);
+       if (reply_filter) {
+           g_mime_stream_filter_add (GMIME_STREAM_FILTER (stream_filter),
+                                     reply_filter);
+           g_object_unref (reply_filter);
+       }
+    }
+
+    if (wrapper && stream_filter)
+       g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
+    if (stream_filter)
+       g_object_unref (stream_filter);
+    if (windows_filter)
+       g_object_unref (windows_filter);
+}
+
+static const char *
+signature_status_to_string (GMimeSignatureStatus status)
+{
+    if (g_mime_signature_status_bad (status))
+       return "bad";
+
+    if (g_mime_signature_status_error (status))
+       return "error";
+
+    if (g_mime_signature_status_good (status))
+       return "good";
+
+    return "unknown";
+}
+
+/* Print signature flags */
+struct key_map_struct {
+    GMimeSignatureStatus bit;
+    const char *string;
+};
+
+static void
+do_format_signature_errors (sprinter_t *sp, struct key_map_struct *key_map,
+                           unsigned int array_map_len, GMimeSignatureStatus errors)
+{
+    sp->map_key (sp, "errors");
+    sp->begin_map (sp);
+
+    for (unsigned int i = 0; i < array_map_len; i++) {
+       if (errors & key_map[i].bit) {
+           sp->map_key (sp, key_map[i].string);
+           sp->boolean (sp, true);
+       }
+    }
+
+    sp->end (sp);
+}
+
+static void
+format_signature_errors (sprinter_t *sp, GMimeSignature *signature)
+{
+    GMimeSignatureStatus errors = g_mime_signature_get_status (signature);
+
+    if (! (errors & GMIME_SIGNATURE_STATUS_ERROR_MASK))
+       return;
+
+    struct key_map_struct key_map[] = {
+       { GMIME_SIGNATURE_STATUS_KEY_REVOKED, "key-revoked" },
+       { GMIME_SIGNATURE_STATUS_KEY_EXPIRED, "key-expired" },
+       { GMIME_SIGNATURE_STATUS_SIG_EXPIRED, "sig-expired" },
+       { GMIME_SIGNATURE_STATUS_KEY_MISSING, "key-missing" },
+       { GMIME_SIGNATURE_STATUS_CRL_MISSING, "crl-missing" },
+       { GMIME_SIGNATURE_STATUS_CRL_TOO_OLD, "crl-too-old" },
+       { GMIME_SIGNATURE_STATUS_BAD_POLICY, "bad-policy" },
+       { GMIME_SIGNATURE_STATUS_SYS_ERROR, "sys-error" },
+       { GMIME_SIGNATURE_STATUS_TOFU_CONFLICT, "tofu-conflict" },
+    };
+
+    do_format_signature_errors (sp, key_map, ARRAY_SIZE (key_map), errors);
+}
+
+/* Signature status sprinter */
+static void
+format_part_sigstatus_sprinter (sprinter_t *sp, GMimeSignatureList *siglist)
+{
+    /* Any changes to the JSON or S-Expression format should be
+     * reflected in the file devel/schemata. */
+
+    sp->begin_list (sp);
+
+    if (! siglist) {
+       sp->end (sp);
+       return;
+    }
+
+    int i;
+
+    for (i = 0; i < g_mime_signature_list_length (siglist); i++) {
+       GMimeSignature *signature = g_mime_signature_list_get_signature (siglist, i);
+
+       sp->begin_map (sp);
+
+       /* status */
+       GMimeSignatureStatus status = g_mime_signature_get_status (signature);
+       sp->map_key (sp, "status");
+       sp->string (sp, signature_status_to_string (status));
+
+       GMimeCertificate *certificate = g_mime_signature_get_certificate (signature);
+       if (g_mime_signature_status_good (status)) {
+           if (certificate) {
+               sp->map_key (sp, "fingerprint");
+               sp->string (sp, g_mime_certificate_get_fingerprint (certificate));
+           }
+           /* these dates are seconds since the epoch; should we
+            * provide a more human-readable format string? */
+           time_t created = g_mime_signature_get_created (signature);
+           if (created != -1) {
+               sp->map_key (sp, "created");
+               sp->integer (sp, created);
+           }
+           time_t expires = g_mime_signature_get_expires (signature);
+           if (expires > 0) {
+               sp->map_key (sp, "expires");
+               sp->integer (sp, expires);
+           }
+           if (certificate) {
+               const char *uid = g_mime_certificate_get_valid_userid (certificate);
+               if (uid) {
+                   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);
+           if (key_id) {
+               sp->map_key (sp, "keyid");
+               sp->string (sp, key_id);
+           }
+       }
+
+       if (notmuch_format_version <= 3) {
+           GMimeSignatureStatus errors = g_mime_signature_get_status (signature);
+           if (g_mime_signature_status_error (errors)) {
+               sp->map_key (sp, "errors");
+               sp->integer (sp, errors);
+           }
+       } else {
+           format_signature_errors (sp, signature);
+       }
+
+       sp->end (sp);
+    }
+
+    sp->end (sp);
+}
+
+static notmuch_status_t
+format_part_text (const void *ctx, sprinter_t *sp, mime_node_t *node,
+                 int indent, const notmuch_show_params_t *params)
+{
+    /* The disposition and content-type metadata are associated with
+     * the envelope for message parts */
+    GMimeObject *meta = node->envelope_part ? (
+       GMIME_OBJECT (node->envelope_part) ) : node->part;
+    GMimeContentType *content_type = g_mime_object_get_content_type (meta);
+    const bool leaf = GMIME_IS_PART (node->part);
+    GMimeStream *stream = params->out_stream;
+    const char *part_type;
+    int i;
+
+    if (node->envelope_file) {
+       notmuch_message_t *message = node->envelope_file;
+
+       part_type = "message";
+       g_mime_stream_printf (stream, "\f%s{ id:%s depth:%d match:%d excluded:%d filename:%s\n",
+                             part_type,
+                             notmuch_message_get_message_id (message),
+                             indent,
+                             _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? 1 : 0,
+                             _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED) ? 1 : 0,
+                             notmuch_message_get_filename (message));
+    } else {
+       char *content_string;
+       const char *disposition = _get_disposition (meta);
+       const char *cid = g_mime_object_get_content_id (meta);
+       const char *filename = leaf ? (
+           g_mime_part_get_filename (GMIME_PART (node->part)) ) : NULL;
+
+       if (disposition &&
+           strcasecmp (disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
+           part_type = "attachment";
+       else
+           part_type = "part";
+
+       g_mime_stream_printf (stream, "\f%s{ ID: %d", part_type, node->part_num);
+       if (filename)
+           g_mime_stream_printf (stream, ", Filename: %s", filename);
+       if (cid)
+           g_mime_stream_printf (stream, ", Content-id: %s", cid);
+
+       content_string = g_mime_content_type_get_mime_type (content_type);
+       g_mime_stream_printf (stream, ", Content-type: %s\n", content_string);
+       g_free (content_string);
+    }
+
+    if (GMIME_IS_MESSAGE (node->part)) {
+       GMimeMessage *message = GMIME_MESSAGE (node->part);
+       char *recipients_string;
+       char *date_string;
+
+       g_mime_stream_printf (stream, "\fheader{\n");
+       if (node->envelope_file)
+           g_mime_stream_printf (stream, "%s\n", _get_one_line_summary (ctx, node->envelope_file));
+       g_mime_stream_printf (stream, "Subject: %s\n", g_mime_message_get_subject (message));
+       g_mime_stream_printf (stream, "From: %s\n", g_mime_message_get_from_string (message));
+       recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_TO);
+       if (recipients_string)
+           g_mime_stream_printf (stream, "To: %s\n", recipients_string);
+       g_free (recipients_string);
+       recipients_string = g_mime_message_get_address_string (message, GMIME_ADDRESS_TYPE_CC);
+       if (recipients_string)
+           g_mime_stream_printf (stream, "Cc: %s\n", recipients_string);
+       g_free (recipients_string);
+       date_string = g_mime_message_get_date_string (node, message);
+       g_mime_stream_printf (stream, "Date: %s\n", date_string);
+       g_mime_stream_printf (stream, "\fheader}\n");
+
+       if (! params->output_body) {
+           g_mime_stream_printf (stream, "\f%s}\n", part_type);
+           return NOTMUCH_STATUS_SUCCESS;
+       }
+       g_mime_stream_printf (stream, "\fbody{\n");
+    }
+
+    if (leaf) {
+       if (g_mime_content_type_is_type (content_type, "text", "*") &&
+           (params->include_html ||
+            ! g_mime_content_type_is_type (content_type, "text", "html"))) {
+           show_text_part_content (node->part, stream, 0);
+       } else {
+           char *content_string = g_mime_content_type_get_mime_type (content_type);
+           g_mime_stream_printf (stream, "Non-text part: %s\n", content_string);
+           g_free (content_string);
+       }
+    }
+
+    for (i = 0; i < node->nchildren; i++)
+       format_part_text (ctx, sp, mime_node_child (node, i), indent, params);
+
+    if (GMIME_IS_MESSAGE (node->part))
+       g_mime_stream_printf (stream, "\fbody}\n");
+
+    g_mime_stream_printf (stream, "\f%s}\n", part_type);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static void
+format_omitted_part_meta_sprinter (sprinter_t *sp, GMimeObject *meta, GMimePart *part)
+{
+    const char *content_charset = g_mime_object_get_content_type_parameter (meta, "charset");
+    const char *cte = g_mime_object_get_header (meta, "content-transfer-encoding");
+    GMimeDataWrapper *wrapper = g_mime_part_get_content (part);
+    GMimeStream *stream = g_mime_data_wrapper_get_stream (wrapper);
+    ssize_t content_length = g_mime_stream_length (stream);
+
+    if (content_charset != NULL) {
+       sp->map_key (sp, "content-charset");
+       sp->string (sp, content_charset);
+    }
+    if (cte != NULL) {
+       sp->map_key (sp, "content-transfer-encoding");
+       sp->string (sp, cte);
+    }
+    if (content_length >= 0) {
+       sp->map_key (sp, "content-length");
+       sp->integer (sp, content_length);
+    }
+}
+
+void
+format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
+                     int duplicate,
+                     bool output_body,
+                     bool include_html)
+{
+    /* Any changes to the JSON or S-Expression format should be
+     * reflected in the file devel/schemata. */
+
+    if (node->envelope_file) {
+       const _notmuch_message_crypto_t *msg_crypto = NULL;
+       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), -1, true, include_html);
+           sp->end (sp);
+       }
+
+       msg_crypto = mime_node_get_message_crypto_status (node);
+       if (notmuch_format_version >= 4) {
+           sp->map_key (sp, "crypto");
+           sp->begin_map (sp);
+           if (msg_crypto->sig_list ||
+               msg_crypto->decryption_status != NOTMUCH_MESSAGE_DECRYPTED_NONE) {
+               if (msg_crypto->sig_list) {
+                   sp->map_key (sp, "signed");
+                   sp->begin_map (sp);
+                   sp->map_key (sp, "status");
+                   format_part_sigstatus_sprinter (sp, msg_crypto->sig_list);
+                   if (msg_crypto->signature_encrypted) {
+                       sp->map_key (sp, "encrypted");
+                       sp->boolean (sp, msg_crypto->signature_encrypted);
+                   }
+                   if (msg_crypto->payload_subject) {
+                       sp->map_key (sp, "headers");
+                       sp->begin_list (sp);
+                       sp->string (sp, "Subject");
+                       sp->end (sp);
+                   }
+                   sp->end (sp);
+               }
+               if (msg_crypto->decryption_status != NOTMUCH_MESSAGE_DECRYPTED_NONE) {
+                   sp->map_key (sp, "decrypted");
+                   sp->begin_map (sp);
+                   sp->map_key (sp, "status");
+                   sp->string (sp, msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL ?
+                               "full" : "partial");
+
+                   if (msg_crypto->payload_subject) {
+                       const char *subject = g_mime_message_get_subject GMIME_MESSAGE (node->part);
+                       if (subject == NULL || strcmp (subject, msg_crypto->payload_subject)) {
+                           /* protected subject differs from the external header */
+                           sp->map_key (sp, "header-mask");
+                           sp->begin_map (sp);
+                           sp->map_key (sp, "Subject");
+                           if (subject == NULL)
+                               sp->null (sp);
+                           else
+                               sp->string (sp, subject);
+                           sp->end (sp);
+                       }
+                   }
+                   sp->end (sp);
+               }
+           }
+           sp->end (sp);
+       }
+
+       sp->map_key (sp, "headers");
+       format_headers_sprinter (sp, GMIME_MESSAGE (node->part), false, msg_crypto);
+
+       sp->end (sp);
+       return;
+    }
+
+    /* The disposition and content-type metadata are associated with
+     * the envelope for message parts */
+    GMimeObject *meta = node->envelope_part ? (
+       GMIME_OBJECT (node->envelope_part) ) : node->part;
+    GMimeContentType *content_type = g_mime_object_get_content_type (meta);
+    char *content_string;
+    const char *disposition = _get_disposition (meta);
+    const char *cid = g_mime_object_get_content_id (meta);
+    const char *filename = GMIME_IS_PART (node->part) ? (
+       g_mime_part_get_filename (GMIME_PART (node->part) ) ) : NULL;
+    int nclose = 0;
+    int i;
+
+    sp->begin_map (sp);
+
+    sp->map_key (sp, "id");
+    sp->integer (sp, node->part_num);
+
+    if (node->decrypt_attempted) {
+       sp->map_key (sp, "encstatus");
+       sp->begin_list (sp);
+       sp->begin_map (sp);
+       sp->map_key (sp, "status");
+       sp->string (sp, node->decrypt_success ? "good" : "bad");
+       sp->end (sp);
+       sp->end (sp);
+    }
+
+    if (node->verify_attempted) {
+       sp->map_key (sp, "sigstatus");
+       format_part_sigstatus_sprinter (sp, node->sig_list);
+    }
+
+    sp->map_key (sp, "content-type");
+    content_string = g_mime_content_type_get_mime_type (content_type);
+    sp->string (sp, content_string);
+    g_free (content_string);
+
+    if (disposition) {
+       sp->map_key (sp, "content-disposition");
+       sp->string (sp, disposition);
+    }
+
+    if (cid) {
+       sp->map_key (sp, "content-id");
+       sp->string (sp, cid);
+    }
+
+    if (filename) {
+       sp->map_key (sp, "filename");
+       sp->string (sp, filename);
+    }
+
+    if (GMIME_IS_PART (node->part)) {
+       /* For non-HTML text parts, we include the content in the
+        * JSON. Since JSON must be Unicode, we handle charset
+        * decoding here and do not report a charset to the caller.
+        * For text/html parts, we do not include the content unless
+        * the --include-html option has been passed. If a html part
+        * is not included, it can be requested directly. This makes
+        * charset decoding the responsibility on the caller so we
+        * report the charset for text/html parts.
+        */
+       if (g_mime_content_type_is_type (content_type, "text", "*") &&
+           (include_html ||
+            ! g_mime_content_type_is_type (content_type, "text", "html"))) {
+           GMimeStream *stream_memory = g_mime_stream_mem_new ();
+           GByteArray *part_content;
+           show_text_part_content (node->part, stream_memory, 0);
+           part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
+           sp->map_key (sp, "content");
+           sp->string_len (sp, (char *) part_content->data, part_content->len);
+           g_object_unref (stream_memory);
+       } else {
+           /* if we have a child part despite being a standard
+            * (non-multipart) MIME part, that means there is
+            * something to unwrap, which we will present in
+            * content: */
+           if (node->nchildren) {
+               sp->map_key (sp, "content");
+               sp->begin_list (sp);
+               nclose = 1;
+           } else
+               format_omitted_part_meta_sprinter (sp, meta, GMIME_PART (node->part));
+       }
+    } else if (GMIME_IS_MULTIPART (node->part)) {
+       sp->map_key (sp, "content");
+       sp->begin_list (sp);
+       nclose = 1;
+    } else if (GMIME_IS_MESSAGE (node->part)) {
+       sp->map_key (sp, "content");
+       sp->begin_list (sp);
+       sp->begin_map (sp);
+
+       sp->map_key (sp, "headers");
+       format_headers_sprinter (sp, GMIME_MESSAGE (node->part), false, NULL);
+
+       sp->map_key (sp, "body");
+       sp->begin_list (sp);
+       nclose = 3;
+    }
+
+    for (i = 0; i < node->nchildren; i++)
+       format_part_sprinter (ctx, sp, mime_node_child (node, i), -1, true, include_html);
+
+    /* Close content structures */
+    for (i = 0; i < nclose; i++)
+       sp->end (sp);
+    /* Close part map */
+    sp->end (sp);
+}
+
+static notmuch_status_t
+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->duplicate, params->output_body,
+                         params->include_html);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Print a message in "mboxrd" format as documented, for example,
+ * here:
+ *
+ * http://qmail.org/qmail-manual-html/man5/mbox.html
+ */
+static notmuch_status_t
+format_part_mbox (const void *ctx, unused (sprinter_t *sp), mime_node_t *node,
+                 unused (int indent),
+                 unused (const notmuch_show_params_t *params))
+{
+    notmuch_message_t *message = node->envelope_file;
+
+    const char *filename;
+    gzFile file;
+    const char *from;
+
+    time_t date;
+    struct tm date_gmtime;
+    char date_asctime[26];
+
+    char *line = NULL;
+    ssize_t line_size;
+    ssize_t line_len;
+
+    if (! message)
+       INTERNAL_ERROR ("format_part_mbox requires a root part");
+
+    filename = notmuch_message_get_filename (message);
+    file = gzopen (filename, "r");
+    if (file == NULL) {
+       fprintf (stderr, "Failed to open %s: %s\n",
+                filename, strerror (errno));
+       return NOTMUCH_STATUS_FILE_ERROR;
+    }
+
+    from = notmuch_message_get_header (message, "from");
+    from = _extract_email_address (ctx, from);
+
+    date = notmuch_message_get_date (message);
+    gmtime_r (&date, &date_gmtime);
+    asctime_r (&date_gmtime, date_asctime);
+
+    printf ("From %s %s", from, date_asctime);
+
+    while ((line_len = gz_getline (message, &line, &line_size, file)) != UTIL_EOF ) {
+       if (_is_from_line (line))
+           putchar ('>');
+       printf ("%s", line);
+    }
+
+    printf ("\n");
+
+    gzclose (file);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+format_part_raw (unused (const void *ctx), unused (sprinter_t *sp),
+                mime_node_t *node, unused (int indent),
+                const notmuch_show_params_t *params)
+{
+    if (node->envelope_file) {
+       /* Special case the entire message to avoid MIME parsing. */
+       const char *filename;
+       GMimeStream *stream = NULL;
+       ssize_t ssize;
+       char buf[4096];
+       notmuch_status_t ret = NOTMUCH_STATUS_FILE_ERROR;
+
+       filename = _get_filename (node->envelope_file, params->duplicate);
+       if (filename == NULL) {
+           fprintf (stderr, "Error: Cannot get message filename.\n");
+           goto DONE;
+       }
+
+       stream = g_mime_stream_gzfile_open (filename);
+       if (stream == NULL) {
+           fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
+           goto DONE;
+       }
+
+       while (! g_mime_stream_eos (stream)) {
+           ssize = g_mime_stream_read (stream, buf, sizeof (buf));
+           if (ssize < 0) {
+               fprintf (stderr, "Error: Read failed from %s\n", filename);
+               goto DONE;
+           }
+
+           if (ssize > 0 && fwrite (buf, ssize, 1, stdout) != 1) {
+               fprintf (stderr, "Error: Write %zd chars to stdout failed\n", ssize);
+               goto DONE;
+           }
+       }
+
+       ret = NOTMUCH_STATUS_SUCCESS;
+
+       /* XXX This DONE is just for the special case of a node in a single file */
+      DONE:
+       if (stream)
+           g_object_unref (stream);
+
+       return ret;
+    }
+
+    GMimeStream *stream_filter = g_mime_stream_filter_new (params->out_stream);
+
+    if (GMIME_IS_PART (node->part)) {
+       /* For leaf parts, we emit only the transfer-decoded
+        * body. */
+       GMimeDataWrapper *wrapper;
+       wrapper = g_mime_part_get_content (GMIME_PART (node->part));
+
+       if (wrapper && stream_filter)
+           g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
+    } else {
+       /* Write out the whole part.  For message parts (the root
+        * part and embedded message parts), this will be the
+        * message including its headers (but not the
+        * encapsulating part's headers).  For multipart parts,
+        * this will include the headers. */
+       if (stream_filter)
+           g_mime_object_write_to_stream (node->part, NULL, stream_filter);
+    }
+
+    if (stream_filter)
+       g_object_unref (stream_filter);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+show_message (void *ctx,
+             const notmuch_show_format_t *format,
+             sprinter_t *sp,
+             notmuch_message_t *message,
+             int indent,
+             notmuch_show_params_t *params)
+{
+    void *local = talloc_new (ctx);
+    mime_node_t *root, *part;
+    notmuch_status_t status;
+    unsigned int session_keys = 0;
+    notmuch_status_t session_key_count_error = NOTMUCH_STATUS_SUCCESS;
+
+    if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
+       session_key_count_error = notmuch_message_count_properties (message, "session-key",
+                                                                   &session_keys);
+
+    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));
+    if (part)
+       status = format->part (local, sp, part, indent, params);
+    if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE && session_key_count_error ==
+       NOTMUCH_STATUS_SUCCESS) {
+       unsigned int new_session_keys = 0;
+       if (notmuch_message_count_properties (message, "session-key", &new_session_keys) ==
+           NOTMUCH_STATUS_SUCCESS &&
+           new_session_keys > session_keys) {
+           /* try a quiet re-indexing */
+           notmuch_indexopts_t *indexopts = notmuch_database_get_default_indexopts (
+               notmuch_message_get_database (message));
+           if (indexopts) {
+               notmuch_indexopts_set_decrypt_policy (indexopts, NOTMUCH_DECRYPT_AUTO);
+               print_status_message ("Error re-indexing message with --decrypt=stash",
+                                     message, notmuch_message_reindex (message, indexopts));
+           }
+       }
+    }
+  DONE:
+    talloc_free (local);
+    return status;
+}
+
+static notmuch_status_t
+show_messages (void *ctx,
+              const notmuch_show_format_t *format,
+              sprinter_t *sp,
+              notmuch_messages_t *messages,
+              int indent,
+              notmuch_show_params_t *params)
+{
+    notmuch_message_t *message;
+    bool match;
+    bool excluded;
+    int next_indent;
+    notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
+
+    sp->begin_list (sp);
+
+    for (;
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages)) {
+       sp->begin_list (sp);
+
+       message = notmuch_messages_get (messages);
+
+       match = _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
+       excluded = _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED);
+
+       next_indent = indent;
+
+       if ((match && (! excluded || ! params->omit_excluded)) || params->entire_thread) {
+           status = show_message (ctx, format, sp, message, indent, params);
+           if (status && ! res)
+               res = status;
+           next_indent = indent + 1;
+       } else {
+           sp->null (sp);
+       }
+
+       status = show_messages (ctx,
+                               format, sp,
+                               notmuch_message_get_replies (message),
+                               next_indent,
+                               params);
+       if (status && ! res)
+           res = status;
+
+       notmuch_message_destroy (message);
+
+       sp->end (sp);
+    }
+
+    sp->end (sp);
+
+    return res;
+}
+
+/* Formatted output of single message */
+static int
+do_show_single (void *ctx,
+               notmuch_query_t *query,
+               const notmuch_show_format_t *format,
+               sprinter_t *sp,
+               notmuch_show_params_t *params)
+{
+    notmuch_messages_t *messages;
+    notmuch_message_t *message;
+    notmuch_status_t status;
+    unsigned int count;
+
+    status = notmuch_query_count_messages (query, &count);
+    if (print_status_query ("notmuch show", query, status))
+       return 1;
+
+    if (count != 1) {
+       fprintf (stderr,
+                "Error: search term did not match precisely one message (matched %u messages).\n",
+                count);
+       return 1;
+    }
+
+    status = notmuch_query_search_messages (query, &messages);
+    if (print_status_query ("notmuch show", query, status))
+       return 1;
+
+    message = notmuch_messages_get (messages);
+
+    if (message == NULL) {
+       fprintf (stderr, "Error: Cannot find matching message.\n");
+       return 1;
+    }
+
+    notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
+
+    return show_message (ctx, format, sp, message, 0, params)
+          != NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Formatted output of threads */
+static int
+do_show_threaded (void *ctx,
+                 notmuch_query_t *query,
+                 const notmuch_show_format_t *format,
+                 sprinter_t *sp,
+                 notmuch_show_params_t *params)
+{
+    notmuch_threads_t *threads;
+    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))
+       return 1;
+
+    sp->begin_list (sp);
+
+    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)
+           INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
+                           notmuch_thread_get_thread_id (thread));
+
+       status = show_messages (ctx, format, sp, messages, 0, params);
+       if (status && ! res)
+           res = status;
+
+       notmuch_thread_destroy (thread);
+
+    }
+
+    sp->end (sp);
+
+    return res != NOTMUCH_STATUS_SUCCESS;
+}
+
+static int
+do_show_unthreaded (void *ctx,
+                   notmuch_query_t *query,
+                   const notmuch_show_format_t *format,
+                   sprinter_t *sp,
+                   notmuch_show_params_t *params)
+{
+    notmuch_messages_t *messages;
+    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))
+       return 1;
+
+    sp->begin_list (sp);
+
+    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);
+
+       message = notmuch_messages_get (messages);
+
+       notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, TRUE);
+       excluded = _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED);
+
+       if (! excluded || ! params->omit_excluded) {
+           status = show_message (ctx, format, sp, message, 0, params);
+           if (status && ! res)
+               res = status;
+       } else {
+           sp->null (sp);
+       }
+       notmuch_message_destroy (message);
+       sp->end (sp);
+       sp->end (sp);
+    }
+    sp->end (sp);
+    return res;
+}
+
+enum {
+    NOTMUCH_FORMAT_NOT_SPECIFIED,
+    NOTMUCH_FORMAT_JSON,
+    NOTMUCH_FORMAT_SEXP,
+    NOTMUCH_FORMAT_TEXT,
+    NOTMUCH_FORMAT_MBOX,
+    NOTMUCH_FORMAT_RAW
+};
+
+static const notmuch_show_format_t format_json = {
+    .new_sprinter = sprinter_json_create,
+    .part = format_part_sprinter_entry,
+};
+
+static const notmuch_show_format_t format_sexp = {
+    .new_sprinter = sprinter_sexp_create,
+    .part = format_part_sprinter_entry,
+};
+
+static const notmuch_show_format_t format_text = {
+    .new_sprinter = sprinter_text_create,
+    .part = format_part_text,
+};
+
+static const notmuch_show_format_t format_mbox = {
+    .new_sprinter = sprinter_text_create,
+    .part = format_part_mbox,
+};
+
+static const notmuch_show_format_t format_raw = {
+    .new_sprinter = sprinter_text_create,
+    .part = format_part_raw,
+};
+
+static const notmuch_show_format_t *formatters[] = {
+    [NOTMUCH_FORMAT_JSON] = &format_json,
+    [NOTMUCH_FORMAT_SEXP] = &format_sexp,
+    [NOTMUCH_FORMAT_TEXT] = &format_text,
+    [NOTMUCH_FORMAT_MBOX] = &format_mbox,
+    [NOTMUCH_FORMAT_RAW] = &format_raw,
+};
+
+int
+notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    notmuch_query_t *query;
+    char *query_string;
+    int opt_index, ret;
+    const notmuch_show_format_t *formatter;
+    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 },
+    };
+    int format = NOTMUCH_FORMAT_NOT_SPECIFIED;
+    bool exclude = true;
+    bool entire_thread_set = false;
+    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 },
+                                     { "sexp", NOTMUCH_FORMAT_SEXP },
+                                     { "mbox", NOTMUCH_FORMAT_MBOX },
+                                     { "raw", NOTMUCH_FORMAT_RAW },
+                                     { 0, 0 } } },
+       { .opt_int = &notmuch_format_version, .name = "format-version" },
+       { .opt_bool = &exclude, .name = "exclude" },
+       { .opt_bool = &params.entire_thread, .name = "entire-thread",
+         .present = &entire_thread_set },
+       { .opt_bool = &unthreaded, .name = "unthreaded" },
+       { .opt_int = &params.part, .name = "part" },
+       { .opt_keyword = (int *) (&params.crypto.decrypt), .name = "decrypt",
+         .keyword_no_arg_value = "true", .keywords =
+             (notmuch_keyword_t []){ { "false", NOTMUCH_DECRYPT_FALSE },
+                                     { "auto", NOTMUCH_DECRYPT_AUTO },
+                                     { "true", NOTMUCH_DECRYPT_NOSTASH },
+                                     { "stash", NOTMUCH_DECRYPT_TRUE },
+                                     { 0, 0 } } },
+       { .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 },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    /* explicit decryption implies verification */
+    if (params.crypto.decrypt == NOTMUCH_DECRYPT_NOSTASH ||
+       params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
+       params.crypto.verify = true;
+
+    /* 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)
+           format = NOTMUCH_FORMAT_RAW;
+       else
+           format = NOTMUCH_FORMAT_TEXT;
+    }
+
+    if (format == NOTMUCH_FORMAT_MBOX) {
+       if (params.part > 0) {
+           fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
+           return EXIT_FAILURE;
+       }
+    } else if (format == NOTMUCH_FORMAT_RAW) {
+       /* raw format only supports single message display */
+       single_message = true;
+    }
+
+    notmuch_exit_if_unsupported_format ();
+
+    /* Default is entire-thread = false except for format=json and
+     * format=sexp. */
+    if (! entire_thread_set &&
+       (format == NOTMUCH_FORMAT_JSON || format == NOTMUCH_FORMAT_SEXP))
+       params.entire_thread = true;
+
+    if (! params.output_body) {
+       if (params.part > 0) {
+           fprintf (stderr, "Warning: --body=false is incompatible with --part > 0. Disabling.\n");
+           params.output_body = true;
+       } else {
+           if (format != NOTMUCH_FORMAT_TEXT &&
+               format != NOTMUCH_FORMAT_JSON &&
+               format != NOTMUCH_FORMAT_SEXP)
+               fprintf (stderr,
+                        "Warning: --body=false only implemented for format=text, format=json and format=sexp\n");
+       }
+    }
+
+    if (params.include_html &&
+       (format != NOTMUCH_FORMAT_TEXT &&
+        format != NOTMUCH_FORMAT_JSON &&
+        format != NOTMUCH_FORMAT_SEXP)) {
+       fprintf (stderr,
+                "Warning: --include-html only implemented for format=text, format=json and format=sexp\n");
+    }
+
+    if (params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE) {
+       status = notmuch_database_reopen (notmuch, NOTMUCH_DATABASE_MODE_READ_WRITE);
+       if (status) {
+           fprintf (stderr, "Error reopening database for READ_WRITE: %s\n",
+                    notmuch_status_to_string (status));
+           return EXIT_FAILURE;
+       }
+    }
+
+    query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
+    if (query_string == NULL) {
+       fprintf (stderr, "Out of memory\n");
+       return EXIT_FAILURE;
+    }
+
+    if (*query_string == '\0') {
+       fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
+       return EXIT_FAILURE;
+    }
+
+    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];
+    sprinter = formatter->new_sprinter (notmuch, stdout);
+
+    params.out_stream = g_mime_stream_stdout_new ();
+
+    /* If a single message is requested we do not use search_excludes. */
+    if (single_message) {
+       ret = do_show_single (notmuch, query, formatter, sprinter, &params);
+    } else {
+       /* We always apply set the exclude flag. The
+        * exclude=true|false option controls whether or not we return
+        * threads that only match in an excluded message */
+       notmuch_config_values_t *exclude_tags;
+       notmuch_status_t status;
+
+       for (exclude_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS);
+            notmuch_config_values_valid (exclude_tags);
+            notmuch_config_values_move_to_next (exclude_tags)) {
+
+           status = notmuch_query_add_tag_exclude (query,
+                                                   notmuch_config_values_get (exclude_tags));
+           if (status && status != NOTMUCH_STATUS_IGNORED) {
+               print_status_query ("notmuch show", query, status);
+               ret = -1;
+               goto DONE;
+           }
+       }
+
+       if (exclude == false) {
+           notmuch_query_set_omit_excluded (query, false);
+           params.omit_excluded = false;
+       }
+
+       if (unthreaded)
+           ret = do_show_unthreaded (notmuch, query, formatter, sprinter, &params);
+       else
+           ret = do_show_threaded (notmuch, query, formatter, sprinter, &params);
+    }
+
+  DONE:
+    g_mime_stream_flush (params.out_stream);
+    g_object_unref (params.out_stream);
+
+    _notmuch_crypto_cleanup (&params.crypto);
+    notmuch_query_destroy (query);
+    notmuch_database_destroy (notmuch);
+
+    return ret ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/notmuch-tag.c b/notmuch-tag.c
new file mode 100644 (file)
index 0000000..71ff06b
--- /dev/null
@@ -0,0 +1,317 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+#include "tag-util.h"
+#include "string-util.h"
+
+static volatile sig_atomic_t interrupted;
+
+static void
+handle_sigint (unused (int sig))
+{
+    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
+     * fail or produce a short write, we want to get out of the signal
+     * handler as quickly as possible, not retry it. */
+    IGNORE_RESULT (write (2, msg, sizeof (msg) - 1));
+    interrupted = 1;
+}
+
+
+static char *
+_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,
+     * furthermore, the presence of a '-' operator at the beginning of
+     * a group can inhibit parsing of the previous operator.  Hence,
+     * the user-provided query MUST appear first, but it is safe to
+     * parenthesize and the exclusion part of the query must not use
+     * the '-' operator (though the NOT operator is fine). */
+
+    char *escaped = NULL;
+    size_t escaped_len = 0;
+    char *query_string;
+    const char *join = "";
+    size_t i;
+
+    /* Don't optimize if there are no tag changes. */
+    if (tag_op_list_size (list) == 0)
+       return talloc_strdup (ctx, orig_query_string);
+
+    /* Build the new query string */
+    if (strcmp (orig_query_string, "*") == 0)
+       query_string = talloc_strdup (ctx, "(");
+    else
+       query_string = talloc_asprintf (ctx, "( %s ) and (", orig_query_string);
+
+    for (i = 0; i < tag_op_list_size (list) && query_string; i++) {
+       /* XXX in case of OOM, query_string will be deallocated when
+        * ctx is, which might be at shutdown */
+       if (make_boolean_term (ctx,
+                              "tag", tag_op_list_tag (list, i),
+                              &escaped, &escaped_len))
+           return NULL;
+
+       query_string = talloc_asprintf_append_buffer (
+           query_string, "%s%s%s", join,
+           tag_op_list_isremove (list, i) ? "" : "not ",
+           escaped);
+       join = " or ";
+    }
+
+    if (query_string)
+       query_string = talloc_strdup_append_buffer (query_string, ")");
+
+    talloc_free (escaped);
+    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
+tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string,
+          tag_op_list_t *tag_ops, tag_op_flag_t flags)
+{
+    notmuch_query_t *query;
+    notmuch_messages_t *messages;
+    notmuch_message_t *message;
+    notmuch_status_t status;
+
+    int ret = NOTMUCH_STATUS_SUCCESS;
+
+    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,
+                                           shared_option_query_syntax (),
+                                           tag_ops);
+       if (query_string == NULL) {
+           fprintf (stderr, "Out of memory.\n");
+           return 1;
+       }
+       flags |= TAG_FLAG_PRE_OPTIMIZED;
+    }
+
+    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);
+
+    status = notmuch_query_search_messages (query, &messages);
+    if (print_status_query ("notmuch tag", query, status))
+       return status;
+
+    for (;
+        notmuch_messages_valid (messages) && ! interrupted;
+        notmuch_messages_move_to_next (messages)) {
+       message = notmuch_messages_get (messages);
+       ret = tag_op_list_apply (message, tag_ops, flags);
+       notmuch_message_destroy (message);
+       if (ret != NOTMUCH_STATUS_SUCCESS)
+           break;
+    }
+
+    notmuch_query_destroy (query);
+
+    return ret || interrupted;
+}
+
+static int
+tag_file (void *ctx, notmuch_database_t *notmuch, tag_op_flag_t flags,
+         FILE *input)
+{
+    char *line = NULL;
+    char *query_string = NULL;
+    size_t line_size = 0;
+    ssize_t line_len;
+    int ret = 0;
+    int warn = 0;
+    tag_op_list_t *tag_ops;
+
+    tag_ops = tag_op_list_create (ctx);
+    if (tag_ops == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return 1;
+    }
+
+    while ((line_len = getline (&line, &line_size, input)) != -1 &&
+          ! interrupted) {
+
+       ret = parse_tag_line (ctx, line, TAG_FLAG_NONE,
+                             &query_string, tag_ops);
+
+       if (ret > 0) {
+           if (ret != TAG_PARSE_SKIPPED)
+               /* remember there has been problematic lines */
+               warn = 1;
+           ret = 0;
+           continue;
+       }
+
+       if (ret < 0)
+           break;
+
+       ret = tag_query (ctx, notmuch, query_string, tag_ops, flags);
+       if (ret)
+           break;
+    }
+
+    if (line)
+       free (line);
+
+    return ret || warn;
+}
+
+int
+notmuch_tag_command (notmuch_database_t *notmuch, int argc, char *argv[])
+{
+    tag_op_list_t *tag_ops = NULL;
+    char *query_string = NULL;
+    struct sigaction action;
+    tag_op_flag_t tag_flags = TAG_FLAG_NONE;
+    bool batch = false;
+    bool remove_all = false;
+    FILE *input = stdin;
+    const char *input_file_name = NULL;
+    int opt_index;
+    int ret;
+    notmuch_bool_t synchronize_flags;
+
+    /* Set up our handler for SIGINT */
+    memset (&action, 0, sizeof (struct sigaction));
+    action.sa_handler = handle_sigint;
+    sigemptyset (&action.sa_mask);
+    action.sa_flags = SA_RESTART;
+    sigaction (SIGINT, &action, NULL);
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_bool = &batch, .name = "batch" },
+       { .opt_string = &input_file_name, .name = "input" },
+       { .opt_bool = &remove_all, .name = "remove-all" },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (notmuch, argv[0]);
+
+    if (input_file_name) {
+       batch = true;
+       input = fopen (input_file_name, "r");
+       if (input == NULL) {
+           fprintf (stderr, "Error opening %s for reading: %s\n",
+                    input_file_name, strerror (errno));
+           return EXIT_FAILURE;
+       }
+    }
+
+    if (batch) {
+       if (opt_index != argc) {
+           fprintf (stderr, "Can't specify both cmdline and stdin!\n");
+           if (input)
+               fclose (input);
+           return EXIT_FAILURE;
+       }
+    } else {
+       tag_ops = tag_op_list_create (notmuch);
+       if (tag_ops == NULL) {
+           fprintf (stderr, "Out of memory.\n");
+           return EXIT_FAILURE;
+       }
+
+       if (parse_tag_command_line (notmuch, argc - opt_index, argv + opt_index,
+                                   &query_string, tag_ops))
+           return EXIT_FAILURE;
+
+       if (tag_op_list_size (tag_ops) == 0 && ! remove_all) {
+           fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
+           return EXIT_FAILURE;
+       }
+
+       if (*query_string == '\0') {
+           fprintf (stderr, "Error: notmuch tag requires at least one search term.\n");
+           return EXIT_FAILURE;
+       }
+    }
+
+    if (print_status_database (
+           "notmuch restore",
+           notmuch,
+           notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+                                    &synchronize_flags)))
+       return EXIT_FAILURE;
+
+    if (synchronize_flags)
+       tag_flags |= TAG_FLAG_MAILDIR_SYNC;
+
+    if (remove_all)
+       tag_flags |= TAG_FLAG_REMOVE_ALL;
+
+    if (batch)
+       ret = tag_file (notmuch, notmuch, tag_flags, input);
+    else
+       ret = tag_query (notmuch, notmuch, query_string, tag_ops, tag_flags);
+
+    notmuch_database_destroy (notmuch);
+
+    if (input != stdin)
+       fclose (input);
+
+    return ret || interrupted ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/notmuch-time.c b/notmuch-time.c
new file mode 100644 (file)
index 0000000..cd45818
--- /dev/null
@@ -0,0 +1,136 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+
+/* Format a nice representation of 'time' relative to the current time.
+ *
+ * Examples include:
+ *
+ *     5 mins. ago     (For times less than 60 minutes ago)
+ *     Today 12:30     (For times >60 minutes but still today)
+ *     Yest. 12:30
+ *     Mon.  12:30     (Before yesterday but fewer than 7 days ago)
+ *     October 12      (Between 7 and 180 days ago (about 6 months))
+ *     2008-06-30      (More than 180 days ago)
+ *
+ * The returned string is either static data (a string literal) or
+ * newly talloced data belonging to 'ctx'. That is, the caller should
+ * not modify nor free the returned value. But when the caller
+ * arranges for 'ctx' to be talloc_freed, then memory allocated here
+ * (if any) will be reclaimed.
+ *
+ */
+#define MINUTE (60)
+#define HOUR (60 * MINUTE)
+#define DAY (24 * HOUR)
+#define RELATIVE_DATE_MAX 20
+const char *
+notmuch_time_relative_date (const void *ctx, time_t then)
+{
+    struct tm tm_now, tm_then;
+    time_t now = time (NULL);
+    time_t delta;
+    char *result;
+
+    localtime_r (&now, &tm_now);
+    localtime_r (&then, &tm_then);
+
+    result = talloc_zero_size (ctx, RELATIVE_DATE_MAX);
+    if (result == NULL)
+       return "when?";
+
+    if (then > now)
+       return "the future";
+
+    delta = now - then;
+
+    if (delta > 180 * DAY) {
+       strftime (result, RELATIVE_DATE_MAX,
+                 "%F", &tm_then); /* 2008-06-30 */
+       return result;
+    }
+
+    if (delta < 3600) {
+       snprintf (result, RELATIVE_DATE_MAX,
+                 "%d mins. ago", (int) (delta / 60));
+       return result;
+    }
+
+    if (delta <= 7 * DAY) {
+       if (tm_then.tm_wday == tm_now.tm_wday &&
+           delta < DAY) {
+           strftime (result, RELATIVE_DATE_MAX,
+                     "Today %R", &tm_then);    /* Today 12:30 */
+           return result;
+       } else if ((tm_now.tm_wday + 7 - tm_then.tm_wday) % 7 == 1) {
+           strftime (result, RELATIVE_DATE_MAX,
+                     "Yest. %R", &tm_then);    /* Yest. 12:30 */
+           return result;
+       } else {
+           if (tm_then.tm_wday != tm_now.tm_wday) {
+               strftime (result, RELATIVE_DATE_MAX,
+                         "%a. %R", &tm_then);  /* Mon. 12:30 */
+               return result;
+           }
+       }
+    }
+
+    strftime (result, RELATIVE_DATE_MAX,
+             "%B %d", &tm_then);               /* October 12 */
+    return result;
+}
+#undef MINUTE
+#undef HOUR
+#undef DAY
+
+void
+notmuch_time_print_formatted_seconds (double seconds)
+{
+    int hours;
+    int minutes;
+
+    if (seconds < 1) {
+       printf ("almost no time");
+       return;
+    }
+
+    if (seconds > 3600) {
+       hours = (int) seconds / 3600;
+       printf ("%dh ", hours);
+       seconds -= hours * 3600;
+    }
+
+    if (seconds > 60) {
+       minutes = (int) seconds / 60;
+       printf ("%dm ", minutes);
+       seconds -= minutes * 60;
+    }
+
+    printf ("%ds", (int) seconds);
+}
+
+/* Compute the number of seconds elapsed from start to end. */
+double
+notmuch_time_elapsed (struct timeval start, struct timeval end)
+{
+    return ((end.tv_sec - start.tv_sec) +
+           (end.tv_usec - start.tv_usec) / 1e6);
+}
diff --git a/notmuch.c b/notmuch.c
new file mode 100644 (file)
index 0000000..814b9e4
--- /dev/null
+++ b/notmuch.c
@@ -0,0 +1,622 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ * Copyright © 2009 Keith Packard
+ *
+ * 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/ .
+ *
+ * Authors: Carl Worth <cworth@cworth.org>
+ *         Keith Packard <keithp@keithp.com>
+ */
+
+#include "notmuch-client.h"
+
+/*
+ * Notmuch subcommand hook.
+ *
+ * The return value will be used as notmuch exit status code,
+ * preferably EXIT_SUCCESS or EXIT_FAILURE.
+ *
+ * Each subcommand should be passed either a config object, or an open
+ * database
+ */
+typedef int (*command_function_t) (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+typedef struct command {
+    const char *name;
+    command_function_t function;
+    notmuch_command_mode_t mode;
+    const char *summary;
+} command_t;
+
+static int
+notmuch_help_command (notmuch_database_t *notmuch, int argc, char *argv[]);
+
+static int
+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;
+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 (notmuch, subcommand_name);
+ * with notmuch = an open database, or NULL.
+ */
+void
+notmuch_process_shared_options (notmuch_database_t *notmuch, const char *subcommand_name)
+{
+    if (print_version) {
+       printf ("notmuch " STRINGIFY (NOTMUCH_VERSION) "\n");
+       exit (EXIT_SUCCESS);
+    }
+
+    if (print_help) {
+       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
+ * database.
+ */
+int
+notmuch_minimal_options (const char *subcommand_name,
+                        int argc, char **argv)
+{
+    int opt_index;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+
+    if (opt_index < 0)
+       return -1;
+
+    /* We can't use argv here as it is sometimes NULL */
+    notmuch_process_shared_options (NULL, subcommand_name);
+    return opt_index;
+}
+
+
+struct _notmuch_client_indexing_cli_choices indexing_cli_choices = { };
+const notmuch_opt_desc_t notmuch_shared_indexing_options [] = {
+    { .opt_keyword = &indexing_cli_choices.decrypt_policy,
+      .present = &indexing_cli_choices.decrypt_policy_set, .keywords =
+         (notmuch_keyword_t []){ { "false", NOTMUCH_DECRYPT_FALSE },
+                                 { "true", NOTMUCH_DECRYPT_TRUE },
+                                 { "auto", NOTMUCH_DECRYPT_AUTO },
+                                 { "nostash", NOTMUCH_DECRYPT_NOSTASH },
+                                 { 0, 0 } },
+      .name = "decrypt" },
+    { }
+};
+
+
+notmuch_status_t
+notmuch_process_shared_indexing_options (notmuch_indexopts_t *opts)
+{
+    if (opts == NULL)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    if (indexing_cli_choices.decrypt_policy_set) {
+       notmuch_status_t status;
+       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));
+           return status;
+       }
+    }
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+
+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,
+      "Interactively set up notmuch for first use." },
+    { "new", notmuch_new_command,
+      NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE |
+      NOTMUCH_COMMAND_DATABASE_CREATE,
+      "Find and import new messages to the notmuch database." },
+    { "insert", notmuch_insert_command, NOTMUCH_COMMAND_DATABASE_EARLY |
+      NOTMUCH_COMMAND_DATABASE_WRITE,
+      "Add a new message into the maildir and notmuch database." },
+    { "search", notmuch_search_command, NOTMUCH_COMMAND_DATABASE_EARLY,
+      "Search for messages matching the given search terms." },
+    { "address", notmuch_address_command, NOTMUCH_COMMAND_DATABASE_EARLY,
+      "Get addresses from messages matching the given search terms." },
+    { "show", notmuch_show_command, NOTMUCH_COMMAND_DATABASE_EARLY,
+      "Show all messages matching the search terms." },
+    { "count", notmuch_count_command, NOTMUCH_COMMAND_DATABASE_EARLY,
+      "Count messages matching the search terms." },
+    { "reply", notmuch_reply_command, NOTMUCH_COMMAND_DATABASE_EARLY,
+      "Construct a reply template for a set of messages." },
+    { "tag", notmuch_tag_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE,
+      "Add/remove tags for all messages matching the search terms." },
+    { "dump", notmuch_dump_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE,
+      "Create a plain-text dump of the tags for each message." },
+    { "restore", notmuch_restore_command, NOTMUCH_COMMAND_DATABASE_EARLY |
+      NOTMUCH_COMMAND_DATABASE_WRITE,
+      "Restore the tags from the given dump file (see 'dump')." },
+    { "compact", notmuch_compact_command, NOTMUCH_COMMAND_DATABASE_EARLY |
+      NOTMUCH_COMMAND_DATABASE_WRITE,
+      "Compact the notmuch database." },
+    { "reindex", notmuch_reindex_command, NOTMUCH_COMMAND_DATABASE_EARLY |
+      NOTMUCH_COMMAND_DATABASE_WRITE,
+      "Re-index all messages matching the search terms." },
+    { "config", notmuch_config_command, NOTMUCH_COMMAND_CONFIG_LOAD,
+      "Get or set settings in the notmuch configuration file." },
+#if WITH_EMACS
+    { "emacs-mua", NULL, 0,
+      "send mail with notmuch and emacs." },
+#endif
+    { "help", notmuch_help_command, NOTMUCH_COMMAND_CONFIG_CREATE, /* create but don't save config */
+      "This message, or more detailed help for the named command." }
+};
+
+typedef struct help_topic {
+    const char *name;
+    const char *summary;
+} help_topic_t;
+
+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 const command_t *
+find_command (const char *name)
+{
+    size_t i;
+
+    for (i = 0; i < ARRAY_SIZE (commands); i++)
+       if ((! name && ! commands[i].name) ||
+           (name && commands[i].name && strcmp (name, commands[i].name) == 0))
+           return &commands[i];
+
+    return NULL;
+}
+
+int notmuch_format_version;
+
+static void
+usage (FILE *out)
+{
+    const command_t *command;
+    const help_topic_t *topic;
+    unsigned int i;
+
+    fprintf (out,
+            "Usage: notmuch --help\n"
+            "       notmuch --version\n"
+            "       notmuch <command> [args...]\n");
+    fprintf (out, "\n");
+    fprintf (out, "The available commands are as follows:\n");
+    fprintf (out, "\n");
+
+    for (i = 0; i < ARRAY_SIZE (commands); i++) {
+       command = &commands[i];
+
+       if (command->name)
+           fprintf (out, "  %-12s  %s\n", command->name, command->summary);
+    }
+
+    fprintf (out, "\n");
+    fprintf (out, "Additional help topics are as follows:\n");
+    fprintf (out, "\n");
+
+    for (i = 0; i < ARRAY_SIZE (help_topics); i++) {
+       topic = &help_topics[i];
+       fprintf (out, "  %-12s  %s\n", topic->name, topic->summary);
+    }
+
+    fprintf (out, "\n");
+    fprintf (out,
+            "Use \"notmuch help <command or topic>\" for more details "
+            "on each command or topic.\n\n");
+}
+
+void
+notmuch_exit_if_unsupported_format (void)
+{
+    if (notmuch_format_version > NOTMUCH_FORMAT_CUR) {
+       fprintf (stderr,
+                "\
+A caller requested output format version %d, but the installed notmuch\n\
+CLI only supports up to format version %d.  You may need to upgrade your\n\
+notmuch CLI.\n",
+                notmuch_format_version, NOTMUCH_FORMAT_CUR);
+       exit (NOTMUCH_EXIT_FORMAT_TOO_NEW);
+    } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN) {
+       fprintf (stderr,
+                "\
+A caller requested output format version %d, which is no longer supported\n\
+by the notmuch CLI (it requires at least version %d).  You may need to\n\
+upgrade your notmuch front-end.\n",
+                notmuch_format_version, NOTMUCH_FORMAT_MIN);
+       exit (NOTMUCH_EXIT_FORMAT_TOO_OLD);
+    } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN_ACTIVE) {
+       /* Warn about old version requests so compatibility issues are
+        * less likely when we drop support for a deprecated format
+        * versions. */
+       fprintf (stderr,
+                "\
+A caller requested deprecated output format version %d, which may not\n\
+be supported in the future.\n", notmuch_format_version);
+    }
+}
+
+static void
+notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch)
+{
+    const char *uuid = NULL;
+
+    if (! notmuch_requested_db_uuid)
+       return;
+    IGNORE_RESULT (notmuch_database_get_revision (notmuch, &uuid));
+
+    if (strcmp (notmuch_requested_db_uuid, uuid) != 0) {
+       fprintf (stderr, "Error: requested database revision %s does not match %s\n",
+                notmuch_requested_db_uuid, uuid);
+       exit (1);
+    }
+}
+
+static void
+exec_man (const char *page)
+{
+    if (execlp ("man", "man", page, (char *) NULL)) {
+       perror ("exec man");
+       exit (1);
+    }
+}
+
+static int
+_help_for (const char *topic_name)
+{
+    char *page;
+
+    if (! topic_name) {
+       printf ("The notmuch mail system.\n\n");
+       usage (stdout);
+       return EXIT_SUCCESS;
+    }
+
+    if (strcmp (topic_name, "help") == 0) {
+       printf ("The notmuch help system.\n\n"
+               "\tNotmuch uses the man command to display help. In case\n"
+               "\tof difficulties check that MANPATH includes the pages\n"
+               "\tinstalled by notmuch.\n\n"
+               "\tTry \"notmuch help\" for a list of topics.\n");
+       return EXIT_SUCCESS;
+    }
+
+    page = talloc_asprintf (NULL, "notmuch-%s", topic_name);
+    exec_man (page);
+
+    return EXIT_FAILURE;
+}
+
+static int
+notmuch_help_command (unused(notmuch_database_t *notmuch), int argc, char *argv[])
+{
+    int opt_index;
+
+    opt_index = notmuch_minimal_options ("help", argc, argv);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    /* skip at least subcommand argument */
+    argc -= opt_index;
+    argv += opt_index;
+
+    if (argc == 0) {
+       return _help_for (NULL);
+    }
+
+    return _help_for (argv[0]);
+}
+
+/* Handle the case of "notmuch" being invoked with no command
+ * argument. For now we just call notmuch_setup_command, but we plan
+ * to be more clever about this in the future.
+ */
+static int
+notmuch_command (notmuch_database_t *notmuch,
+                unused(int argc), unused(char **argv))
+{
+
+    const char *config_path;
+
+    /* If the user has not created a configuration file, then run
+     * notmuch_setup_command which will give a nice welcome message,
+     * and interactively guide the user through the configuration. */
+    config_path = notmuch_config_path (notmuch);
+    if (access (config_path, R_OK | F_OK) == -1) {
+       if (errno != ENOENT) {
+           fprintf (stderr, "Error: %s config file access failed: %s\n", config_path,
+                    strerror (errno));
+           return EXIT_FAILURE;
+       } else {
+           return notmuch_setup_command (notmuch, 0, NULL);
+       }
+    }
+
+    printf ("Notmuch is configured and appears to have a database. Excellent!\n\n"
+           "At this point you can start exploring the functionality of notmuch by\n"
+           "using commands such as:\n\n"
+           "\tnotmuch search tag:inbox\n\n"
+           "\tnotmuch search to:\"%s\"\n\n"
+           "\tnotmuch search from:\"%s\"\n\n"
+           "\tnotmuch search subject:\"my favorite things\"\n\n"
+           "See \"notmuch help search\" for more details.\n\n"
+           "You can also use \"notmuch show\" with any of the thread IDs resulting\n"
+           "from a search. Finally, you may want to explore using a more sophisticated\n"
+           "interface to notmuch such as the emacs interface implemented in notmuch.el\n"
+           "or any other interface described at https://notmuchmail.org\n\n"
+           "And don't forget to run \"notmuch new\" whenever new mail arrives.\n\n"
+           "Have fun, and may your inbox never have much mail.\n\n",
+           notmuch_config_get (notmuch, NOTMUCH_CONFIG_USER_NAME),
+           notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL));
+
+    return EXIT_SUCCESS;
+}
+
+/*
+ * Try to run subcommand in argv[0] as notmuch- prefixed external
+ * command. argv must be NULL terminated (argv passed to main always
+ * is).
+ *
+ * Does not return if the external command is found and
+ * executed. Return true if external command is not found. Return
+ * false on errors.
+ */
+static bool
+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);
+
+    /*
+     * This will only return on errors. Not finding an external
+     * command (ENOENT) is not an error from our perspective.
+     */
+    execvp (argv[0], argv);
+    if (errno != ENOENT) {
+       fprintf (stderr, "Error: Running external command '%s' failed: %s\n",
+                argv[0], strerror (errno));
+       ret = false;
+    }
+
+    talloc_free (argv[0]);
+    argv[0] = old_argv0;
+
+    return ret;
+}
+
+int
+main (int argc, char *argv[])
+{
+    void *local;
+    char *talloc_report;
+    const char *command_name = NULL;
+    const command_t *command;
+    const char *config_file_name = NULL;
+    notmuch_database_t *notmuch = NULL;
+    int opt_index;
+    int ret = EXIT_SUCCESS;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_string = &config_file_name, .name = "config", .allow_empty = TRUE },
+       { .opt_inherit = notmuch_shared_options },
+       { }
+    };
+
+    notmuch_client_init ();
+
+    local = talloc_new (NULL);
+
+    /* Globally default to the current output format version. */
+    notmuch_format_version = NOTMUCH_FORMAT_CUR;
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0) {
+       ret = EXIT_FAILURE;
+       goto DONE;
+    }
+
+    if (opt_index < argc)
+       command_name = argv[opt_index];
+
+    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 (config_file_name, argv + opt_index))
+           fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",
+                    command_name);
+       ret = EXIT_FAILURE;
+       goto DONE;
+    }
+
+    if (command->mode & NOTMUCH_COMMAND_DATABASE_EARLY) {
+       char *status_string = NULL;
+       notmuch_database_mode_t mode;
+       notmuch_status_t status;
+
+       if (command->mode & NOTMUCH_COMMAND_DATABASE_WRITE ||
+           command->mode & NOTMUCH_COMMAND_DATABASE_CREATE)
+           mode = NOTMUCH_DATABASE_MODE_READ_WRITE;
+       else
+           mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
+
+       if (command->mode & NOTMUCH_COMMAND_DATABASE_CREATE) {
+           status = notmuch_database_create_with_config (NULL,
+                                                         config_file_name,
+                                                         NULL,
+                                                         &notmuch,
+                                                         &status_string);
+           if (status && status != NOTMUCH_STATUS_DATABASE_EXISTS) {
+               if (status_string) {
+                   fputs (status_string, stderr);
+                   free (status_string);
+               }
+
+               if (status == NOTMUCH_STATUS_NO_CONFIG)
+                   fputs ("Try running 'notmuch setup' to create a configuration.\n", stderr);
+
+               return EXIT_FAILURE;
+           }
+       }
+
+       if (notmuch == NULL) {
+           status = notmuch_database_open_with_config (NULL,
+                                                       mode,
+                                                       config_file_name,
+                                                       NULL,
+                                                       &notmuch,
+                                                       &status_string);
+           if (status) {
+               if (status_string) {
+                   fputs (status_string, stderr);
+                   free (status_string);
+               }
+
+               return EXIT_FAILURE;
+           }
+       }
+    }
+
+    if (command->mode & NOTMUCH_COMMAND_CONFIG_LOAD) {
+       char *status_string = NULL;
+       notmuch_status_t status;
+       status = notmuch_database_load_config (NULL,
+                                              config_file_name,
+                                              NULL,
+                                              &notmuch,
+                                              &status_string);
+       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.\n", stderr);
+               goto DONE;
+           }
+           break;
+       case NOTMUCH_STATUS_NO_DATABASE:
+           if (! command_name) {
+               printf ("Notmuch is configured, but no database was found.\n");
+               printf ("You probably want to run \"notmuch new\" now to create a database.\n\n"
+                       "Note that the first run of \"notmuch new\" can take a very long time\n"
+                       "and that the resulting database will use roughly the same amount of\n"
+                       "storage space as the email being indexed.\n\n");
+               status = NOTMUCH_STATUS_SUCCESS;
+               goto DONE;
+           }
+           break;
+       case NOTMUCH_STATUS_SUCCESS:
+           break;
+       default:
+           fputs ("Error: unable to load config file.\n", stderr);
+           ret = 1;
+           goto DONE;
+       }
+
+    }
+
+    ret = (command->function)(notmuch, argc - opt_index, argv + opt_index);
+
+  DONE:
+    talloc_report = getenv ("NOTMUCH_TALLOC_REPORT");
+    if (talloc_report && strcmp (talloc_report, "") != 0) {
+       /* this relies on the previous call to
+        * talloc_enable_null_tracking
+        */
+
+       FILE *report = fopen (talloc_report, "a");
+       if (report) {
+           talloc_report_full (NULL, report);
+       } else {
+           ret = 1;
+           fprintf (stderr, "ERROR: unable to write talloc log. ");
+           perror (talloc_report);
+       }
+    }
+
+    talloc_free (local);
+
+    return ret;
+}
diff --git a/packaging/debian b/packaging/debian
new file mode 100644 (file)
index 0000000..c8e8ddd
--- /dev/null
@@ -0,0 +1,2 @@
+The Debian packaging exists in the top-level "debian" directory within
+this source-code repository.
diff --git a/packaging/fedora/notmuch.spec b/packaging/fedora/notmuch.spec
new file mode 100644 (file)
index 0000000..5146edd
--- /dev/null
@@ -0,0 +1,171 @@
+%global git 6b9a717c
+%global date %(date +%Y%m%d)
+
+# If you are doing a git snapshot:
+#
+# Release should be 1%{git}%{?dist}
+# Source0 should be notmuch-%{version}-%{git}.tar.gz
+# git version is generated by 'git show-ref --hash=8 HEAD'
+#
+# To create a tarball:
+#
+# git clone git://notmuchmail.org/git/notmuch
+# cd notmuch
+# git archive --format=tar --prefix=notmuch-0.4/ HEAD | gzip > notmuch-0.4-`git show-ref --hash=8 HEAD`.tar.gz
+#
+
+Name:           notmuch
+Version:        0.15.2
+Release:        3%{?dist}
+Summary:        Thread-based email index, search and tagging
+
+Group:          Applications/Internet
+License:        GPLv3+
+URL:            https://notmuchmail.org/
+
+Source0:        https://notmuchmail.org/releases/notmuch-%{version}.tar.gz
+
+BuildRequires:  xapian-core-devel gmime-devel libtalloc-devel
+BuildRequires:  zlib-devel emacs-el emacs-nox python ruby ruby-devel perl
+
+%description
+Fast system for indexing, searching, and tagging email.  Even if you
+receive 12000 messages per month or have on the order of millions of
+messages that you've been saving for decades, Notmuch will be able to
+quickly search all of it.
+
+Notmuch is not much of an email program. It doesn't receive messages
+(no POP or IMAP support). It doesn't send messages (no mail composer,
+no network code at all). And for what it does do (email search) that
+work is provided by an external library, Xapian. So if Notmuch
+provides no user interface and Xapian does all the heavy lifting, then
+what's left here? Not much.
+
+%package devel
+Summary:        Development libraries and header files for %{name}
+Group:          Development/Libraries
+Requires:       %{name} = %{version}-%{release}
+
+%description devel
+The %{name}-devel package contains libraries and header files for
+developing applications that use %{name}.
+
+%package -n emacs-notmuch
+Summary:        Not much support for Emacs
+Group:          Applications/Editors
+BuildArch:      noarch
+Requires:       %{name} = %{version}-%{release}, emacs(bin) >= %{_emacs_version}
+
+%description -n emacs-notmuch
+%{summary}.
+
+%package -n python-notmuch
+Summary:        Python bindings for notmuch
+Group:          Development/Libraries
+BuildArch:      noarch
+Requires:       %{name} = %{version}-%{release}
+
+%description -n python-notmuch
+%{summary}.
+
+%package -n notmuch-ruby
+Summary:        Ruby bindings for notmuch
+Group:          Development/Libraries
+Requires:       %{name} = %{version}-%{release}
+
+%description -n notmuch-ruby
+%{summary}.
+
+%package mutt
+Summary:        Notmuch (of a) helper for Mutt
+Group:          Development/Libraries
+BuildArch:      noarch
+Requires:       %{name} = %{version}-%{release}
+Requires:       perl(Term::ReadLine::Gnu)
+
+%description mutt
+notmuch-mutt provide integration among the Mutt mail user agent and
+the Notmuch mail indexer.
+
+%prep
+%setup -q
+
+%build
+./configure --prefix=%{_prefix} --libdir=%{_libdir} --sysconfdir=%{_sysconfdir} \
+    --mandir=%{_mandir} --includedir=%{_includedir} --emacslispdir=%{_emacs_sitelispdir}
+make %{?_smp_mflags} CFLAGS="%{optflags}"
+
+pushd bindings/python
+    python setup.py build
+popd
+
+pushd bindings/ruby
+    export CONFIGURE_ARGS="--with-cflags='%{optflags}'"
+    ruby extconf.rb --vendor
+    make
+popd
+
+pushd contrib/notmuch-mutt
+    make
+popd
+
+%install
+make install DESTDIR=%{buildroot}
+
+pushd bindings/python
+    python setup.py install -O1 --skip-build --root %{buildroot}
+popd
+
+pushd bindings/ruby
+    make install DESTDIR=%{buildroot}
+popd
+
+install contrib/notmuch-mutt/notmuch-mutt %{buildroot}%{_bindir}/notmuch-mutt
+install contrib/notmuch-mutt/notmuch-mutt.1 %{buildroot}%{_mandir}/man1/notmuch-mutt.1
+
+%post -p /sbin/ldconfig
+
+%postun -p /sbin/ldconfig
+
+%files
+%doc AUTHORS COPYING COPYING-GPL-3 INSTALL README
+%{_sysconfdir}/bash_completion.d/notmuch
+%{_datadir}/zsh/functions/Completion/Unix/_notmuch
+%{_bindir}/notmuch
+%{_mandir}/man?/*
+%{_libdir}/libnotmuch.so.3*
+
+%files devel
+%{_libdir}/libnotmuch.so
+%{_includedir}/*
+
+%files -n emacs-notmuch
+%{_emacs_sitelispdir}/*
+
+%files -n python-notmuch
+%doc bindings/python/README
+%{python_sitelib}/*
+
+%files -n notmuch-ruby
+%{ruby_vendorarchdir}/*
+
+%files mutt
+%{_bindir}/notmuch-mutt
+%{_mandir}/man1/notmuch-mutt.1*
+
+%changelog
+* Sun Apr 28 2013 Felipe Contreras <felipe.contreras@gmail.com> - 0.15.2-3
+- Add ruby bingings
+
+* Sun Apr 28 2013 Felipe Contreras <felipe.contreras@gmail.com> - 0.15.2-2
+- Sync with Fedora
+
+* Sun Apr 28 2013 Felipe Contreras <felipe.contreras@gmail.com> - 0.15.2-1
+- Update to latest upstream
+
+* Tue Nov  2 2010 Scott Henson <shenson@redhat.com> - 0.4-1
+- New upstream release
+
+* Wed Nov 18 2009 Jeffrey C. Ollie <jeff@ocjtech.us> - 0.0-0.3.306635c2
+- First version
+
diff --git a/parse-time-string/Makefile b/parse-time-string/Makefile
new file mode 100644 (file)
index 0000000..fa25832
--- /dev/null
@@ -0,0 +1,5 @@
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/parse-time-string/Makefile.local b/parse-time-string/Makefile.local
new file mode 100644 (file)
index 0000000..ee8030c
--- /dev/null
@@ -0,0 +1,14 @@
+# -*- makefile-gmake -*-
+
+dir := parse-time-string
+extra_cflags += -I$(srcdir)/$(dir)
+
+libparse-time-string_c_srcs := $(dir)/parse-time-string.c
+
+libparse-time-string_modules := $(libparse-time-string_c_srcs:.c=.o)
+
+$(dir)/libparse-time-string.a: $(libparse-time-string_modules)
+       $(call quiet,AR) rcs $@ $^
+
+SRCS := $(SRCS) $(libparse-time-string_c_srcs)
+CLEAN := $(CLEAN) $(libparse-time-string_modules) $(dir)/libparse-time-string.a
diff --git a/parse-time-string/README b/parse-time-string/README
new file mode 100644 (file)
index 0000000..300ff1f
--- /dev/null
@@ -0,0 +1,9 @@
+PARSE TIME STRING
+=================
+
+parse_time_string() is a date/time parser originally written for
+notmuch by Jani Nikula <jani@nikula.org>. However, there is nothing
+notmuch specific in it, and it should be kept reusable for other
+projects, and ready to be packaged on its own as needed. Please do not
+add dependencies on or references to anything notmuch specific. The
+parser should only depend on the C library.
diff --git a/parse-time-string/parse-time-string.c b/parse-time-string/parse-time-string.c
new file mode 100644 (file)
index 0000000..33372ec
--- /dev/null
@@ -0,0 +1,1510 @@
+/*
+ * parse time string - user friendly date and time parser
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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/>.
+ *
+ * Author: Jani Nikula <jani@nikula.org>
+ */
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include "compat.h"
+#include "parse-time-string.h"
+
+/*
+ * IMPLEMENTATION DETAILS
+ *
+ * At a high level, the parsing is done in two phases: 1) actual
+ * parsing of the input string and storing the parsed data into
+ * 'struct state', and 2) processing of the data in 'struct state'
+ * according to current time (or provided reference time) and
+ * rounding. This is evident in the main entry point function
+ * parse_time_string().
+ *
+ * 1) The parsing phase - parse_input()
+ *
+ * Parsing is greedy and happens from left to right. The parsing is as
+ * unambiguous as possible; only unambiguous date/time formats are
+ * accepted. Redundant or contradictory absolute date/time in the
+ * input (e.g. date specified multiple times/ways) is not
+ * accepted. Relative date/time on the other hand just accumulates if
+ * present multiple times (e.g. "5 days 5 days" just turns into 10
+ * days).
+ *
+ * Parsing decisions are made on the input format, not value. For
+ * example, "20/5/2005" fails because the recognized format here is
+ * MM/D/YYYY, even though the values would suggest DD/M/YYYY.
+ *
+ * Parsing is mostly stateless in the sense that parsing decisions are
+ * not made based on the values of previously parsed data, or whether
+ * certain data is present in the first place. (There are a few
+ * exceptions to the latter part, though, such as parsing of time zone
+ * that would otherwise look like plain time.)
+ *
+ * When the parser encounters a number that is not greedily parsed as
+ * part of a format, the interpretation is postponed until the next
+ * token is parsed. The parser for the next token may consume the
+ * previously postponed number. For example, when parsing "20 May" the
+ * meaning of "20" is not known until "May" is parsed. If the parser
+ * for the next token does not consume the postponed number, the
+ * number is handled as a "lone" number before parser for the next
+ * token finishes.
+ *
+ * 2) The processing phase - create_output()
+ *
+ * Once the parser in phase 1 has finished, 'struct state' contains
+ * all the information from the input string, and it's no longer
+ * needed. Since the parser does not even handle the concept of "now",
+ * the processing initializes the fields referring to the current
+ * date/time.
+ *
+ * If requested, the result is rounded towards past or future. The
+ * idea behind rounding is to support parsing date/time ranges in an
+ * obvious way. For example, for a range defined as two dates (without
+ * time), one would typically want to have an inclusive range from the
+ * beginning of start date to the end of the end date. The caller
+ * would use rounding towards past in the start date, and towards
+ * future in the end date.
+ *
+ * The absolute date and time is shifted by the relative date and
+ * time, and time zone adjustments are made. Daylight saving time
+ * (DST) is specifically *not* handled at all.
+ *
+ * Finally, the result is stored to time_t.
+ */
+
+#define unused(x) x __attribute__ ((unused))
+
+/* XXX: Redefine these to add i18n support. The keyword table uses
+ * N_() to mark strings to be translated; they are accessed
+ * dynamically using _(). */
+#define _(s) (s)        /* i18n: define as gettext (s) */
+#define N_(s) (s)       /* i18n: define as gettext_noop (s) */
+
+#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))
+
+/*
+ * Field indices in the tm and set arrays of struct state.
+ *
+ * NOTE: There's some code that depends on the ordering of this enum.
+ */
+enum field {
+    /* Keep SEC...YEAR in this order. */
+    TM_ABS_SEC,         /* seconds */
+    TM_ABS_MIN,         /* minutes */
+    TM_ABS_HOUR,        /* hours */
+    TM_ABS_MDAY,        /* day of the month */
+    TM_ABS_MON,         /* month */
+    TM_ABS_YEAR,        /* year */
+
+    TM_WDAY,            /* day of the week. special: may be relative */
+    TM_ABS_ISDST,       /* daylight saving time */
+
+    TM_AMPM,            /* am vs. pm */
+    TM_TZ,              /* timezone in minutes */
+
+    /* Keep SEC...YEAR in this order. */
+    TM_REL_SEC,         /* seconds relative to absolute or reference time */
+    TM_REL_MIN,         /* minutes ... */
+    TM_REL_HOUR,        /* hours ... */
+    TM_REL_DAY,         /* days ... */
+    TM_REL_MON,         /* months ... */
+    TM_REL_YEAR,        /* years ... */
+    TM_REL_WEEK,        /* weeks ... */
+
+    TM_NONE,            /* not a field */
+
+    TM_SIZE            = TM_NONE,
+    TM_FIRST_ABS       = TM_ABS_SEC,
+    TM_FIRST_REL       = TM_REL_SEC,
+};
+
+/* Values for the set array of struct state. */
+enum field_set {
+    FIELD_UNSET,        /* The field has not been touched by parser. */
+    FIELD_SET,          /* The field has been set by parser. */
+    FIELD_NOW,          /* The field will be set to reference time. */
+};
+
+static enum field
+next_abs_field (enum field field)
+{
+    /* NOTE: Depends on the enum ordering. */
+    return field < TM_ABS_YEAR ? field + 1 : TM_NONE;
+}
+
+static enum field
+abs_to_rel_field (enum field field)
+{
+    assert (field <= TM_ABS_YEAR);
+
+    /* NOTE: Depends on the enum ordering. */
+    return field + (TM_FIRST_REL - TM_FIRST_ABS);
+}
+
+/* Get the smallest acceptable value for field. */
+static int
+get_field_epoch_value (enum field field)
+{
+    if (field == TM_ABS_MDAY || field == TM_ABS_MON)
+       return 1;
+    else if (field == TM_ABS_YEAR)
+       return 1970;
+    else
+       return 0;
+}
+
+/* The parsing state. */
+struct state {
+    int tm[TM_SIZE];                    /* parsed date and time */
+    enum field_set set[TM_SIZE];        /* set status of tm */
+
+    enum field last_field;              /* Previously set field. */
+    char delim;
+
+    int postponed_length;               /* Number of digits in postponed value. */
+    int postponed_value;
+    char postponed_delim;               /* The delimiter preceding postponed number. */
+};
+
+/*
+ * Helpers for postponed numbers.
+ *
+ * postponed_length is the number of digits in postponed value. 0
+ * means there is no postponed number. -1 means there is a postponed
+ * number, but it comes from a keyword, and it doesn't have digits.
+ */
+static int
+get_postponed_length (struct state *state)
+{
+    return state->postponed_length;
+}
+
+/*
+ * Consume a previously postponed number. Return true if a number was
+ * in fact postponed, false otherwise. Store the postponed number's
+ * value in *v, length in the input string in *n (or -1 if the number
+ * was written out and parsed as a keyword), and the preceding
+ * delimiter to *d. If a number was not postponed, *v, *n and *d are
+ * unchanged.
+ */
+static bool
+consume_postponed_number (struct state *state, int *v, int *n, char *d)
+{
+    if (! state->postponed_length)
+       return false;
+
+    if (n)
+       *n = state->postponed_length;
+
+    if (v)
+       *v = state->postponed_value;
+
+    if (d)
+       *d = state->postponed_delim;
+
+    state->postponed_length = 0;
+    state->postponed_value = 0;
+    state->postponed_delim = 0;
+
+    return true;
+}
+
+static int parse_postponed_number (struct state *state, enum field next_field);
+
+/*
+ * Postpone a number to be handled later. If one exists already,
+ * handle it first. n may be -1 to indicate a keyword that has no
+ * number length.
+ */
+static int
+set_postponed_number (struct state *state, int v, int n)
+{
+    int r;
+    char d = state->delim;
+
+    /* Parse a previously postponed number, if any. */
+    r = parse_postponed_number (state, TM_NONE);
+    if (r)
+       return r;
+
+    state->postponed_length = n;
+    state->postponed_value = v;
+    state->postponed_delim = d;
+
+    return 0;
+}
+
+static void
+set_delim (struct state *state, char delim)
+{
+    state->delim = delim;
+}
+
+static void
+unset_delim (struct state *state)
+{
+    state->delim = 0;
+}
+
+/*
+ * Field set/get/mod helpers.
+ */
+
+/* Return true if field has been set. */
+static bool
+is_field_set (struct state *state, enum field field)
+{
+    assert (field < ARRAY_SIZE (state->tm));
+
+    return state->set[field] != FIELD_UNSET;
+}
+
+static void
+unset_field (struct state *state, enum field field)
+{
+    assert (field < ARRAY_SIZE (state->tm));
+
+    state->set[field] = FIELD_UNSET;
+    state->tm[field] = 0;
+}
+
+/*
+ * Set field to value. A field can only be set once to ensure the
+ * input does not contain redundant and potentially conflicting data.
+ */
+static int
+set_field (struct state *state, enum field field, int value)
+{
+    int r;
+
+    /* Fields can only be set once. */
+    if (is_field_set (state, field))
+       return -PARSE_TIME_ERR_ALREADYSET;
+
+    state->set[field] = FIELD_SET;
+
+    /* Parse a previously postponed number, if any. */
+    r = parse_postponed_number (state, field);
+    if (r)
+       return r;
+
+    unset_delim (state);
+
+    state->tm[field] = value;
+    state->last_field = field;
+
+    return 0;
+}
+
+/*
+ * Mark n fields in fields to be set to the reference date/time in the
+ * specified time zone, or local timezone if not specified. The fields
+ * will be initialized after parsing is complete and timezone is
+ * known.
+ */
+static int
+set_fields_to_now (struct state *state, enum field *fields, size_t n)
+{
+    size_t i;
+    int r;
+
+    for (i = 0; i < n; i++) {
+       r = set_field (state, fields[i], 0);
+       if (r)
+           return r;
+       state->set[fields[i]] = FIELD_NOW;
+    }
+
+    return 0;
+}
+
+/* Modify field by adding value to it. To be used on relative fields,
+ * which can be modified multiple times (to accumulate). */
+static int
+add_to_field (struct state *state, enum field field, int value)
+{
+    int r;
+
+    assert (field < ARRAY_SIZE (state->tm));
+
+    state->set[field] = FIELD_SET;
+
+    /* Parse a previously postponed number, if any. */
+    r = parse_postponed_number (state, field);
+    if (r)
+       return r;
+
+    unset_delim (state);
+
+    state->tm[field] += value;
+    state->last_field = field;
+
+    return 0;
+}
+
+/*
+ * Get field value. Make sure the field is set before query. It's most
+ * likely an error to call this while parsing (for example fields set
+ * as FIELD_NOW will only be set to some value after parsing).
+ */
+static int
+get_field (struct state *state, enum field field)
+{
+    assert (field < ARRAY_SIZE (state->tm));
+
+    return state->tm[field];
+}
+
+/*
+ * Validity checkers.
+ */
+static bool
+is_valid_12hour (int h)
+{
+    return h >= 1 && h <= 12;
+}
+
+static bool
+is_valid_time (int h, int m, int s)
+{
+    /* Allow 24:00:00 to denote end of day. */
+    if (h == 24 && m == 0 && s == 0)
+       return true;
+
+    return h >= 0 && h <= 23 && m >= 0 && m <= 59 && s >= 0 && s <= 59;
+}
+
+static bool
+is_valid_mday (int mday)
+{
+    return mday >= 1 && mday <= 31;
+}
+
+static bool
+is_valid_mon (int mon)
+{
+    return mon >= 1 && mon <= 12;
+}
+
+static bool
+is_valid_year (int year)
+{
+    return year >= 1970;
+}
+
+static bool
+is_valid_date (int year, int mon, int mday)
+{
+    return is_valid_year (year) && is_valid_mon (mon) && is_valid_mday (mday);
+}
+
+/* Unset indicator for time and date set helpers. */
+#define UNSET -1
+
+/* Time set helper. No input checking. Use UNSET (-1) to leave unset. */
+static int
+set_abs_time (struct state *state, int hour, int min, int sec)
+{
+    int r;
+
+    if (hour != UNSET) {
+       if ((r = set_field (state, TM_ABS_HOUR, hour)))
+           return r;
+    }
+
+    if (min != UNSET) {
+       if ((r = set_field (state, TM_ABS_MIN, min)))
+           return r;
+    }
+
+    if (sec != UNSET) {
+       if ((r = set_field (state, TM_ABS_SEC, sec)))
+           return r;
+    }
+
+    return 0;
+}
+
+/* Date set helper. No input checking. Use UNSET (-1) to leave unset. */
+static int
+set_abs_date (struct state *state, int year, int mon, int mday)
+{
+    int r;
+
+    if (year != UNSET) {
+       if ((r = set_field (state, TM_ABS_YEAR, year)))
+           return r;
+    }
+
+    if (mon != UNSET) {
+       if ((r = set_field (state, TM_ABS_MON, mon)))
+           return r;
+    }
+
+    if (mday != UNSET) {
+       if ((r = set_field (state, TM_ABS_MDAY, mday)))
+           return r;
+    }
+
+    return 0;
+}
+
+/*
+ * Keyword parsing and handling.
+ */
+struct keyword;
+typedef int (*setter_t)(struct state *state, struct keyword *kw);
+
+struct keyword {
+    const char *name;   /* keyword */
+    enum field field;   /* field to set, or FIELD_NONE if N/A */
+    int value;          /* value to set, or 0 if N/A */
+    setter_t set;       /* function to use for setting, if non-NULL */
+};
+
+/*
+ * Setter callback functions for keywords.
+ */
+static int
+kw_set_rel (struct state *state, struct keyword *kw)
+{
+    int multiplier = 1;
+
+    /* Get a previously set multiplier, if any. */
+    consume_postponed_number (state, &multiplier, NULL, NULL);
+
+    /* Accumulate relative field values. */
+    return add_to_field (state, kw->field, multiplier * kw->value);
+}
+
+static int
+kw_set_number (struct state *state, struct keyword *kw)
+{
+    /* -1 = no length, from keyword. */
+    return set_postponed_number (state, kw->value, -1);
+}
+
+static int
+kw_set_month (struct state *state, struct keyword *kw)
+{
+    int n = get_postponed_length (state);
+
+    /* Consume postponed number if it could be mday. This handles "20
+     * January". */
+    if (n == 1 || n == 2) {
+       int r, v;
+
+       consume_postponed_number (state, &v, NULL, NULL);
+
+       if (! is_valid_mday (v))
+           return -PARSE_TIME_ERR_INVALIDDATE;
+
+       r = set_field (state, TM_ABS_MDAY, v);
+       if (r)
+           return r;
+    }
+
+    return set_field (state, kw->field, kw->value);
+}
+
+static int
+kw_set_ampm (struct state *state, struct keyword *kw)
+{
+    int n = get_postponed_length (state);
+
+    /* Consume postponed number if it could be hour. This handles
+     * "5pm". */
+    if (n == 1 || n == 2) {
+       int r, v;
+
+       consume_postponed_number (state, &v, NULL, NULL);
+
+       if (! is_valid_12hour (v))
+           return -PARSE_TIME_ERR_INVALIDTIME;
+
+       r = set_abs_time (state, v, 0, 0);
+       if (r)
+           return r;
+    }
+
+    return set_field (state, kw->field, kw->value);
+}
+
+static int
+kw_set_timeofday (struct state *state, struct keyword *kw)
+{
+    return set_abs_time (state, kw->value, 0, 0);
+}
+
+static int
+kw_set_today (struct state *state, unused (struct keyword *kw))
+{
+    enum field fields[] = { TM_ABS_YEAR, TM_ABS_MON, TM_ABS_MDAY };
+
+    return set_fields_to_now (state, fields, ARRAY_SIZE (fields));
+}
+
+static int
+kw_set_now (struct state *state, unused (struct keyword *kw))
+{
+    enum field fields[] = { TM_ABS_HOUR, TM_ABS_MIN, TM_ABS_SEC };
+
+    return set_fields_to_now (state, fields, ARRAY_SIZE (fields));
+}
+
+static int
+kw_set_ordinal (struct state *state, struct keyword *kw)
+{
+    int n, v;
+
+    /* Require a postponed number. */
+    if (! consume_postponed_number (state, &v, &n, NULL))
+       return -PARSE_TIME_ERR_DATEFORMAT;
+
+    /* Ordinals are mday. */
+    if (n != 1 && n != 2)
+       return -PARSE_TIME_ERR_DATEFORMAT;
+
+    /* Be strict about st, nd, rd, and lax about th. */
+    if (strcasecmp (kw->name, "st") == 0 && v != 1 && v != 21 && v != 31)
+       return -PARSE_TIME_ERR_INVALIDDATE;
+    else if (strcasecmp (kw->name, "nd") == 0 && v != 2 && v != 22)
+       return -PARSE_TIME_ERR_INVALIDDATE;
+    else if (strcasecmp (kw->name, "rd") == 0 && v != 3 && v != 23)
+       return -PARSE_TIME_ERR_INVALIDDATE;
+    else if (strcasecmp (kw->name, "th") == 0 && ! is_valid_mday (v))
+       return -PARSE_TIME_ERR_INVALIDDATE;
+
+    return set_field (state, TM_ABS_MDAY, v);
+}
+
+static int
+kw_ignore (unused (struct state *state), unused (struct keyword *kw))
+{
+    return 0;
+}
+
+/*
+ * Accepted keywords.
+ *
+ * A keyword may optionally contain a '|' to indicate the minimum
+ * match length. Without one, full match is required. It's advisable
+ * to keep the minimum match parts unique across all keywords. If
+ * they're not, the first match wins.
+ *
+ * If keyword begins with '*', then the matching will be case
+ * sensitive. Otherwise the matching is case insensitive.
+ *
+ * If .set is NULL, the field specified by .field will be set to
+ * .value.
+ *
+ * Note: Observe how "m" and "mi" match minutes, "M" and "mo" and
+ * "mont" match months, but "mon" matches Monday.
+ */
+static struct keyword keywords[] = {
+    /* Weekdays. */
+    { N_ ("sun|day"),    TM_WDAY,        0,      NULL },
+    { N_ ("mon|day"),    TM_WDAY,        1,      NULL },
+    { N_ ("tue|sday"),   TM_WDAY,        2,      NULL },
+    { N_ ("wed|nesday"), TM_WDAY,        3,      NULL },
+    { N_ ("thu|rsday"),  TM_WDAY,        4,      NULL },
+    { N_ ("fri|day"),    TM_WDAY,        5,      NULL },
+    { N_ ("sat|urday"),  TM_WDAY,        6,      NULL },
+
+    /* Months. */
+    { N_ ("jan|uary"),   TM_ABS_MON,     1,      kw_set_month },
+    { N_ ("feb|ruary"),  TM_ABS_MON,     2,      kw_set_month },
+    { N_ ("mar|ch"),     TM_ABS_MON,     3,      kw_set_month },
+    { N_ ("apr|il"),     TM_ABS_MON,     4,      kw_set_month },
+    { N_ ("may"),        TM_ABS_MON,     5,      kw_set_month },
+    { N_ ("jun|e"),      TM_ABS_MON,     6,      kw_set_month },
+    { N_ ("jul|y"),      TM_ABS_MON,     7,      kw_set_month },
+    { N_ ("aug|ust"),    TM_ABS_MON,     8,      kw_set_month },
+    { N_ ("sep|tember"), TM_ABS_MON,     9,      kw_set_month },
+    { N_ ("oct|ober"),   TM_ABS_MON,     10,     kw_set_month },
+    { N_ ("nov|ember"),  TM_ABS_MON,     11,     kw_set_month },
+    { N_ ("dec|ember"),  TM_ABS_MON,     12,     kw_set_month },
+
+    /* Durations. */
+    { N_ ("y|ears"),     TM_REL_YEAR,    1,      kw_set_rel },
+    { N_ ("mo|nths"),    TM_REL_MON,     1,      kw_set_rel },
+    { N_ ("*M"),         TM_REL_MON,     1,      kw_set_rel },
+    { N_ ("w|eeks"),     TM_REL_WEEK,    1,      kw_set_rel },
+    { N_ ("d|ays"),      TM_REL_DAY,     1,      kw_set_rel },
+    { N_ ("h|ours"),     TM_REL_HOUR,    1,      kw_set_rel },
+    { N_ ("hr|s"),       TM_REL_HOUR,    1,      kw_set_rel },
+    { N_ ("mi|nutes"),   TM_REL_MIN,     1,      kw_set_rel },
+    { N_ ("mins"),       TM_REL_MIN,     1,      kw_set_rel },
+    { N_ ("*m"),         TM_REL_MIN,     1,      kw_set_rel },
+    { N_ ("s|econds"),   TM_REL_SEC,     1,      kw_set_rel },
+    { N_ ("secs"),       TM_REL_SEC,     1,      kw_set_rel },
+
+    /* Numbers. */
+    { N_ ("one"),        TM_NONE,        1,      kw_set_number },
+    { N_ ("two"),        TM_NONE,        2,      kw_set_number },
+    { N_ ("three"),      TM_NONE,        3,      kw_set_number },
+    { N_ ("four"),       TM_NONE,        4,      kw_set_number },
+    { N_ ("five"),       TM_NONE,        5,      kw_set_number },
+    { N_ ("six"),        TM_NONE,        6,      kw_set_number },
+    { N_ ("seven"),      TM_NONE,        7,      kw_set_number },
+    { N_ ("eight"),      TM_NONE,        8,      kw_set_number },
+    { N_ ("nine"),       TM_NONE,        9,      kw_set_number },
+    { N_ ("ten"),        TM_NONE,        10,     kw_set_number },
+    { N_ ("dozen"),      TM_NONE,        12,     kw_set_number },
+    { N_ ("hundred"),    TM_NONE,        100,    kw_set_number },
+
+    /* Special number forms. */
+    { N_ ("this"),       TM_NONE,        0,      kw_set_number },
+    { N_ ("last"),       TM_NONE,        1,      kw_set_number },
+
+    /* Other special keywords. */
+    { N_ ("yesterday"),  TM_REL_DAY,     1,      kw_set_rel },
+    { N_ ("today"),      TM_NONE,        0,      kw_set_today },
+    { N_ ("now"),        TM_NONE,        0,      kw_set_now },
+    { N_ ("noon"),       TM_NONE,        12,     kw_set_timeofday },
+    { N_ ("midnight"),   TM_NONE,        0,      kw_set_timeofday },
+    { N_ ("am"),         TM_AMPM,        0,      kw_set_ampm },
+    { N_ ("a.m."),       TM_AMPM,        0,      kw_set_ampm },
+    { N_ ("pm"),         TM_AMPM,        1,      kw_set_ampm },
+    { N_ ("p.m."),       TM_AMPM,        1,      kw_set_ampm },
+    { N_ ("st"),         TM_NONE,        0,      kw_set_ordinal },
+    { N_ ("nd"),         TM_NONE,        0,      kw_set_ordinal },
+    { N_ ("rd"),         TM_NONE,        0,      kw_set_ordinal },
+    { N_ ("th"),         TM_NONE,        0,      kw_set_ordinal },
+    { N_ ("ago"),        TM_NONE,        0,      kw_ignore },
+
+    /* Timezone codes: offset in minutes. XXX: Add more codes. */
+    { N_ ("pst"),        TM_TZ,          -8 * 60,  NULL },
+    { N_ ("mst"),        TM_TZ,          -7 * 60,  NULL },
+    { N_ ("cst"),        TM_TZ,          -6 * 60,  NULL },
+    { N_ ("est"),        TM_TZ,          -5 * 60,  NULL },
+    { N_ ("ast"),        TM_TZ,          -4 * 60,  NULL },
+    { N_ ("nst"),        TM_TZ,          -(3 * 60 + 30),     NULL },
+
+    { N_ ("gmt"),        TM_TZ,          0,      NULL },
+    { N_ ("utc"),        TM_TZ,          0,      NULL },
+
+    { N_ ("wet"),        TM_TZ,          0,      NULL },
+    { N_ ("cet"),        TM_TZ,          1 * 60,   NULL },
+    { N_ ("eet"),        TM_TZ,          2 * 60,   NULL },
+    { N_ ("fet"),        TM_TZ,          3 * 60,   NULL },
+
+    { N_ ("wat"),        TM_TZ,          1 * 60,   NULL },
+    { N_ ("cat"),        TM_TZ,          2 * 60,   NULL },
+    { N_ ("eat"),        TM_TZ,          3 * 60,   NULL },
+};
+
+/*
+ * Compare strings str and keyword. Return the number of matching
+ * chars on match, 0 for no match.
+ *
+ * All of the alphabetic characters (isalpha) in str up to the first
+ * non-alpha character (or end of string) must match the
+ * keyword. Consequently, the value returned on match is the number of
+ * consecutive alphabetic characters in str.
+ *
+ * Abbreviated match is accepted if the keyword contains a '|'
+ * character, and str matches keyword up to that character. Any alpha
+ * characters after that in str must still match the keyword following
+ * the '|' character. If no '|' is present, all of keyword must match.
+ *
+ * Excessive, consecutive, and misplaced (at the beginning or end) '|'
+ * characters in keyword are handled gracefully. Only the first one
+ * matters.
+ *
+ * If match_case is true, the matching is case sensitive.
+ */
+static size_t
+match_keyword (const char *str, const char *keyword, bool match_case)
+{
+    const char *s = str;
+    bool prefix_matched = false;
+
+    for (;;) {
+       while (*keyword == '|') {
+           prefix_matched = true;
+           keyword++;
+       }
+
+       if (! *s || ! isalpha ((unsigned char) *s) || ! *keyword)
+           break;
+
+       if (match_case) {
+           if (*s != *keyword)
+               return 0;
+       } else {
+           if (tolower ((unsigned char) *s) !=
+               tolower ((unsigned char) *keyword))
+               return 0;
+       }
+       s++;
+       keyword++;
+    }
+
+    /* did not match all of the keyword in input string */
+    if (*s && isalpha ((unsigned char) *s))
+       return 0;
+
+    /* did not match enough of keyword */
+    if (*keyword && ! prefix_matched)
+       return 0;
+
+    return s - str;
+}
+
+/*
+ * Parse a keyword. Return < 0 on error, number of parsed chars on
+ * success.
+ */
+static ssize_t
+parse_keyword (struct state *state, const char *s)
+{
+    unsigned int i;
+    size_t n = 0;
+    struct keyword *kw = NULL;
+    int r;
+
+    for (i = 0; i < ARRAY_SIZE (keywords); i++) {
+       const char *keyword = _ (keywords[i].name);
+       bool mcase = false;
+
+       /* Match case if keyword begins with '*'. */
+       if (*keyword == '*') {
+           mcase = true;
+           keyword++;
+       }
+
+       n = match_keyword (s, keyword, mcase);
+       if (n) {
+           kw = &keywords[i];
+           break;
+       }
+    }
+
+    if (! kw)
+       return -PARSE_TIME_ERR_KEYWORD;
+
+    if (kw->set)
+       r = kw->set (state, kw);
+    else
+       r = set_field (state, kw->field, kw->value);
+
+    if (r < 0)
+       return r;
+
+    return n;
+}
+
+/*
+ * Non-keyword parsers and their helpers.
+ */
+
+static int
+set_user_tz (struct state *state, char sign, int hour, int min)
+{
+    int tz = hour * 60 + min;
+
+    assert (sign == '+' || sign == '-');
+
+    if (hour < 0 || hour > 14 || min < 0 || min > 59 || min % 15)
+       return -PARSE_TIME_ERR_INVALIDTIME;
+
+    if (sign == '-')
+       tz = -tz;
+
+    return set_field (state, TM_TZ, tz);
+}
+
+/*
+ * Parse a previously postponed number if one exists. Independent
+ * parsing of a postponed number when it wasn't consumed during
+ * parsing of the following token.
+ */
+static int
+parse_postponed_number (struct state *state, unused (enum field next_field))
+{
+    int v, n;
+    char d;
+
+    /* Bail out if there's no postponed number. */
+    if (! consume_postponed_number (state, &v, &n, &d))
+       return 0;
+
+    if (n == 1 || n == 2) {
+       /* Notable exception: Previous field affects parsing. This
+        * handles "January 20". */
+       if (state->last_field == TM_ABS_MON) {
+           /* D[D] */
+           if (! is_valid_mday (v))
+               return -PARSE_TIME_ERR_INVALIDDATE;
+
+           return set_field (state, TM_ABS_MDAY, v);
+       } else if (n == 2) {
+           /* XXX: Only allow if last field is hour, min, or sec? */
+           if (d == '+' || d == '-') {
+               /* +/-HH */
+               return set_user_tz (state, d, v, 0);
+           }
+       }
+    } else if (n == 4) {
+       /* Notable exception: Value affects parsing. Time zones are
+        * always at most 1400 and we don't understand years before
+        * 1970. */
+       if (! is_valid_year (v)) {
+           if (d == '+' || d == '-') {
+               /* +/-HHMM */
+               return set_user_tz (state, d, v / 100, v % 100);
+           }
+       } else {
+           /* YYYY */
+           return set_field (state, TM_ABS_YEAR, v);
+       }
+    } else if (n == 6) {
+       /* HHMMSS */
+       int hour = v / 10000;
+       int min = (v / 100) % 100;
+       int sec = v % 100;
+
+       if (! is_valid_time (hour, min, sec))
+           return -PARSE_TIME_ERR_INVALIDTIME;
+
+       return set_abs_time (state, hour, min, sec);
+    } else if (n == 8) {
+       /* YYYYMMDD */
+       int year = v / 10000;
+       int mon = (v / 100) % 100;
+       int mday = v % 100;
+
+       if (! is_valid_date (year, mon, mday))
+           return -PARSE_TIME_ERR_INVALIDDATE;
+
+       return set_abs_date (state, year, mon, mday);
+    }
+
+    return -PARSE_TIME_ERR_FORMAT;
+}
+
+static int tm_get_field (const struct tm *tm, enum field field);
+
+static int
+set_timestamp (struct state *state, time_t t)
+{
+    struct tm tm;
+    enum field f;
+    int r;
+
+    if (gmtime_r (&t, &tm) == NULL)
+       return -PARSE_TIME_ERR_LIB;
+
+    for (f = TM_ABS_SEC; f != TM_NONE; f = next_abs_field (f)) {
+       r = set_field (state, f, tm_get_field (&tm, f));
+       if (r)
+           return r;
+    }
+
+    r = set_field (state, TM_TZ, 0);
+    if (r)
+       return r;
+
+    /* XXX: Prevent TM_AMPM with timestamp, e.g. "@123456 pm" */
+
+    return 0;
+}
+
+/* Parse a single number. Typically postpone parsing until later. */
+static int
+parse_single_number (struct state *state, unsigned long v,
+                    unsigned long n)
+{
+    assert (n);
+
+    if (state->delim == '@')
+       return set_timestamp (state, (time_t) v);
+
+    if (v > INT_MAX)
+       return -PARSE_TIME_ERR_FORMAT;
+
+    return set_postponed_number (state, v, n);
+}
+
+static bool
+is_time_sep (char c)
+{
+    return c == ':';
+}
+
+static bool
+is_date_sep (char c)
+{
+    return c == '/' || c == '-' || c == '.';
+}
+
+static bool
+is_sep (char c)
+{
+    return is_time_sep (c) || is_date_sep (c);
+}
+
+/* Two-digit year: 00...69 is 2000s, 70...99 1900s, if n == 0 keep
+ * unset. */
+static int
+expand_year (unsigned long year, size_t n)
+{
+    if (n == 2) {
+       return (year < 70 ? 2000 : 1900) + year;
+    } else if (n == 4) {
+       return year;
+    } else {
+       return UNSET;
+    }
+}
+
+/* Parse a date number triplet. */
+static int
+parse_date (struct state *state, char sep,
+           unsigned long v1, unsigned long v2, unsigned long v3,
+           size_t n1, size_t n2, size_t n3)
+{
+    int year = UNSET, mon = UNSET, mday = UNSET;
+
+    assert (is_date_sep (sep));
+
+    switch (sep) {
+    case '/': /* Date: M[M]/D[D][/YY[YY]] or M[M]/YYYY */
+       if (n1 != 1 && n1 != 2)
+           return -PARSE_TIME_ERR_DATEFORMAT;
+
+       if ((n2 == 1 || n2 == 2) && (n3 == 0 || n3 == 2 || n3 == 4)) {
+           /* M[M]/D[D][/YY[YY]] */
+           year = expand_year (v3, n3);
+           mon = v1;
+           mday = v2;
+       } else if (n2 == 4 && n3 == 0) {
+           /* M[M]/YYYY */
+           year = v2;
+           mon = v1;
+       } else {
+           return -PARSE_TIME_ERR_DATEFORMAT;
+       }
+       break;
+
+    case '-': /* Date: YYYY-MM[-DD] or DD-MM[-YY[YY]] or MM-YYYY */
+       if (n1 == 4 && n2 == 2 && (n3 == 0 || n3 == 2)) {
+           /* YYYY-MM[-DD] */
+           year = v1;
+           mon = v2;
+           if (n3)
+               mday = v3;
+       } else if (n1 == 2 && n2 == 2 && (n3 == 0 || n3 == 2 || n3 == 4)) {
+           /* DD-MM[-YY[YY]] */
+           year = expand_year (v3, n3);
+           mon = v2;
+           mday = v1;
+       } else if (n1 == 2 && n2 == 4 && n3 == 0) {
+           /* MM-YYYY */
+           year = v2;
+           mon = v1;
+       } else {
+           return -PARSE_TIME_ERR_DATEFORMAT;
+       }
+       break;
+
+    case '.': /* Date: D[D].M[M][.[YY[YY]]] */
+       if ((n1 != 1 && n1 != 2) || (n2 != 1 && n2 != 2) ||
+           (n3 != 0 && n3 != 2 && n3 != 4))
+           return -PARSE_TIME_ERR_DATEFORMAT;
+
+       year = expand_year (v3, n3);
+       mon = v2;
+       mday = v1;
+       break;
+    }
+
+    if (year != UNSET && ! is_valid_year (year))
+       return -PARSE_TIME_ERR_INVALIDDATE;
+
+    if (mon != UNSET && ! is_valid_mon (mon))
+       return -PARSE_TIME_ERR_INVALIDDATE;
+
+    if (mday != UNSET && ! is_valid_mday (mday))
+       return -PARSE_TIME_ERR_INVALIDDATE;
+
+    return set_abs_date (state, year, mon, mday);
+}
+
+/* Parse a time number triplet. */
+static int
+parse_time (struct state *state, char sep,
+           unsigned long v1, unsigned long v2, unsigned long v3,
+           size_t n1, size_t n2, size_t n3)
+{
+    assert (is_time_sep (sep));
+
+    if ((n1 != 1 && n1 != 2) || n2 != 2 || (n3 != 0 && n3 != 2))
+       return -PARSE_TIME_ERR_TIMEFORMAT;
+
+    /*
+     * Notable exception: Previously set fields affect
+     * parsing. Interpret (+|-)HH:MM as time zone only if hour and
+     * minute have been set.
+     *
+     * XXX: This could be fixed by restricting the delimiters
+     * preceding time. For '+' it would be justified, but for '-' it
+     * might be inconvenient. However prefer to allow '-' as an
+     * insignificant delimiter preceding time for convenience, and
+     * handle '+' the same way for consistency between positive and
+     * negative time zones.
+     */
+    if (is_field_set (state, TM_ABS_HOUR) &&
+       is_field_set (state, TM_ABS_MIN) &&
+       n1 == 2 && n2 == 2 && n3 == 0 &&
+       (state->delim == '+' || state->delim == '-')) {
+       return set_user_tz (state, state->delim, v1, v2);
+    }
+
+    if (! is_valid_time (v1, v2, n3 ? v3 : 0))
+       return -PARSE_TIME_ERR_INVALIDTIME;
+
+    return set_abs_time (state, v1, v2, n3 ? (int) v3 : UNSET);
+}
+
+/* strtoul helper that assigns length. */
+static unsigned long
+strtoul_len (const char *s, const char **endp, size_t *len)
+{
+    unsigned long val = strtoul (s, (char **) endp, 10);
+
+    *len = *endp - s;
+    return val;
+}
+
+/*
+ * Parse a (group of) number(s). Return < 0 on error, number of parsed
+ * chars on success.
+ */
+static ssize_t
+parse_number (struct state *state, const char *s)
+{
+    int r;
+    unsigned long v1, v2, v3 = 0;
+    size_t n1, n2, n3 = 0;
+    const char *p = s;
+    char sep;
+
+    v1 = strtoul_len (p, &p, &n1);
+
+    if (! is_sep (*p) || ! isdigit ((unsigned char) *(p + 1))) {
+       /* A single number. */
+       r = parse_single_number (state, v1, n1);
+       if (r)
+           return r;
+
+       return p - s;
+    }
+
+    sep = *p;
+    v2 = strtoul_len (p + 1, &p, &n2);
+
+    /* A group of two or three numbers? */
+    if (*p == sep && isdigit ((unsigned char) *(p + 1)))
+       v3 = strtoul_len (p + 1, &p, &n3);
+
+    if (is_time_sep (sep))
+       r = parse_time (state, sep, v1, v2, v3, n1, n2, n3);
+    else
+       r = parse_date (state, sep, v1, v2, v3, n1, n2, n3);
+
+    if (r)
+       return r;
+
+    return p - s;
+}
+
+/*
+ * Parse delimiter(s). Throw away all except the last one, which is
+ * stored for parsing the next non-delimiter. Return < 0 on error,
+ * number of parsed chars on success.
+ *
+ * XXX: We might want to be more strict here.
+ */
+static ssize_t
+parse_delim (struct state *state, const char *s)
+{
+    const char *p = s;
+
+    /*
+     * Skip non-alpha and non-digit, and store the last for further
+     * processing.
+     */
+    while (*p && ! isalnum ((unsigned char) *p)) {
+       set_delim (state, *p);
+       p++;
+    }
+
+    return p - s;
+}
+
+/*
+ * Parse a date/time string. Return < 0 on error, number of parsed
+ * chars on success.
+ */
+static ssize_t
+parse_input (struct state *state, const char *s)
+{
+    const char *p = s;
+    ssize_t n;
+    int r;
+
+    while (*p) {
+       if (isalpha ((unsigned char) *p)) {
+           n = parse_keyword (state, p);
+       } else if (isdigit ((unsigned char) *p)) {
+           n = parse_number (state, p);
+       } else {
+           n = parse_delim (state, p);
+       }
+
+       if (n <= 0) {
+           if (n == 0)
+               n = -PARSE_TIME_ERR;
+
+           return n;
+       }
+
+       p += n;
+    }
+
+    /* Parse a previously postponed number, if any. */
+    r = parse_postponed_number (state, TM_NONE);
+    if (r < 0)
+       return r;
+
+    return p - s;
+}
+
+/*
+ * Processing the parsed input.
+ */
+
+/*
+ * Initialize reference time to tm. Use time zone in state if
+ * specified, otherwise local time. Use now for reference time if
+ * non-NULL, otherwise current time.
+ */
+static int
+initialize_now (struct state *state, const time_t *ref, struct tm *tm)
+{
+    time_t t;
+
+    if (ref) {
+       t = *ref;
+    } else {
+       if (time (&t) == (time_t) -1)
+           return -PARSE_TIME_ERR_LIB;
+    }
+
+    if (is_field_set (state, TM_TZ)) {
+       /* Some other time zone. */
+
+       /* Adjust now according to the TZ. */
+       t += get_field (state, TM_TZ) * 60;
+
+       /* It's not gm, but this doesn't mess with the TZ. */
+       if (gmtime_r (&t, tm) == NULL)
+           return -PARSE_TIME_ERR_LIB;
+    } else {
+       /* Local time. */
+       if (localtime_r (&t, tm) == NULL)
+           return -PARSE_TIME_ERR_LIB;
+    }
+
+    return 0;
+}
+
+/*
+ * Normalize tm according to mktime(3); if structure members are
+ * outside their valid interval, they will be normalized (so that, for
+ * example, 40 October is changed into 9 November), and tm_wday and
+ * tm_yday are set to values determined from the contents of the other
+ * fields.
+ *
+ * Both mktime(3) and localtime_r(3) use local time, but they cancel
+ * each other out here, making this function agnostic to time zone.
+ */
+static int
+normalize_tm (struct tm *tm)
+{
+    time_t t = mktime (tm);
+
+    if (t == (time_t) -1)
+       return -PARSE_TIME_ERR_LIB;
+
+    if (! localtime_r (&t, tm))
+       return -PARSE_TIME_ERR_LIB;
+
+    return 0;
+}
+
+/* Get field out of a struct tm. */
+static int
+tm_get_field (const struct tm *tm, enum field field)
+{
+    switch (field) {
+    case TM_ABS_SEC:    return tm->tm_sec;
+    case TM_ABS_MIN:    return tm->tm_min;
+    case TM_ABS_HOUR:   return tm->tm_hour;
+    case TM_ABS_MDAY:   return tm->tm_mday;
+    case TM_ABS_MON:    return tm->tm_mon + 1; /* 0- to 1-based */
+    case TM_ABS_YEAR:   return 1900 + tm->tm_year;
+    case TM_WDAY:       return tm->tm_wday;
+    case TM_ABS_ISDST:  return tm->tm_isdst;
+    default:
+       assert (false);
+       break;
+    }
+
+    return 0;
+}
+
+/* Modify hour according to am/pm setting. */
+static int
+fixup_ampm (struct state *state)
+{
+    int hour, hdiff = 0;
+
+    if (! is_field_set (state, TM_AMPM))
+       return 0;
+
+    if (! is_field_set (state, TM_ABS_HOUR))
+       return -PARSE_TIME_ERR_TIMEFORMAT;
+
+    hour = get_field (state, TM_ABS_HOUR);
+    if (! is_valid_12hour (hour))
+       return -PARSE_TIME_ERR_INVALIDTIME;
+
+    if (get_field (state, TM_AMPM)) {
+       /* 12pm is noon. */
+       if (hour != 12)
+           hdiff = 12;
+    } else {
+       /* 12am is midnight, beginning of day. */
+       if (hour == 12)
+           hdiff = -12;
+    }
+
+    add_to_field (state, TM_REL_HOUR, -hdiff);
+
+    return 0;
+}
+
+/* Combine absolute and relative fields, and round. */
+static int
+create_output (struct state *state, time_t *t_out, const time_t *ref,
+              int round)
+{
+    struct tm tm = { .tm_isdst = -1 };
+    struct tm now;
+    time_t t;
+    enum field f;
+    int r;
+    int week_round = PARSE_TIME_NO_ROUND;
+
+    r = initialize_now (state, ref, &now);
+    if (r)
+       return r;
+
+    /* Initialize fields flagged as "now" to reference time. */
+    for (f = TM_ABS_SEC; f != TM_NONE; f = next_abs_field (f)) {
+       if (state->set[f] == FIELD_NOW) {
+           state->tm[f] = tm_get_field (&now, f);
+           state->set[f] = FIELD_SET;
+       }
+    }
+
+    /*
+     * If WDAY is set but MDAY is not, we consider WDAY relative
+     *
+     * XXX: This fails on stuff like "two months monday" because two
+     * months ago wasn't the same day as today. Postpone until we know
+     * date?
+     */
+    if (is_field_set (state, TM_WDAY) &&
+       ! is_field_set (state, TM_ABS_MDAY)) {
+       int wday = get_field (state, TM_WDAY);
+       int today = tm_get_field (&now, TM_WDAY);
+       int rel_days;
+
+       if (today > wday)
+           rel_days = today - wday;
+       else
+           rel_days = today + 7 - wday;
+
+       /* This also prevents special week rounding from happening. */
+       add_to_field (state, TM_REL_DAY, rel_days);
+
+       unset_field (state, TM_WDAY);
+    }
+
+    r = fixup_ampm (state);
+    if (r)
+       return r;
+
+    /*
+     * Iterate fields from most accurate to least accurate, and set
+     * unset fields according to requested rounding.
+     */
+    for (f = TM_ABS_SEC; f != TM_NONE; f = next_abs_field (f)) {
+       if (round != PARSE_TIME_NO_ROUND) {
+           enum field r = abs_to_rel_field (f);
+
+           if (is_field_set (state, f) || is_field_set (state, r)) {
+               if (round >= PARSE_TIME_ROUND_UP && f != TM_ABS_SEC) {
+                   /*
+                    * This is the most accurate field
+                    * specified. Round up adjusting it towards
+                    * future.
+                    */
+                   add_to_field (state, r, -1);
+
+                   /*
+                    * Go back a second if the result is to be used
+                    * for inclusive comparisons.
+                    */
+                   if (round == PARSE_TIME_ROUND_UP_INCLUSIVE)
+                       add_to_field (state, TM_REL_SEC, 1);
+               }
+               round = PARSE_TIME_NO_ROUND; /* No more rounding. */
+           } else {
+               if (f == TM_ABS_MDAY &&
+                   is_field_set (state, TM_REL_WEEK)) {
+                   /* Week is most accurate. */
+                   week_round = round;
+                   round = PARSE_TIME_NO_ROUND;
+               } else {
+                   set_field (state, f, get_field_epoch_value (f));
+               }
+           }
+       }
+
+       if (! is_field_set (state, f))
+           set_field (state, f, tm_get_field (&now, f));
+    }
+
+    /* Special case: rounding with week accuracy. */
+    if (week_round != PARSE_TIME_NO_ROUND) {
+       /* Temporarily set more accurate fields to now. */
+       set_field (state, TM_ABS_SEC, tm_get_field (&now, TM_ABS_SEC));
+       set_field (state, TM_ABS_MIN, tm_get_field (&now, TM_ABS_MIN));
+       set_field (state, TM_ABS_HOUR, tm_get_field (&now, TM_ABS_HOUR));
+       set_field (state, TM_ABS_MDAY, tm_get_field (&now, TM_ABS_MDAY));
+    }
+
+    /*
+     * Set all fields. They may contain out of range values before
+     * normalization by mktime(3).
+     */
+    tm.tm_sec = get_field (state, TM_ABS_SEC) - get_field (state, TM_REL_SEC);
+    tm.tm_min = get_field (state, TM_ABS_MIN) - get_field (state, TM_REL_MIN);
+    tm.tm_hour = get_field (state, TM_ABS_HOUR) - get_field (state, TM_REL_HOUR);
+    tm.tm_mday = get_field (state, TM_ABS_MDAY) -
+                get_field (state, TM_REL_DAY) - 7 * get_field (state, TM_REL_WEEK);
+    tm.tm_mon = get_field (state, TM_ABS_MON) - get_field (state, TM_REL_MON);
+    tm.tm_mon--; /* 1- to 0-based */
+    tm.tm_year = get_field (state, TM_ABS_YEAR) - get_field (state, TM_REL_YEAR) - 1900;
+
+    /*
+     * It's always normal time.
+     *
+     * XXX: This is probably not a solution that universally
+     * works. Just make sure DST is not taken into account. We don't
+     * want rounding to be affected by DST.
+     */
+    tm.tm_isdst = -1;
+
+    /* Special case: rounding with week accuracy. */
+    if (week_round != PARSE_TIME_NO_ROUND) {
+       /* Normalize to get proper tm.wday. */
+       r = normalize_tm (&tm);
+       if (r < 0)
+           return r;
+
+       /* Set more accurate fields back to zero. */
+       tm.tm_sec = 0;
+       tm.tm_min = 0;
+       tm.tm_hour = 0;
+       tm.tm_isdst = -1;
+
+       /* Monday is the true 1st day of week, but this is easier. */
+       if (week_round >= PARSE_TIME_ROUND_UP) {
+           tm.tm_mday += 7 - tm.tm_wday;
+           if (week_round == PARSE_TIME_ROUND_UP_INCLUSIVE)
+               tm.tm_sec--;
+       } else {
+           tm.tm_mday -= tm.tm_wday;
+       }
+    }
+
+    if (is_field_set (state, TM_TZ)) {
+       /* tm is in specified TZ, convert to UTC for timegm(3). */
+       tm.tm_min -= get_field (state, TM_TZ);
+       t = timegm (&tm);
+    } else {
+       /* tm is in local time. */
+       t = mktime (&tm);
+    }
+
+    if (t == (time_t) -1)
+       return -PARSE_TIME_ERR_LIB;
+
+    *t_out = t;
+
+    return 0;
+}
+
+/* Internally, all errors are < 0. parse_time_string() returns errors > 0. */
+#define EXTERNAL_ERR(r) (-r)
+
+int
+parse_time_string (const char *s, time_t *t, const time_t *ref, int round)
+{
+    struct state state = { .last_field = TM_NONE };
+    int r;
+
+    if (! s || ! t)
+       return EXTERNAL_ERR (-PARSE_TIME_ERR);
+
+    r = parse_input (&state, s);
+    if (r < 0)
+       return EXTERNAL_ERR (r);
+
+    r = create_output (&state, t, ref, round);
+    if (r < 0)
+       return EXTERNAL_ERR (r);
+
+    return 0;
+}
diff --git a/parse-time-string/parse-time-string.h b/parse-time-string/parse-time-string.h
new file mode 100644 (file)
index 0000000..c90d694
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * parse time string - user friendly date and time parser
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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/>.
+ *
+ * Author: Jani Nikula <jani@nikula.org>
+ */
+
+#ifndef PARSE_TIME_STRING_H
+#define PARSE_TIME_STRING_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <time.h>
+
+/* return values for parse_time_string() */
+enum {
+    PARSE_TIME_OK = 0,
+    PARSE_TIME_ERR,             /* unspecified error */
+    PARSE_TIME_ERR_LIB,         /* library call failed */
+    PARSE_TIME_ERR_ALREADYSET,  /* attempt to set unit twice */
+    PARSE_TIME_ERR_FORMAT,      /* generic date/time format error */
+    PARSE_TIME_ERR_DATEFORMAT,  /* date format error */
+    PARSE_TIME_ERR_TIMEFORMAT,  /* time format error */
+    PARSE_TIME_ERR_INVALIDDATE, /* date value error */
+    PARSE_TIME_ERR_INVALIDTIME, /* time value error */
+    PARSE_TIME_ERR_KEYWORD,     /* unknown keyword */
+};
+
+/* round values for parse_time_string() */
+enum {
+    PARSE_TIME_ROUND_DOWN              = -1,
+    PARSE_TIME_NO_ROUND                        = 0,
+    PARSE_TIME_ROUND_UP                        = 1,
+    PARSE_TIME_ROUND_UP_INCLUSIVE      = 2,
+};
+
+/**
+ * parse_time_string() - user friendly date and time parser
+ * @s:         string to parse
+ * @t:         pointer to time_t to store parsed time in
+ * @ref:       pointer to time_t containing reference date/time, or NULL
+ * @round:     PARSE_TIME_NO_ROUND, PARSE_TIME_ROUND_DOWN, or
+ *             PARSE_TIME_ROUND_UP
+ *
+ * Parse a date/time string 's' and store the parsed date/time result
+ * in 't'.
+ *
+ * A reference date/time is used for determining the "date/time units"
+ * (roughly equivalent to struct tm members) not specified by 's'. If
+ * 'ref' is non-NULL, it must contain a pointer to a time_t to be used
+ * as reference date/time. Otherwise, the current time is used.
+ *
+ * If 's' does not specify a full date/time, the 'round' parameter
+ * specifies if and how the result should be rounded as follows:
+ *
+ *   PARSE_TIME_NO_ROUND: All date/time units that are not specified
+ *   by 's' are set to the corresponding unit derived from the
+ *   reference date/time.
+ *
+ *   PARSE_TIME_ROUND_DOWN: All date/time units that are more accurate
+ *   than the most accurate unit specified by 's' are set to the
+ *   smallest valid value for that unit. Rest of the unspecified units
+ *   are set as in PARSE_TIME_NO_ROUND.
+ *
+ *   PARSE_TIME_ROUND_UP: All date/time units that are more accurate
+ *   than the most accurate unit specified by 's' are set to the
+ *   smallest valid value for that unit. The most accurate unit
+ *   specified by 's' is incremented by one (and this is rolled over
+ *   to the less accurate units as necessary), unless the most
+ *   accurate unit is seconds. Rest of the unspecified units are set
+ *   as in PARSE_TIME_NO_ROUND.
+ *
+ *   PARSE_TIME_ROUND_UP_INCLUSIVE: Same as PARSE_TIME_ROUND_UP, minus
+ *   one second, unless the most accurate unit specified by 's' is
+ *   seconds. This is useful for callers that require a value for
+ *   inclusive comparison of the result.
+ *
+ * Return 0 (PARSE_TIME_OK) for successfully parsed date/time, or one
+ * of PARSE_TIME_ERR_* on error. 't' is not modified on error.
+ */
+int parse_time_string (const char *s, time_t *t, const time_t *ref, int round);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PARSE_TIME_STRING_H */
diff --git a/performance-test/.gitignore b/performance-test/.gitignore
new file mode 100644 (file)
index 0000000..8a5dabf
--- /dev/null
@@ -0,0 +1,4 @@
+/tmp.*/
+/log.*/
+/corpus/
+/notmuch.cache.*/
diff --git a/performance-test/M00-new.sh b/performance-test/M00-new.sh
new file mode 100755 (executable)
index 0000000..5858ab3
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+test_description='notmuch new'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+# ensure initial 'notmuch new' is run by memory_start
+uncache_database
+
+memory_start
+
+# run 'notmuch new' a second time, to test different code paths
+memory_run "notmuch new" "notmuch new"
+
+memory_done
diff --git a/performance-test/M01-dump-restore.sh b/performance-test/M01-dump-restore.sh
new file mode 100755 (executable)
index 0000000..7850b41
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+test_description='dump and restore'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+memory_start
+
+memory_run 'load nmbug tags' 'notmuch restore --accumulate --input=corpus.tags/nmbug.sup-dump'
+memory_run 'dump *' 'notmuch dump --output=tags.sup'
+memory_run 'restore *' 'notmuch restore --input=tags.sup'
+memory_run 'dump --format=batch-tag *' 'notmuch dump --format=batch-tag --output=tags.bt'
+memory_run 'restore --format=batch-tag *' 'notmuch restore --format=batch-tag --input=tags.bt'
+
+memory_done
diff --git a/performance-test/M02-show.sh b/performance-test/M02-show.sh
new file mode 100755 (executable)
index 0000000..40c5d4d
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+test_description='show'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+memory_start
+
+memory_run 'show *' "notmuch show '*' 1>/dev/null"
+memory_run 'show --format=json *' "notmuch show --format=json '*' 1>/dev/null"
+memory_run 'show --format=sexp *' "notmuch show --format=sexp '*' 1>/dev/null"
+
+memory_done
diff --git a/performance-test/M03-search.sh b/performance-test/M03-search.sh
new file mode 100755 (executable)
index 0000000..a73a36a
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+test_description='search'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+memory_start
+
+memory_run 'search *' "notmuch search '*' 1>/dev/null"
+memory_run 'search --format=json *' "notmuch search --format=json '*' 1>/dev/null"
+memory_run 'search --format=sexp *' "notmuch search --format=sexp '*' 1>/dev/null"
+
+memory_done
diff --git a/performance-test/M04-reply.sh b/performance-test/M04-reply.sh
new file mode 100755 (executable)
index 0000000..3b0f9e7
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+test_description='search'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+memory_start
+
+for id in $(notmuch search --output=messages '*' | shuf -n 5); do
+    memory_run "reply $id" "notmuch reply \"$id\" 1>/dev/null"
+    memory_run "reply --format=json $id" "notmuch reply --format=json \"$id\" 1>/dev/null"
+    memory_run "reply --format=sexp $id" "notmuch reply --format=sexp \"$id\" 1>/dev/null"
+done
+
+memory_done
diff --git a/performance-test/M05-reindex.sh b/performance-test/M05-reindex.sh
new file mode 100755 (executable)
index 0000000..8ea7e7e
--- /dev/null
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+test_description='reindex'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+memory_start
+
+memory_run 'reindex *' "notmuch reindex '*'"
+
+memory_done
diff --git a/performance-test/M06-insert.sh b/performance-test/M06-insert.sh
new file mode 100755 (executable)
index 0000000..12330c7
--- /dev/null
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+test_description='search'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+memory_start
+
+mkdir -p "$MAIL_DIR"/{cur,new,tmp}
+
+for count in {1..20}; do
+    generate_message "[file]=\"insert-$count\"" "[dir]='tmp/'"
+    memory_run "insert $count" "notmuch insert < $gen_msg_filename"
+done
+
+memory_done
diff --git a/performance-test/Makefile b/performance-test/Makefile
new file mode 100644 (file)
index 0000000..de492a7
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/performance-test/Makefile.local b/performance-test/Makefile.local
new file mode 100644 (file)
index 0000000..b9f580c
--- /dev/null
@@ -0,0 +1,44 @@
+# -*- makefile-gmake -*-
+
+dir := performance-test
+
+include $(srcdir)/$(dir)/version.sh
+
+TIME_TEST_SCRIPT := ${dir}/notmuch-time-test
+MEMORY_TEST_SCRIPT := ${dir}/notmuch-memory-test
+
+CORPUS_NAME := notmuch-email-corpus-$(PERFTEST_VERSION).tar.xz
+TXZFILE := ${dir}/download/${CORPUS_NAME}
+SIGFILE := ${TXZFILE}.asc
+DEFAULT_URL :=  https://notmuchmail.org/releases/${CORPUS_NAME}
+
+perf-test: time-test memory-test
+
+time-test: setup-perf-test all
+       @echo
+       $(TIME_TEST_SCRIPT) $(OPTIONS)
+
+memory-test: setup-perf-test all
+       @echo
+       $(MEMORY_TEST_SCRIPT) $(OPTIONS)
+
+
+.PHONY: download-corpus setup-perf-test
+
+# Note that this intentionally does not depend on download-corpus.
+setup-perf-test: $(TXZFILE)
+       gpg --verify $(SIGFILE)
+
+$(TXZFILE):
+       @printf "\nPlease download ${TXZFILE} using:\n\n"
+       @printf "\t%% make download-corpus\n\n"
+       @echo or see https://notmuchmail.org/corpus for download locations
+       @echo
+       @false
+
+download-corpus:
+       wget -O ${TXZFILE} ${DEFAULT_URL}
+
+CLEAN := $(CLEAN) $(dir)/tmp.* $(dir)/log.*
+DISTCLEAN := $(DISTCLEAN) $(dir)/corpus $(dir)/notmuch.cache.*
+DATACLEAN := $(DATACLEAN) $(TXZFILE)
diff --git a/performance-test/README b/performance-test/README
new file mode 100644 (file)
index 0000000..1ca0df2
--- /dev/null
@@ -0,0 +1,104 @@
+Performance Tests
+-----------------
+
+This directory contains two kinds of performance tests: time tests,
+and memory tests. The former use gnu time, and the latter use
+valgrind.
+
+Pre-requisites
+--------------
+
+In addition to having notmuch, you need:
+
+- gpg
+- gnu tar
+- gnu time (for the time tests)
+- 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:
+----------------------------
+
+First, you need to get the corpus.  If you don't already have the gpg
+key for David Bremner, run
+
+   % gpg --locate-external-key 'david@tethera.net'
+
+This should get you a key with fingerprint
+
+    7A18 807F 100A 4570 C596  8420 7E4E 65C8 720B 706B
+
+(the last 8 digits are printed as the "key id").
+
+To fetch the actual corpus it should work to run
+
+   % make download-corpus
+
+In case that fails or is too slow, check
+
+   https://notmuchmail.org/corpus
+
+for a list of mirrors.
+
+Running tests
+-------------
+
+The easiest way to run performance tests is to say "make perf-test".
+This will run all time and memory tests.  Be aware that the memory
+tests are quite time consuming when run on the full corpus, and that
+depending on your interests it may be more sensible to run "make
+time-test" or "make memory-test".  You can also invoke one of the
+scripts notmuch-time-test or notmuch-memory-test or run a more
+specific subset of tests by simply invoking one of the executable
+scripts in this directory, (such as ./T00-new).  Each test script
+supports the following arguments
+
+--small / --medium / --large   Choose corpus size.
+--debug                                Enable debugging. In particular don't delete
+                                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
+-------------
+
+Have a look at "T01-dump-restore" for an example time test and
+"M00-new" for an example memory test. In both cases sourcing
+"perf-test-lib.sh" is mandatory.
+
+Basics:
+
+- '(time|memory)_start' unpacks the mail corpus and calls notmuch new if it
+   cannot find a cache of the appropriate corpus.
+- '(time|memory)_run' runs the command under time or valgrind. Currently
+  "memory_run" does not support i/o redirection in the command.
+- '(time|memory)_done' does the cleanup; comment it out or pass --debug to the
+  script to leave the temporary files around.
+
+Utility functions include
+
+- 'add_email_corpus' unpacks a set of messages and tags
+- 'cache_database': makes a snapshot of the current database
+- 'uncache_database': forces the next '(time|memory)_start' to rebuild the
+  database.
+
+Scripts are run in the order specified in notmuch-perf-test. In the
+future this order might be chosen automatically so please follow the
+convention of starting the name with 'T' or 'M' followed by two digits
+to specify the order.
diff --git a/performance-test/T00-new.sh b/performance-test/T00-new.sh
new file mode 100755 (executable)
index 0000000..de260b2
--- /dev/null
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+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
+
+# arithmetic context is to eat extra whitespace on e.g. some BSDs
+count=$((`wc -l < $manifest`))
+
+perl -nle 'rename $_, "$_.renamed"' $manifest
+
+time_run "new ($count mv)" 'notmuch new'
+
+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'
+
+time_done
diff --git a/performance-test/T01-dump-restore.sh b/performance-test/T01-dump-restore.sh
new file mode 100755 (executable)
index 0000000..2a53e3b
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+test_description='dump and restore'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+time_start
+
+time_run 'load nmbug tags' 'notmuch restore --accumulate < corpus.tags/nmbug.sup-dump'
+time_run 'dump *' 'notmuch dump > tags.out'
+time_run 'restore *' 'notmuch restore < tags.out'
+
+time_done
diff --git a/performance-test/T02-tag.sh b/performance-test/T02-tag.sh
new file mode 100755 (executable)
index 0000000..9c895d6
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+test_description='tagging'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+time_start
+
+time_run 'tag * +new_tag' "notmuch tag +new_tag '*'"
+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_done
diff --git a/performance-test/T03-reindex.sh b/performance-test/T03-reindex.sh
new file mode 100755 (executable)
index 0000000..b58950d
--- /dev/null
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+test_description='reindexing'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+time_start
+
+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/T04-thread-subquery.sh b/performance-test/T04-thread-subquery.sh
new file mode 100755 (executable)
index 0000000..ba81f38
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+test_description='thread subqueries'
+
+. $(dirname "$0")/perf-test-lib.sh || exit 1
+
+time_start
+
+time_run "search thread:{} ..." "notmuch search thread:{date:2010} and thread:{from:linus}"
+time_run "search thread:{} ..." "notmuch search thread:{date:2010} and thread:{from:linus}"
+time_run "search thread:{} ..." "notmuch search thread:{date:2010} and thread:{from:linus}"
+
+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/.gitignore b/performance-test/download/.gitignore
new file mode 100644 (file)
index 0000000..5c35620
--- /dev/null
@@ -0,0 +1,2 @@
+/*.tar.gz
+/*.tar.xz
diff --git a/performance-test/download/notmuch-email-corpus-0.3.tar.xz.asc b/performance-test/download/notmuch-email-corpus-0.3.tar.xz.asc
new file mode 100644 (file)
index 0000000..f109e81
--- /dev/null
@@ -0,0 +1,9 @@
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+iJwEAAECAAYFAlC9a90ACgkQTiiN/0Um85nAMAP+LCWdKzolcl/KW+JcCd0Dk+9v
+0vvtBVEhBes0TbK6iWrxCV2OIuYG/RhnFlJTZ4MjgaTRxzDubpC+JktaJdLmIQUN
+B7ZIDMjFduCwmtyLiuu/00CjxJKUXm7vx+ULGpvp0uxFE/vaqGP997BHwBjjfBVm
+YX6BlLX1SV6TfENkuRE=
+=Mks5
+-----END PGP SIGNATURE-----
diff --git a/performance-test/download/notmuch-email-corpus-0.4.tar.xz.asc b/performance-test/download/notmuch-email-corpus-0.4.tar.xz.asc
new file mode 100644 (file)
index 0000000..72dedd8
--- /dev/null
@@ -0,0 +1,14 @@
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.15 (GNU/Linux)
+
+iQGcBAABCAAGBQJSdaDkAAoJEPIClx2kp54sQ54L/ikkvF1fy88hjLitN59v6g2J
+vw85YNRifNHyp/UXI6nt2eXFzyWJiRHuvHFoBgmEsJVxauOKw61Gs2zd53x9Ear4
+MGcQWyiM1cnwX/nD7GvxRQNh33f+FEamTjg+QhG47K0A2YdLWcDC7r9GMatGT11x
+5KE24WQGOqtgQn/9qNtJvkiKIehpRiDTaW/QJ7mTCYeJFjIHJUY8dxyfiTtkJ0z7
+cJ6omehvWSw4STbEg65XJgqykxMdltNEavfvSbAT73FgmkkyXxul0s5hDZ/esd0n
+re3dyDxGt085POiAgPti05a4tJI5EQC2wLBUFri0s2JdMtazcD6yVuHNbVzZ4Do3
+nL/sgwKGUq5wRrPqPWp6HXtZ9zG+/V7hFNrr/l42qGrLqsSh0bqvEnUiwczZLBGy
+NEs4G8VjmfS2cMKePsWaekBAvFUtb47PSB6JIPwpCNvKXDrcCb28eOQVB2atgj1h
+9SktOtWYJhWIQp2YW9iae30Z6lhCcdPRRHTFMQq2nQ==
+=eSMY
+-----END PGP SIGNATURE-----
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-----
diff --git a/performance-test/notmuch-memory-test b/performance-test/notmuch-memory-test
new file mode 100755 (executable)
index 0000000..047aac7
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+# Run tests
+#
+# Copyright (c) 2005 Junio C Hamano
+# Copyright (c) 2010 Notmuch Developers
+#
+# Adapted from a Makefile to a shell script by Carl Worth (2010)
+
+if [ ${BASH_VERSINFO[0]} -lt 4 ]; then
+    echo "Error: The notmuch test suite requires a bash version >= 4.0"
+    echo "due to use of associative arrays within the test suite."
+    echo "Please try again with a newer bash (or help us fix the"
+    echo "test suite to be more portable). Thanks."
+    exit 1
+fi
+
+cd "$(dirname "$0")"
+
+for test in M*.sh; do
+    ./"$test" "$@"
+done
diff --git a/performance-test/notmuch-time-test b/performance-test/notmuch-time-test
new file mode 100755 (executable)
index 0000000..4dd21fe
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+# Run tests
+#
+# Copyright (c) 2005 Junio C Hamano
+# Copyright (c) 2010 Notmuch Developers
+#
+# Adapted from a Makefile to a shell script by Carl Worth (2010)
+
+if [ ${BASH_VERSINFO[0]} -lt 4 ]; then
+    echo "Error: The notmuch test suite requires a bash version >= 4.0"
+    echo "due to use of associative arrays within the test suite."
+    echo "Please try again with a newer bash (or help us fix the"
+    echo "test suite to be more portable). Thanks."
+    exit 1
+fi
+
+cd "$(dirname "$0")"
+
+for test in T*.sh; do
+    ./"$test" "$@"
+done
diff --git a/performance-test/perf-test-lib.sh b/performance-test/perf-test-lib.sh
new file mode 100644 (file)
index 0000000..c34f8cd
--- /dev/null
@@ -0,0 +1,252 @@
+. $(dirname "$0")/version.sh || exit 1
+
+debug=""
+corpus_size=large
+perf_callgraph=lbr
+use_perf=0
+
+while test "$#" -ne 0
+do
+       case "$1" in
+       -d|--debug)
+               debug=t;
+               shift
+               ;;
+       -p|--perf)
+               use_perf=1;
+               shift
+               ;;
+       -c|--call-graph)
+               shift
+               perf_callgraph=$1
+               shift
+               ;;
+       -s|--small)
+               corpus_size=small;
+               shift
+               ;;
+       -m|--medium)
+               corpus_size=medium;
+               shift
+               ;;
+       -l|--large)
+               corpus_size=large;
+               shift
+               ;;
+       *)
+               echo "error: unknown performance test option '$1'" >&2; exit 1 ;;
+       esac
+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
+
+. "$NOTMUCH_SRCDIR/test/test-lib-common.sh" || exit 1
+
+set -e
+
+# It appears that people try to run tests without building...
+if [[ ! -x "$NOTMUCH_BUILDDIR/notmuch" ]]; then
+       echo >&2 'You do not seem to have built notmuch yet.'
+       exit 1
+fi
+
+DB_CACHE_DIR=${TEST_DIRECTORY}/notmuch.cache.$corpus_size
+
+add_email_corpus ()
+{
+    rm -rf ${MAIL_DIR}
+
+    CORPUS_DIR=${TEST_DIRECTORY}/corpus
+    mkdir -p "${CORPUS_DIR}"
+
+    MAIL_CORPUS="${CORPUS_DIR}/mail.${corpus_size}"
+    TAG_CORPUS="${CORPUS_DIR}/tags"
+
+    if command -v pixz > /dev/null; then
+       XZ=pixz
+    else
+       XZ=xz
+    fi
+
+    if [ ! -d "${CORPUS_DIR}/manifest" ]; then
+
+       printf "Unpacking manifests\n"
+       tar --extract --use-compress-program ${XZ} --strip-components=1 \
+           --directory ${TEST_DIRECTORY}/corpus \
+           --wildcards --file ../download/notmuch-email-corpus-${PERFTEST_VERSION}.tar.xz \
+           'notmuch-email-corpus/manifest/*'
+    fi
+
+    file_list=$(mktemp file_listXXXXXX)
+    declare -a extract_dirs
+    if [ ! -d "$TAG_CORPUS" ] ; then
+       extract_dirs=("${extract_dirs[@]}" notmuch-email-corpus/tags)
+    fi
+
+    if [ ! -d "$MAIL_CORPUS" ] ; then
+       if [[ "$corpus_size" != "large" ]]; then
+           sed s,^,notmuch-email-corpus/, < \
+               ${TEST_DIRECTORY}/corpus/manifest/MANIFEST.${corpus_size} >> $file_list
+       else
+           extract_dirs=("${extract_dirs[@]}" notmuch-email-corpus/mail)
+       fi
+    fi
+
+    if [[ -s $file_list || -n "${extract_dirs[*]}" ]]; then
+
+       printf "Unpacking corpus\n"
+       tar --checkpoint=.5000 --extract --strip-components=1 \
+           --directory ${TEST_DIRECTORY}/corpus \
+           --use-compress-program ${XZ} \
+           --file ../download/notmuch-email-corpus-${PERFTEST_VERSION}.tar.xz \
+           --anchored --recursion \
+           --files-from $file_list "${extract_dirs[@]}"
+
+       printf "\n"
+
+       if [[ ! -d ${MAIL_CORPUS} ]]; then
+           printf "creating link farm\n"
+
+           if [[ "$corpus_size" = large ]]; then
+               cp -rl ${TEST_DIRECTORY}/corpus/mail ${MAIL_CORPUS}
+           else
+               while read -r file; do
+                   tdir=${MAIL_CORPUS}/$(dirname $file)
+                   mkdir -p $tdir
+                   ln ${TEST_DIRECTORY}/corpus/$file $tdir
+               done <${TEST_DIRECTORY}/corpus/manifest/MANIFEST.${corpus_size}
+           fi
+       fi
+
+    fi
+
+    rm $file_list
+    cp -lr $TAG_CORPUS $TMP_DIRECTORY/corpus.tags
+    cp -lr $MAIL_CORPUS $MAIL_DIR
+}
+
+notmuch_new_with_cache ()
+{
+    if [ -d $DB_CACHE_DIR ]; then
+       cp -r $DB_CACHE_DIR ${MAIL_DIR}/.notmuch
+    else
+       "$1" 'Initial notmuch new' "notmuch new"
+       cache_database
+    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
+}
+
+memory_start ()
+{
+    add_email_corpus
+
+    make_log_dir
+
+    notmuch_new_with_cache memory_run
+}
+
+memory_run ()
+{
+    test_count=$(($test_count+1))
+
+    log_file=$log_dir/$test_count.log
+    talloc_log=$log_dir/$test_count.talloc
+
+    printf "[ %d ]\t%s\n" $test_count "$1"
+
+    NOTMUCH_TALLOC_REPORT="$talloc_log" eval "valgrind --leak-check=full --log-file='$log_file' $2"
+
+    awk '/LEAK SUMMARY/,/suppressed/ { sub(/^==[0-9]*==/," "); print }' "$log_file"
+    echo
+    sed -n -e 's/.*[(]total *\([^)]*\)[)]/talloced at exit: \1/p' $talloc_log
+    echo
+}
+
+memory_done ()
+{
+    time_done
+}
+
+cache_database ()
+{
+    if [ -d $MAIL_DIR/.notmuch ]; then
+       cp -r $MAIL_DIR/.notmuch $DB_CACHE_DIR
+    else
+       echo "Warning: No database found to cache"
+    fi
+}
+
+uncache_database ()
+{
+    rm -rf $DB_CACHE_DIR
+}
+
+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; 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
+    return 0
+}
+
+time_done ()
+{
+    if [ "$test_failure" = "0" ]; then
+       rm -rf "$remove_tmp"
+       exit 0
+    else
+       exit 1
+    fi
+}
+
+cd -P "$test" || error "Cannot set up test environment"
+test_failure=0
+test_count=0
+
+printf "\n%-55s [%s %s]\n"  \
+    "$(basename "$0"): Testing ${test_description:-notmuch performance}" \
+    "${PERFTEST_VERSION}"  "${corpus_size}"
diff --git a/performance-test/version.sh b/performance-test/version.sh
new file mode 100644 (file)
index 0000000..357b9da
--- /dev/null
@@ -0,0 +1,3 @@
+# this should be both a valid Makefile fragment and valid POSIX(ish) shell.
+
+PERFTEST_VERSION=0.5
diff --git a/query-string.c b/query-string.c
new file mode 100644 (file)
index 0000000..cc8b27d
--- /dev/null
@@ -0,0 +1,56 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-client.h"
+
+/* Construct a single query string from the passed arguments, using
+ * 'ctx' as the talloc owner for all allocations.
+ *
+ * Currently, the arguments are just connected with space characters,
+ * but we might do more processing in the future, (such as inserting
+ * any AND operators needed to work around Xapian QueryParser bugs).
+ *
+ * This function returns NULL in case of insufficient memory.
+ */
+char *
+query_string_from_args (void *ctx, int argc, char *argv[])
+{
+    char *query_string;
+    int i;
+
+    query_string = talloc_strdup (ctx, "");
+    if (query_string == NULL)
+       return NULL;
+
+    for (i = 0; i < argc; i++) {
+       if (i != 0) {
+           query_string = talloc_strdup_append (query_string, " ");
+           if (query_string == NULL)
+               return NULL;
+       }
+
+       query_string = talloc_strdup_append (query_string, argv[i]);
+       if (query_string == NULL)
+           return NULL;
+    }
+
+    return query_string;
+}
+
diff --git a/sprinter-json.c b/sprinter-json.c
new file mode 100644 (file)
index 0000000..502f89f
--- /dev/null
@@ -0,0 +1,203 @@
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#include "sprinter.h"
+
+struct sprinter_json {
+    struct sprinter vtable;
+    FILE *stream;
+    /* Top of the state stack, or NULL if the printer is not currently
+     * inside any aggregate types. */
+    struct json_state *state;
+
+    /* A flag to signify that a separator should be inserted in the
+     * output as soon as possible.
+     */
+    bool insert_separator;
+};
+
+struct json_state {
+    struct json_state *parent;
+    /* True if nothing has been printed in this aggregate yet.
+     * Suppresses the comma before a value. */
+    bool first;
+    /* The character that closes the current aggregate. */
+    char close;
+};
+
+/* Helper function to set up the stream to print a value.  If this
+ * value follows another value, prints a comma. */
+static struct sprinter_json *
+json_begin_value (struct sprinter *sp)
+{
+    struct sprinter_json *spj = (struct sprinter_json *) sp;
+
+    if (spj->state) {
+       if (! spj->state->first) {
+           fputc (',', spj->stream);
+           if (spj->insert_separator) {
+               fputc ('\n', spj->stream);
+               spj->insert_separator = false;
+           } else {
+               fputc (' ', spj->stream);
+           }
+       } else {
+           spj->state->first = false;
+       }
+    }
+    return spj;
+}
+
+/* Helper function to begin an aggregate type.  Prints the open
+ * character and pushes a new state frame. */
+static void
+json_begin_aggregate (struct sprinter *sp, char open, char close)
+{
+    struct sprinter_json *spj = json_begin_value (sp);
+    struct json_state *state = talloc (spj, struct json_state);
+
+    fputc (open, spj->stream);
+    state->parent = spj->state;
+    state->first = true;
+    state->close = close;
+    spj->state = state;
+}
+
+static void
+json_begin_map (struct sprinter *sp)
+{
+    json_begin_aggregate (sp, '{', '}');
+}
+
+static void
+json_begin_list (struct sprinter *sp)
+{
+    json_begin_aggregate (sp, '[', ']');
+}
+
+static void
+json_end (struct sprinter *sp)
+{
+    struct sprinter_json *spj = (struct sprinter_json *) sp;
+    struct json_state *state = spj->state;
+
+    fputc (spj->state->close, spj->stream);
+    spj->state = state->parent;
+    talloc_free (state);
+    if (spj->state == NULL)
+       fputc ('\n', spj->stream);
+}
+
+/* This implementation supports embedded NULs as allowed by the JSON
+ * specification and Unicode.  Support for *parsing* embedded NULs
+ * varies, but is generally not a problem outside of C-based parsers
+ * (Python's json module and Emacs' json.el take embedded NULs in
+ * stride). */
+static void
+json_string_len (struct sprinter *sp, const char *val, size_t len)
+{
+    static const char *const escapes[] = {
+       ['\"'] = "\\\"", ['\\'] = "\\\\", ['\b'] = "\\b",
+       ['\f'] = "\\f",  ['\n'] = "\\n",  ['\t'] = "\\t"
+    };
+    struct sprinter_json *spj = json_begin_value (sp);
+
+    fputc ('"', spj->stream);
+    for (; len; ++val, --len) {
+       unsigned char ch = *val;
+       if (ch < ARRAY_SIZE (escapes) && escapes[ch])
+           fputs (escapes[ch], spj->stream);
+       else if (ch >= 32)
+           fputc (ch, spj->stream);
+       else
+           fprintf (spj->stream, "\\u%04x", ch);
+    }
+    fputc ('"', spj->stream);
+}
+
+static void
+json_string (struct sprinter *sp, const char *val)
+{
+    if (val == NULL)
+       val = "";
+    json_string_len (sp, val, strlen (val));
+}
+
+static void
+json_integer (struct sprinter *sp, int64_t val)
+{
+    struct sprinter_json *spj = json_begin_value (sp);
+
+    fprintf (spj->stream, "%" PRId64, val);
+}
+
+static void
+json_boolean (struct sprinter *sp, bool val)
+{
+    struct sprinter_json *spj = json_begin_value (sp);
+
+    fputs (val ? "true" : "false", spj->stream);
+}
+
+static void
+json_null (struct sprinter *sp)
+{
+    struct sprinter_json *spj = json_begin_value (sp);
+
+    fputs ("null", spj->stream);
+}
+
+static void
+json_map_key (struct sprinter *sp, const char *key)
+{
+    struct sprinter_json *spj = (struct sprinter_json *) sp;
+
+    json_string (sp, key);
+    fputs (": ", spj->stream);
+    spj->state->first = true;
+}
+
+static void
+json_set_prefix (unused (struct sprinter *sp), unused (const char *name))
+{
+}
+
+static void
+json_separator (struct sprinter *sp)
+{
+    struct sprinter_json *spj = (struct sprinter_json *) sp;
+
+    spj->insert_separator = true;
+}
+
+struct sprinter *
+sprinter_json_create (notmuch_database_t *db, FILE *stream)
+{
+    static const struct sprinter_json template = {
+       .vtable = {
+           .begin_map = json_begin_map,
+           .begin_list = json_begin_list,
+           .end = json_end,
+           .string = json_string,
+           .string_len = json_string_len,
+           .integer = json_integer,
+           .boolean = json_boolean,
+           .null = json_null,
+           .map_key = json_map_key,
+           .separator = json_separator,
+           .set_prefix = json_set_prefix,
+           .is_text_printer = false,
+       }
+    };
+    struct sprinter_json *res;
+
+    res = talloc (db, struct sprinter_json);
+    if (! res)
+       return NULL;
+
+    *res = template;
+    res->vtable.notmuch = db;
+    res->stream = stream;
+    return &res->vtable;
+}
diff --git a/sprinter-sexp.c b/sprinter-sexp.c
new file mode 100644 (file)
index 0000000..e37cb1f
--- /dev/null
@@ -0,0 +1,238 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2012 Peter Feigl
+ *
+ * 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: Peter Feigl <peter.feigl@gmx.at>
+ */
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#include "sprinter.h"
+#include <ctype.h>
+
+struct sprinter_sexp {
+    struct sprinter vtable;
+    FILE *stream;
+    /* Top of the state stack, or NULL if the printer is not currently
+     * inside any aggregate types. */
+    struct sexp_state *state;
+
+    /* A flag to signify that a separator should be inserted in the
+     * output as soon as possible. */
+    bool insert_separator;
+};
+
+struct sexp_state {
+    struct sexp_state *parent;
+
+    /* True if nothing has been printed in this aggregate yet.
+     * Suppresses the space before a value. */
+    bool first;
+};
+
+/* Helper function to set up the stream to print a value.  If this
+ * value follows another value, prints a space. */
+static struct sprinter_sexp *
+sexp_begin_value (struct sprinter *sp)
+{
+    struct sprinter_sexp *sps = (struct sprinter_sexp *) sp;
+
+    if (sps->state) {
+       if (! sps->state->first) {
+           if (sps->insert_separator) {
+               fputc ('\n', sps->stream);
+               sps->insert_separator = false;
+           } else {
+               fputc (' ', sps->stream);
+           }
+       } else {
+           sps->state->first = false;
+       }
+    }
+    return sps;
+}
+
+/* Helper function to begin an aggregate type.  Prints the open
+ * character and pushes a new state frame. */
+static void
+sexp_begin_aggregate (struct sprinter *sp)
+{
+    struct sprinter_sexp *sps = sexp_begin_value (sp);
+    struct sexp_state *state = talloc (sps, struct sexp_state);
+
+    fputc ('(', sps->stream);
+    state->parent = sps->state;
+    state->first = true;
+    sps->state = state;
+}
+
+static void
+sexp_begin_map (struct sprinter *sp)
+{
+    sexp_begin_aggregate (sp);
+}
+
+static void
+sexp_begin_list (struct sprinter *sp)
+{
+    sexp_begin_aggregate (sp);
+}
+
+static void
+sexp_end (struct sprinter *sp)
+{
+    struct sprinter_sexp *sps = (struct sprinter_sexp *) sp;
+    struct sexp_state *state = sps->state;
+
+    fputc (')', sps->stream);
+    sps->state = state->parent;
+    talloc_free (state);
+    if (sps->state == NULL)
+       fputc ('\n', sps->stream);
+}
+
+static void
+sexp_string_len (struct sprinter *sp, const char *val, size_t len)
+{
+    /* Some characters need escaping. " and \ work fine in all Lisps,
+     * \n is not supported in CL, but all others work fine.
+     * Characters below 32 are printed as \123o (three-digit
+     * octals), which work fine in most Schemes and Emacs. */
+    static const char *const escapes[] = {
+       ['\"'] = "\\\"", ['\\'] = "\\\\",  ['\n'] = "\\n"
+    };
+    struct sprinter_sexp *sps = sexp_begin_value (sp);
+
+    fputc ('"', sps->stream);
+    for (; len; ++val, --len) {
+       unsigned char ch = *val;
+       if (ch < ARRAY_SIZE (escapes) && escapes[ch])
+           fputs (escapes[ch], sps->stream);
+       else if (ch >= 32)
+           fputc (ch, sps->stream);
+       else
+           fprintf (sps->stream, "\\%03o", ch);
+    }
+    fputc ('"', sps->stream);
+}
+
+static void
+sexp_string (struct sprinter *sp, const char *val)
+{
+    if (val == NULL)
+       val = "";
+    sexp_string_len (sp, val, strlen (val));
+}
+
+/* Prints a symbol, i.e. the name preceded by a colon. This should work
+ * in all Lisps, at least as a symbol, if not as a proper keyword */
+static void
+sexp_keyword (struct sprinter *sp, const char *val)
+{
+    unsigned int i = 0;
+    struct sprinter_sexp *sps = (struct sprinter_sexp *) sp;
+    char ch;
+
+    if (val == NULL)
+       INTERNAL_ERROR ("illegal symbol NULL");
+
+    for (i = 0; i < strlen (val); i++) {
+       ch = val[i];
+       if (! (isalnum (ch) || (ch == '-') || (ch == '_'))) {
+           INTERNAL_ERROR ("illegal character in symbol %s: %c", val, ch);
+       }
+    }
+    fputc (':', sps->stream);
+    fputs (val, sps->stream);
+}
+
+static void
+sexp_integer (struct sprinter *sp, int64_t val)
+{
+    struct sprinter_sexp *sps = sexp_begin_value (sp);
+
+    fprintf (sps->stream, "%" PRId64, val);
+}
+
+static void
+sexp_boolean (struct sprinter *sp, bool val)
+{
+    struct sprinter_sexp *sps = sexp_begin_value (sp);
+
+    fputs (val ? "t" : "nil", sps->stream);
+}
+
+static void
+sexp_null (struct sprinter *sp)
+{
+    struct sprinter_sexp *sps = sexp_begin_value (sp);
+
+    fputs ("nil", sps->stream);
+}
+
+static void
+sexp_map_key (struct sprinter *sp, const char *key)
+{
+    sexp_begin_value (sp);
+
+    sexp_keyword (sp, key);
+}
+
+static void
+sexp_set_prefix (unused (struct sprinter *sp), unused (const char *name))
+{
+}
+
+static void
+sexp_separator (struct sprinter *sp)
+{
+    struct sprinter_sexp *sps = (struct sprinter_sexp *) sp;
+
+    sps->insert_separator = true;
+}
+
+struct sprinter *
+sprinter_sexp_create (notmuch_database_t *db, FILE *stream)
+{
+    static const struct sprinter_sexp template = {
+       .vtable = {
+           .begin_map = sexp_begin_map,
+           .begin_list = sexp_begin_list,
+           .end = sexp_end,
+           .string = sexp_string,
+           .string_len = sexp_string_len,
+           .integer = sexp_integer,
+           .boolean = sexp_boolean,
+           .null = sexp_null,
+           .map_key = sexp_map_key,
+           .separator = sexp_separator,
+           .set_prefix = sexp_set_prefix,
+           .is_text_printer = false,
+       }
+    };
+    struct sprinter_sexp *res;
+
+    res = talloc (db, struct sprinter_sexp);
+    if (! res)
+       return NULL;
+
+    *res = template;
+    res->vtable.notmuch = db;
+    res->stream = stream;
+    return &res->vtable;
+}
diff --git a/sprinter-text.c b/sprinter-text.c
new file mode 100644 (file)
index 0000000..99330a9
--- /dev/null
@@ -0,0 +1,159 @@
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <talloc.h>
+#include "sprinter.h"
+
+/* "Structured printer" interface for unstructured text printing.
+ * Note that --output=summary is dispatched and formatted in
+ * notmuch-search.c, the code in this file is only used for all other
+ * output types.
+ */
+
+struct sprinter_text {
+    struct sprinter vtable;
+    FILE *stream;
+
+    /* The current prefix to be printed with string/integer/boolean
+     * data.
+     */
+    const char *current_prefix;
+
+    /* A flag to indicate if this is the first tag. Used in list of tags
+     * for summary.
+     */
+    bool first_tag;
+};
+
+static void
+text_string_len (struct sprinter *sp, const char *val, size_t len)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    if (sptxt->current_prefix != NULL)
+       fprintf (sptxt->stream, "%s:", sptxt->current_prefix);
+
+    fwrite (val, len, 1, sptxt->stream);
+}
+
+static void
+text_string (struct sprinter *sp, const char *val)
+{
+    if (val == NULL)
+       val = "";
+    text_string_len (sp, val, strlen (val));
+}
+
+static void
+text_integer (struct sprinter *sp, int64_t val)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    fprintf (sptxt->stream, "%" PRId64, val);
+}
+
+static void
+text_boolean (struct sprinter *sp, bool val)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    fputs (val ? "true" : "false", sptxt->stream);
+}
+
+static void
+text_separator (struct sprinter *sp)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    fputc ('\n', sptxt->stream);
+}
+
+static void
+text0_separator (struct sprinter *sp)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    fputc ('\0', sptxt->stream);
+}
+
+static void
+text_set_prefix (struct sprinter *sp, const char *prefix)
+{
+    struct sprinter_text *sptxt = (struct sprinter_text *) sp;
+
+    sptxt->current_prefix = prefix;
+}
+
+/* The structure functions begin_map, begin_list, end and map_key
+ * don't do anything in the text formatter.
+ */
+
+static void
+text_begin_map (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_begin_list (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_end (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_null (unused (struct sprinter *sp))
+{
+}
+
+static void
+text_map_key (unused (struct sprinter *sp), unused (const char *key))
+{
+}
+
+struct sprinter *
+sprinter_text_create (notmuch_database_t *db, FILE *stream)
+{
+    static const struct sprinter_text template = {
+       .vtable = {
+           .begin_map = text_begin_map,
+           .begin_list = text_begin_list,
+           .end = text_end,
+           .string = text_string,
+           .string_len = text_string_len,
+           .integer = text_integer,
+           .boolean = text_boolean,
+           .null = text_null,
+           .map_key = text_map_key,
+           .separator = text_separator,
+           .set_prefix = text_set_prefix,
+           .is_text_printer = true,
+       },
+    };
+    struct sprinter_text *res;
+
+    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 (notmuch_database_t *db, FILE *stream)
+{
+    struct sprinter *sp;
+
+    sp = sprinter_text_create (db, stream);
+    if (! sp)
+       return NULL;
+
+    sp->separator = text0_separator;
+
+    return sp;
+}
diff --git a/sprinter.h b/sprinter.h
new file mode 100644 (file)
index 0000000..fd08641
--- /dev/null
@@ -0,0 +1,89 @@
+#ifndef NOTMUCH_SPRINTER_H
+#define NOTMUCH_SPRINTER_H
+
+/* Necessary for bool */
+#include "notmuch-client.h"
+
+/* Structure printer interface. This is used to create output
+ * structured as maps (with key/value pairs), lists and primitives
+ * (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.
+     */
+    void (*begin_map)(struct sprinter *);
+
+    /* Start a new list/array structure.
+     */
+    void (*begin_list)(struct sprinter *);
+
+    /* End the last opened list or map structure.
+     */
+    void (*end)(struct sprinter *);
+
+    /* Print one string/integer/boolean/null element (possibly inside
+     * a list or map, followed or preceded by separators).  For string
+     * and string_len, the char * must be UTF-8 encoded.  string_len
+     * allows non-terminated strings and strings with embedded NULs
+     * (though the handling of the latter is format-dependent). For
+     * string (but not string_len) the string pointer passed may be
+     * NULL.
+     */
+    void (*string)(struct sprinter *, const char *);
+    void (*string_len)(struct sprinter *, const char *, size_t);
+    void (*integer)(struct sprinter *, int64_t);
+    void (*boolean)(struct sprinter *, bool);
+    void (*null)(struct sprinter *);
+
+    /* Print the key of a map's key/value pair. The char * must be UTF-8
+     * encoded.
+     */
+    void (*map_key)(struct sprinter *, const char *);
+
+    /* Insert a separator (usually extra whitespace). For the text
+     * printers, this is a syntactic separator. For the structured
+     * printers, this is for improved readability without affecting
+     * the abstract syntax of the structure being printed. For JSON,
+     * this could simply be a line break.
+     */
+    void (*separator)(struct sprinter *);
+
+    /* Set the current string prefix. This only affects the text
+     * printer, which will print this string, followed by a colon,
+     * before any string. For other printers, this does nothing.
+     */
+    void (*set_prefix)(struct sprinter *, const char *);
+
+    /* True if this is the special-cased plain text printer.
+     */
+    bool is_text_printer;
+} sprinter_t;
+
+
+/* Create a new unstructured printer that emits the default text format
+ * for "notmuch search". */
+struct sprinter *
+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 (notmuch_database_t *db, FILE *stream);
+
+/* Create a new structure printer that emits JSON. */
+struct sprinter *
+sprinter_json_create (notmuch_database_t *db, FILE *stream);
+
+/* Create a new structure printer that emits S-Expressions. */
+struct sprinter *
+sprinter_sexp_create (notmuch_database_t *db, FILE *stream);
+
+#endif // NOTMUCH_SPRINTER_H
diff --git a/status.c b/status.c
new file mode 100644 (file)
index 0000000..09d82a1
--- /dev/null
+++ b/status.c
@@ -0,0 +1,88 @@
+#include "notmuch-client.h"
+
+notmuch_status_t
+print_status_query (const char *loc,
+                   const notmuch_query_t *query,
+                   notmuch_status_t status)
+{
+    if (status) {
+       const char *msg;
+       notmuch_database_t *notmuch;
+
+       fprintf (stderr, "%s: %s\n", loc,
+                notmuch_status_to_string (status));
+
+       notmuch = notmuch_query_get_database (query);
+       msg = notmuch_database_status_string (notmuch);
+       if (msg)
+           fputs (msg, stderr);
+    }
+    return status;
+}
+
+notmuch_status_t
+print_status_message (const char *loc,
+                     const notmuch_message_t *message,
+                     notmuch_status_t status)
+{
+    if (status) {
+       const char *msg;
+       notmuch_database_t *notmuch;
+
+       fprintf (stderr, "%s: %s\n", loc,
+                notmuch_status_to_string (status));
+
+       notmuch = notmuch_message_get_database (message);
+       msg = notmuch_database_status_string (notmuch);
+       if (msg)
+           fputs (msg, stderr);
+    }
+    return status;
+}
+
+notmuch_status_t
+print_status_database (const char *loc,
+                      const notmuch_database_t *notmuch,
+                      notmuch_status_t status)
+{
+    if (status) {
+       const char *msg;
+
+       fprintf (stderr, "%s: %s\n", loc,
+                notmuch_status_to_string (status));
+       msg = notmuch_database_status_string (notmuch);
+       if (msg)
+           fputs (msg, stderr);
+    }
+    return status;
+}
+
+int
+status_to_exit (notmuch_status_t status)
+{
+    switch (status) {
+    case NOTMUCH_STATUS_SUCCESS:
+       return EXIT_SUCCESS;
+    case NOTMUCH_STATUS_OUT_OF_MEMORY:
+    case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
+    case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+    case NOTMUCH_STATUS_FILE_ERROR:
+       return EX_TEMPFAIL;
+    default:
+       return EXIT_FAILURE;
+    }
+}
+
+notmuch_status_t
+print_status_gzbytes (const char *loc, gzFile file, int bytes)
+{
+    if (bytes <= 0) {
+       int errnum;
+       const char *errstr = gzerror (file, &errnum);
+       fprintf (stderr, "%s: zlib error %s (%d)\n", loc, errstr, errnum);
+       return NOTMUCH_STATUS_FILE_ERROR;
+    } else {
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+}
+
diff --git a/tag-util.c b/tag-util.c
new file mode 100644 (file)
index 0000000..accf299
--- /dev/null
@@ -0,0 +1,426 @@
+#include <assert.h>
+#include "string-util.h"
+#include "tag-util.h"
+#include "hex-escape.h"
+
+#define TAG_OP_LIST_INITIAL_SIZE 10
+
+struct _tag_operation_t {
+    const char *tag;
+    bool remove;
+};
+
+struct _tag_op_list_t {
+    tag_operation_t *ops;
+    size_t count;
+    size_t size;
+};
+
+static tag_parse_status_t
+line_error (tag_parse_status_t status,
+           const char *line,
+           const char *format, ...)
+{
+    va_list va_args;
+
+    va_start (va_args, format);
+
+    fprintf (stderr, status < 0 ? "Error: " : "Warning: ");
+    vfprintf (stderr, format, va_args);
+    fprintf (stderr, " [%s]\n", line);
+
+    va_end (va_args);
+
+    return status;
+}
+
+const char *
+illegal_tag (const char *tag, bool remove)
+{
+    if (*tag == '\0' && ! remove)
+       return "empty tag forbidden";
+
+    /* This disallows adding tags starting with "-", in particular the
+     * non-removable tag "-" and enables notmuch tag to take long
+     * options more easily.
+     */
+
+    if (*tag == '-' && ! remove)
+       return "tag starting with '-' forbidden";
+
+    return NULL;
+}
+
+tag_parse_status_t
+parse_tag_line (void *ctx, char *line,
+               tag_op_flag_t flags,
+               char **query_string,
+               tag_op_list_t *tag_ops)
+{
+    char *tok = line;
+    size_t tok_len = 0;
+    char *line_for_error;
+    tag_parse_status_t ret = TAG_PARSE_SUCCESS;
+
+    chomp_newline (line);
+
+    line_for_error = talloc_strdup (ctx, line);
+    if (line_for_error == NULL) {
+       fprintf (stderr, "Error: out of memory\n");
+       return TAG_PARSE_OUT_OF_MEMORY;
+    }
+
+    /* remove leading space */
+    while (*tok == ' ' || *tok == '\t')
+       tok++;
+
+    /* Skip empty and comment lines. */
+    if (*tok == '\0' || *tok == '#') {
+       ret = TAG_PARSE_SKIPPED;
+       goto DONE;
+    }
+
+    tag_op_list_reset (tag_ops);
+
+    /* Parse tags. */
+    while ((tok = strtok_len (tok + tok_len, " ", &tok_len)) != NULL) {
+       bool remove;
+       char *tag;
+
+       /* Optional explicit end of tags marker. */
+       if (tok_len == 2 && strncmp (tok, "--", tok_len) == 0) {
+           tok = strtok_len (tok + tok_len, " ", &tok_len);
+           if (tok == NULL) {
+               ret = line_error (TAG_PARSE_INVALID, line_for_error,
+                                 "no query string after --");
+               goto DONE;
+           }
+           break;
+       }
+
+       /* Implicit end of tags. */
+       if (*tok != '-' && *tok != '+')
+           break;
+
+       /* If tag is terminated by NUL, there's no query string. */
+       if (*(tok + tok_len) == '\0') {
+           ret = line_error (TAG_PARSE_INVALID, line_for_error,
+                             "no query string");
+           goto DONE;
+       }
+
+       /* Terminate, and start next token after terminator. */
+       *(tok + tok_len++) = '\0';
+
+       remove = (*tok == '-');
+       tag = tok + 1;
+
+       /* Maybe refuse illegal tags. */
+       if (! (flags & TAG_FLAG_BE_GENEROUS)) {
+           const char *msg = illegal_tag (tag, remove);
+           if (msg) {
+               ret = line_error (TAG_PARSE_INVALID, line_for_error, msg);
+               goto DONE;
+           }
+       }
+
+       /* Decode tag. */
+       if (hex_decode_inplace (tag) != HEX_SUCCESS) {
+           ret = line_error (TAG_PARSE_INVALID, line_for_error,
+                             "hex decoding of tag %s failed", tag);
+           goto DONE;
+       }
+
+       if (tag_op_list_append (tag_ops, tag, remove)) {
+           ret = line_error (TAG_PARSE_OUT_OF_MEMORY, line_for_error,
+                             "aborting");
+           goto DONE;
+       }
+    }
+
+    if (tok == NULL) {
+       /* use a different error message for testing */
+       ret = line_error (TAG_PARSE_INVALID, line_for_error,
+                         "missing query string");
+       goto DONE;
+    }
+
+    /* tok now points to the query string */
+    *query_string = tok;
+
+  DONE:
+    talloc_free (line_for_error);
+    return ret;
+}
+
+tag_parse_status_t
+parse_tag_command_line (void *ctx, int argc, char **argv,
+                       char **query_str, tag_op_list_t *tag_ops)
+{
+    int i;
+
+    for (i = 0; i < argc; i++) {
+       if (strcmp (argv[i], "--") == 0) {
+           i++;
+           break;
+       }
+
+       if (argv[i][0] != '+' && argv[i][0] != '-')
+           break;
+
+       bool is_remove = argv[i][0] == '-';
+       const char *msg;
+
+       msg = illegal_tag (argv[i] + 1, is_remove);
+       if (msg) {
+           fprintf (stderr, "Error: %s\n", msg);
+           return TAG_PARSE_INVALID;
+       }
+
+       tag_op_list_append (tag_ops, argv[i] + 1, is_remove);
+    }
+
+    *query_str = query_string_from_args (ctx, argc - i, &argv[i]);
+
+    if (*query_str == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return TAG_PARSE_OUT_OF_MEMORY;
+    }
+
+    return TAG_PARSE_SUCCESS;
+}
+
+
+static inline void
+message_error (notmuch_message_t *message,
+              notmuch_status_t status,
+              const char *format, ...)
+{
+    va_list va_args;
+
+    va_start (va_args, format);
+
+    vfprintf (stderr, format, va_args);
+    fprintf (stderr, "Message-ID: %s\n", notmuch_message_get_message_id (message));
+    fprintf (stderr, "Status: %s\n", notmuch_status_to_string (status));
+
+    va_end (va_args);
+}
+
+static int
+makes_changes (notmuch_message_t *message,
+              tag_op_list_t *list,
+              tag_op_flag_t flags)
+{
+    size_t i;
+
+    notmuch_tags_t *tags;
+    bool changes = false;
+
+    /* First, do we delete an existing tag? */
+    for (tags = notmuch_message_get_tags (message);
+        ! changes && notmuch_tags_valid (tags);
+        notmuch_tags_move_to_next (tags)) {
+       const char *cur_tag = notmuch_tags_get (tags);
+       int last_op =  (flags & TAG_FLAG_REMOVE_ALL) ? -1 : 0;
+
+       /* scan backwards to get last operation */
+       i = list->count;
+       while (i > 0) {
+           i--;
+           if (strcmp (cur_tag, list->ops[i].tag) == 0) {
+               last_op = list->ops[i].remove ? -1 : 1;
+               break;
+           }
+       }
+
+       changes = (last_op == -1);
+    }
+    notmuch_tags_destroy (tags);
+
+    if (changes)
+       return true;
+
+    /* Now check for adding new tags */
+    for (i = 0; i < list->count; i++) {
+       bool exists = false;
+
+       if (list->ops[i].remove)
+           continue;
+
+       for (tags = notmuch_message_get_tags (message);
+            notmuch_tags_valid (tags);
+            notmuch_tags_move_to_next (tags)) {
+           const char *cur_tag = notmuch_tags_get (tags);
+           if (strcmp (cur_tag, list->ops[i].tag) == 0) {
+               exists = true;
+               break;
+           }
+       }
+       notmuch_tags_destroy (tags);
+
+       /* the following test is conservative,
+        * in the sense it ignores cases like +foo ... -foo
+        * but this is OK from a correctness point of view
+        */
+       if (! exists)
+           return true;
+    }
+    return false;
+
+}
+
+notmuch_status_t
+tag_op_list_apply (notmuch_message_t *message,
+                  tag_op_list_t *list,
+                  tag_op_flag_t flags)
+{
+    size_t i;
+    notmuch_status_t status = 0;
+    tag_operation_t *tag_ops = list->ops;
+
+    if (! (flags & TAG_FLAG_PRE_OPTIMIZED) && ! makes_changes (message, list, flags))
+       return NOTMUCH_STATUS_SUCCESS;
+
+    status = notmuch_message_freeze (message);
+    if (status) {
+       message_error (message, status, "freezing message");
+       return status;
+    }
+
+    if (flags & TAG_FLAG_REMOVE_ALL) {
+       status = notmuch_message_remove_all_tags (message);
+       if (status) {
+           message_error (message, status, "removing all tags");
+           return status;
+       }
+    }
+
+    for (i = 0; i < list->count; i++) {
+       if (tag_ops[i].remove) {
+           status = notmuch_message_remove_tag (message, tag_ops[i].tag);
+           if (status) {
+               message_error (message, status, "removing tag %s", tag_ops[i].tag);
+               return status;
+           }
+       } else {
+           status = notmuch_message_add_tag (message, tag_ops[i].tag);
+           if (status) {
+               message_error (message, status, "adding tag %s", tag_ops[i].tag);
+               return status;
+           }
+
+       }
+    }
+
+    status = notmuch_message_thaw (message);
+    if (status) {
+       message_error (message, status, "thawing message");
+       return status;
+    }
+
+
+    if (flags & TAG_FLAG_MAILDIR_SYNC) {
+       status = notmuch_message_tags_to_maildir_flags (message);
+       if (status) {
+           message_error (message, status, "syncing tags to maildir");
+           return status;
+       }
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+
+}
+
+
+/* Array of tagging operations (add or remove.  Size will be increased
+ * as necessary. */
+
+tag_op_list_t *
+tag_op_list_create (void *ctx)
+{
+    tag_op_list_t *list;
+
+    list = talloc (ctx, tag_op_list_t);
+    if (list == NULL)
+       return NULL;
+
+    list->size = TAG_OP_LIST_INITIAL_SIZE;
+    list->count = 0;
+
+    list->ops = talloc_array (list, tag_operation_t, list->size);
+    if (list->ops == NULL)
+       return NULL;
+
+    return list;
+}
+
+
+int
+tag_op_list_append (tag_op_list_t *list,
+                   const char *tag,
+                   bool remove)
+{
+    /* Make room if current array is full.  This should be a fairly
+     * rare case, considering the initial array size.
+     */
+
+    if (list->count == list->size) {
+       list->size *= 2;
+       list->ops = talloc_realloc (list, list->ops, tag_operation_t,
+                                   list->size);
+       if (list->ops == NULL) {
+           fprintf (stderr, "Out of memory.\n");
+           return 1;
+       }
+    }
+
+    /* add the new operation */
+
+    list->ops[list->count].tag = tag;
+    list->ops[list->count].remove = remove;
+    list->count++;
+    return 0;
+}
+
+/*
+ *   Is the i'th tag operation a remove?
+ */
+
+bool
+tag_op_list_isremove (const tag_op_list_t *list, size_t i)
+{
+    assert (i < list->count);
+    return list->ops[i].remove;
+}
+
+/*
+ * Reset a list to contain no operations
+ */
+
+void
+tag_op_list_reset (tag_op_list_t *list)
+{
+    list->count = 0;
+}
+
+/*
+ * Return the number of operations in a list
+ */
+
+size_t
+tag_op_list_size (const tag_op_list_t *list)
+{
+    return list->count;
+}
+
+/*
+ *   return the i'th tag in the list
+ */
+
+const char *
+tag_op_list_tag (const tag_op_list_t *list, size_t i)
+{
+    assert (i < list->count);
+    return list->ops[i].tag;
+}
diff --git a/tag-util.h b/tag-util.h
new file mode 100644 (file)
index 0000000..411e8ca
--- /dev/null
@@ -0,0 +1,163 @@
+#ifndef _TAG_UTIL_H
+#define _TAG_UTIL_H
+
+#include "notmuch-client.h"
+
+typedef struct _tag_operation_t tag_operation_t;
+typedef struct _tag_op_list_t tag_op_list_t;
+
+/* Use powers of 2 */
+typedef enum {
+    TAG_FLAG_NONE              = 0,
+
+    /* Operations are synced to maildir, if possible.
+     */
+    TAG_FLAG_MAILDIR_SYNC      = (1 << 0),
+
+    /* Remove all tags from message before applying list.
+     */
+    TAG_FLAG_REMOVE_ALL                = (1 << 1),
+
+    /* Don't try to avoid database operations. Useful when we
+     * know that message passed needs these operations.
+     */
+    TAG_FLAG_PRE_OPTIMIZED     = (1 << 2),
+
+    /* Accept strange tags that might be user error;
+     * intended for use by notmuch-restore.
+     */
+    TAG_FLAG_BE_GENEROUS       = (1 << 3)
+
+} tag_op_flag_t;
+
+/* These should obey the convention that fatal errors are negative,
+ * skipped lines are positive.
+ */
+typedef enum {
+    TAG_PARSE_OUT_OF_MEMORY    = -1,
+
+    /* Line parsed successfully. */
+    TAG_PARSE_SUCCESS          = 0,
+
+    /* Line has a syntax error */
+    TAG_PARSE_INVALID          = 1,
+
+    /* Line was blank or a comment */
+    TAG_PARSE_SKIPPED          = 2
+
+} tag_parse_status_t;
+
+/* Parse a string of the following format:
+ *
+ * +<tag>|-<tag> [...] [--] <search-terms>
+ *
+ * Each line is interpreted similarly to "notmuch tag" command line
+ * arguments. The delimiter is one or more spaces ' '. Any characters
+ * in <tag> and <search-terms> MAY be hex encoded with %NN where NN is
+ * the hexadecimal value of the character. Any ' ' and '%' characters
+ * in <tag> and <search-terms> MUST be hex encoded (using %20 and %25,
+ * respectively). Any characters that are not part of <tag> or
+ * <search-terms> MUST NOT be hex encoded.
+ *
+ * Leading and trailing space ' ' is ignored. Empty lines and lines
+ * beginning with '#' are ignored.
+ *
+ *
+ * Output Parameters:
+ *     ops     contains a list of tag operations
+ *     query_str the search terms.
+ */
+tag_parse_status_t
+parse_tag_line (void *ctx, char *line,
+               tag_op_flag_t flags,
+               char **query_str, tag_op_list_t *ops);
+
+
+
+/* Parse a command line of the following format:
+ *
+ * +<tag>|-<tag> [...] [--] <search-terms>
+ *
+ * Output Parameters:
+ *     ops     contains a list of tag operations
+ *     query_str the search terms.
+ *
+ * The ops argument is not cleared.
+ */
+
+tag_parse_status_t
+parse_tag_command_line (void *ctx, int argc, char **argv,
+                       char **query_str, tag_op_list_t *ops);
+
+/*
+ * Test tags for some forbidden cases.
+ *
+ * Relax the checks if 'remove' is true to allow removal of previously
+ * added forbidden tags.
+ *
+ * return: NULL if OK,
+ *        explanatory message otherwise.
+ */
+const char *
+illegal_tag (const char *tag, bool remove);
+
+/*
+ * Create an empty list of tag operations
+ *
+ * ctx is passed to talloc
+ */
+
+tag_op_list_t *
+tag_op_list_create (void *ctx);
+
+/*
+ * Add a tag operation (delete iff remove == true) to a list.
+ * The list is expanded as necessary.
+ */
+
+int
+tag_op_list_append (tag_op_list_t *list,
+                   const char *tag,
+                   bool remove);
+
+/*
+ * Apply a list of tag operations, in order, to a given message.
+ *
+ * Flags can be bitwise ORed; see enum above for possibilities.
+ */
+
+notmuch_status_t
+tag_op_list_apply (notmuch_message_t *message,
+                  tag_op_list_t *tag_ops,
+                  tag_op_flag_t flags);
+
+/*
+ * Return the number of operations in a list
+ */
+
+size_t
+tag_op_list_size (const tag_op_list_t *list);
+
+/*
+ * Reset a list to contain no operations
+ */
+
+void
+tag_op_list_reset (tag_op_list_t *list);
+
+
+/*
+ *   return the i'th tag in the list
+ */
+
+const char *
+tag_op_list_tag (const tag_op_list_t *list, size_t i);
+
+/*
+ *   Is the i'th tag operation a remove?
+ */
+
+bool
+tag_op_list_isremove (const tag_op_list_t *list, size_t i);
+
+#endif
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644 (file)
index 0000000..f596840
--- /dev/null
@@ -0,0 +1,11 @@
+/arg-test
+/hex-xcode
+/parse-time
+/random-corpus
+/smtp-dummy
+/symbol-test
+/make-db-version
+/test-results
+/ghost-report
+/tmp.*
+/message-id-parse
diff --git a/test/Makefile b/test/Makefile
new file mode 100644 (file)
index 0000000..de492a7
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/test/Makefile.local b/test/Makefile.local
new file mode 100644 (file)
index 0000000..4057473
--- /dev/null
@@ -0,0 +1,84 @@
+# -*- makefile-gmake -*-
+
+dir := test
+
+# save against changes in $(dir)
+test_src_dir := $(dir)
+extra_cflags += -I$(srcdir)
+
+smtp_dummy_srcs =              \
+       $(notmuch_compat_srcs)  \
+       $(dir)/smtp-dummy.c
+
+smtp_dummy_modules = $(smtp_dummy_srcs:.c=.o)
+
+$(dir)/arg-test: $(dir)/arg-test.o command-line-arguments.o util/libnotmuch_util.a
+       $(call quiet,CC) $^ -o $@ $(LDFLAGS)
+
+$(dir)/message-id-parse: $(dir)/message-id-parse.o lib/libnotmuch.a util/libnotmuch_util.a
+       $(call quiet,CC) $^ -o $@ $(LDFLAGS) $(TALLOC_LDFLAGS)
+
+$(dir)/hex-xcode: $(dir)/hex-xcode.o command-line-arguments.o util/libnotmuch_util.a
+       $(call quiet,CC) $^ -o $@ $(LDFLAGS) $(TALLOC_LDFLAGS)
+
+random_corpus_deps =  $(dir)/random-corpus.o  $(dir)/database-test.o \
+                       notmuch-config.o status.o command-line-arguments.o \
+                       lib/libnotmuch.a util/libnotmuch_util.a \
+                       parse-time-string/libparse-time-string.a
+
+$(dir)/random-corpus: $(random_corpus_deps)
+       $(call quiet,CXX) $^ -o $@ $(LDFLAGS) $(CONFIGURE_LDFLAGS)
+
+$(dir)/smtp-dummy: $(smtp_dummy_modules)
+       $(call quiet,CC) $^ -o $@ $(LDFLAGS)
+
+$(dir)/symbol-test: $(dir)/symbol-test.o lib/$(LINKER_NAME)
+       $(call quiet,CXX) $^ -o $@ $(LDFLAGS) -Llib -lnotmuch $(XAPIAN_LDFLAGS)
+
+$(dir)/parse-time: $(dir)/parse-time.o parse-time-string/parse-time-string.o
+       $(call quiet,CC) $^ -o $@ $(LDFLAGS)
+
+$(dir)/make-db-version: $(dir)/make-db-version.o
+       $(call quiet,CXX) $^ -o $@ $(LDFLAGS) $(XAPIAN_LDFLAGS)
+
+$(dir)/ghost-report: $(dir)/ghost-report.o
+       $(call quiet,CXX) $^ -o $@ $(LDFLAGS) $(XAPIAN_LDFLAGS)
+
+.PHONY: test check
+
+test_main_srcs=$(dir)/arg-test.c \
+             $(dir)/hex-xcode.c \
+             $(dir)/random-corpus.c \
+             $(dir)/parse-time.c \
+             $(dir)/smtp-dummy.c \
+             $(dir)/symbol-test.cc \
+             $(dir)/make-db-version.cc \
+             $(dir)/ghost-report.cc \
+             $(dir)/message-id-parse.c
+
+test_srcs=$(test_main_srcs) $(dir)/database-test.c
+
+TEST_BINARIES := $(test_main_srcs:.c=)
+TEST_BINARIES := $(TEST_BINARIES:.cc=)
+
+test-binaries: $(TEST_BINARIES)
+
+test:  all test-binaries
+ifeq ($V,)
+       @echo 'Use "$(MAKE) V=1" to see the details for passing and known broken tests.'
+       @env NOTMUCH_TEST_QUIET=1 $(NOTMUCH_SRCDIR)/$(test_src_dir)/notmuch-test $(OPTIONS)
+else
+# The user has explicitly enabled quiet execution.
+ifeq ($V,0)
+       @env NOTMUCH_TEST_QUIET=1 $(NOTMUCH_SRCDIR)/$(test_src_dir)/notmuch-test $(OPTIONS)
+else
+       @$(NOTMUCH_SRCDIR)/$(test_src_dir)/notmuch-test $(OPTIONS)
+endif
+endif
+
+check: test
+
+SRCS := $(SRCS) $(test_srcs)
+CLEAN += $(TEST_BINARIES) $(addsuffix .o,$(TEST_BINARIES)) \
+        $(dir)/database-test.o \
+        $(dir)/test-results $(dir)/tmp.*
diff --git a/test/README b/test/README
new file mode 100644 (file)
index 0000000..a81808b
--- /dev/null
@@ -0,0 +1,338 @@
+Notmuch test suite
+==================
+This directory contains the test suite for notmuch.
+
+When fixing bugs or enhancing notmuch, you are strongly encouraged to
+add tests in this directory to cover what you are trying to fix or
+enhance.
+
+Prerequisites
+-------------
+The test system itself requires:
+
+  - bash(1) version 4.0 or newer
+
+Without bash 4.0+ the tests just refuse to run.
+
+Some tests require external dependencies to run. Without them, they
+will be skipped, or (rarely) marked failed. Please install these, so
+that you know if you break anything.
+
+  - GNU tar(1)
+  - dtach(1)
+  - emacs(1)
+  - emacsclient(1)
+  - gdb(1)
+  - gpg(1)
+  - python(1)
+  - xapian-metadata(1)
+
+If your system lacks these tools or have older, non-upgradable versions
+of these, please (possibly compile and) install these to some other
+path, for example /usr/local/bin or /opt/gnu/bin. Then prepend the
+chosen directory to your PATH before running the tests.
+
+e.g. env PATH=/opt/gnu/bin:$PATH make test
+
+For FreeBSD you need to install latest gdb from ports or packages and
+provide path to it in TEST_GDB environment variable before executing
+the tests, native FreeBSD gdb does not not work.  If you install
+coreutils, which provides GNU versions of basic utils like 'date' and
+'base64' on FreeBSD, the test suite will use these instead of the
+native ones. This provides robustness against portability issues with
+these system tools. Most often the tests are written, reviewed and
+tested on Linux system so such portability issues arise from time to
+time.
+
+Running Tests
+-------------
+The easiest way to run tests is to say "make test", (or simply run the
+notmuch-test script). Either command will run all available tests.
+
+Alternately, you can run a specific subset of tests by simply invoking
+one of the executable scripts in this directory, (such as ./T*-search.sh,
+./T*-reply.sh, etc). Note that you will probably want "make test-binaries"
+before running individual tests.
+
+The following command-line options are available when running tests:
+
+--debug::
+       This may help the person who is developing a new test.
+       It causes the command defined with test_debug to run.
+
+--immediate::
+       This causes the test to immediately exit upon the first
+       failed test.
+
+--valgrind::
+       Execute notmuch with valgrind and exit with status
+       126 on errors (just like regular tests, this will only stop
+       the test script when running under -i).  Valgrind errors
+       go to stderr, so you might want to pass the -v option, too.
+
+       Since it makes no sense to run the tests with --valgrind and
+       not see any output, this option implies --verbose.  For
+       convenience, it also implies --tee.
+
+--tee::
+       In addition to printing the test output to the terminal,
+       write it to files named 't/test-results/$TEST_NAME.out'.
+       As the names depend on the tests' file names, it is safe to
+       run the tests with this option in parallel.
+
+When invoking the test suite via "make test" any of the above options
+can be specified as follows:
+
+       make test OPTIONS="--verbose"
+
+You can choose an emacs binary (and corresponding emacsclient) to run
+the tests in one of the following ways.
+
+       TEST_EMACS=my-emacs TEST_EMACSCLIENT=my-emacsclient make test
+       TEST_EMACS=my-emacs TEST_EMACSCLIENT=my-emacsclient ./T*-emacs.sh
+       make test TEST_EMACS=my-emacs TEST_EMACSCLIENT=my-emacsclient
+
+Some tests may require a c compiler. You can choose the name and flags similarly
+to with emacs, e.g.
+
+     make test TEST_CC=gcc TEST_CFLAGS="-g -O2"
+
+Parallel Execution
+------------------
+If either the moreutils or GNU "parallel" utility is available all
+tests will be run in parallel.  If the NOTMUCH_TEST_SERIALIZE variable
+is non-null all tests will be executed sequentially.
+
+Quiet Execution
+---------------
+Normally, when new script starts and when test PASSes you get a message
+printed on screen. This printing can be disabled by setting the
+NOTMUCH_TEST_QUIET variable to a non-null value. Message on test
+failures and skips are still printed.
+
+Skipping Tests
+--------------
+If, for any reason, you need to skip one or more tests, you can do so
+by setting the NOTMUCH_SKIP_TESTS variable to the name of one or more
+sections of tests.
+
+For example:
+
+    $ NOTMUCH_SKIP_TESTS="search reply" make test
+
+Even more fine-grained skipping is possible by appending a test number
+(or glob pattern) after the section name. For example, the first
+search test and the second reply test could be skipped with:
+
+    $ NOTMUCH_SKIP_TESTS="search.1 reply.2" make test
+
+Note that some tests in the existing test suite rely on previous test
+items, so you cannot arbitrarily skip any test and expect the
+remaining tests to be unaffected.
+
+Currently we do not consider skipped tests as build failures. For
+maximum robustness, when setting up automated build processes, you
+should explicitly skip tests, rather than relying on notmuch's
+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
+Tddd-testname.sh where 'ddd' is three digits and 'testname' the "bare"
+name of your test. Tests will be run in order the 'ddd' part determines.
+
+The test script should start with the standard "#!/usr/bin/env bash"
+and an assignment to variable 'test_description', like this:
+
+       #!/usr/bin/env bash
+
+       test_description='xxx test (option --frotz)
+
+       This test exercises the "notmuch xxx" command when
+       given the option --frotz.'
+
+Source 'test-lib.sh'
+--------------------
+After assigning test_description, the test script should source
+test-lib.sh like this:
+
+       . ./test-lib.sh || exit 1
+
+This test harness library does the following things:
+
+ - If the script is invoked with command line argument --help
+   (or -h), it shows the test_description and exits.
+
+ - Creates a temporary directory with default notmuch-config and a
+   mail store with a corpus of mail, (initially, 50 early messages
+   sent to the notmuch list). This directory is
+   test/tmp.<test-basename>. The path to notmuch-config is exported in
+   NOTMUCH_CONFIG environment variable and mail store path is stored
+   in MAIL_DIR variable.
+
+ - Defines standard test helper functions for your scripts to
+   use.  These functions are designed to make all scripts behave
+   consistently when command line arguments --verbose (or -v),
+   --debug (or -d), and --immediate (or -i) is given.
+
+End with test_done
+------------------
+Your script will be a sequence of tests, using helper functions
+from the test harness library.  At the end of the script, call
+'test_done'.
+
+Test harness library
+--------------------
+There are a handful helper functions defined in the test harness
+library for your script to use.
+
+ test_begin_subtest <message>
+
+   Set the test description message for a subsequent test_expect_*
+   invocation (see below).
+
+ test_expect_success <script>
+
+   This takes a string as parameter, and evaluates the
+   <script>.  If it yields success, test is considered
+   successful.
+
+ test_expect_code <code> <script>
+
+   This takes two strings as parameter, and evaluates the <script>.
+   If it yields <code> exit status, test is considered successful.
+
+ test_subtest_known_broken
+
+   Mark the current test as broken.  Such tests are expected to fail.
+   Unlike the normal tests, which say "PASS" on success and "FAIL" on
+   failure, these will say "FIXED" on success and "BROKEN" on failure.
+   Failures from these tests won't cause -i (immediate) to stop.  A
+   test must call this before any test_expect_* function.
+
+ test_expect_equal <output> <expected>
+
+   This is an often-used convenience function built on top of
+   test_expect_success. It uses the message from the last
+   test_begin_subtest call, so call before calling
+   test_expect_equal. This function generates a successful test if
+   both the <output> and <expected> strings are identical. If not, it
+   will generate a failure and print the difference of the two
+   strings.
+
+ test_expect_equal_file <file1> <file2>
+
+   Identical to test_expect_equal, except that <file1> and <file2>
+   are files instead of strings.  This is a much more robust method to
+   compare formatted textual information, since it also notices
+   whitespace and closing newline differences.
+
+ test_expect_equal_json <output> <expected>
+
+   Identical to test_expect_equal, except that the two strings are
+   treated as JSON and canonicalized before equality testing.  This is
+   useful to abstract away from whitespace differences in the expected
+   output and that generated by running a notmuch command.
+
+ test_debug <script>
+
+   This takes a single argument, <script>, and evaluates it only
+   when the test script is started with --debug command line
+   argument.  This is primarily meant for use during the
+   development of a new test script.
+
+ test_emacs <emacs-lisp-expressions>
+
+   This function executes the provided emacs lisp script within
+   emacs. The script can be a sequence of emacs lisp expressions,
+   (that is, they will be evaluated within a progn form). Emacs
+   stdout and stderr is not available, the common way to get output
+   is to save it to a file. There are some auxiliary functions
+   useful in emacs tests provided in test-lib.el. Do not use `setq'
+   for setting variables in Emacs tests because it affects other
+   tests that may run in the same Emacs instance.  Use `let' instead
+   so the scope of the changed variables is limited to a single test.
+
+ test_emacs_expect_t <emacs-lisp-expressions>
+
+  This function executes the provided emacs lisp script within
+  emacs in a manner similar to 'test_emacs'. The expressions should
+  return the value `t' to indicate that the test has passed. If the
+  test does not return `t' then it is considered failed and all data
+  returned by the test is reported to the tester.
+
+ test_done
+
+   Your test script must have test_done at the end.  Its purpose
+   is to summarize successes and failures in the test script and
+   exit with an appropriate error code.
+
+There are also a number of notmuch-specific auxiliary functions and
+variables which are useful in writing tests:
+
+  generate_message
+
+    Generates a message with an optional template. Most tests will
+    actually prefer to call add_message. See below.
+
+  add_message
+
+    Generate a message and add it to the database (by calling "notmuch
+    new"). It is sufficient to simply call add_message with no
+    arguments if you don't care about the content of the message. If
+    more control is needed, arguments can be provide to specify many
+    different header values for the new message. See the documentation
+    within test-lib.sh or refer to many example calls within existing
+    tests.
+
+  add_email_corpus
+
+    This function should be called at the beginning of a test file
+    when a test needs to operate on a non-empty body of messages. It
+    will initialize the mail database to a known state of 50 sample
+    messages, (culled from the early history of the notmuch mailing
+    list).
+
+  notmuch_counter_reset
+  $notmuch_counter_command
+  notmuch_counter_value
+
+    These allow to count how many times notmuch binary is called.
+    notmuch_counter_reset() function generates a script that counts
+    how many times it is called and resets the counter to zero.  The
+    function sets $notmuch_counter_command variable to the path to the
+    generated script that should be called instead of notmuch to do
+    the counting.  The notmuch_counter_value() function prints the
+    current counter value.
+
+There are also functions which remove various environment-dependent
+values from notmuch output; these are useful to ensure that test
+results remain consistent across different machines.
+
+ notmuch_search_sanitize
+ notmuch_show_sanitize
+ notmuch_show_sanitize_all
+ notmuch_json_show_sanitize
+
+   All these functions should receive the text to be sanitized as the
+   input of a pipe, e.g.
+   output=`notmuch search "..." | notmuch_search_sanitize`
diff --git a/test/T000-basic.sh b/test/T000-basic.sh
new file mode 100755 (executable)
index 0000000..642f918
--- /dev/null
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='the test framework itself.'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+################################################################
+# Test harness
+test_begin_subtest 'success is reported like this'
+test_expect_success ':'
+
+test_begin_subtest 'test runs if prerequisite is satisfied'
+test_set_prereq HAVEIT
+test_expect_success 'test_have_prereq HAVEIT'
+
+test_begin_subtest 'tests clean up after themselves'
+clean=no
+test_expect_success 'test_when_finished clean=yes'
+
+test_begin_subtest 'tests clean up even after a failure'
+cleaner=no
+test_expect_code 1 'test_when_finished cleaner=yes && (exit 1)'
+
+if test $clean$cleaner != yesyes
+then
+       say "bug in test framework: cleanup commands do not work reliably"
+       exit 1
+fi
+
+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 () {
+    sed -e 's/\(.*\-\-\- test-verbose\.4\.\expected\).*/\1/' \
+       -e 's/\(.*\+\+\+ test-verbose\.4\.\output\).*/\1/'
+}
+
+test_begin_subtest "Ensure that test output is suppressed unless the test fails"
+output=$(cd $TEST_DIRECTORY; NOTMUCH_TEST_QUIET= $NOTMUCH_SRCDIR/test/test-verbose 2>&1 | suppress_diff_date)
+expected=$(cat $EXPECTED/test-verbose-no | suppress_diff_date)
+test_expect_equal "$output" "$expected"
+
+test_begin_subtest "Ensure that -v does not suppress test output"
+output=$(cd $TEST_DIRECTORY; NOTMUCH_TEST_QUIET= $NOTMUCH_SRCDIR/test/test-verbose -v 2>&1 | suppress_diff_date)
+expected=$(cat $EXPECTED/test-verbose-yes | suppress_diff_date)
+# Do not include the results of test-verbose in totals
+rm $TEST_DIRECTORY/test-results/test-verbose
+rm -r $TEST_DIRECTORY/tmp.test-verbose
+test_expect_equal "$output" "$expected"
+
+
+################################################################
+# Test mail store prepared in test-lib.sh
+
+test_begin_subtest 'test that mail store was created'
+test_expect_success 'test -d "${MAIL_DIR}"'
+
+test_begin_subtest 'mail store should be empty'
+find "${MAIL_DIR}" -type f -print >should-be-empty
+test_expect_success 'cmp -s /dev/null should-be-empty'
+
+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$,,')"
+
+test_begin_subtest 'notmuch is compiled with debugging symbols'
+readelf --sections $(command -v notmuch) | grep \.debug
+test_expect_equal 0 $?
+
+test_done
diff --git a/test/T010-help-test.sh b/test/T010-help-test.sh
new file mode 100755 (executable)
index 0000000..827edc1
--- /dev/null
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+test_description="online help"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest 'notmuch --help'
+test_expect_success 'notmuch --help'
+
+test_begin_subtest 'notmuch help'
+test_expect_success 'notmuch help'
+
+test_begin_subtest 'notmuch --version'
+test_expect_success 'notmuch --version'
+
+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'
+
+    test_begin_subtest 'notmuch help tag (man pages not available)'
+    test_expect_success 'test_must_fail notmuch help tag >/dev/null'
+fi
+
+test_done
diff --git a/test/T020-compact.sh b/test/T020-compact.sh
new file mode 100755 (executable)
index 0000000..d77db00
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+test_description='"notmuch compact"'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_message '[subject]=One'
+add_message '[subject]=Two'
+add_message '[subject]=Three'
+
+notmuch tag +tag1 \*
+notmuch tag +tag2 subject:Two
+notmuch tag -tag1 +tag3 subject:Three
+
+test_begin_subtest "Running compact"
+test_expect_success "notmuch compact --backup=${TMP_DIRECTORY}/xapian.old"
+
+test_begin_subtest "Compact preserves database"
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag1 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 tag2 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Three (inbox tag3 unread)"
+
+test_begin_subtest "Restoring Backup"
+test_expect_success 'rm -Rf ${MAIL_DIR}/.notmuch/xapian &&
+     mv ${TMP_DIRECTORY}/xapian.old ${MAIL_DIR}/.notmuch/xapian'
+
+test_begin_subtest "Checking restored backup"
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag1 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 tag2 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Three (inbox tag3 unread)"
+
+test_done
diff --git a/test/T030-config.sh b/test/T030-config.sh
new file mode 100755 (executable)
index 0000000..621e0b6
--- /dev/null
@@ -0,0 +1,211 @@
+#!/usr/bin/env bash
+
+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"
+
+test_begin_subtest "Get list value"
+cat <<EOF > EXPECTED
+inbox
+unread
+EOF
+notmuch config get new.tags | sort > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Set string value"
+notmuch config set foo.string "this is a string value"
+test_expect_equal "$(notmuch config get foo.string)" "this is a string value"
+
+test_begin_subtest "Set string value again"
+notmuch config set foo.string "this is another string value"
+test_expect_equal "$(notmuch config get foo.string)" "this is another string value"
+
+test_begin_subtest "Set list value"
+notmuch config set foo.list this "is a" "list value"
+test_expect_equal "$(notmuch config get foo.list)" "\
+this
+is a
+list value"
+
+test_begin_subtest "Set list value again"
+notmuch config set foo.list this "is another" "list value"
+test_expect_equal "$(notmuch config get foo.list)" "\
+this
+is another
+list value"
+
+test_begin_subtest "Remove key"
+notmuch config set foo.remove baz
+notmuch config set foo.remove
+test_expect_equal "$(notmuch config get foo.remove)" ""
+
+test_begin_subtest "Remove non-existent key"
+notmuch config set foo.nonexistent
+test_expect_equal "$(notmuch config get foo.nonexistent)" ""
+
+test_begin_subtest "List all items"
+notmuch config list 2>&1 | notmuch_config_sanitize > OUTPUT
+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
+search.exclude_tags=
+user.name=Notmuch Test Suite
+user.other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org
+user.primary_email=test_suite@notmuchmail.org
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+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"
+test_expect_equal "$(notmuch --config=alt-config config get user.name)" \
+    "Another Name"
+
+test_begin_subtest "Top level --config:FILE option"
+test_expect_equal "$(notmuch --config:alt-config config get user.name)" \
+    "Another Name"
+
+test_begin_subtest "Top level --config<space>FILE option"
+test_expect_equal "$(notmuch --config alt-config config get user.name)" \
+    "Another Name"
+
+test_begin_subtest "Top level --config=FILE option changed the right file"
+test_expect_equal "$(notmuch config get user.name)" \
+    "Notmuch Test Suite"
+
+test_begin_subtest "Read config file through a symlink"
+ln -s alt-config alt-config-link
+test_expect_equal "$(notmuch --config=alt-config-link config get user.name)" \
+    "Another Name"
+
+test_begin_subtest "Write config file through a symlink"
+notmuch --config=alt-config-link config set user.name "Link Name"
+test_expect_equal "$(notmuch --config=alt-config-link config get user.name)" \
+    "Link Name"
+
+test_begin_subtest "Writing config file through symlink follows symlink"
+test_expect_equal "$(readlink alt-config-link)" "alt-config"
+
+test_begin_subtest "Round trip arbitrary key"
+key=g${RANDOM}.m${RANDOM}
+value=${RANDOM}
+notmuch config set ${key} ${value}
+output=$(notmuch config get ${key})
+test_expect_equal "${output}" "${value}"
+
+test_begin_subtest "Clear arbitrary key"
+notmuch config set ${key}
+output=$(notmuch config get ${key})
+test_expect_equal "${output}" ""
+
+db_path=${HOME}/database-path
+
+test_begin_subtest "Absolute database path returned"
+notmuch config set database.path ${HOME}/Maildir
+test_expect_equal "$(notmuch config get database.path)" \
+                 "${HOME}/Maildir"
+
+ln -s `pwd`/mail home/Maildir
+add_email_corpus
+test_begin_subtest "Relative database path expanded"
+notmuch config set database.path Maildir
+path=$(notmuch config get database.path | notmuch_dir_sanitize)
+count=$(notmuch count '*')
+test_expect_equal "${path} ${count}" \
+                 "CWD/home/Maildir 52"
+
+test_begin_subtest "Add config to database"
+notmuch new
+key=g${RANDOM}.m${RANDOM}
+value=${RANDOM}
+notmuch config set --database ${key} ${value}
+notmuch dump --include=config > OUTPUT
+cat <<EOF > EXPECTED
+#notmuch-dump batch-tag:3 config
+#@ ${key} ${value}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Roundtrip config to/from database"
+notmuch new
+key=g${RANDOM}.m${RANDOM}
+value=${RANDOM}
+notmuch config set --database ${key} ${value}
+output=$(notmuch config get ${key})
+test_expect_equal "${output}" "${value}"
+
+test_begin_subtest "set built_with.* yields error"
+test_expect_code 1 "notmuch config set built_with.compact false"
+
+test_begin_subtest "get built_with.{compact,field_processor} prints true"
+for key in compact field_processor; do
+    notmuch config get built_with.${key}
+done > OUTPUT
+cat <<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
diff --git a/test/T035-read-config.sh b/test/T035-read-config.sh
new file mode 100755 (executable)
index 0000000..ac0f420
--- /dev/null
@@ -0,0 +1,476 @@
+#!/usr/bin/env bash
+test_description='Various options for reading configuration'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+backup_config () {
+    local test_name=$(basename $0 .sh)
+    cp ${NOTMUCH_CONFIG} notmuch-config-backup.${test_name}
+}
+
+xdg_config () {
+    local dir
+    local profile=${1:-default}
+    if [[ $profile != default ]]; then
+       export NOTMUCH_PROFILE=$profile
+    fi
+    backup_config
+    dir="${HOME}/.config/notmuch/${profile}"
+    rm -rf $dir
+    mkdir -p $dir
+    CONFIG_PATH=$dir/config
+    mv ${NOTMUCH_CONFIG} ${CONFIG_PATH}
+    unset NOTMUCH_CONFIG
+}
+
+restore_config () {
+    local test_name=$(basename $0 .sh)
+    export NOTMUCH_CONFIG="${TMP_DIRECTORY}/notmuch-config"
+    unset CONFIG_PATH
+    unset NOTMUCH_PROFILE
+    cp notmuch-config-backup.${test_name} ${NOTMUCH_CONFIG}
+}
+
+add_email_corpus
+
+test_begin_subtest "count with saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> notmuch-config
+notmuch count query:$query_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "count with saved query from config file (xdg)"
+query_name="test${RANDOM}"
+xdg_config
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch count query:$query_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "count with saved query from config file (xdg+profile)"
+query_name="test${RANDOM}"
+xdg_config work
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch count query:$query_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+cat <<EOF > EXPECTED
+Before:
+#notmuch-dump batch-tag:3 tags
+
+After:
+#notmuch-dump batch-tag:3 tags
++attachment +inbox +signed +unread -- id:20091118005829.GB25380@dottiness.seas.harvard.edu
++attachment +inbox +signed +unread -- id:20091118010116.GC25380@dottiness.seas.harvard.edu
++inbox +signed +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
++inbox +signed +unread -- id:20091117203301.GV3165@dottiness.seas.harvard.edu
++inbox +signed +unread -- id:20091118002059.067214ed@hikari
++inbox +signed +unread -- id:20091118005040.GA25380@dottiness.seas.harvard.edu
++inbox +signed +unread -- id:87iqd9rn3l.fsf@vertex.dottedmag
+EOF
+
+test_begin_subtest "dump with saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+CONFIG_PATH=notmuch-config
+printf "Before:\n" > OUTPUT
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+printf "\nAfter:\n" >> OUTPUT
+printf "\n[query]\n${query_name} = tag:signed\n" >> ${CONFIG_PATH}
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "dump with saved query from config file (xdg)"
+backup_config
+query_name="test${RANDOM}"
+xdg_config
+printf "Before:\n" > OUTPUT
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+printf "\nAfter:\n" >> OUTPUT
+printf "\n[query]\n${query_name} = tag:signed\n" >> ${CONFIG_PATH}
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "dump with saved query from config file (xdg+profile)"
+backup_config
+query_name="test${RANDOM}"
+xdg_config work
+printf "Before:\n" > OUTPUT
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+printf "\nAfter:\n" >> OUTPUT
+printf "\n[query]\n${query_name} = tag:signed\n" >> ${CONFIG_PATH}
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "restore with xdg config"
+backup_config
+notmuch dump '*' > EXPECTED
+notmuch tag -inbox '*'
+xdg_config
+notmuch restore --input=EXPECTED
+notmuch dump > OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "restore with xdg+profile config"
+backup_config
+notmuch dump '*' > EXPECTED
+notmuch tag -inbox '*'
+xdg_config work
+notmuch restore --input=EXPECTED
+notmuch dump > OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Insert message with custom new.tags (xdg)"
+backup_config
+xdg_config
+tag=test${RANDOM}
+notmuch --config=${CONFIG_PATH} config set new.tags $tag
+generate_message \
+    "[subject]=\"insert-subject\"" \
+    "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" \
+    "[body]=\"insert-message\""
+mkdir -p ${MAIL_DIR}/{cur,new,tmp}
+notmuch insert < "$gen_msg_filename"
+notmuch dump id:$gen_msg_id > OUTPUT
+cat <<EOF > EXPECTED
+#notmuch-dump batch-tag:3 config,properties,tags
++$tag -- id:$gen_msg_id
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Insert message with custom new.tags (xdg+profile)"
+backup_config
+tag=test${RANDOM}
+xdg_config $tag
+notmuch --config=${CONFIG_PATH} config set new.tags $tag
+generate_message \
+    "[subject]=\"insert-subject\"" \
+    "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" \
+    "[body]=\"insert-message\""
+mkdir -p ${MAIL_DIR}/{cur,new,tmp}
+notmuch insert < "$gen_msg_filename"
+notmuch dump id:$gen_msg_id > OUTPUT
+cat <<EOF > EXPECTED
+#notmuch-dump batch-tag:3 config,properties,tags
++$tag -- id:$gen_msg_id
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "reindex with saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+count1=$(notmuch count --lastmod '*' | cut -f3)
+printf "\n[query]\n${query_name} = tag:inbox\n" >> notmuch-config
+notmuch reindex query:$query_name
+count2=$(notmuch count --lastmod '*' | cut -f3)
+restore_config
+test_expect_success "test '$count2 -gt $count1'"
+
+test_begin_subtest "reindex with saved query from config file (xdg)"
+query_name="test${RANDOM}"
+count1=$(notmuch count --lastmod '*' | cut -f3)
+xdg_config
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch reindex query:$query_name
+count2=$(notmuch count --lastmod '*' | cut -f3)
+restore_config
+test_expect_success "test '$count2 -gt $count1'"
+
+test_begin_subtest "reindex with saved query from config file (xdg+profile)"
+query_name="test${RANDOM}"
+count1=$(notmuch count --lastmod '*' | cut -f3)
+xdg_config $query_name
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch reindex query:$query_name
+count2=$(notmuch count --lastmod '*' | cut -f3)
+restore_config
+test_expect_success "test '$count2 -gt $count1'"
+
+
+
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=test_suite@notmuchmail.org \
+           '[cc]="Other Parties <cc@example.com>"' \
+            [subject]=notmuch-reply-test \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="reply with CC"'
+
+cat <<EOF > EXPECTED
+Before:
+After:
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>
+Cc: Other Parties <cc@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:
+> reply with CC
+EOF
+
+test_begin_subtest "reply with saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+printf "Before:\n" > OUTPUT
+notmuch reply query:$query_name 2>&1 >> OUTPUT
+printf "\n[query]\n${query_name} = id:${gen_msg_id}\n" >> notmuch-config
+printf "After:\n" >> OUTPUT
+notmuch reply query:$query_name >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "reply with saved query from config file (xdg)"
+backup_config
+query_name="test${RANDOM}"
+xdg_config
+printf "Before:\n" > OUTPUT
+notmuch reply query:$query_name 2>&1 >> OUTPUT
+printf "\n[query]\n${query_name} = id:${gen_msg_id}\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch reply query:$query_name >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "reply with saved query from config file (xdg+profile)"
+backup_config
+query_name="test${RANDOM}"
+xdg_config $query_name
+printf "Before:\n" > OUTPUT
+notmuch reply query:$query_name 2>&1 >> OUTPUT
+printf "\n[query]\n${query_name} = id:${gen_msg_id}\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch reply query:$query_name >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+backup_database
+test_begin_subtest "search with alternate config"
+notmuch tag -- +foobar17 '*'
+cp notmuch-config alt-config
+notmuch --config=alt-config config set search.exclude_tags foobar17
+output=$(notmuch --config=alt-config count '*')
+test_expect_equal "$output" "0"
+restore_database
+
+cat <<EOF > EXPECTED
+Before:
+After:
+thread:XXX   2009-11-18 [1/2] Carl Worth| Alex Botero-Lowry; [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Ingmar Vanhassel; [notmuch] [PATCH] Typsos (inbox unread)
+thread:XXX   2009-11-18 [1/3] Carl Worth| Adrian Perez de Castro, Keith Packard; [notmuch] Introducing myself (inbox signed unread)
+thread:XXX   2009-11-18 [1/3] Carl Worth| Israel Herraiz, Keith Packard; [notmuch] New to the list (inbox unread)
+thread:XXX   2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Jan Janak; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+thread:XXX   2009-11-18 [1/3(4)] Carl Worth| Aron Griffis, Keith Packard; [notmuch] archive (inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Keith Packard; [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread)
+thread:XXX   2009-11-18 [1/7] Carl Worth| Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard; [notmuch] Working with Maildir storage? (inbox signed unread)
+thread:XXX   2009-11-18 [2/5] Carl Worth| Mikhail Gusarov, Keith Packard; [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread)
+thread:XXX   2009-11-17 [1/2] Carl Worth| Alex Botero-Lowry; [notmuch] preliminary FreeBSD support (attachment inbox unread)
+EOF
+
+test_begin_subtest "search with saved query from config file"
+query_name="test${RANDOM}"
+backup_config
+printf "Before:\n" > OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+printf "\n[query]\n${query_name} = from:cworth\n" >> notmuch-config
+printf "After:\n" >> OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search with saved query from config file (xdg)"
+query_name="test${RANDOM}"
+xdg_config
+printf "Before:\n" > OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+printf "\n[query]\n${query_name} = from:cworth\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search with saved query from config file (xdg + profile)"
+query_name="test${RANDOM}"
+xdg_config $query_name
+printf "Before:\n" > OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+printf "\n[query]\n${query_name} = from:cworth\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+cat <<EOF > EXPECTED
+Before:
+After:
+Alex Botero-Lowry <alex.boterolowry@gmail.com>
+Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+François Boulogne <boulogne.f@gmail.com>
+Jjgod Jiang <gzjjgod@gmail.com>
+EOF
+
+test_begin_subtest "address: saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+printf "Before:\n" > OUTPUT
+notmuch address --deduplicate=no --output=sender query:$query_name 2>&1 | sort >> OUTPUT
+printf "\n[query]\n${query_name} = from:gmail.com\n" >> notmuch-config
+printf "After:\n" >> OUTPUT
+notmuch address --output=sender query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "address: saved query from config file (xdg)"
+query_name="test${RANDOM}"
+xdg_config
+printf "Before:\n" > OUTPUT
+notmuch address --deduplicate=no --output=sender query:$query_name 2>&1 | sort >> OUTPUT
+printf "\n[query]\n${query_name} = from:gmail.com\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch address --output=sender query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "address: saved query from config file (xdg+profile)"
+query_name="test${RANDOM}"
+xdg_config $query_name
+printf "Before:\n" > OUTPUT
+notmuch address --deduplicate=no --output=sender query:$query_name 2>&1 | sort >> OUTPUT
+printf "\n[query]\n${query_name} = from:gmail.com\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch address --output=sender query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "show with alternate config"
+backup_database
+cp notmuch-config alt-config
+notmuch --config=alt-config config set search.exclude_tags foobar17
+notmuch tag -- +foobar17 '*'
+output=$(notmuch --config=alt-config show '*' && echo OK)
+restore_database
+test_expect_equal "$output" "OK"
+
+test_begin_subtest "show with alternate config (xdg)"
+backup_database
+notmuch tag -- +foobar17 '*'
+xdg_config
+notmuch --config=${CONFIG_PATH} config set search.exclude_tags foobar17
+output=$(notmuch show '*' && echo OK)
+restore_database
+restore_config
+test_expect_equal "$output" "OK"
+
+test_begin_subtest "show with alternate config (xdg+profile)"
+backup_database
+notmuch tag -- +foobar17 '*'
+xdg_config foobar17
+notmuch --config=${CONFIG_PATH} config set search.exclude_tags foobar17
+output=$(notmuch show '*' && echo OK)
+restore_database
+restore_config
+test_expect_equal "$output" "OK"
+
+# reset to known state
+add_email_corpus
+
+test_begin_subtest "tag with saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+tag_name="tag${RANDOM}"
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> notmuch-config
+notmuch tag +$tag_name -- query:${query_name}
+notmuch count tag:$tag_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "tag with saved query from config file (xdg)"
+xdg_config
+query_name="test${RANDOM}"
+tag_name="tag${RANDOM}"
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch tag +$tag_name -- query:${query_name}
+notmuch count tag:$tag_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "tag with saved query from config file (xdg+profile)"
+query_name="test${RANDOM}"
+xdg_config ${query_name}
+tag_name="tag${RANDOM}"
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch tag +$tag_name -- query:${query_name}
+notmuch count tag:$tag_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "running compact (xdg)"
+xdg_config
+notmuch compact
+output=$(notmuch count '*')
+restore_config
+test_expect_equal "52" "$output"
+
+test_begin_subtest "running compact (xdg + profile)"
+xdg_config ${RANDOM}
+notmuch compact
+output=$(notmuch count '*')
+restore_config
+test_expect_equal "52" "$output"
+
+test_begin_subtest "run notmuch-new (xdg)"
+xdg_config
+generate_message
+output=$(NOTMUCH_NEW --debug)
+restore_config
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "run notmuch-new (xdg + profile)"
+xdg_config ${RANDOM}
+generate_message
+output=$(NOTMUCH_NEW --debug)
+restore_config
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_done
diff --git a/test/T040-setup.sh b/test/T040-setup.sh
new file mode 100755 (executable)
index 0000000..39846d3
--- /dev/null
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+
+test_description='"notmuch setup"'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "Notmuch new without a config suggests notmuch setup"
+output=$(notmuch --config=new-notmuch-config new 2>&1)
+test_expect_equal "$output" "\
+Error: cannot load config file.
+Try running 'notmuch setup' to create a configuration."
+
+test_begin_subtest "Create a new config interactively"
+notmuch --config=new-notmuch-config > log.${test_count} <<EOF
+Test Suite
+test.suite@example.com
+another.suite@example.com
+
+/path/to/maildir
+foo bar
+baz
+EOF
+
+expected_dir=$NOTMUCH_SRCDIR/test/setup.expected-output
+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
+cat <<EOF > EXPECTED
+Notmuch is configured, but no database was found.
+You probably want to run "notmuch new" now to create a database.
+
+Note that the first run of "notmuch new" can take a very long time
+and that the resulting database will use roughly the same amount of
+storage space as the email being indexed.
+
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T050-new.sh b/test/T050-new.sh
new file mode 100755 (executable)
index 0000000..52888be
--- /dev/null
@@ -0,0 +1,473 @@
+#!/usr/bin/env bash
+test_description='"notmuch new" in several variations'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "No new messages"
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "No new mail."
+
+
+test_begin_subtest "Single new message"
+generate_message
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Single message (full-scan)"
+generate_message
+output=$(NOTMUCH_NEW --debug --full-scan 2>&1)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Multiple new messages"
+generate_message
+generate_message
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 2 new messages to the database."
+
+test_begin_subtest "Multiple new messages (full-scan)"
+generate_message
+generate_message
+output=$(NOTMUCH_NEW --debug --full-scan 2>&1)
+test_expect_equal "$output" "Added 2 new messages to the database."
+
+test_begin_subtest "No new messages (non-empty DB)"
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "No new mail."
+
+test_begin_subtest "No new messages (full-scan)"
+output=$(NOTMUCH_NEW --debug --full-scan 2>&1)
+test_expect_equal "$output" "No new mail."
+
+test_begin_subtest "New directories"
+rm -rf "${MAIL_DIR}"/* "${MAIL_DIR}"/.notmuch
+mkdir "${MAIL_DIR}"/def
+mkdir "${MAIL_DIR}"/ghi
+generate_message [dir]=def
+
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+
+test_begin_subtest "Alternate inode order"
+
+rm -rf "${MAIL_DIR}"/.notmuch
+mv "${MAIL_DIR}"/ghi "${MAIL_DIR}"/abc
+rm "${MAIL_DIR}"/def/*
+generate_message [dir]=abc
+
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+
+test_begin_subtest "Message moved in"
+rm -rf "${MAIL_DIR}"/* "${MAIL_DIR}"/.notmuch
+generate_message
+tmp_msg_filename=tmp/"$gen_msg_filename"
+mkdir -p "$(dirname "$tmp_msg_filename")"
+mv "$gen_msg_filename" "$tmp_msg_filename"
+notmuch new > /dev/null
+mv "$tmp_msg_filename" "$gen_msg_filename"
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+
+test_begin_subtest "Renamed message"
+
+generate_message
+notmuch new > /dev/null
+mv "$gen_msg_filename" "${gen_msg_filename}"-renamed
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "(D) add_files, pass 2: queuing passed file ${gen_msg_filename} for deletion from database
+No new mail. Detected 1 file rename."
+
+
+test_begin_subtest "Deleted message"
+
+rm "${gen_msg_filename}"-renamed
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "(D) add_files, pass 3: queuing leftover file ${gen_msg_filename}-renamed for deletion from database
+No new mail. Removed 1 message."
+
+
+
+test_begin_subtest "Renamed directory"
+
+generate_message [dir]=dir
+generate_message [dir]=dir
+generate_message [dir]=dir
+
+notmuch new > /dev/null
+
+mv "${MAIL_DIR}"/dir "${MAIL_DIR}"/dir-renamed
+
+output=$(NOTMUCH_NEW --debug --full-scan)
+test_expect_equal "$output" "(D) add_files, pass 2: queuing passed directory ${MAIL_DIR}/dir for deletion from database
+No new mail. Detected 3 file renames."
+
+
+test_begin_subtest "Deleted directory"
+rm -rf "${MAIL_DIR}"/dir-renamed
+
+output=$(NOTMUCH_NEW --debug --full-scan)
+test_expect_equal "$output" "(D) add_files, pass 2: queuing passed directory ${MAIL_DIR}/dir-renamed for deletion from database
+No new mail. Removed 3 messages."
+
+
+test_begin_subtest "New directory (at end of list)"
+
+generate_message [dir]=zzz
+generate_message [dir]=zzz
+generate_message [dir]=zzz
+
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 3 new messages to the database."
+
+
+test_begin_subtest "Deleted directory (end of list)"
+
+rm -rf "${MAIL_DIR}"/zzz
+
+output=$(NOTMUCH_NEW --debug --full-scan)
+test_expect_equal "$output" "(D) add_files, pass 3: queuing leftover directory ${MAIL_DIR}/zzz for deletion from database
+No new mail. Removed 3 messages."
+
+
+test_begin_subtest "New symlink to directory"
+
+rm -rf "${MAIL_DIR}"/.notmuch
+mv "${MAIL_DIR}" "${TMP_DIRECTORY}"/actual_maildir
+
+mkdir "${MAIL_DIR}"
+ln -s "${TMP_DIRECTORY}"/actual_maildir "${MAIL_DIR}"/symlink
+
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+
+test_begin_subtest "New symlink to a file"
+generate_message
+external_msg_filename="${TMP_DIRECTORY}"/external/"$(basename "$gen_msg_filename")"
+mkdir -p "$(dirname "$external_msg_filename")"
+mv "$gen_msg_filename" "$external_msg_filename"
+ln -s "$external_msg_filename" "$gen_msg_filename"
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+
+test_begin_subtest "Broken symlink aborts"
+ln -s does-not-exist "${MAIL_DIR}/broken"
+output=$(NOTMUCH_NEW --debug 2>&1)
+test_expect_equal "$output" \
+"Error reading file ${MAIL_DIR}/broken: No such file or directory
+Note: A fatal error was encountered: Something went wrong trying to read or write a file
+No new mail."
+rm "${MAIL_DIR}/broken"
+
+
+test_begin_subtest "New two-level directory"
+
+generate_message [dir]=two/levels
+generate_message [dir]=two/levels
+generate_message [dir]=two/levels
+
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 3 new messages to the database."
+
+
+test_begin_subtest "Deleted two-level directory"
+
+rm -rf "${MAIL_DIR}"/two
+
+output=$(NOTMUCH_NEW --debug --full-scan)
+test_expect_equal "$output" "(D) add_files, pass 3: queuing leftover directory ${MAIL_DIR}/two for deletion from database
+No new mail. Removed 3 messages."
+
+test_begin_subtest "One character directory at top level"
+
+generate_message [dir]=A
+generate_message [dir]=A/B
+generate_message [dir]=A/B/C
+
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 3 new messages to the database."
+
+test_begin_subtest "Support single-message mbox"
+cat > "${MAIL_DIR}"/mbox_file1 <<EOF
+From test_suite@notmuchmail.org Fri Jan  5 15:43:57 2001
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Test mbox message 1
+
+Body.
+EOF
+output=$(NOTMUCH_NEW --debug 2>&1)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+# This test requires that notmuch new has been run at least once.
+test_begin_subtest "Skip and report non-mail files"
+generate_message
+mkdir -p "${MAIL_DIR}"/.git && touch "${MAIL_DIR}"/.git/config
+touch "${MAIL_DIR}"/ignored_file
+touch "${MAIL_DIR}"/.ignored_hidden_file
+cat > "${MAIL_DIR}"/mbox_file <<EOF
+From test_suite@notmuchmail.org Fri Jan  5 15:43:57 2001
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Test mbox message 1
+
+Body.
+
+From test_suite@notmuchmail.org Fri Jan  5 15:43:57 2001
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Test mbox message 2
+
+Body 2.
+EOF
+output=$(NOTMUCH_NEW --debug --full-scan 2>&1)
+test_expect_equal "$output" \
+"Note: Ignoring non-mail file: ${MAIL_DIR}/.git/config
+Note: Ignoring non-mail file: ${MAIL_DIR}/.ignored_hidden_file
+Note: Ignoring non-mail file: ${MAIL_DIR}/ignored_file
+Note: Ignoring non-mail file: ${MAIL_DIR}/mbox_file
+Added 1 new message to the database."
+rm "${MAIL_DIR}"/mbox_file
+
+test_begin_subtest "Ignore files and directories specified in new.ignore"
+generate_message
+notmuch config set new.ignore .git ignored_file .ignored_hidden_file
+touch "${MAIL_DIR}"/.git # change .git's mtime for notmuch new to rescan.
+NOTMUCH_NEW --debug 2>&1 | sort > OUTPUT
+cat <<EOF > EXPECTED
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/.git
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/.git
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/ignored_file
+Added 1 new message to the database.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Ignore files and directories specified in new.ignore (full-scan)"
+generate_message
+notmuch config set new.ignore .git ignored_file .ignored_hidden_file
+NOTMUCH_NEW --debug --full-scan 2>&1 | sort > OUTPUT
+# reuse EXPECTED from previous test
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Ignore files and directories specified in new.ignore (multiple occurrences)"
+notmuch config set new.ignore .git ignored_file .ignored_hidden_file
+notmuch new > /dev/null # ensure that files/folders will be printed in ASCII order.
+mkdir -p "${MAIL_DIR}"/one/two/three/.git
+touch "${MAIL_DIR}"/{one,one/two,one/two/three}/ignored_file
+output=$(NOTMUCH_NEW --debug --full-scan 2>&1 | sort)
+test_expect_equal "$output" \
+"(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/.git
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/ignored_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/ignored_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/three/.git
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/.git
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/.git
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file
+No new mail."
+
+
+test_begin_subtest "Don't stop for ignored broken symlinks"
+notmuch config set new.ignore .git ignored_file .ignored_hidden_file broken_link
+ln -s i_do_not_exist "${MAIL_DIR}"/broken_link
+output=$(NOTMUCH_NEW 2>&1)
+test_expect_equal "$output" "No new mail."
+
+test_begin_subtest "Ignore files and directories specified in new.ignore (regexp)"
+notmuch config set new.ignore ".git" "/^bro.*ink\$/" "/ignored.*file/"
+output=$(NOTMUCH_NEW --debug --full-scan 2>&1 | sort)
+test_expect_equal "$output" \
+"(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/.git
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/broken_link
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/ignored_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/ignored_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/three/.git
+(D) add_files, pass 1: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/.git
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/.ignored_hidden_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/broken_link
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/.git
+(D) add_files, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file
+No new mail."
+
+test_begin_subtest "Quiet: No new mail."
+output=$(NOTMUCH_NEW --quiet)
+test_expect_equal "$output" ""
+
+test_begin_subtest "Quiet: new, removed and renamed messages."
+# new
+generate_message
+# deleted
+notmuch search --format=text0 --output=files --limit=1 '*' | xargs -0 rm
+# moved
+mkdir "${MAIL_DIR}"/moved_messages
+notmuch search --format=text0 --output=files --offset=1 --limit=1 '*' | xargs -0 -I {} mv {} "${MAIL_DIR}"/moved_messages
+output=$(NOTMUCH_NEW --quiet)
+test_expect_equal "$output" ""
+
+OLDCONFIG=$(notmuch config get new.tags)
+
+test_begin_subtest "Empty tags in new.tags are ignored"
+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)
+test_expect_equal "$output" "Error: tag '-foo' in new.tags: tag starting with '-' forbidden"
+
+test_begin_subtest "Invalid tags set exit code"
+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}
+generate_message [dir]=$name
+NOTMUCH_NEW > OUTPUT
+notmuch new >> OUTPUT
+rm -r ${MAIL_DIR}/${name}
+notmuch new >> OUTPUT
+cat <<EOF > EXPECTED
+Added 1 new message to the database.
+No new mail.
+No new mail. Removed 1 message.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "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"
+
+
+make_shim dif-shim<<EOF
+#include <notmuch-test.h>
+
+WRAP_DLFUNC(notmuch_status_t, notmuch_database_index_file, \
+ (notmuch_database_t *database, const char *filename, notmuch_indexopts_t *indexopts, notmuch_message_t **message))
+
+  if (unlink ("${MAIL_DIR}/vanish")) {
+     fprintf (stderr, "unlink failed\n");
+     exit (42);
+  }
+  return notmuch_database_index_file_orig (database, filename, indexopts, message);
+}
+EOF
+
+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
+Error opening ${MAIL_DIR}/vanish: No such file or directory
+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"
+
+test_begin_subtest "reference loop ordered by date"
+threadid=$(notmuch search --output=threads id:mid-loop-12@example.org)
+notmuch show --format=mbox $threadid | grep '^Date' > OUTPUT
+cat <<EOF > EXPECTED
+Date: Thu, 16 Jun 2016 22:14:41 -0400
+Date: Fri, 17 Jun 2016 22:14:41 -0400
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
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
diff --git a/test/T055-path-config.sh b/test/T055-path-config.sh
new file mode 100755 (executable)
index 0000000..efc79e8
--- /dev/null
@@ -0,0 +1,391 @@
+#!/usr/bin/env bash
+test_description='Configuration of mail-root and database path'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_require_external_prereq xapian-metdata
+
+backup_config () {
+    local test_name=$(basename $0 .sh)
+    cp ${NOTMUCH_CONFIG} notmuch-config-backup.${test_name}
+}
+
+restore_config () {
+    local test_name=$(basename $0 .sh)
+    export NOTMUCH_CONFIG="${TMP_DIRECTORY}/notmuch-config"
+    unset CONFIG_PATH
+    unset DATABASE_PATH
+    unset NOTMUCH_PROFILE
+    unset XAPIAN_PATH
+    unset MAILDIR
+    rm -f "$HOME/mail"
+    cp notmuch-config-backup.${test_name} ${NOTMUCH_CONFIG}
+}
+
+split_config () {
+    local dir
+    backup_config
+    dir="$TMP_DIRECTORY/database.$test_count"
+    rm -rf $dir
+    mkdir $dir
+    notmuch config set database.path $dir
+    notmuch config set database.mail_root $MAIL_DIR
+    DATABASE_PATH=$dir
+    XAPIAN_PATH="$dir/xapian"
+}
+
+symlink_config () {
+    local dir
+    backup_config
+    dir="$TMP_DIRECTORY/link.$test_count"
+    ln -s $MAIL_DIR $dir
+    notmuch config set database.path $dir
+    notmuch config set database.mail_root $MAIL_DIR
+    XAPIAN_PATH="$MAIL_DIR/.notmuch/xapian"
+    unset DATABASE_PATH
+}
+
+
+home_mail_config () {
+    local dir
+    backup_config
+    dir="${HOME}/mail"
+    ln -s $MAIL_DIR $dir
+    notmuch config set database.path
+    notmuch config set database.mail_root
+    XAPIAN_PATH="$MAIL_DIR/.notmuch/xapian"
+    unset DATABASE_PATH
+}
+
+maildir_env_config () {
+    local dir
+    backup_config
+    dir="${HOME}/env_points_here"
+    ln -s $MAIL_DIR $dir
+    export MAILDIR=$dir
+    notmuch config set database.path
+    notmuch config set database.mail_root
+    XAPIAN_PATH="${MAIL_DIR}/.notmuch/xapian"
+    unset DATABASE_PATH
+}
+
+xdg_config () {
+    local dir
+    local profile=${1:-default}
+
+    if [[ $profile != default ]]; then
+       export NOTMUCH_PROFILE=$profile
+    fi
+
+    backup_config
+    DATABASE_PATH="${HOME}/.local/share/notmuch/${profile}"
+    rm -rf $DATABASE_PATH
+    mkdir -p $DATABASE_PATH
+
+    config_dir="${HOME}/.config/notmuch/${profile}"
+    mkdir -p ${config_dir}
+    CONFIG_PATH=$config_dir/config
+    mv ${NOTMUCH_CONFIG} $CONFIG_PATH
+    unset NOTMUCH_CONFIG
+
+    XAPIAN_PATH="${DATABASE_PATH}/xapian"
+    notmuch --config=${CONFIG_PATH} config set database.mail_root ${TMP_DIRECTORY}/mail
+    notmuch --config=${CONFIG_PATH} config set database.path
+}
+
+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
+
+    case $config in
+       traditional)
+           backup_config
+           XAPIAN_PATH="$MAIL_DIR/.notmuch/xapian"
+           ;;
+       split)
+           split_config
+           mv mail/.notmuch/xapian $DATABASE_PATH
+           ;;
+       XDG)
+           xdg_config
+           mv mail/.notmuch/xapian $DATABASE_PATH
+           ;;
+       XDG+profile)
+           xdg_config ${RANDOM}
+           mv mail/.notmuch/xapian $DATABASE_PATH
+           ;;
+       symlink)
+           symlink_config
+           ;;
+       home_mail)
+           home_mail_config
+           ;;
+       maildir_env)
+           maildir_env_config
+           ;;
+       mailroot_only)
+           mailroot_only_config
+           ;;
+    esac
+
+    test_begin_subtest "count ($config)"
+    output=$(notmuch count '*')
+    test_expect_equal "$output" '52'
+
+    test_begin_subtest "count+tag ($config)"
+    tag="tag${RANDOM}"
+    notmuch tag +$tag '*'
+    output=$(notmuch count tag:$tag)
+    notmuch tag -$tag '*'
+    test_expect_equal "$output" '52'
+
+    test_begin_subtest "address ($config)"
+    notmuch address --deduplicate=no --sort=newest-first --output=sender --output=recipients path:foo >OUTPUT
+    cat <<EOF >EXPECTED
+Carl Worth <cworth@cworth.org>
+notmuch@notmuchmail.org
+EOF
+    test_expect_equal_file EXPECTED OUTPUT
+
+    test_begin_subtest "dump ($config)"
+    notmuch dump is:attachment and is:signed | sort > OUTPUT
+    cat <<EOF > EXPECTED
+#notmuch-dump batch-tag:3 config,properties,tags
++attachment +inbox +signed +unread -- id:20091118005829.GB25380@dottiness.seas.harvard.edu
++attachment +inbox +signed +unread -- id:20091118010116.GC25380@dottiness.seas.harvard.edu
+EOF
+    test_expect_equal_file EXPECTED OUTPUT
+
+    test_begin_subtest "dump + tag + restore ($config)"
+    notmuch dump '*' > EXPECTED
+    notmuch tag -inbox '*'
+    notmuch restore < EXPECTED
+    notmuch dump > OUTPUT
+    test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+    test_begin_subtest "reindex ($config)"
+    notmuch search --output=messages '*' > EXPECTED
+    notmuch reindex '*'
+    notmuch search --output=messages '*' > OUTPUT
+    test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+    test_begin_subtest "use existing database ($config)"
+    output=$(notmuch new)
+    test_expect_equal "$output" 'No new mail.'
+
+    test_begin_subtest "create database ($config)"
+    rm -rf $DATABASE_PATH/{.notmuch,}/xapian
+    notmuch new
+    output=$(notmuch count '*')
+    test_expect_equal "$output" '52'
+
+    test_begin_subtest "detect new files ($config)"
+    generate_message
+    generate_message
+    notmuch new
+    output=$(notmuch count '*')
+    test_expect_equal "$output" '54'
+
+    test_begin_subtest "Show a raw message ($config)"
+    add_message
+    notmuch show --format=raw id:$gen_msg_id > OUTPUT
+    test_expect_equal_file_nonempty $gen_msg_filename OUTPUT
+    rm -f $gen_msg_filename
+
+    test_begin_subtest "reply ($config)"
+    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"'
+    notmuch reply id:${gen_msg_id} 2>&1 > OUTPUT
+    cat <<EOF > 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}>
+References: <${gen_msg_id}>
+
+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\"" \
+       "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" \
+       "[body]=\"insert-message\""
+    mkdir -p "$MAIL_DIR"/{cur,new,tmp}
+    notmuch insert < "$gen_msg_filename"
+    cur_msg_filename=$(notmuch search --output=files "subject:insert-subject")
+    test_expect_equal_file_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_nonempty EXPECTED OUTPUT
+
+    test_begin_subtest "upgrade backup ($config)"
+    features=$(xapian-metadata get $XAPIAN_PATH features | grep -v "^relative directory paths")
+    xapian-metadata set $XAPIAN_PATH features "$features"
+    output=$(notmuch new | grep Welcome)
+    test_expect_equal \
+       "$output" \
+       "Welcome to a new version of notmuch! Your database will now be upgraded."
+
+    test_begin_subtest "notmuch +config -database suggests notmuch new ($config)"
+    mv "$XAPIAN_PATH" "${XAPIAN_PATH}.bak"
+    notmuch > OUTPUT
+cat <<EOF > EXPECTED
+Notmuch is configured, but no database was found.
+You probably want to run "notmuch new" now to create a database.
+
+Note that the first run of "notmuch new" can take a very long time
+and that the resulting database will use roughly the same amount of
+storage space as the email being indexed.
+
+EOF
+    mv "${XAPIAN_PATH}.bak" "$XAPIAN_PATH"
+
+   test_expect_equal_file EXPECTED OUTPUT
+
+   test_begin_subtest "Set config value ($config)"
+   name=${RANDOM}
+   value=${RANDOM}
+   notmuch config set test${test_count}.${name} ${value}
+   output=$(notmuch config get test${test_count}.${name})
+   notmuch config set test${test_count}.${name}
+   output2=$(notmuch config get test${test_count}.${name})
+   test_expect_equal "${output}+${output2}" "${value}+"
+
+   test_begin_subtest "Set config value in database ($config)"
+   name=${RANDOM}
+   value=${RANDOM}
+   notmuch config set --database test${test_count}.${name} ${value}
+   output=$(notmuch config get test${test_count}.${name})
+   notmuch config set --database test${test_count}.${name}
+   output2=$(notmuch config get test${test_count}.${name})
+   test_expect_equal "${output}+${output2}" "${value}+"
+
+   test_begin_subtest "Config list ($config)"
+   notmuch config list | notmuch_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=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
+search.exclude_tags=
+user.name=Notmuch Test Suite
+user.other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org
+user.primary_email=test_suite@notmuchmail.org
+EOF
+   test_expect_equal_file EXPECTED OUTPUT
+
+   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)"
+          name=${RANDOM}
+          value=${RANDOM}
+          key=test${test_count}.${name}
+          notmuch config set --database ${key}  ${value}
+          notmuch config set ${key} shadow${value}
+          output=$(notmuch --config='' config get ${key})
+          notmuch config set --database ${key}
+          output2=$(notmuch --config='' config get ${key})
+          notmuch config set ${key}
+          test_expect_equal "${output}+${output2}" "${value}+"
+          ;&
+       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
+          ;;
+       *)
+          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
+done
+
+test_done
diff --git a/test/T060-count.sh b/test/T060-count.sh
new file mode 100755 (executable)
index 0000000..6e855b5
--- /dev/null
@@ -0,0 +1,184 @@
+#!/usr/bin/env bash
+test_description='"notmuch count" for messages and threads'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+# Note: The 'wc -l' results below are wrapped in arithmetic evaluation
+# $((...)) to strip whitespace. This is for portability, as 'wc -l'
+# emits whitespace on some BSD variants.
+
+test_begin_subtest "message count is the default for notmuch count"
+test_expect_equal \
+    "$((`notmuch search --output=messages '*' | wc -l`))" \
+    "`notmuch count '*'`"
+
+test_begin_subtest "message count with --output=messages"
+test_expect_equal \
+    "$((`notmuch search --output=messages '*' | wc -l`))" \
+    "`notmuch count --output=messages '*'`"
+
+test_begin_subtest "thread count with --output=threads"
+test_expect_equal \
+    "$((`notmuch search --output=threads '*' | wc -l`))" \
+    "`notmuch count --output=threads '*'`"
+
+test_begin_subtest "thread count is the default for notmuch search"
+test_expect_equal \
+    "$((`notmuch search '*' | wc -l`))" \
+    "`notmuch count --output=threads '*'`"
+
+test_begin_subtest "files count"
+test_expect_equal \
+    "$((`notmuch search --output=files '*' | wc -l`))" \
+    "`notmuch count --output=files '*'`"
+
+test_begin_subtest "files count for a duplicate message-id"
+test_expect_equal \
+    "2" \
+    "`notmuch count --output=files id:20091117232137.GA7669@griffis1.net`"
+
+test_begin_subtest "count with no matching messages"
+test_expect_equal \
+    "0" \
+    "`notmuch count --output=messages from:cworth and not from:cworth`"
+
+test_begin_subtest "count with no matching threads"
+test_expect_equal \
+    "0" \
+    "`notmuch count --output=threads from:cworth and not from:cworth`"
+
+test_begin_subtest "message count is the default for batch count"
+notmuch count --batch >OUTPUT <<EOF
+
+from:cworth
+EOF
+notmuch count --output=messages >EXPECTED
+notmuch count --output=messages from:cworth >>EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "batch message count"
+notmuch count --batch --output=messages >OUTPUT <<EOF
+from:cworth
+
+tag:inbox
+EOF
+notmuch count --output=messages from:cworth >EXPECTED
+notmuch count --output=messages >>EXPECTED
+notmuch count --output=messages tag:inbox >>EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "batch thread count"
+notmuch count --batch --output=threads >OUTPUT <<EOF
+
+from:cworth
+from:cworth and not from:cworth
+foo
+EOF
+notmuch count --output=threads >EXPECTED
+notmuch count --output=threads from:cworth >>EXPECTED
+notmuch count --output=threads from:cworth and not from:cworth >>EXPECTED
+notmuch count --output=threads foo >>EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "batch message count with input file"
+cat >INPUT <<EOF
+from:cworth
+
+tag:inbox
+EOF
+notmuch count --input=INPUT --output=messages >OUTPUT
+notmuch count --output=messages from:cworth >EXPECTED
+notmuch count --output=messages >>EXPECTED
+notmuch count --output=messages tag:inbox >>EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+backup_database
+test_begin_subtest "error message for database open"
+target=(${MAIL_DIR}/.notmuch/xapian/postlist.*)
+dd if=/dev/zero of="$target" count=3
+notmuch count '*' 2>OUTPUT 1>/dev/null
+output=$(sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' OUTPUT)
+test_expect_equal "${output}" "A Xapian exception occurred opening database"
+restore_database
+
+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"
+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
+Query string was: *
+EOF
+sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean
+test_expect_equal_file EXPECTED OUTPUT.clean
+restore_database
+
+test_begin_subtest "count library function is non-destructive"
+cat<<EOF > EXPECTED
+1: 52 messages
+2: 52 messages
+Exclude 'spam'
+3: 52 messages
+4: 52 messages
+EOF
+test_python <<EOF
+import sys
+import notmuch
+
+query_string = 'tag:inbox or tag:spam'
+tag_string = 'spam'
+
+database = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+query = notmuch.Query(database, query_string)
+
+print("1: {} messages".format(query.count_messages()))
+print("2: {} messages".format(query.count_messages()))
+print("Exclude '{}'".format(tag_string))
+query.exclude_tag(tag_string)
+print("3: {} messages".format(query.count_messages()))
+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
diff --git a/test/T070-insert.sh b/test/T070-insert.sh
new file mode 100755 (executable)
index 0000000..7395327
--- /dev/null
@@ -0,0 +1,300 @@
+#!/usr/bin/env bash
+test_description='"notmuch insert"'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+# subtests about file permissions assume that we're working with umask
+# 022 by default.
+umask 022
+
+# Create directories and database before inserting.
+mkdir -p "$MAIL_DIR"/{cur,new,tmp}
+mkdir -p "$MAIL_DIR"/Drafts/{cur,new,tmp}
+notmuch new > /dev/null
+
+# We use generate_message to create the temporary message files.
+# They happen to be in the mail directory already but that is okay
+# since we do not call notmuch new hereafter.
+
+gen_insert_msg () {
+    generate_message \
+       "[subject]=\"insert-subject\"" \
+       "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" \
+       "[body]=\"insert-message\""
+}
+
+test_begin_subtest "Insert zero-length file"
+test_expect_code 1 "notmuch insert < /dev/null"
+
+# This test is a proxy for other errors that may occur while trying to
+# add a message to the notmuch database, e.g. database locked.
+test_begin_subtest "Insert non-message"
+test_expect_code 1 "echo bad_message | notmuch insert"
+
+test_begin_subtest "Database empty so far"
+test_expect_equal "0" "`notmuch count --output=messages '*'`"
+
+test_begin_subtest "Insert message"
+gen_insert_msg
+notmuch insert < "$gen_msg_filename"
+cur_msg_filename=$(notmuch search --output=files "subject:insert-subject")
+test_expect_equal_file "$cur_msg_filename" "$gen_msg_filename"
+
+test_begin_subtest "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" | notmuch_json_show_sanitize)
+expected='[[[{
+ "id": "XXXXX",
+ "crypto": {},
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["inbox","unread"],
+ "headers": {
+  "Subject": "insert-subject",
+  "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": "insert-message\n"}]},
+ []]]]'
+test_expect_equal_json "$output" "$expected"
+
+test_begin_subtest "Insert duplicate message"
+notmuch insert +duptag -unread < "$gen_msg_filename"
+output=$((`notmuch search --output=files "subject:insert-subject" | wc -l`))
+test_expect_equal "$output" 2
+
+test_begin_subtest "Duplicate message does not change tags"
+output=$(notmuch search --format=json --output=tags "subject:insert-subject")
+test_expect_equal_json "$output" '["inbox", "unread"]'
+
+test_begin_subtest "Insert message, add tag"
+gen_insert_msg
+notmuch insert +custom < "$gen_msg_filename"
+output=$(notmuch search --output=messages tag:custom)
+test_expect_equal "$output" "id:$gen_msg_id"
+
+test_begin_subtest "Insert tagged world-readable message"
+gen_insert_msg
+notmuch insert --world-readable +world-readable-test < "$gen_msg_filename"
+cur_msg_filename=$(notmuch search --output=files "tag:world-readable-test")
+test_expect_equal_file "$cur_msg_filename" "$gen_msg_filename"
+
+test_begin_subtest "Permissions on inserted world-readable message should be 0644"
+test_expect_equal "644" "$(stat -c %a "$cur_msg_filename")"
+
+test_begin_subtest "Insert tagged world-readable message with group-only umask"
+oldumask=$(umask)
+umask 027
+gen_insert_msg
+notmuch insert --world-readable +world-readable-umask-test < "$gen_msg_filename"
+cur_msg_filename=$(notmuch search --output=files "tag:world-readable-umask-test")
+umask "$oldumask"
+test_expect_equal_file "$cur_msg_filename" "$gen_msg_filename"
+
+test_begin_subtest "Permissions on inserted world-readable message with funny umask should be 0640"
+test_expect_equal "640" "$(stat -c %a "$cur_msg_filename")"
+
+test_begin_subtest "Insert message, add/remove tags"
+gen_insert_msg
+notmuch insert +custom -unread < "$gen_msg_filename"
+output=$(notmuch search --output=messages tag:custom NOT tag:unread)
+test_expect_equal "$output" "id:$gen_msg_id"
+
+test_begin_subtest "Insert message with default tags stays in new/"
+gen_insert_msg
+notmuch insert < "$gen_msg_filename"
+output=$(notmuch search --output=files id:$gen_msg_id)
+dirname=$(dirname "$output")
+test_expect_equal "$dirname" "$MAIL_DIR/new"
+
+test_begin_subtest "Insert message with non-maildir synced tags stays in new/"
+gen_insert_msg
+notmuch insert +custom -inbox < "$gen_msg_filename"
+output=$(notmuch search --output=files id:$gen_msg_id)
+dirname=$(dirname "$output")
+test_expect_equal "$dirname" "$MAIL_DIR/new"
+
+test_begin_subtest "Insert message with custom new.tags goes to cur/"
+OLDCONFIG=$(notmuch config get new.tags)
+notmuch config set new.tags test
+gen_insert_msg
+notmuch insert < "$gen_msg_filename"
+output=$(notmuch search --output=files id:$gen_msg_id)
+dirname=$(dirname "$output")
+notmuch config set new.tags $OLDCONFIG
+test_expect_equal "$dirname" "$MAIL_DIR/cur"
+
+# additional check on the previous message
+test_begin_subtest "Insert message with custom new.tags actually gets the tags"
+output=$(notmuch search --output=tags id:$gen_msg_id)
+test_expect_equal "$output" "test"
+
+test_begin_subtest "Insert message with maildir synced tags goes to cur/"
+gen_insert_msg
+notmuch insert +flagged < "$gen_msg_filename"
+output=$(notmuch search --output=files id:$gen_msg_id)
+dirname=$(dirname "$output")
+test_expect_equal "$dirname" "$MAIL_DIR/cur"
+
+test_begin_subtest "Insert message with maildir sync off goes to new/"
+OLDCONFIG=$(notmuch config get maildir.synchronize_flags)
+notmuch config set maildir.synchronize_flags false
+gen_insert_msg
+notmuch insert +flagged < "$gen_msg_filename"
+output=$(notmuch search --output=files id:$gen_msg_id)
+dirname=$(dirname "$output")
+notmuch config set maildir.synchronize_flags $OLDCONFIG
+test_expect_equal "$dirname" "$MAIL_DIR/new"
+
+test_begin_subtest "Insert message into folder"
+gen_insert_msg
+notmuch insert --folder=Drafts < "$gen_msg_filename"
+output=$(notmuch search --output=files path:Drafts/new)
+dirname=$(dirname "$output")
+test_expect_equal "$dirname" "$MAIL_DIR/Drafts/new"
+
+test_begin_subtest "Insert message into top level folder"
+gen_insert_msg
+notmuch insert --folder="" < "$gen_msg_filename"
+output=$(notmuch search --output=files id:${gen_msg_id})
+dirname=$(dirname "$output")
+test_expect_equal "$dirname" "$MAIL_DIR/new"
+
+test_begin_subtest "Insert message into folder with trailing /"
+gen_insert_msg
+notmuch insert --folder=Drafts/ < "$gen_msg_filename"
+output=$(notmuch search --output=files id:${gen_msg_id})
+dirname=$(dirname "$output")
+test_expect_equal "$dirname" "$MAIL_DIR/Drafts/new"
+
+test_begin_subtest "Insert message into folder, add/remove tags"
+gen_insert_msg
+notmuch insert --folder=Drafts +draft -unread < "$gen_msg_filename"
+output=$(notmuch search --output=messages path:Drafts/cur tag:draft NOT tag:unread)
+test_expect_equal "$output" "id:$gen_msg_id"
+
+test_begin_subtest "Insert message into non-existent folder"
+gen_insert_msg
+test_expect_code 1 "notmuch insert --folder=nonesuch < $gen_msg_filename"
+
+test_begin_subtest "Insert message, create folder"
+gen_insert_msg
+notmuch insert --folder=F --create-folder +folder < "$gen_msg_filename"
+output=$(notmuch search --output=files path:F/new tag:folder)
+basename=$(basename "$output")
+test_expect_equal_file "$gen_msg_filename" "$MAIL_DIR/F/new/${basename}"
+
+test_begin_subtest "Insert message, create subfolder"
+gen_insert_msg
+notmuch insert --folder=F/G/H/I/J --create-folder +folder < "$gen_msg_filename"
+output=$(notmuch search --output=files path:F/G/H/I/J/new tag:folder)
+basename=$(basename "$output")
+test_expect_equal_file "$gen_msg_filename" "${MAIL_DIR}/F/G/H/I/J/new/${basename}"
+
+test_begin_subtest "Created subfolder should have permissions 0700"
+test_expect_equal "700" "$(stat -c %a "${MAIL_DIR}/F/G/H/I/J")"
+test_begin_subtest "Created subfolder new/ should also have permissions 0700"
+test_expect_equal "700" "$(stat -c %a "${MAIL_DIR}/F/G/H/I/J/new")"
+
+test_begin_subtest "Insert message, create world-readable subfolder"
+gen_insert_msg
+notmuch insert --folder=F/G/H/I/J/K --create-folder --world-readable +folder-world-readable < "$gen_msg_filename"
+output=$(notmuch search --output=files path:F/G/H/I/J/K/new tag:folder-world-readable)
+basename=$(basename "$output")
+test_expect_equal_file "$gen_msg_filename" "${MAIL_DIR}/F/G/H/I/J/K/new/${basename}"
+
+test_begin_subtest "Created world-readable subfolder should have permissions 0755"
+test_expect_equal "755" "$(stat -c %a "${MAIL_DIR}/F/G/H/I/J/K")"
+test_begin_subtest "Created world-readable subfolder new/ should also have permissions 0755"
+test_expect_equal "755" "$(stat -c %a "${MAIL_DIR}/F/G/H/I/J/K/new")"
+
+test_begin_subtest "Insert message, create existing subfolder"
+gen_insert_msg
+notmuch insert --folder=F/G/H/I/J --create-folder +folder < "$gen_msg_filename"
+output=$(notmuch count path:F/G/H/I/J/new tag:folder)
+test_expect_equal "$output" "2"
+
+test_begin_subtest "Insert message, create invalid subfolder"
+gen_insert_msg
+test_expect_code 1 "notmuch insert --folder=../G --create-folder < $gen_msg_filename"
+
+OLDCONFIG=$(notmuch config get new.tags)
+
+test_begin_subtest "Empty tags in new.tags are ignored"
+notmuch config set new.tags "foo;;bar"
+gen_insert_msg
+notmuch insert < $gen_msg_filename
+output=$(notmuch show --format=json id:$gen_msg_id)
+test_json_nodes <<<"$output" \
+               'new_tags:[0][0][0]["tags"] = ["bar", "foo"]'
+
+test_begin_subtest "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
+output=$(notmuch insert < $gen_msg_filename 2>&1)
+test_expect_equal "$output" "Error: tag '-foo' in new.tags: tag starting with '-' forbidden"
+
+test_begin_subtest "Invalid tags set exit code"
+test_expect_code 1 "notmuch insert < $gen_msg_filename 2>&1"
+
+notmuch config set new.tags $OLDCONFIG
+
+# DUPLICATE_MESSAGE_ID is not tested here, because it should actually pass.
+# pregenerate all of the test shims
+for code in FILE_NOT_EMAIL READ_ONLY_DATABASE UPGRADE_REQUIRED PATH_ERROR OUT_OF_MEMORY XAPIAN_EXCEPTION; do
+    make_shim shim-$code <<EOF
+#include <notmuch.h>
+#include <stdio.h>
+notmuch_status_t
+notmuch_database_index_file (notmuch_database_t *notmuch,
+                             const char *filename,
+                             notmuch_indexopts_t *indexopts,
+                             notmuch_message_t **message_ret)
+{
+  return NOTMUCH_STATUS_$code;
+}
+EOF
+done
+
+gen_insert_msg
+
+for code in FILE_NOT_EMAIL READ_ONLY_DATABASE UPGRADE_REQUIRED PATH_ERROR; do
+    test_begin_subtest "EXIT_FAILURE when index_file returns $code"
+    test_expect_code 1 "notmuch_with_shim shim-$code insert < \"$gen_msg_filename\""
+
+    test_begin_subtest "success exit with --keep when index_file returns $code"
+    test_expect_code 0 "notmuch_with_shim shim-$code insert --keep < \"$gen_msg_filename\""
+done
+
+for code in OUT_OF_MEMORY XAPIAN_EXCEPTION ; do
+    test_begin_subtest "EX_TEMPFAIL when index_file returns $code"
+    test_expect_code 75 "notmuch_with_shim shim-$code insert < \"$gen_msg_filename\""
+
+    test_begin_subtest "success exit with --keep when index_file returns $code"
+    test_expect_code 0 "notmuch_with_shim shim-$code insert --keep < \"$gen_msg_filename\""
+done
+
+test_begin_subtest "insert converts mboxes on delivery"
+notmuch insert +unmboxed < "${TEST_DIRECTORY}"/corpora/insert/mbox-attachment.eml
+output=$(notmuch count tag:unmboxed)
+test_expect_equal "${output}" 1
+
+test_done
diff --git a/test/T080-search.sh b/test/T080-search.sh
new file mode 100755 (executable)
index 0000000..515eb88
--- /dev/null
@@ -0,0 +1,199 @@
+#!/usr/bin/env bash
+test_description='"notmuch search" in several variations'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "Search body"
+add_message '[subject]="body search"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [body]=bodysearchtest
+output=$(notmuch search 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 from:"
+add_message '[subject]="search by from"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom
+output=$(notmuch search 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 to:"
+add_message '[subject]="search by to"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto
+output=$(notmuch search 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 subject:"
+add_message [subject]=subjectsearchtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(notmuch search 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 (utf-8):"
+add_message [subject]=utf8-sübjéct '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(notmuch search 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 id:"
+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)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; search by mid (inbox unread)"
+
+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 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 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 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 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 '"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 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 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 '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 '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)"
+
+test_begin_subtest "Search by from: without prefix (name and address)"
+output=$(notmuch search '"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)"
+
+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 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 '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 '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 "Search by to: without prefix (name and address)"
+output=$(notmuch search '"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 "Search by subject: (phrase)"
+add_message '[subject]="subject search test (phrase)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+add_message '[subject]="this phrase should not match the subject search test"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(notmuch search 'subject:"subject search test (phrase)"' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; subject search test (phrase) (inbox unread)"
+
+test_begin_subtest 'Search for all messages ("*")'
+notmuch search '*' | 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   2010-12-16 [1/1] Olivier Berger; Essai accentué (inbox unread)
+thread:XXX   2009-11-18 [1/1] Chris Wilson; [notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread)
+thread:XXX   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)
+thread:XXX   2009-11-18 [2/2] Ingmar Vanhassel, Carl Worth; [notmuch] [PATCH] Typsos (inbox unread)
+thread:XXX   2009-11-18 [3/3] Adrian Perez de Castro, Keith Packard, Carl Worth; [notmuch] Introducing myself (inbox signed unread)
+thread:XXX   2009-11-18 [3/3] Israel Herraiz, Keith Packard, Carl Worth; [notmuch] New to the list (inbox unread)
+thread:XXX   2009-11-18 [3/3] Jan Janak, Carl Worth; [notmuch] What a great idea! (inbox unread)
+thread:XXX   2009-11-18 [2/2] Jan Janak, Carl Worth; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+thread:XXX   2009-11-18 [3/3(4)] Aron Griffis, Keith Packard, Carl Worth; [notmuch] archive (inbox unread)
+thread:XXX   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)
+thread:XXX   2009-11-18 [7/7] Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth; [notmuch] Working with Maildir storage? (inbox signed unread)
+thread:XXX   2009-11-18 [5/5] Mikhail Gusarov, Carl Worth, Keith Packard; [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread)
+thread:XXX   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)
+thread:XXX   2009-11-18 [1/1] Alexander Botero-Lowry; [notmuch] request for pull (inbox unread)
+thread:XXX   2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)
+thread:XXX   2009-11-18 [1/1] Rolland Santimano; [notmuch] Link to mailing list archives ? (inbox unread)
+thread:XXX   2009-11-18 [1/1] Jan Janak; [notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread)
+thread:XXX   2009-11-18 [1/1] Stewart Smith; [notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread)
+thread:XXX   2009-11-18 [1/1] Stewart Smith; [notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox unread)
+thread:XXX   2009-11-18 [1/1] Stewart Smith; [notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread)
+thread:XXX   2009-11-18 [2/2] Lars Kellogg-Stedman; [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread)
+thread:XXX   2009-11-17 [1/1] Mikhail Gusarov; [notmuch] [PATCH] Handle rename of message file (inbox unread)
+thread:XXX   2009-11-17 [2/2] Alex Botero-Lowry, Carl Worth; [notmuch] preliminary FreeBSD support (attachment inbox unread)
+thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; body search (inbox unread)
+thread:XXX   2000-01-01 [1/1] searchbyfrom; search by from (inbox unread)
+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)
+thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; body search (phrase) (inbox unread)
+thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; negative result (inbox unread)
+thread:XXX   2000-01-01 [1/1] searchbyfrom@example.com; search by from (address) (inbox unread)
+thread:XXX   2000-01-01 [1/1] Search By From Name; search by from (name) (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)
+thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; subject search test (phrase) (inbox unread)
+thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; this phrase should not match the subject search test (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search 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 "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)"
+
+
+cat <<EOF > ${MAIL_DIR}/termpos
+From: Source <source@example.com>
+To: Dest <dest@example.com>
+Subject: part overlap test
+Date: Sat, 01 January 2000 00:00:00 +0000
+Message-ID: <termpos>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="==-=="
+
+--==-==
+Content-Type: text/plain
+
+a b c
+
+--==-==
+Content-Type: text/plain
+
+x y z
+
+--==-==--
+EOF
+notmuch new > /dev/null
+
+test_begin_subtest "headers do not have adjacent term positions"
+# Regression test for a bug where term positions for non-prefixed
+# terms weren't updated
+output=$(notmuch search id:termpos and '"com dest"')
+test_expect_equal "$output" ""
+
+test_begin_subtest "parts have non-overlapping term positions"
+output=$(notmuch search id:termpos and '"a y c"')
+test_expect_equal "$output" ""
+
+test_begin_subtest "parts do not have adjacent term positions"
+output=$(notmuch search id:termpos and '"c x"')
+test_expect_equal "$output" ""
+
+test_done
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
diff --git a/test/T090-search-output.sh b/test/T090-search-output.sh
new file mode 100755 (executable)
index 0000000..0d85c60
--- /dev/null
@@ -0,0 +1,446 @@
+#!/usr/bin/env bash
+test_description='various settings for "notmuch search --output="'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "--output=threads"
+notmuch search --output=threads '*' | sed -e s/thread:.*/thread:THREADID/ >OUTPUT
+cat <<EOF >EXPECTED
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+thread:THREADID
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=threads --format=json"
+notmuch search --format=json --output=threads '*' | sed -e s/\".*\"/\"THREADID\"/ >OUTPUT
+cat <<EOF >EXPECTED
+["THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID",
+"THREADID"]
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--output=messages"
+notmuch search --output=messages '*' >OUTPUT
+cat <<EOF >EXPECTED
+id:4EFC743A.3060609@april.org
+id:877h1wv7mg.fsf@inf-8657.int-evry.fr
+id:1258544095-16616-1-git-send-email-chris@chris-wilson.co.uk
+id:877htoqdbo.fsf@yoom.home.cworth.org
+id:878we4qdqf.fsf@yoom.home.cworth.org
+id:87aaykqe24.fsf@yoom.home.cworth.org
+id:87bpj0qeng.fsf@yoom.home.cworth.org
+id:87fx8cqf8v.fsf@yoom.home.cworth.org
+id:87hbssqfix.fsf@yoom.home.cworth.org
+id:87iqd8qgiz.fsf@yoom.home.cworth.org
+id:87k4xoqgnl.fsf@yoom.home.cworth.org
+id:87ocn0qh6d.fsf@yoom.home.cworth.org
+id:87pr7gqidx.fsf@yoom.home.cworth.org
+id:867hto2p0t.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me
+id:1258532999-9316-1-git-send-email-keithp@keithp.com
+id:86aayk2rbj.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me
+id:86d43g2w3y.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me
+id:ddd65cda0911172214t60d22b63hcfeb5a19ab54a39b@mail.gmail.com
+id:86einw2xof.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me
+id:736613.51770.qm@web113505.mail.gq1.yahoo.com
+id:1258520223-15328-1-git-send-email-jan@ryngle.com
+id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com
+id:1258510940-7018-1-git-send-email-stewart@flamingspork.com
+id:yunzl6kd1w0.fsf@aiko.keithp.com
+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:20091118005040.GA25380@dottiness.seas.harvard.edu
+id:cf0c4d610911171623q3e27a0adx802e47039b57604b@mail.gmail.com
+id:1258500222-32066-1-git-send-email-ingmar@exherbo.org
+id:20091117232137.GA7669@griffis1.net
+id:20091118002059.067214ed@hikari
+id:1258498485-sup-142@elly
+id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com
+id:f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com
+id:1258496327-12086-1-git-send-email-jan@ryngle.com
+id:1258493565-13508-1-git-send-email-keithp@keithp.com
+id:yunaayketfm.fsf@aiko.keithp.com
+id:yunbpj0etua.fsf@aiko.keithp.com
+id:1258491078-29658-1-git-send-email-dottedmag@dottedmag.net
+id:87fx8can9z.fsf@vertex.dottedmag
+id:20091117203301.GV3165@dottiness.seas.harvard.edu
+id:87lji4lx9v.fsf@yoom.home.cworth.org
+id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com
+id:87iqd9rn3l.fsf@vertex.dottedmag
+id:20091117190054.GU3165@dottiness.seas.harvard.edu
+id:87lji5cbwo.fsf@yoom.home.cworth.org
+id:1258471718-6781-2-git-send-email-dottedmag@dottedmag.net
+id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=messages --duplicate=1"
+notmuch search --output=messages --duplicate=1 '*' >OUTPUT
+# reuse same EXPECTED as above
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=messages --duplicate=2"
+notmuch search --output=messages --duplicate=2 '*' >OUTPUT
+cat <<EOF >EXPECTED
+id:20091117232137.GA7669@griffis1.net
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=messages --duplicate=3"
+notmuch search --output=messages --duplicate=3 '*' >OUTPUT
+cat <<EOF >EXPECTED
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=messages --format=json"
+notmuch search --format=json --output=messages '*' >OUTPUT
+cat <<EOF >EXPECTED
+["4EFC743A.3060609@april.org",
+"877h1wv7mg.fsf@inf-8657.int-evry.fr",
+"1258544095-16616-1-git-send-email-chris@chris-wilson.co.uk",
+"877htoqdbo.fsf@yoom.home.cworth.org",
+"878we4qdqf.fsf@yoom.home.cworth.org",
+"87aaykqe24.fsf@yoom.home.cworth.org",
+"87bpj0qeng.fsf@yoom.home.cworth.org",
+"87fx8cqf8v.fsf@yoom.home.cworth.org",
+"87hbssqfix.fsf@yoom.home.cworth.org",
+"87iqd8qgiz.fsf@yoom.home.cworth.org",
+"87k4xoqgnl.fsf@yoom.home.cworth.org",
+"87ocn0qh6d.fsf@yoom.home.cworth.org",
+"87pr7gqidx.fsf@yoom.home.cworth.org",
+"867hto2p0t.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me",
+"1258532999-9316-1-git-send-email-keithp@keithp.com",
+"86aayk2rbj.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me",
+"86d43g2w3y.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me",
+"ddd65cda0911172214t60d22b63hcfeb5a19ab54a39b@mail.gmail.com",
+"86einw2xof.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me",
+"736613.51770.qm@web113505.mail.gq1.yahoo.com",
+"1258520223-15328-1-git-send-email-jan@ryngle.com",
+"ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com",
+"1258510940-7018-1-git-send-email-stewart@flamingspork.com",
+"yunzl6kd1w0.fsf@aiko.keithp.com",
+"yun1vjwegii.fsf@aiko.keithp.com",
+"yun3a4cegoa.fsf@aiko.keithp.com",
+"1258509400-32511-1-git-send-email-stewart@flamingspork.com",
+"1258506353-20352-1-git-send-email-stewart@flamingspork.com",
+"20091118010116.GC25380@dottiness.seas.harvard.edu",
+"20091118005829.GB25380@dottiness.seas.harvard.edu",
+"20091118005040.GA25380@dottiness.seas.harvard.edu",
+"cf0c4d610911171623q3e27a0adx802e47039b57604b@mail.gmail.com",
+"1258500222-32066-1-git-send-email-ingmar@exherbo.org",
+"20091117232137.GA7669@griffis1.net",
+"20091118002059.067214ed@hikari",
+"1258498485-sup-142@elly",
+"f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com",
+"f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com",
+"1258496327-12086-1-git-send-email-jan@ryngle.com",
+"1258493565-13508-1-git-send-email-keithp@keithp.com",
+"yunaayketfm.fsf@aiko.keithp.com",
+"yunbpj0etua.fsf@aiko.keithp.com",
+"1258491078-29658-1-git-send-email-dottedmag@dottedmag.net",
+"87fx8can9z.fsf@vertex.dottedmag",
+"20091117203301.GV3165@dottiness.seas.harvard.edu",
+"87lji4lx9v.fsf@yoom.home.cworth.org",
+"cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com",
+"87iqd9rn3l.fsf@vertex.dottedmag",
+"20091117190054.GU3165@dottiness.seas.harvard.edu",
+"87lji5cbwo.fsf@yoom.home.cworth.org",
+"1258471718-6781-2-git-send-email-dottedmag@dottedmag.net",
+"1258471718-6781-1-git-send-email-dottedmag@dottedmag.net"]
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=messages --format=json --duplicate=1"
+notmuch search --output=messages --format=json --duplicate=1 '*' >OUTPUT
+# reuse same EXPECTED as above
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=messages --format=json --duplicate=2"
+notmuch search --output=messages --format=json --duplicate=2 '*' >OUTPUT
+cat <<EOF >EXPECTED
+["20091117232137.GA7669@griffis1.net"]
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=messages --format=json --duplicate=3"
+notmuch search --output=messages --format=json --duplicate=3 '*' >OUTPUT
+cat <<EOF >EXPECTED
+[]
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=files"
+notmuch search --output=files '*' | notmuch_search_files_sanitize | sort >OUTPUT
+cat <<EOF >EXPECTED
+MAIL_DIR/01:2,
+MAIL_DIR/02:2,
+MAIL_DIR/bar/17:2,
+MAIL_DIR/bar/18:2,
+MAIL_DIR/bar/baz/05:2,
+MAIL_DIR/bar/baz/23:2,
+MAIL_DIR/bar/baz/24:2,
+MAIL_DIR/bar/baz/cur/25:2,
+MAIL_DIR/bar/baz/cur/26:2,
+MAIL_DIR/bar/baz/new/27:2,
+MAIL_DIR/bar/baz/new/28:2,
+MAIL_DIR/bar/cur/19:2,
+MAIL_DIR/bar/cur/20:2,
+MAIL_DIR/bar/new/21:2,
+MAIL_DIR/bar/new/22:2,
+MAIL_DIR/cur/29:2,
+MAIL_DIR/cur/30:2,
+MAIL_DIR/cur/31:2,
+MAIL_DIR/cur/32:2,
+MAIL_DIR/cur/33:2,
+MAIL_DIR/cur/34:2,
+MAIL_DIR/cur/35:2,
+MAIL_DIR/cur/36:2,
+MAIL_DIR/cur/37:2,
+MAIL_DIR/cur/38:2,
+MAIL_DIR/cur/39:2,
+MAIL_DIR/cur/40:2,
+MAIL_DIR/cur/41:2,
+MAIL_DIR/cur/42:2,
+MAIL_DIR/cur/43:2,
+MAIL_DIR/cur/44:2,
+MAIL_DIR/cur/45:2,
+MAIL_DIR/cur/46:2,
+MAIL_DIR/cur/47:2,
+MAIL_DIR/cur/48:2,
+MAIL_DIR/cur/49:2,
+MAIL_DIR/cur/50:2,
+MAIL_DIR/cur/51:2,
+MAIL_DIR/cur/52:2,
+MAIL_DIR/cur/53:2,
+MAIL_DIR/foo/06:2,
+MAIL_DIR/foo/baz/11:2,
+MAIL_DIR/foo/baz/12:2,
+MAIL_DIR/foo/baz/cur/13:2,
+MAIL_DIR/foo/baz/cur/14:2,
+MAIL_DIR/foo/baz/new/15:2,
+MAIL_DIR/foo/baz/new/16:2,
+MAIL_DIR/foo/cur/07:2,
+MAIL_DIR/foo/cur/08:2,
+MAIL_DIR/foo/new/03:2,
+MAIL_DIR/foo/new/09:2,
+MAIL_DIR/foo/new/10:2,
+MAIL_DIR/new/04:2,
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+dup1=$(notmuch search --output=files id:20091117232137.GA7669@griffis1.net | head -n 1 | sed -e "s,$MAIL_DIR,MAIL_DIR,")
+dup2=$(notmuch search --output=files id:20091117232137.GA7669@griffis1.net | tail -n 1 | sed -e "s,$MAIL_DIR,MAIL_DIR,")
+
+test_begin_subtest "--output=files --duplicate=1"
+notmuch search --output=files --duplicate=1 '*' | notmuch_search_files_sanitize | sort >OUTPUT
+cat <<EOF | sort >EXPECTED
+$dup1
+MAIL_DIR/cur/52:2,
+MAIL_DIR/cur/53:2,
+MAIL_DIR/cur/50:2,
+MAIL_DIR/cur/49:2,
+MAIL_DIR/cur/48:2,
+MAIL_DIR/cur/47:2,
+MAIL_DIR/cur/46:2,
+MAIL_DIR/cur/45:2,
+MAIL_DIR/cur/44:2,
+MAIL_DIR/cur/43:2,
+MAIL_DIR/cur/42:2,
+MAIL_DIR/cur/41:2,
+MAIL_DIR/cur/40:2,
+MAIL_DIR/cur/39:2,
+MAIL_DIR/cur/38:2,
+MAIL_DIR/cur/37:2,
+MAIL_DIR/cur/36:2,
+MAIL_DIR/cur/35:2,
+MAIL_DIR/cur/34:2,
+MAIL_DIR/cur/33:2,
+MAIL_DIR/cur/32:2,
+MAIL_DIR/cur/31:2,
+MAIL_DIR/cur/30:2,
+MAIL_DIR/cur/29:2,
+MAIL_DIR/bar/baz/new/28:2,
+MAIL_DIR/bar/baz/new/27:2,
+MAIL_DIR/bar/baz/cur/26:2,
+MAIL_DIR/bar/baz/cur/25:2,
+MAIL_DIR/bar/baz/24:2,
+MAIL_DIR/bar/baz/23:2,
+MAIL_DIR/bar/new/22:2,
+MAIL_DIR/bar/new/21:2,
+MAIL_DIR/bar/cur/19:2,
+MAIL_DIR/bar/cur/20:2,
+MAIL_DIR/bar/17:2,
+MAIL_DIR/foo/baz/new/16:2,
+MAIL_DIR/foo/baz/new/15:2,
+MAIL_DIR/foo/baz/cur/14:2,
+MAIL_DIR/foo/baz/cur/13:2,
+MAIL_DIR/foo/baz/12:2,
+MAIL_DIR/foo/baz/11:2,
+MAIL_DIR/foo/new/10:2,
+MAIL_DIR/foo/new/09:2,
+MAIL_DIR/foo/cur/08:2,
+MAIL_DIR/foo/06:2,
+MAIL_DIR/bar/baz/05:2,
+MAIL_DIR/new/04:2,
+MAIL_DIR/foo/new/03:2,
+MAIL_DIR/foo/cur/07:2,
+MAIL_DIR/02:2,
+MAIL_DIR/01:2,
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=files --format=json"
+notmuch search --format=json --output=files '*' | notmuch_search_files_sanitize \
+    | test_sort_json >OUTPUT
+cat <<EOF | test_sort_json >EXPECTED
+["MAIL_DIR/cur/52:2,",
+"MAIL_DIR/cur/53:2,",
+"MAIL_DIR/cur/50:2,",
+"MAIL_DIR/cur/49:2,",
+"MAIL_DIR/cur/48:2,",
+"MAIL_DIR/cur/47:2,",
+"MAIL_DIR/cur/46:2,",
+"MAIL_DIR/cur/45:2,",
+"MAIL_DIR/cur/44:2,",
+"MAIL_DIR/cur/43:2,",
+"MAIL_DIR/cur/42:2,",
+"MAIL_DIR/cur/41:2,",
+"MAIL_DIR/cur/40:2,",
+"MAIL_DIR/cur/39:2,",
+"MAIL_DIR/cur/38:2,",
+"MAIL_DIR/cur/37:2,",
+"MAIL_DIR/cur/36:2,",
+"MAIL_DIR/cur/35:2,",
+"MAIL_DIR/cur/34:2,",
+"MAIL_DIR/cur/33:2,",
+"MAIL_DIR/cur/32:2,",
+"MAIL_DIR/cur/31:2,",
+"MAIL_DIR/cur/30:2,",
+"MAIL_DIR/cur/29:2,",
+"MAIL_DIR/bar/baz/new/28:2,",
+"MAIL_DIR/bar/baz/new/27:2,",
+"MAIL_DIR/bar/baz/cur/26:2,",
+"MAIL_DIR/bar/baz/cur/25:2,",
+"MAIL_DIR/bar/baz/24:2,",
+"MAIL_DIR/bar/baz/23:2,",
+"MAIL_DIR/bar/new/22:2,",
+"MAIL_DIR/bar/new/21:2,",
+"MAIL_DIR/bar/cur/19:2,",
+"MAIL_DIR/bar/18:2,",
+"MAIL_DIR/cur/51:2,",
+"MAIL_DIR/bar/cur/20:2,",
+"MAIL_DIR/bar/17:2,",
+"MAIL_DIR/foo/baz/new/16:2,",
+"MAIL_DIR/foo/baz/new/15:2,",
+"MAIL_DIR/foo/baz/cur/14:2,",
+"MAIL_DIR/foo/baz/cur/13:2,",
+"MAIL_DIR/foo/baz/12:2,",
+"MAIL_DIR/foo/baz/11:2,",
+"MAIL_DIR/foo/new/10:2,",
+"MAIL_DIR/foo/new/09:2,",
+"MAIL_DIR/foo/cur/08:2,",
+"MAIL_DIR/foo/06:2,",
+"MAIL_DIR/bar/baz/05:2,",
+"MAIL_DIR/new/04:2,",
+"MAIL_DIR/foo/new/03:2,",
+"MAIL_DIR/foo/cur/07:2,",
+"MAIL_DIR/02:2,",
+"MAIL_DIR/01:2,"]
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=files --format=json --duplicate=2"
+notmuch search --format=json --output=files --duplicate=2 '*' | notmuch_search_files_sanitize >OUTPUT
+cat <<EOF >EXPECTED
+["$dup2"]
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=tags"
+notmuch search --output=tags '*' >OUTPUT
+cat <<EOF >EXPECTED
+attachment
+inbox
+signed
+unread
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=tags --format=json"
+notmuch search --format=json --output=tags '*' >OUTPUT
+cat <<EOF >EXPECTED
+["attachment",
+"inbox",
+"signed",
+"unread"]
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "sanitize output for quoted-printable line-breaks in author and subject"
+add_message "[subject]='two =?ISO-8859-1?Q?line=0A_subject?=
+       headers'"
+notmuch search id:"$gen_msg_id" | notmuch_search_sanitize >OUTPUT
+cat <<EOF >EXPECTED
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; two line  subject headers (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search for non-existent message prints nothing"
+notmuch search "no-message-matches-this" > OUTPUT
+: >EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search --format=json for non-existent message prints proper empty json"
+notmuch search --format=json "no-message-matches-this" > OUTPUT
+echo "[]" >EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T095-address.sh b/test/T095-address.sh
new file mode 100755 (executable)
index 0000000..0cafbe2
--- /dev/null
@@ -0,0 +1,335 @@
+#!/usr/bin/env bash
+test_description='"notmuch address" in several variants'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "--output=sender"
+notmuch address --output=sender '*' >OUTPUT
+cat <<EOF >EXPECTED
+François Boulogne <boulogne.f@gmail.com>
+Olivier Berger <olivier.berger@it-sudparis.eu>
+Chris Wilson <chris@chris-wilson.co.uk>
+Carl Worth <cworth@cworth.org>
+Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+Keith Packard <keithp@keithp.com>
+Jjgod Jiang <gzjjgod@gmail.com>
+Rolland Santimano <rollandsantimano@yahoo.com>
+Jan Janak <jan@ryngle.com>
+Stewart Smith <stewart@flamingspork.com>
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Alex Botero-Lowry <alex.boterolowry@gmail.com>
+Ingmar Vanhassel <ingmar@exherbo.org>
+Aron Griffis <agriffis@n01se.net>
+Adrian Perez de Castro <aperez@igalia.com>
+Israel Herraiz <isra@herraiz.org>
+Mikhail Gusarov <dottedmag@dottedmag.net>
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "without --output"
+notmuch address '*' >OUTPUT
+# Use EXPECTED from previous subtest
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=sender --format=json"
+notmuch address --output=sender --format=json '*' >OUTPUT
+cat <<EOF >EXPECTED
+[{"name": "François Boulogne", "address": "boulogne.f@gmail.com", "name-addr": "François Boulogne <boulogne.f@gmail.com>"},
+{"name": "Olivier Berger", "address": "olivier.berger@it-sudparis.eu", "name-addr": "Olivier Berger <olivier.berger@it-sudparis.eu>"},
+{"name": "Chris Wilson", "address": "chris@chris-wilson.co.uk", "name-addr": "Chris Wilson <chris@chris-wilson.co.uk>"},
+{"name": "Carl Worth", "address": "cworth@cworth.org", "name-addr": "Carl Worth <cworth@cworth.org>"},
+{"name": "Alexander Botero-Lowry", "address": "alex.boterolowry@gmail.com", "name-addr": "Alexander Botero-Lowry <alex.boterolowry@gmail.com>"},
+{"name": "Keith Packard", "address": "keithp@keithp.com", "name-addr": "Keith Packard <keithp@keithp.com>"},
+{"name": "Jjgod Jiang", "address": "gzjjgod@gmail.com", "name-addr": "Jjgod Jiang <gzjjgod@gmail.com>"},
+{"name": "Rolland Santimano", "address": "rollandsantimano@yahoo.com", "name-addr": "Rolland Santimano <rollandsantimano@yahoo.com>"},
+{"name": "Jan Janak", "address": "jan@ryngle.com", "name-addr": "Jan Janak <jan@ryngle.com>"},
+{"name": "Stewart Smith", "address": "stewart@flamingspork.com", "name-addr": "Stewart Smith <stewart@flamingspork.com>"},
+{"name": "Lars Kellogg-Stedman", "address": "lars@seas.harvard.edu", "name-addr": "Lars Kellogg-Stedman <lars@seas.harvard.edu>"},
+{"name": "Alex Botero-Lowry", "address": "alex.boterolowry@gmail.com", "name-addr": "Alex Botero-Lowry <alex.boterolowry@gmail.com>"},
+{"name": "Ingmar Vanhassel", "address": "ingmar@exherbo.org", "name-addr": "Ingmar Vanhassel <ingmar@exherbo.org>"},
+{"name": "Aron Griffis", "address": "agriffis@n01se.net", "name-addr": "Aron Griffis <agriffis@n01se.net>"},
+{"name": "Adrian Perez de Castro", "address": "aperez@igalia.com", "name-addr": "Adrian Perez de Castro <aperez@igalia.com>"},
+{"name": "Israel Herraiz", "address": "isra@herraiz.org", "name-addr": "Israel Herraiz <isra@herraiz.org>"},
+{"name": "Mikhail Gusarov", "address": "dottedmag@dottedmag.net", "name-addr": "Mikhail Gusarov <dottedmag@dottedmag.net>"}]
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=recipients"
+notmuch address --output=recipients '*' >OUTPUT
+cat <<EOF >EXPECTED
+Allan McRae <allan@archlinux.org>
+"Discussion about the Arch User Repository (AUR)" <aur-general@archlinux.org>
+olivier.berger@it-sudparis.eu
+notmuch@notmuchmail.org
+notmuch <notmuch@notmuchmail.org>
+Keith Packard <keithp@keithp.com>
+Mikhail Gusarov <dottedmag@dottedmag.net>
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=sender --output=recipients"
+notmuch address --output=sender --output=recipients '*' >OUTPUT
+cat <<EOF >EXPECTED
+François Boulogne <boulogne.f@gmail.com>
+Allan McRae <allan@archlinux.org>
+"Discussion about the Arch User Repository (AUR)" <aur-general@archlinux.org>
+Olivier Berger <olivier.berger@it-sudparis.eu>
+olivier.berger@it-sudparis.eu
+Chris Wilson <chris@chris-wilson.co.uk>
+notmuch@notmuchmail.org
+Carl Worth <cworth@cworth.org>
+Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+Keith Packard <keithp@keithp.com>
+Jjgod Jiang <gzjjgod@gmail.com>
+Rolland Santimano <rollandsantimano@yahoo.com>
+Jan Janak <jan@ryngle.com>
+Stewart Smith <stewart@flamingspork.com>
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+notmuch <notmuch@notmuchmail.org>
+Alex Botero-Lowry <alex.boterolowry@gmail.com>
+Ingmar Vanhassel <ingmar@exherbo.org>
+Aron Griffis <agriffis@n01se.net>
+Adrian Perez de Castro <aperez@igalia.com>
+Israel Herraiz <isra@herraiz.org>
+Mikhail Gusarov <dottedmag@dottedmag.net>
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=sender --output=count"
+notmuch address --output=sender --output=count '*' | sort -n >OUTPUT
+cat <<EOF >EXPECTED
+1      Adrian Perez de Castro <aperez@igalia.com>
+1      Aron Griffis <agriffis@n01se.net>
+1      Chris Wilson <chris@chris-wilson.co.uk>
+1      François Boulogne <boulogne.f@gmail.com>
+1      Ingmar Vanhassel <ingmar@exherbo.org>
+1      Israel Herraiz <isra@herraiz.org>
+1      Olivier Berger <olivier.berger@it-sudparis.eu>
+1      Rolland Santimano <rollandsantimano@yahoo.com>
+2      Alex Botero-Lowry <alex.boterolowry@gmail.com>
+2      Jjgod Jiang <gzjjgod@gmail.com>
+3      Stewart Smith <stewart@flamingspork.com>
+4      Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+4      Jan Janak <jan@ryngle.com>
+5      Lars Kellogg-Stedman <lars@seas.harvard.edu>
+5      Mikhail Gusarov <dottedmag@dottedmag.net>
+7      Keith Packard <keithp@keithp.com>
+12     Carl Worth <cworth@cworth.org>
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=recipients --output=address"
+notmuch address --output=recipients --output=address '*' >OUTPUT
+cat <<EOF >EXPECTED
+allan@archlinux.org
+aur-general@archlinux.org
+olivier.berger@it-sudparis.eu
+notmuch@notmuchmail.org
+notmuch@notmuchmail.org
+keithp@keithp.com
+dottedmag@dottedmag.net
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=sender --output=address --output=count"
+notmuch address --output=sender --output=address --output=count '*' | sort -n >OUTPUT
+cat <<EOF >EXPECTED
+1      agriffis@n01se.net
+1      aperez@igalia.com
+1      boulogne.f@gmail.com
+1      chris@chris-wilson.co.uk
+1      ingmar@exherbo.org
+1      isra@herraiz.org
+1      olivier.berger@it-sudparis.eu
+1      rollandsantimano@yahoo.com
+2      alex.boterolowry@gmail.com
+2      gzjjgod@gmail.com
+3      stewart@flamingspork.com
+4      alex.boterolowry@gmail.com
+4      jan@ryngle.com
+5      dottedmag@dottedmag.net
+5      lars@seas.harvard.edu
+7      keithp@keithp.com
+12     cworth@cworth.org
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=count --format=json"
+# Since the iteration order of GHashTable is not specified, we
+# preprocess and sort the results to keep the order stable here.
+notmuch address --output=count --format=json '*' | \
+    sed -e 's/^\[//' -e 's/]$//' -e 's/,$//' | sort >OUTPUT
+cat <<EOF >EXPECTED
+{"name": "Adrian Perez de Castro", "address": "aperez@igalia.com", "name-addr": "Adrian Perez de Castro <aperez@igalia.com>", "count": 1}
+{"name": "Alex Botero-Lowry", "address": "alex.boterolowry@gmail.com", "name-addr": "Alex Botero-Lowry <alex.boterolowry@gmail.com>", "count": 2}
+{"name": "Alexander Botero-Lowry", "address": "alex.boterolowry@gmail.com", "name-addr": "Alexander Botero-Lowry <alex.boterolowry@gmail.com>", "count": 4}
+{"name": "Aron Griffis", "address": "agriffis@n01se.net", "name-addr": "Aron Griffis <agriffis@n01se.net>", "count": 1}
+{"name": "Carl Worth", "address": "cworth@cworth.org", "name-addr": "Carl Worth <cworth@cworth.org>", "count": 12}
+{"name": "Chris Wilson", "address": "chris@chris-wilson.co.uk", "name-addr": "Chris Wilson <chris@chris-wilson.co.uk>", "count": 1}
+{"name": "François Boulogne", "address": "boulogne.f@gmail.com", "name-addr": "François Boulogne <boulogne.f@gmail.com>", "count": 1}
+{"name": "Ingmar Vanhassel", "address": "ingmar@exherbo.org", "name-addr": "Ingmar Vanhassel <ingmar@exherbo.org>", "count": 1}
+{"name": "Israel Herraiz", "address": "isra@herraiz.org", "name-addr": "Israel Herraiz <isra@herraiz.org>", "count": 1}
+{"name": "Jan Janak", "address": "jan@ryngle.com", "name-addr": "Jan Janak <jan@ryngle.com>", "count": 4}
+{"name": "Jjgod Jiang", "address": "gzjjgod@gmail.com", "name-addr": "Jjgod Jiang <gzjjgod@gmail.com>", "count": 2}
+{"name": "Keith Packard", "address": "keithp@keithp.com", "name-addr": "Keith Packard <keithp@keithp.com>", "count": 7}
+{"name": "Lars Kellogg-Stedman", "address": "lars@seas.harvard.edu", "name-addr": "Lars Kellogg-Stedman <lars@seas.harvard.edu>", "count": 5}
+{"name": "Mikhail Gusarov", "address": "dottedmag@dottedmag.net", "name-addr": "Mikhail Gusarov <dottedmag@dottedmag.net>", "count": 5}
+{"name": "Olivier Berger", "address": "olivier.berger@it-sudparis.eu", "name-addr": "Olivier Berger <olivier.berger@it-sudparis.eu>", "count": 1}
+{"name": "Rolland Santimano", "address": "rollandsantimano@yahoo.com", "name-addr": "Rolland Santimano <rollandsantimano@yahoo.com>", "count": 1}
+{"name": "Stewart Smith", "address": "stewart@flamingspork.com", "name-addr": "Stewart Smith <stewart@flamingspork.com>", "count": 3}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--deduplicate=no --sort=oldest-first --output=sender"
+notmuch address --deduplicate=no --sort=oldest-first --output=sender '*' >OUTPUT
+cat <<EOF >EXPECTED
+Mikhail Gusarov <dottedmag@dottedmag.net>
+Mikhail Gusarov <dottedmag@dottedmag.net>
+Carl Worth <cworth@cworth.org>
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Mikhail Gusarov <dottedmag@dottedmag.net>
+Alex Botero-Lowry <alex.boterolowry@gmail.com>
+Carl Worth <cworth@cworth.org>
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Mikhail Gusarov <dottedmag@dottedmag.net>
+Mikhail Gusarov <dottedmag@dottedmag.net>
+Keith Packard <keithp@keithp.com>
+Keith Packard <keithp@keithp.com>
+Keith Packard <keithp@keithp.com>
+Jan Janak <jan@ryngle.com>
+Jan Janak <jan@ryngle.com>
+Jan Janak <jan@ryngle.com>
+Israel Herraiz <isra@herraiz.org>
+Adrian Perez de Castro <aperez@igalia.com>
+Aron Griffis <agriffis@n01se.net>
+Ingmar Vanhassel <ingmar@exherbo.org>
+Alex Botero-Lowry <alex.boterolowry@gmail.com>
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Stewart Smith <stewart@flamingspork.com>
+Stewart Smith <stewart@flamingspork.com>
+Keith Packard <keithp@keithp.com>
+Keith Packard <keithp@keithp.com>
+Keith Packard <keithp@keithp.com>
+Stewart Smith <stewart@flamingspork.com>
+Jjgod Jiang <gzjjgod@gmail.com>
+Jan Janak <jan@ryngle.com>
+Rolland Santimano <rollandsantimano@yahoo.com>
+Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+Jjgod Jiang <gzjjgod@gmail.com>
+Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+Keith Packard <keithp@keithp.com>
+Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+Carl Worth <cworth@cworth.org>
+Carl Worth <cworth@cworth.org>
+Carl Worth <cworth@cworth.org>
+Carl Worth <cworth@cworth.org>
+Carl Worth <cworth@cworth.org>
+Carl Worth <cworth@cworth.org>
+Carl Worth <cworth@cworth.org>
+Carl Worth <cworth@cworth.org>
+Carl Worth <cworth@cworth.org>
+Carl Worth <cworth@cworth.org>
+Chris Wilson <chris@chris-wilson.co.uk>
+Olivier Berger <olivier.berger@it-sudparis.eu>
+François Boulogne <boulogne.f@gmail.com>
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--deduplicate=no --sort=newest-first --output=sender --output=recipients"
+notmuch address --deduplicate=no --sort=newest-first --output=sender --output=recipients path:foo/new >OUTPUT
+cat <<EOF >EXPECTED
+Mikhail Gusarov <dottedmag@dottedmag.net>
+notmuch@notmuchmail.org
+Mikhail Gusarov <dottedmag@dottedmag.net>
+notmuch@notmuchmail.org
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+notmuch@notmuchmail.org
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--deduplicate=address --output=sender --output=recipients"
+notmuch address --deduplicate=address --output=sender --output=recipients '*' | sort >OUTPUT
+cat <<EOF >EXPECTED
+"Discussion about the Arch User Repository (AUR)" <aur-general@archlinux.org>
+Adrian Perez de Castro <aperez@igalia.com>
+Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+Allan McRae <allan@archlinux.org>
+Aron Griffis <agriffis@n01se.net>
+Carl Worth <cworth@cworth.org>
+Chris Wilson <chris@chris-wilson.co.uk>
+François Boulogne <boulogne.f@gmail.com>
+Ingmar Vanhassel <ingmar@exherbo.org>
+Israel Herraiz <isra@herraiz.org>
+Jan Janak <jan@ryngle.com>
+Jjgod Jiang <gzjjgod@gmail.com>
+Keith Packard <keithp@keithp.com>
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Mikhail Gusarov <dottedmag@dottedmag.net>
+Olivier Berger <olivier.berger@it-sudparis.eu>
+Rolland Santimano <rollandsantimano@yahoo.com>
+Stewart Smith <stewart@flamingspork.com>
+notmuch@notmuchmail.org
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+generate_message '[from]="Foo Bar <foo.bar@example.com>"'
+generate_message '[from]="Foo Bar <Foo.Bar@Example.Com>"'
+generate_message '[from]="Foo Bar <foo.bar@example.com>"'
+generate_message '[from]="Bar <Foo.Bar@Example.Com>"'
+generate_message '[from]="Foo <foo.bar@example.com>"'
+generate_message '[from]="<foo.bar@example.com>"'
+generate_message '[from]="foo.bar@example.com"'
+generate_message '[from]="Baz <foo.bar+baz@example.com>"'
+generate_message '[from]="Foo Bar <foo.bar+baz@example.com>"'
+generate_message '[from]="Baz <foo.bar+baz@example.com>"'
+notmuch new > /dev/null
+
+test_begin_subtest "--deduplicate=no --output=sender"
+notmuch address --deduplicate=no --output=sender from:example.com | sort >OUTPUT
+cat <<EOF >EXPECTED
+Bar <Foo.Bar@Example.Com>
+Baz <foo.bar+baz@example.com>
+Baz <foo.bar+baz@example.com>
+Foo <foo.bar@example.com>
+Foo Bar <Foo.Bar@Example.Com>
+Foo Bar <foo.bar+baz@example.com>
+Foo Bar <foo.bar@example.com>
+Foo Bar <foo.bar@example.com>
+foo.bar@example.com
+foo.bar@example.com
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--deduplicate=mailbox --output=sender --output=count"
+notmuch address --deduplicate=mailbox --output=sender --output=count from:example.com | sort -n >OUTPUT
+cat <<EOF >EXPECTED
+1      Bar <Foo.Bar@Example.Com>
+1      Foo <foo.bar@example.com>
+1      Foo Bar <Foo.Bar@Example.Com>
+1      Foo Bar <foo.bar+baz@example.com>
+2      Baz <foo.bar+baz@example.com>
+2      Foo Bar <foo.bar@example.com>
+2      foo.bar@example.com
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--deduplicate=address --output=sender --output=count"
+notmuch address --deduplicate=address --output=sender --output=count from:example.com | sort -n >OUTPUT
+cat <<EOF >EXPECTED
+3      Baz <foo.bar+baz@example.com>
+7      Foo Bar <foo.bar@example.com>
+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
diff --git a/test/T100-search-by-folder.sh b/test/T100-search-by-folder.sh
new file mode 100755 (executable)
index 0000000..b4f6294
--- /dev/null
@@ -0,0 +1,166 @@
+#!/usr/bin/env bash
+test_description='"notmuch search" by folder: and path: (with variations)'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+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 "Single-world folder: specification (multiple results)"
+output=$(notmuch search folder:bad folder:bad/news folder: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)"
+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)"
+
+test_begin_subtest "Two-word path to narrow results to one"
+output=$(notmuch search folder:bad/news | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread)"
+
+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-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"
+notmuch new
+output=$(notmuch search folder:bad/news | notmuch_search_sanitize)
+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-XXX"
+
+test_begin_subtest "After removing duplicate instance of matching path part #2"
+output=$(notmuch search folder:duplicate/bad/news | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Bears (inbox unread)"
+
+test_begin_subtest "After rename, old path returns nothing"
+mv "${MAIL_DIR}/duplicate/bad/news" "${MAIL_DIR}/duplicate/bad/olds"
+notmuch new
+output=$(notmuch search folder:duplicate/bad/news | notmuch_search_sanitize)
+test_expect_equal "$output" ""
+
+test_begin_subtest "After rename, new path returns result"
+output=$(notmuch search folder:duplicate/bad/olds | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Bears (inbox unread)"
+
+# folder: and path: searches with full corpus
+add_email_corpus
+
+# add some more dupes
+cp $MAIL_DIR/foo/new/03:2, $MAIL_DIR/new
+cp $MAIL_DIR/bar/baz/05:2, $MAIL_DIR/foo
+notmuch new >/dev/null
+
+test_begin_subtest "folder: search"
+output=$(notmuch search --output=files folder:foo | notmuch_search_files_sanitize | sort)
+# bar/baz/05:2, is a duplicate of foo/05:2,
+# new/03:2, is a duplicate of foo/new/03:2,
+test_expect_equal "$output" "MAIL_DIR/bar/baz/05:2,
+MAIL_DIR/foo/05:2,
+MAIL_DIR/foo/06:2,
+MAIL_DIR/foo/cur/07:2,
+MAIL_DIR/foo/cur/08:2,
+MAIL_DIR/foo/new/03:2,
+MAIL_DIR/foo/new/09:2,
+MAIL_DIR/foo/new/10:2,
+MAIL_DIR/new/03:2,"
+
+test_begin_subtest "top level folder: search"
+output=$(notmuch search --output=files folder:'""' | notmuch_search_files_sanitize | sort)
+# bar/18:2, is a duplicate of cur/51:2,
+# foo/new/03:2, is a duplicate of new/03:2,
+test_expect_equal "$output" "MAIL_DIR/01:2,
+MAIL_DIR/02:2,
+MAIL_DIR/bar/18:2,
+MAIL_DIR/cur/29:2,
+MAIL_DIR/cur/30:2,
+MAIL_DIR/cur/31:2,
+MAIL_DIR/cur/32:2,
+MAIL_DIR/cur/33:2,
+MAIL_DIR/cur/34:2,
+MAIL_DIR/cur/35:2,
+MAIL_DIR/cur/36:2,
+MAIL_DIR/cur/37:2,
+MAIL_DIR/cur/38:2,
+MAIL_DIR/cur/39:2,
+MAIL_DIR/cur/40:2,
+MAIL_DIR/cur/41:2,
+MAIL_DIR/cur/42:2,
+MAIL_DIR/cur/43:2,
+MAIL_DIR/cur/44:2,
+MAIL_DIR/cur/45:2,
+MAIL_DIR/cur/46:2,
+MAIL_DIR/cur/47:2,
+MAIL_DIR/cur/48:2,
+MAIL_DIR/cur/49:2,
+MAIL_DIR/cur/50:2,
+MAIL_DIR/cur/51:2,
+MAIL_DIR/cur/52:2,
+MAIL_DIR/cur/53:2,
+MAIL_DIR/foo/new/03:2,
+MAIL_DIR/new/03:2,
+MAIL_DIR/new/04:2,"
+
+test_begin_subtest "path: search"
+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 "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,
+MAIL_DIR/02:2,"
+
+test_begin_subtest "recursive path: search"
+output=$(notmuch search --output=files path:"bar/**" | notmuch_search_files_sanitize | sort)
+# cur/51:2, is a duplicate of bar/18:2,
+# foo/05:2, is a duplicate of bar/baz/05:2,
+test_expect_equal "$output" "MAIL_DIR/bar/17:2,
+MAIL_DIR/bar/18:2,
+MAIL_DIR/bar/baz/05:2,
+MAIL_DIR/bar/baz/23:2,
+MAIL_DIR/bar/baz/24:2,
+MAIL_DIR/bar/baz/cur/25:2,
+MAIL_DIR/bar/baz/cur/26:2,
+MAIL_DIR/bar/baz/new/27:2,
+MAIL_DIR/bar/baz/new/28:2,
+MAIL_DIR/bar/cur/19:2,
+MAIL_DIR/bar/cur/20:2,
+MAIL_DIR/bar/new/21:2,
+MAIL_DIR/bar/new/22:2,
+MAIL_DIR/cur/51:2,
+MAIL_DIR/foo/05:2,"
+
+test_done
diff --git a/test/T110-search-position-overlap-bug.sh b/test/T110-search-position-overlap-bug.sh
new file mode 100755 (executable)
index 0000000..f4d5ee1
--- /dev/null
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+# Test to demonstrate a position overlap bug.
+#
+# At one point, notmuch would index terms incorrectly in the case of
+# calling index_terms multiple times for a single field. The term
+# generator was being reset to position 0 each time. This means that
+# with text such as:
+#
+#      To: a@b.c, x@y.z
+#
+# one could get a bogus match by searching for:
+#
+#      To: a@y.c
+#
+# Thanks to Mark Anderson for reporting the bug, (and providing a nice,
+# minimal test case that inspired what is used here), in
+# id:3wd4o8wa7fx.fsf@testarossa.amd.com
+
+test_description='that notmuch does not overlap term positions'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_message '[to]="a@b.c, x@y.z"'
+
+test_begin_subtest "Search for a@b.c matches"
+output=$(notmuch search a@b.c | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Test message #1 (inbox unread)"
+
+test_begin_subtest "Search for x@y.z matches"
+output=$(notmuch search x@y.z | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Test message #1 (inbox unread)"
+
+test_begin_subtest "Search for a@y.c must not match"
+output=$(notmuch search a@y.c | notmuch_search_sanitize)
+test_expect_equal "$output" ""
+
+test_done
diff --git a/test/T120-search-insufficient-from-quoting.sh b/test/T120-search-insufficient-from-quoting.sh
new file mode 100755 (executable)
index 0000000..509fec8
--- /dev/null
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+test_description='messages with unquoted . in name'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_message \
+  '[from]="Some.Name for Someone <bugs@quoting.com>"' \
+  '[subject]="This message needs more quoting on the From line"'
+
+add_message \
+  '[from]="\"Some.Name for Someone\" <bugs@quoting.com>"' \
+  '[subject]="This message has necessary quoting in place"'
+
+add_message \
+  '[from]="No.match Here <filler@mail.com>"' \
+  '[subject]="This message needs more quoting on the From line"'
+
+add_message \
+  '[from]="\"No.match Here\" <filler@mail.com>"' \
+  '[subject]="This message has necessary quoting in place"'
+
+
+test_begin_subtest "Search by first name"
+output=$(notmuch search from:Some.Name | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Some.Name for Someone; This message needs more quoting on the From line (inbox unread)
+thread:XXX   2001-01-05 [1/1] Some.Name for Someone; This message has necessary quoting in place (inbox unread)"
+
+test_begin_subtest "Search by last name:"
+output=$(notmuch search from:Someone | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Some.Name for Someone; This message needs more quoting on the From line (inbox unread)
+thread:XXX   2001-01-05 [1/1] Some.Name for Someone; This message has necessary quoting in place (inbox unread)"
+
+test_begin_subtest "Search by address:"
+output=$(notmuch search from:bugs@quoting.com | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Some.Name for Someone; This message needs more quoting on the From line (inbox unread)
+thread:XXX   2001-01-05 [1/1] Some.Name for Someone; This message has necessary quoting in place (inbox unread)"
+
+test_begin_subtest "Search for all messages:"
+output=$(notmuch search '*' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Some.Name for Someone; This message needs more quoting on the From line (inbox unread)
+thread:XXX   2001-01-05 [1/1] Some.Name for Someone; This message has necessary quoting in place (inbox unread)
+thread:XXX   2001-01-05 [1/1] No.match Here; This message needs more quoting on the From line (inbox unread)
+thread:XXX   2001-01-05 [1/1] No.match Here; This message has necessary quoting in place (inbox unread)"
+
+test_done
diff --git a/test/T130-search-limiting.sh b/test/T130-search-limiting.sh
new file mode 100755 (executable)
index 0000000..8a30e7a
--- /dev/null
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+test_description='"notmuch search" --offset and --limit parameters'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+for outp in messages threads; do
+    test_begin_subtest "${outp}: limit does the right thing"
+    notmuch search --output=${outp} "*" | head -n 20 >expected
+    notmuch search --output=${outp} --limit=20 "*" >output
+    test_expect_equal_file expected output
+
+    test_begin_subtest "${outp}: concatenation of limited searches"
+    notmuch search --output=${outp} "*" | head -n 20 >expected
+    notmuch search --output=${outp} --limit=10 "*" >output
+    notmuch search --output=${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} "*"`
+    notmuch search --output=${outp} "*" >expected
+    notmuch search --output=${outp} --limit=$((1 + ${N})) "*" >output
+    test_expect_equal_file expected output
+
+    test_begin_subtest "${outp}: limit = 0"
+    test_expect_equal "`notmuch search --output=${outp} --limit=0 "*"`" ""
+
+    test_begin_subtest "${outp}: offset does the right thing"
+    # note: tail -n +N is 1-based
+    notmuch search --output=${outp} "*" | tail -n +21 >expected
+    notmuch search --output=${outp} --offset=20 "*" >output
+    test_expect_equal_file expected output
+
+    test_begin_subtest "${outp}: offset = 0"
+    notmuch search --output=${outp} "*" >expected
+    notmuch search --output=${outp} --offset=0 "*" >output
+    test_expect_equal_file expected output
+
+    test_begin_subtest "${outp}: negative offset"
+    notmuch search --output=${outp} "*" | tail -n 20 >expected
+    notmuch search --output=${outp} --offset=-20 "*" >output
+    test_expect_equal_file expected output
+
+    test_begin_subtest "${outp}: negative offset"
+    notmuch search --output=${outp} "*" | tail -n 1 >expected
+    notmuch search --output=${outp} --offset=-1 "*" >output
+    test_expect_equal_file expected output
+
+    test_begin_subtest "${outp}: negative offset combined with limit"
+    notmuch search --output=${outp} "*" | tail -n 20 | head -n 10 >expected
+    notmuch search --output=${outp} --offset=-20 --limit=10 "*" >output
+    test_expect_equal_file expected output
+
+    test_begin_subtest "${outp}: negative offset combined with equal limit"
+    notmuch search --output=${outp} "*" | tail -n 20 >expected
+    notmuch search --output=${outp} --offset=-20 --limit=20 "*" >output
+    test_expect_equal_file expected output
+
+    test_begin_subtest "${outp}: negative offset combined with large limit"
+    notmuch search --output=${outp} "*" | tail -n 20 >expected
+    notmuch search --output=${outp} --offset=-20 --limit=50 "*" >output
+    test_expect_equal_file expected output
+
+    test_begin_subtest "${outp}: negative offset larger then results"
+    N=`notmuch count --output=${outp} "*"`
+    notmuch search --output=${outp} "*" >expected
+    notmuch search --output=${outp} --offset=-$((1 + ${N})) "*" >output
+    test_expect_equal_file expected output
+done
+
+test_done
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
diff --git a/test/T140-excludes.sh b/test/T140-excludes.sh
new file mode 100755 (executable)
index 0000000..e9cc9cb
--- /dev/null
@@ -0,0 +1,469 @@
+#!/usr/bin/env bash
+test_description='"notmuch search, count and show" with excludes in several variations'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+# 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 () {
+    local subject="$1"
+    local length="$2"
+    generate_message '[subject]="'"${subject}: top message"'"' '[body]="'"body of top message"'"'
+    parent_id=$gen_msg_id
+    gen_thread_msg_id[0]=$gen_msg_id
+    for i in `seq 1 $length`
+    do
+       generate_message '[subject]="'"${subject}: reply $i"'"' \
+                        "[in-reply-to]=\<$parent_id\>" \
+                        '[body]="'"body of reply $i"'"'
+       gen_thread_msg_id[$i]=$gen_msg_id
+       parent_id=$gen_msg_id
+    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]} 2>/dev/null)
+}
+
+#############################################
+# These are the original search exclude tests.
+
+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 subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)"
+
+test_begin_subtest "Search, exclude \"deleted\" messages; alternate config file"
+cp ${NOTMUCH_CONFIG} alt-config
+notmuch config set search.exclude_tags
+notmuch --config=alt-config search subject:deleted | notmuch_search_sanitize > OUTPUT
+cp alt-config ${NOTMUCH_CONFIG}
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Search, exclude \"deleted\" messages from message search"
+output=$(notmuch search --output=messages subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "id:$not_deleted_id"
+
+test_begin_subtest "Search, exclude \"deleted\" messages from message search --exclude=false"
+output=$(notmuch search --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 message search (non-existent exclude-tag)"
+notmuch config set search.exclude_tags deleted non_existent_tag
+output=$(notmuch search --output=messages subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "id:$not_deleted_id"
+notmuch config set search.exclude_tags deleted
+
+test_begin_subtest "Search, exclude \"deleted\" messages from search, overridden"
+output=$(notmuch search subject:deleted and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Deleted (deleted inbox unread)"
+
+test_begin_subtest "Search, exclude \"deleted\" messages from threads"
+add_message '[subject]="Not deleted reply"' '[in-reply-to]="<$gen_msg_id>"'
+output=$(notmuch search subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+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 --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 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)"
+
+
+########################################################
+# We construct some threads for the tests. We use the tag "test" to
+# indicate which messages we will search for.
+
+# A thread of deleted messages; test matches one of them.
+generate_thread "All messages excluded: single match" 5
+notmuch tag +deleted $gen_thread_id
+notmuch tag +test id:${gen_thread_msg_id[2]}
+
+# A thread of deleted messages; test matches two of them.
+generate_thread "All messages excluded: double match" 5
+notmuch tag +deleted $gen_thread_id
+notmuch tag +test id:${gen_thread_msg_id[2]}
+notmuch tag +test id:${gen_thread_msg_id[4]}
+
+# A thread some messages deleted; test only matches a deleted message.
+generate_thread "Some messages excluded: single excluded match" 5
+notmuch tag +deleted +test id:${gen_thread_msg_id[3]}
+
+# A thread some messages deleted; test only matches a non-deleted message.
+generate_thread "Some messages excluded: single non-excluded match" 5
+notmuch tag +deleted id:${gen_thread_msg_id[2]}
+notmuch tag +test id:${gen_thread_msg_id[4]}
+
+# A thread no messages deleted; test matches a message.
+generate_thread "No messages excluded: single match" 5
+notmuch tag +test id:${gen_thread_msg_id[3]}
+
+# Temporarily remove excludes to get list of matching messages
+notmuch config set search.exclude_tags
+matching_message_ids=( `notmuch search --output=messages tag:test` )
+notmuch config set search.exclude_tags deleted
+
+#########################################
+# Notmuch search tests
+
+test_begin_subtest "Search, default exclusion (thread summary)"
+output=$(notmuch search tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; No messages excluded: single match: reply 3 (inbox test unread)"
+
+test_begin_subtest "Search, default exclusion (messages)"
+output=$(notmuch search --output=messages tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[4]}
+${matching_message_ids[5]}"
+
+test_begin_subtest "Search, exclude=true (thread summary)"
+output=$(notmuch search --exclude=true tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; No messages excluded: single match: reply 3 (inbox test unread)"
+
+test_begin_subtest "Search, exclude=true (messages)"
+output=$(notmuch search --exclude=true --output=messages tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[4]}
+${matching_message_ids[5]}"
+
+test_begin_subtest "Search, exclude=false (thread summary)"
+output=$(notmuch search --exclude=false tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; No messages excluded: single match: reply 3 (inbox test unread)"
+
+test_begin_subtest "Search, exclude=false (messages)"
+output=$(notmuch search --exclude=false --output=messages tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}
+${matching_message_ids[4]}
+${matching_message_ids[5]}"
+
+test_begin_subtest "Search, exclude=flag (thread summary)"
+output=$(notmuch search --exclude=flag tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [0/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [0/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [0/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; No messages excluded: single match: reply 3 (inbox test unread)"
+
+test_begin_subtest "Search, exclude=flag (messages)"
+output=$(notmuch search --exclude=flag --output=messages tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}
+${matching_message_ids[4]}
+${matching_message_ids[5]}"
+
+test_begin_subtest "Search, exclude=all (thread summary)"
+output=$(notmuch search --exclude=all tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/5] Notmuch Test Suite; Some messages excluded: single non-excluded match: reply 4 (inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; No messages excluded: single match: reply 3 (inbox test unread)"
+
+test_begin_subtest "Search, exclude=all (messages)"
+output=$(notmuch search --exclude=all --output=messages tag:test | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[4]}
+${matching_message_ids[5]}"
+
+test_begin_subtest "Search, default exclusion: tag in query (thread summary)"
+output=$(notmuch search tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)"
+
+test_begin_subtest "Search, default exclusion: tag in query (messages)"
+output=$(notmuch search --output=messages tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}"
+
+test_begin_subtest "Search, exclude=true: tag in query (thread summary)"
+output=$(notmuch search --exclude=true tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)"
+
+test_begin_subtest "Search, exclude=true: tag in query (messages)"
+output=$(notmuch search --exclude=true --output=messages tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}"
+
+test_begin_subtest "Search, exclude=false: tag in query (thread summary)"
+output=$(notmuch search --exclude=false tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)"
+
+test_begin_subtest "Search, exclude=false: tag in query (messages)"
+output=$(notmuch search --exclude=false --output=messages tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}"
+
+test_begin_subtest "Search, exclude=flag: tag in query (thread summary)"
+output=$(notmuch search --exclude=flag tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)"
+
+test_begin_subtest "Search, exclude=flag: tag in query (messages)"
+output=$(notmuch search --exclude=flag --output=messages tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}"
+
+test_begin_subtest "Search, exclude=all: tag in query (thread summary)"
+output=$(notmuch search --exclude=all tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; All messages excluded: single match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [2/6] Notmuch Test Suite; All messages excluded: double match: reply 2 (deleted inbox test unread)
+thread:XXX   2001-01-05 [1/6] Notmuch Test Suite; Some messages excluded: single excluded match: reply 3 (deleted inbox test unread)"
+
+test_begin_subtest "Search, exclude=all: tag in query (messages)"
+output=$(notmuch search --exclude=all --output=messages tag:test and tag:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "${matching_message_ids[0]}
+${matching_message_ids[1]}
+${matching_message_ids[2]}
+${matching_message_ids[3]}"
+
+#########################################################
+# Notmuch count tests
+
+test_begin_subtest "Count, default exclusion (messages)"
+output=$(notmuch count tag:test)
+test_expect_equal "$output" "2"
+
+test_begin_subtest "Count, default exclusion (threads)"
+output=$(notmuch count --output=threads tag:test)
+test_expect_equal "$output" "2"
+
+test_begin_subtest "Count, exclude=true (messages)"
+output=$(notmuch count --exclude=true tag:test)
+test_expect_equal "$output" "2"
+
+test_begin_subtest "Count, exclude=true (threads)"
+output=$(notmuch count --output=threads --exclude=true tag:test)
+test_expect_equal "$output" "2"
+
+test_begin_subtest "Count, exclude=false (messages)"
+output=$(notmuch count --exclude=false tag:test)
+test_expect_equal "$output" "6"
+
+test_begin_subtest "Count, exclude=false (threads)"
+output=$(notmuch count --output=threads --exclude=false tag:test)
+test_expect_equal "$output" "5"
+
+test_begin_subtest "Count, default exclusion: tag in query (messages)"
+output=$(notmuch count tag:test and tag:deleted)
+test_expect_equal "$output" "4"
+
+test_begin_subtest "Count, default exclusion: tag in query (threads)"
+output=$(notmuch count --output=threads tag:test and tag:deleted)
+test_expect_equal "$output" "3"
+
+test_begin_subtest "Count, default exclusion, batch"
+notmuch count  --batch --output=messages<<EOF > OUTPUT
+tag:test
+tag:test and tag:deleted
+tag:test
+tag:test and tag:deleted
+EOF
+cat <<EOF >EXPECTED
+2
+4
+2
+4
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Count, exclude=true: tag in query (messages)"
+output=$(notmuch count --exclude=true tag:test and tag:deleted)
+test_expect_equal "$output" "4"
+
+test_begin_subtest "Count, exclude=true: tag in query (threads)"
+output=$(notmuch count --output=threads --exclude=true tag:test and tag:deleted)
+test_expect_equal "$output" "3"
+
+test_begin_subtest "Count, exclude=false: tag in query (messages)"
+output=$(notmuch count --exclude=false tag:test and tag:deleted)
+test_expect_equal "$output" "4"
+
+test_begin_subtest "Count, exclude=false: tag in query (threads)"
+output=$(notmuch count --output=threads --exclude=false tag:test and tag:deleted)
+test_expect_equal "$output" "3"
+
+#############################################################
+# Show tests
+
+test_begin_subtest "Show, default exclusion"
+output=$(notmuch show tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" "\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3"
+
+test_begin_subtest "Show, default exclusion (entire-thread)"
+output=$(notmuch show --entire-thread tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" "\fmessage{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: top message
+\fmessage{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 1
+\fmessage{ id:XXXXX depth:2 match:0 excluded:1 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 2
+\fmessage{ id:XXXXX depth:3 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 3
+\fmessage{ id:XXXXX depth:4 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+\fmessage{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 5
+\fmessage{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: top message
+\fmessage{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 1
+\fmessage{ id:XXXXX depth:2 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 2
+\fmessage{ id:XXXXX depth:3 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3
+\fmessage{ id:XXXXX depth:4 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 4
+\fmessage{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 5"
+
+test_begin_subtest "Show, exclude=true"
+output=$(notmuch show --exclude=true tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" "\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3"
+
+test_begin_subtest "Show, exclude=true (entire-thread)"
+output=$(notmuch show --entire-thread --exclude=true tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" "\fmessage{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: top message
+\fmessage{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 1
+\fmessage{ id:XXXXX depth:2 match:0 excluded:1 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 2
+\fmessage{ id:XXXXX depth:3 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 3
+\fmessage{ id:XXXXX depth:4 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+\fmessage{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 5
+\fmessage{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: top message
+\fmessage{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 1
+\fmessage{ id:XXXXX depth:2 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 2
+\fmessage{ id:XXXXX depth:3 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3
+\fmessage{ id:XXXXX depth:4 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 4
+\fmessage{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 5"
+
+test_begin_subtest "Show, exclude=false"
+output=$(notmuch show --exclude=false tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" "\fmessage{ id:XXXXX depth:0 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 2
+\fmessage{ id:XXXXX depth:0 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 2
+\fmessage{ id:XXXXX depth:1 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 4
+\fmessage{ id:XXXXX depth:0 match:1 excluded:1 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 3
+\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3"
+
+test_begin_subtest "Show, exclude=false (entire-thread)"
+output=$(notmuch show --entire-thread --exclude=false tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{")
+test_expect_equal "$output" "\fmessage{ id:XXXXX depth:0 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: top message
+\fmessage{ id:XXXXX depth:1 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 1
+\fmessage{ id:XXXXX depth:2 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 2
+\fmessage{ id:XXXXX depth:3 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 3
+\fmessage{ id:XXXXX depth:4 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 4
+\fmessage{ id:XXXXX depth:5 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: single match: reply 5
+\fmessage{ id:XXXXX depth:0 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: top message
+\fmessage{ id:XXXXX depth:1 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 1
+\fmessage{ id:XXXXX depth:2 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 2
+\fmessage{ id:XXXXX depth:3 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 3
+\fmessage{ id:XXXXX depth:4 match:1 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 4
+\fmessage{ id:XXXXX depth:5 match:0 excluded:1 filename:XXXXX
+Subject: All messages excluded: double match: reply 5
+\fmessage{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single excluded match: top message
+\fmessage{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 1
+\fmessage{ id:XXXXX depth:2 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 2
+\fmessage{ id:XXXXX depth:3 match:1 excluded:1 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 3
+\fmessage{ id:XXXXX depth:4 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 4
+\fmessage{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single excluded match: reply 5
+\fmessage{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: top message
+\fmessage{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 1
+\fmessage{ id:XXXXX depth:2 match:0 excluded:1 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 2
+\fmessage{ id:XXXXX depth:3 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 3
+\fmessage{ id:XXXXX depth:4 match:1 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 4
+\fmessage{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: Some messages excluded: single non-excluded match: reply 5
+\fmessage{ id:XXXXX depth:0 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: top message
+\fmessage{ id:XXXXX depth:1 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 1
+\fmessage{ id:XXXXX depth:2 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 2
+\fmessage{ id:XXXXX depth:3 match:1 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 3
+\fmessage{ id:XXXXX depth:4 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 4
+\fmessage{ id:XXXXX depth:5 match:0 excluded:0 filename:XXXXX
+Subject: No messages excluded: single match: reply 5"
+
+
+test_done
diff --git a/test/T150-tagging.sh b/test/T150-tagging.sh
new file mode 100755 (executable)
index 0000000..273c0af
--- /dev/null
@@ -0,0 +1,357 @@
+#!/usr/bin/env bash
+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'
+
+test_begin_subtest "Adding tags"
+notmuch tag +tag1 +tag2 +tag3 \*
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag1 tag2 tag3 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 tag2 tag3 unread)"
+
+test_begin_subtest "Removing tags"
+notmuch tag -tag1 -tag2 \*
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag3 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag3 unread)"
+
+test_begin_subtest "No tag operations"
+test_expect_code 1 'notmuch tag One'
+
+test_begin_subtest "No query"
+test_expect_code 1 'notmuch tag +tag2'
+
+test_begin_subtest "Redundant tagging"
+notmuch tag +tag1 -tag3 One
+notmuch tag +tag1 -tag3 \*
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag1 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 unread)"
+
+test_begin_subtest "Remove all"
+notmuch tag --remove-all One
+notmuch tag --remove-all +tag5 +tag6 +unread Two
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One ()
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (tag5 tag6 unread)"
+
+test_begin_subtest "Remove all with batch"
+notmuch tag +tag1 One
+notmuch tag --remove-all --batch <<EOF
+-- One
++tag3 +tag4 +inbox -- Two
+EOF
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One ()
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag3 tag4)"
+
+test_begin_subtest "Remove all with a no-op"
+notmuch tag +inbox +tag1 +unread One
+notmuch tag --remove-all +foo +inbox +tag1 -foo +unread Two
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag1 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 unread)"
+
+test_begin_subtest "Special characters in tags"
+notmuch tag +':" ' \*
+notmuch tag -':" ' Two
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (:\"  inbox tag1 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 unread)"
+
+test_begin_subtest "Tagging order"
+notmuch tag +tag4 -tag4 One
+notmuch tag -tag4 +tag4 Two
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (:\"  inbox tag1 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 tag4 unread)"
+
+test_begin_subtest "--batch"
+notmuch tag --batch <<EOF
+# %20 is a space in tag
+-:"%20 -tag1 +tag5 +tag6 -- One
++tag1 -tag1 -tag4 +tag4 -- Two
+-tag6 One
++tag5 Two
+EOF
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag5 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag4 tag5 unread)"
+
+# generate a common input file for the next several tests.
+cat > batch.in <<EOF
+# %40 is an @ in tag
++%40 -tag5 +tag6 -- One
++tag1 -tag1 -tag4 +tag4 -- Two
+-tag5 +tag6 Two
+EOF
+
+cat > batch.expected <<EOF
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (@ inbox tag6 unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag4 tag6 unread)
+EOF
+
+test_begin_subtest "--input"
+notmuch dump --format=batch-tag > backup.tags
+notmuch tag --input=batch.in
+notmuch search \* | notmuch_search_sanitize > OUTPUT
+notmuch restore --format=batch-tag < backup.tags
+test_expect_equal_file batch.expected OUTPUT
+
+test_begin_subtest "--batch --input"
+notmuch dump --format=batch-tag > backup.tags
+notmuch tag --batch --input=batch.in
+notmuch search \* | notmuch_search_sanitize > OUTPUT
+notmuch restore --format=batch-tag < backup.tags
+test_expect_equal_file batch.expected OUTPUT
+
+test_begin_subtest "--batch --input --remove-all"
+notmuch dump --format=batch-tag > backup.tags
+notmuch tag +foo +bar -- One
+notmuch tag +tag7 -- Two
+notmuch tag --batch --input=batch.in --remove-all
+notmuch search \* | notmuch_search_sanitize > OUTPUT
+notmuch restore --format=batch-tag < backup.tags
+cat > batch_removeall.expected <<EOF
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (@ tag6)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (tag6)
+EOF
+test_expect_equal_file batch_removeall.expected OUTPUT
+rm batch_removeall.expected
+
+test_begin_subtest "--batch, dependence on previous line"
+notmuch dump --format=batch-tag > backup.tags
+notmuch tag --batch<<EOF
++trigger -- One
++second_tag -- tag:trigger
+EOF
+NOTMUCH_DUMP_TAGS tag:second_tag > OUTPUT
+notmuch restore --format=batch-tag < backup.tags
+cat <<EOF >EXPECTED
++inbox +second_tag +tag5 +trigger +unread -- id:msg-001@notmuch-test-suite
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--batch, blank lines and comments"
+notmuch dump | sort > EXPECTED
+notmuch tag --batch <<EOF
+# this line is a comment; the next has only white space
+        
+
+# the previous line is empty
+EOF
+notmuch dump | sort > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest '--batch: checking error messages'
+notmuch dump --format=batch-tag > BACKUP
+notmuch tag --batch <<EOF 2>OUTPUT
+# the next line has a space
+# this line has no tag operations, but this is permitted in batch format.
+a
++0
++a +b
+# trailing whitespace
++a +b 
++c +d --
+# this is a harmless comment, do not yell about it.
+
+# the previous line was blank; also no yelling please
++%zz -- id:whatever
+# the next non-comment line should report an an empty tag error for
+# batch tagging, but not for restore
++ +e -- id:foo
++- -- id:foo
+EOF
+
+cat <<EOF > EXPECTED
+Warning: no query string [+0]
+Warning: no query string [+a +b]
+Warning: missing query string [+a +b ]
+Warning: no query string after -- [+c +d --]
+Warning: hex decoding of tag %zz failed [+%zz -- id:whatever]
+Warning: empty tag forbidden [+ +e -- id:foo]
+Warning: tag starting with '-' forbidden [+- -- id:foo]
+EOF
+
+notmuch restore --format=batch-tag < BACKUP
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest '--batch: tags with quotes'
+notmuch dump --format=batch-tag > BACKUP
+
+notmuch tag --batch <<EOF
++%22%27%22%27%22%22%27%27 -- One
+-%22%27%22%27%22%22%27%27 -- One
++%22%27%22%22%22%27 -- One
++%22%27%22%27%22%22%27%27 -- Two
+EOF
+
+cat <<EOF > EXPECTED
++%22%27%22%22%22%27 +inbox +tag5 +unread -- id:msg-001@notmuch-test-suite
++%22%27%22%27%22%22%27%27 +inbox +tag4 +tag5 +unread -- id:msg-002@notmuch-test-suite
+EOF
+
+NOTMUCH_DUMP_TAGS > OUTPUT
+notmuch restore --format=batch-tag < BACKUP
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest '--batch: tags with punctuation and space'
+notmuch dump --format=batch-tag > BACKUP
+
+notmuch tag --batch <<EOF
++%21@%23%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e -- One
+-%21@%23%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e -- One
++%21@%23%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%20%60%7e -- Two
+-%21@%23%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%20%60%7e -- Two
++%21@%23%20%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e -- One
++%21@%23%20%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e -- Two
+EOF
+
+cat <<EOF > EXPECTED
++%21@%23%20%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e +inbox +tag4 +tag5 +unread -- id:msg-002@notmuch-test-suite
++%21@%23%20%24%25%5e%26%2a%29-_=+%5b%7b%5c%20%7c%3b%3a%27%20%22,.%3c%60%7e +inbox +tag5 +unread -- id:msg-001@notmuch-test-suite
+EOF
+
+NOTMUCH_DUMP_TAGS > OUTPUT
+notmuch restore --format=batch-tag < BACKUP
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest '--batch: unicode tags'
+notmuch dump --format=batch-tag > BACKUP
+
+notmuch tag --batch <<EOF
++%2a@%7d%cf%b5%f4%85%80%adO3%da%a7 -- One
++=%e0%ac%95%c8%b3+%ef%aa%95%c8%a64w%c7%9d%c9%a2%cf%b3%d6%82%24B%c4%a9%c5%a1UX%ee%99%b0%27E7%ca%a4%d0%8b%5d -- One
++A%e1%a0%bc%de%8b%d5%b2V%d9%9b%f3%b5%a2%a3M%d8%a1u@%f0%a0%ac%948%7e%f0%ab%86%af%27 -- One
++R -- One
++%da%88=f%cc%b9I%ce%af%7b%c9%97%e3%b9%8bH%cb%92X%d2%8c6 -- One
++%dc%9crh%d2%86B%e5%97%a2%22t%ed%99%82d -- One
++L%df%85%ef%a1%a5m@%d3%96%c2%ab%d4%9f%ca%b8%f3%b3%a2%bf%c7%b1_u%d7%b4%c7%b1 -- One
++P%c4%98%2f -- One
++%7e%d1%8b%25%ec%a0%ae%d1%a0M%3b%e3%b6%b7%e9%a4%87%3c%db%9a%cc%a8%e1%96%9d -- One
++%c4%bf7%c7%ab9H%c4%99k%ea%91%bd%c3%8ck%e2%b3%8dk%c5%952V%e4%99%b2%d9%b3%e4%8b%bda%5b%24%c7%9b -- One
++%2a@%7d%cf%b5%f4%85%80%adO3%da%a7  +=%e0%ac%95%c8%b3+%ef%aa%95%c8%a64w%c7%9d%c9%a2%cf%b3%d6%82%24B%c4%a9%c5%a1UX%ee%99%b0%27E7%ca%a4%d0%8b%5d  +A%e1%a0%bc%de%8b%d5%b2V%d9%9b%f3%b5%a2%a3M%d8%a1u@%f0%a0%ac%948%7e%f0%ab%86%af%27  +R  +%da%88=f%cc%b9I%ce%af%7b%c9%97%e3%b9%8bH%cb%92X%d2%8c6  +%dc%9crh%d2%86B%e5%97%a2%22t%ed%99%82d  +L%df%85%ef%a1%a5m@%d3%96%c2%ab%d4%9f%ca%b8%f3%b3%a2%bf%c7%b1_u%d7%b4%c7%b1  +P%c4%98%2f  +%7e%d1%8b%25%ec%a0%ae%d1%a0M%3b%e3%b6%b7%e9%a4%87%3c%db%9a%cc%a8%e1%96%9d  +%c4%bf7%c7%ab9H%c4%99k%ea%91%bd%c3%8ck%e2%b3%8dk%c5%952V%e4%99%b2%d9%b3%e4%8b%bda%5b%24%c7%9b -- Two
+EOF
+
+cat <<EOF > EXPECTED
++%2a@%7d%cf%b5%f4%85%80%adO3%da%a7 +=%e0%ac%95%c8%b3+%ef%aa%95%c8%a64w%c7%9d%c9%a2%cf%b3%d6%82%24B%c4%a9%c5%a1UX%ee%99%b0%27E7%ca%a4%d0%8b%5d +A%e1%a0%bc%de%8b%d5%b2V%d9%9b%f3%b5%a2%a3M%d8%a1u@%f0%a0%ac%948%7e%f0%ab%86%af%27 +L%df%85%ef%a1%a5m@%d3%96%c2%ab%d4%9f%ca%b8%f3%b3%a2%bf%c7%b1_u%d7%b4%c7%b1 +P%c4%98%2f +R +inbox +tag4 +tag5 +unread +%7e%d1%8b%25%ec%a0%ae%d1%a0M%3b%e3%b6%b7%e9%a4%87%3c%db%9a%cc%a8%e1%96%9d +%c4%bf7%c7%ab9H%c4%99k%ea%91%bd%c3%8ck%e2%b3%8dk%c5%952V%e4%99%b2%d9%b3%e4%8b%bda%5b%24%c7%9b +%da%88=f%cc%b9I%ce%af%7b%c9%97%e3%b9%8bH%cb%92X%d2%8c6 +%dc%9crh%d2%86B%e5%97%a2%22t%ed%99%82d -- id:msg-002@notmuch-test-suite
++%2a@%7d%cf%b5%f4%85%80%adO3%da%a7 +=%e0%ac%95%c8%b3+%ef%aa%95%c8%a64w%c7%9d%c9%a2%cf%b3%d6%82%24B%c4%a9%c5%a1UX%ee%99%b0%27E7%ca%a4%d0%8b%5d +A%e1%a0%bc%de%8b%d5%b2V%d9%9b%f3%b5%a2%a3M%d8%a1u@%f0%a0%ac%948%7e%f0%ab%86%af%27 +L%df%85%ef%a1%a5m@%d3%96%c2%ab%d4%9f%ca%b8%f3%b3%a2%bf%c7%b1_u%d7%b4%c7%b1 +P%c4%98%2f +R +inbox +tag5 +unread +%7e%d1%8b%25%ec%a0%ae%d1%a0M%3b%e3%b6%b7%e9%a4%87%3c%db%9a%cc%a8%e1%96%9d +%c4%bf7%c7%ab9H%c4%99k%ea%91%bd%c3%8ck%e2%b3%8dk%c5%952V%e4%99%b2%d9%b3%e4%8b%bda%5b%24%c7%9b +%da%88=f%cc%b9I%ce%af%7b%c9%97%e3%b9%8bH%cb%92X%d2%8c6 +%dc%9crh%d2%86B%e5%97%a2%22t%ed%99%82d -- id:msg-001@notmuch-test-suite
+EOF
+
+NOTMUCH_DUMP_TAGS > OUTPUT
+notmuch restore --format=batch-tag < BACKUP
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--batch: only space and % needs to be encoded."
+notmuch dump --format=batch-tag > BACKUP
+
+notmuch tag --batch <<EOF
++winner *
++foo::bar%25 -- (One and Two) or (One and tag:winner)
++found::it -- tag:foo::bar%
+# ignore this line and the next
+
++space%20in%20tags -- Two
+# add tag '(tags)', among other stunts.
++crazy{ +(tags) +&are +#possible\ -- tag:"space in tags"
++match*crazy -- tag:crazy{
++some_tag -- id:"this is ""nauty)"""
+EOF
+
+cat <<EOF > EXPECTED
++%23possible%5c +%26are +%28tags%29 +crazy%7b +inbox +match%2acrazy +space%20in%20tags +tag4 +tag5 +unread +winner -- id:msg-002@notmuch-test-suite
++foo%3a%3abar%25 +found%3a%3ait +inbox +tag5 +unread +winner -- id:msg-001@notmuch-test-suite
+EOF
+
+NOTMUCH_DUMP_TAGS > OUTPUT
+notmuch restore --format=batch-tag < BACKUP
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest '--batch: unicode message-ids'
+
+${TEST_DIRECTORY}/random-corpus --config-path=${NOTMUCH_CONFIG} \
+     --num-messages=100
+
+notmuch dump --format=batch-tag | sed 's/^.* -- /+common_tag -- /' | \
+    sort > EXPECTED
+
+notmuch dump --format=batch-tag | sed 's/^.* -- /  -- /' > INTERMEDIATE_STEP
+notmuch restore --format=batch-tag < INTERMEDIATE_STEP
+
+notmuch tag --batch < EXPECTED
+
+notmuch dump --format=batch-tag| \
+    sort > OUTPUT
+
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Empty tag names"
+test_expect_code 1 'notmuch tag + One'
+
+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
diff --git a/test/T160-json.sh b/test/T160-json.sh
new file mode 100755 (executable)
index 0000000..318c978
--- /dev/null
@@ -0,0 +1,205 @@
+#!/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" | 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" | 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" | 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\""
+output=$(notmuch search --format=json "json-search-message" | notmuch_search_sanitize)
+test_expect_equal_json "$output" "[{\"thread\": \"XXX\",
+ \"timestamp\": 946728000,
+ \"date_relative\": \"2000-01-01\",
+ \"matched\": 1,
+ \"total\": 1,
+ \"authors\": \"Notmuch Test Suite\",
+ \"subject\": \"json-search-subject\",
+ \"query\": [\"id:$gen_msg_id\", null],
+ \"tags\": [\"inbox\",
+ \"unread\"]}]"
+
+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" | 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'
+id="json-show-inline-attachment-filename@notmuchmail.org"
+emacs_fcc_message \
+    "$subject" \
+    'This is a test message with inline attachment with a filename' \
+    "(mml-attach-file \"$NOTMUCH_SRCDIR/test/README\" nil nil \"inline\")
+     (message-goto-eoh)
+     (insert \"Message-ID: <$id>\n\")"
+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\", \"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\""
+output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_search_sanitize)
+test_expect_equal_json "$output" "[{\"thread\": \"XXX\",
+ \"timestamp\": 946728000,
+ \"date_relative\": \"2000-01-01\",
+ \"matched\": 1,
+ \"total\": 1,
+ \"authors\": \"Notmuch Test Suite\",
+ \"subject\": \"json-search-utf8-body-sübjéct\",
+ \"query\": [\"id:$gen_msg_id\", null],
+ \"tags\": [\"inbox\",
+ \"unread\"]}]"
+
+if [ -z "${NOTMUCH_TEST_INSTALLED-}" ]; then
+test_begin_subtest "Search message: json, 64-bit timestamp"
+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\""
+output=$(notmuch search --format=json "json-search-64bit-timestamp-message" | notmuch_search_sanitize)
+test_expect_equal_json "$output" "[{\"thread\": \"XXX\",
+ \"timestamp\": 32472187200,
+ \"date_relative\": \"the future\",
+ \"matched\": 1,
+ \"total\": 1,
+ \"authors\": \"Notmuch Test Suite\",
+ \"subject\": \"json-search-64bit-timestamp-subject\",
+ \"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 \\*"
+
+test_begin_subtest "Format version: too high"
+test_expect_code 21 "notmuch search --format-version=999 \\*"
+
+test_begin_subtest "Show message: multiple filenames"
+add_message '[id]=message-id@example.com [filename]=copy1 [date]="Fri, 05 Jan 2001 15:43:52 +0000"'
+add_message '[id]=message-id@example.com [filename]=copy2 [date]="Fri, 05 Jan 2001 15:43:52 +0000"'
+cat <<EOF > EXPECTED
+[
+    [
+        [
+            {
+                "date_relative": "2001-01-05",
+               "duplicate": 1,
+                "excluded": false,
+                "filename": [
+                    "${MAIL_DIR}/copy1",
+                    "${MAIL_DIR}/copy2"
+                ],
+                "headers": {
+                    "Date": "Fri, 05 Jan 2001 15:43:52 +0000",
+                    "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+                    "Subject": "Show message: multiple filenames",
+                    "To": "Notmuch Test Suite <test_suite@notmuchmail.org>"
+                },
+                "id": "message-id@example.com",
+                "crypto": {},
+                "match": true,
+                "tags": [
+                    "inbox",
+                    "unread"
+                ],
+                "timestamp": 978709432
+            },
+            []
+        ]
+    ]
+]
+EOF
+output=$(notmuch show --format=json --body=false id:message-id@example.com)
+test_expect_equal_json "$output" "$(cat EXPECTED)"
+
+test_begin_subtest "Show message: multiple filenames, format version 2"
+cat <<EOF > EXPECTED
+[
+    [
+        [
+            {
+                "date_relative": "2001-01-05",
+               "duplicate": 1,
+                "excluded": false,
+                "filename": "${MAIL_DIR}/copy1",
+                "headers": {
+                    "Date": "Fri, 05 Jan 2001 15:43:52 +0000",
+                    "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+                    "Subject": "Show message: multiple filenames",
+                    "To": "Notmuch Test Suite <test_suite@notmuchmail.org>"
+                },
+                "id": "message-id@example.com",
+                "match": true,
+                "tags": [
+                    "inbox",
+                    "unread"
+                ],
+                "timestamp": 978709432
+            },
+            []
+        ]
+    ]
+]
+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
diff --git a/test/T170-sexp.sh b/test/T170-sexp.sh
new file mode 100755 (executable)
index 0000000..0be94bd
--- /dev/null
@@ -0,0 +1,63 @@
+#!/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" | 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" | 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" | 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_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" | 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_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'
+id="sexp-show-inline-attachment-filename@notmuchmail.org"
+emacs_fcc_message \
+    "$subject" \
+    'This is a test message with inline attachment with a filename' \
+    "(mml-attach-file \"$NOTMUCH_SRCDIR/test/README\" nil nil \"inline\")
+     (message-goto-eoh)
+     (insert \"Message-ID: <$id>\n\")"
+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\") :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
diff --git a/test/T180-text.sh b/test/T180-text.sh
new file mode 100755 (executable)
index 0000000..ad2cb1f
--- /dev/null
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+test_description="--format=text output"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "Show message: text"
+add_message "[subject]=\"text-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"text-show-message\""
+output=$(notmuch show --format=text "text-show-message" | notmuch_show_sanitize_all)
+test_expect_equal "$output" "\
+\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2000-01-01) (inbox unread)
+Subject: text-show-subject
+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
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+text-show-message
+\fpart}
+\fbody}
+\fmessage}"
+
+test_begin_subtest "Search message: text"
+add_message "[subject]=\"text-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"text-search-message\""
+output=$(notmuch search --format=text "text-search-message" | notmuch_search_sanitize)
+test_expect_equal "$output" \
+"thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; text-search-subject (inbox unread)"
+
+test_begin_subtest "Show message: text, utf-8"
+add_message "[subject]=\"text-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"tëxt-show-méssage\""
+output=$(notmuch show --format=text "tëxt-show-méssage" | notmuch_show_sanitize_all)
+test_expect_equal "$output" "\
+\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2000-01-01) (inbox unread)
+Subject: text-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
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+tëxt-show-méssage
+\fpart}
+\fbody}
+\fmessage}"
+
+test_begin_subtest "Search message: text, utf-8"
+add_message "[subject]=\"text-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"tëxt-search-méssage\""
+output=$(notmuch search --format=text "tëxt-search-méssage" | notmuch_search_sanitize)
+test_expect_equal "$output" \
+"thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; text-search-utf8-body-sübjéct (inbox unread)"
+
+add_email_corpus
+
+test_begin_subtest "Search message tags: text0"
+cat <<EOF > EXPECTED
+attachment inbox signed unread
+EOF
+notmuch search --format=text0 --output=tags '*' | xargs -0 | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+# Use tr(1) to convert --output=text0 to --output=text for
+# comparison. Also translate newlines to spaces to fail with more
+# noise if they are present as delimiters instead of null
+# characters. This assumes there are no newlines in the data.
+test_begin_subtest "Compare text vs. text0 for threads"
+notmuch search --format=text --output=threads '*' | notmuch_search_sanitize > EXPECTED
+notmuch search --format=text0 --output=threads '*' | tr "\n\0" " \n" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Compare text vs. text0 for messages"
+notmuch search --format=text --output=messages '*' | notmuch_search_sanitize > EXPECTED
+notmuch search --format=text0 --output=messages '*' | tr "\n\0" " \n" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Compare text vs. text0 for files"
+notmuch search --format=text --output=files '*' | notmuch_search_sanitize > EXPECTED
+notmuch search --format=text0 --output=files '*' | tr "\n\0" " \n" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Compare text vs. text0 for tags"
+notmuch search --format=text --output=tags '*' | notmuch_search_sanitize > EXPECTED
+notmuch search --format=text0 --output=tags '*' | tr "\n\0" " \n" | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T190-multipart.sh b/test/T190-multipart.sh
new file mode 100755 (executable)
index 0000000..cfe48ac
--- /dev/null
@@ -0,0 +1,833 @@
+#!/usr/bin/env bash
+test_description="output of multipart message"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+cat <<EOF > embedded_message_body
+Content-Type: multipart/alternative; boundary="==-=-=="
+
+--==-=-==
+Content-Type: text/html
+
+<p>This is an embedded message, with a multipart/alternative part.</p>
+
+--==-=-==
+Content-Type: text/plain
+
+This is an embedded message, with a multipart/alternative part.
+
+--==-=-==--
+EOF
+cat <<EOF > embedded_message
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Subject: html message
+Date: Fri, 05 Jan 2001 15:42:57 +0000
+User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu)
+Message-ID: <87liy5ap01.fsf@yoom.home.cworth.org>
+MIME-Version: 1.0
+EOF
+
+cat embedded_message_body >> embedded_message
+
+cat <<EOF > multipart_body
+Content-Type: multipart/signed; boundary="==-=-=";
+       micalg=pgp-sha1; protocol="application/pgp-signature"
+
+--==-=-=
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: inline
+
+EOF
+
+cat embedded_message >> multipart_body
+cat <<EOF >> multipart_body
+
+--=-=-=
+Content-Disposition: attachment; filename=attachment
+
+This is a text attachment.
+
+--=-=-=
+
+And this message is signed.
+
+-Carl
+
+--=-=-=--
+
+--==-=-=
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.11 (GNU/Linux)
+
+iEYEARECAAYFAk3SA/gACgkQ6JDdNq8qSWj0sACghqVJEQJUs3yV8zbTzhgnSIcD
+W6cAmQE4dcYrx/LPLtYLZm1jsGauE5hE
+=zkga
+-----END PGP SIGNATURE-----
+--==-=-=--
+EOF
+
+cat <<EOF > ${MAIL_DIR}/multipart
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Subject: Multipart message
+Date: Fri, 05 Jan 2001 15:43:57 +0000
+User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu)
+Message-ID: <87liy5ap00.fsf@yoom.home.cworth.org>
+MIME-Version: 1.0
+EOF
+
+cat multipart_body >> ${MAIL_DIR}/multipart
+
+cat <<EOF > ${MAIL_DIR}/base64-part-with-crlf
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Subject: Test message with a BASE64 encoded binary containing CRLF pair
+Date: Fri, 05 Jan 2001 15:43:57 +0000
+User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu)
+Message-ID: <base64-part-with-crlf>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="==-=-=";
+
+--==-=-=
+
+The attached BASE64-encoded part expands to a binary containing a CRLF
+pair (that is one bye of 0x0D followed by one byte of 0x0A). This is
+designed to ensure that notmuch is not corrupting the output of this
+part by converting the CRLF pair to an LF only (as would be appropriate
+for display of a text part on a Linux system, for example).
+
+The part should be a 3-byte file with the following sequence of 3
+hexadecimal bytes:
+
+       EF 0D 0A
+
+--==-=-=
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename=crlf.bin
+Content-Transfer-Encoding: base64
+
+7w0K
+--==-=-=--
+EOF
+
+cat <<EOF > content_types
+From: Todd <todd@example.com>
+To: todd@example.com
+Subject: odd content types
+Date: Mon, 12 Jan 2014 18:12:32 +0000
+User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu)
+Message-ID: <KfjfO2WJBw2hrV2p0gjT@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="==-=-=="
+
+--==-=-==
+Content-Type: application/unique_identifier
+
+<p>This is an embedded message, with a multipart/alternative part.</p>
+
+--==-=-==
+Content-Type: text/some_other_identifier
+
+This is an embedded message, with a multipart/alternative part.
+
+--==-=-==--
+EOF
+cat content_types >> ${MAIL_DIR}/odd_content_type
+notmuch new > /dev/null
+
+test_begin_subtest "--format=text --part=0, full message"
+notmuch show --format=text --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fmessage{ id:87liy5ap00.fsf@yoom.home.cworth.org depth:0 match:1 excluded:0 filename:${MAIL_DIR}/multipart
+\fheader{
+Carl Worth <cworth@cworth.org> (2001-01-05) (attachment inbox signed unread)
+Subject: Multipart message
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Date: Fri, 05 Jan 2001 15:43:57 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: multipart/signed
+\fpart{ ID: 2, Content-type: multipart/mixed
+\fpart{ ID: 3, Content-type: message/rfc822
+\fheader{
+Subject: html message
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Date: Fri, 05 Jan 2001 15:42:57 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 4, Content-type: multipart/alternative
+\fpart{ ID: 5, Content-type: text/html
+Non-text part: text/html
+\fpart}
+\fpart{ ID: 6, Content-type: text/plain
+This is an embedded message, with a multipart/alternative part.
+\fpart}
+\fpart}
+\fbody}
+\fpart}
+\fattachment{ ID: 7, Filename: attachment, Content-type: text/plain
+This is a text attachment.
+\fattachment}
+\fpart{ ID: 8, Content-type: text/plain
+And this message is signed.
+
+-Carl
+\fpart}
+\fpart}
+\fpart{ ID: 9, Content-type: application/pgp-signature
+Non-text part: application/pgp-signature
+\fpart}
+\fpart}
+\fbody}
+\fmessage}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --part=0 --body=false, message header"
+notmuch show --format=text --part=0  --body=false 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fmessage{ id:87liy5ap00.fsf@yoom.home.cworth.org depth:0 match:1 excluded:0 filename:${MAIL_DIR}/multipart
+\fheader{
+Carl Worth <cworth@cworth.org> (2001-01-05) (attachment inbox signed unread)
+Subject: Multipart message
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Date: Fri, 05 Jan 2001 15:43:57 +0000
+\fheader}
+\fmessage}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --part=1, message body"
+notmuch show --format=text --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fpart{ ID: 1, Content-type: multipart/signed
+\fpart{ ID: 2, Content-type: multipart/mixed
+\fpart{ ID: 3, Content-type: message/rfc822
+\fheader{
+Subject: html message
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Date: Fri, 05 Jan 2001 15:42:57 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 4, Content-type: multipart/alternative
+\fpart{ ID: 5, Content-type: text/html
+Non-text part: text/html
+\fpart}
+\fpart{ ID: 6, Content-type: text/plain
+This is an embedded message, with a multipart/alternative part.
+\fpart}
+\fpart}
+\fbody}
+\fpart}
+\fattachment{ ID: 7, Filename: attachment, Content-type: text/plain
+This is a text attachment.
+\fattachment}
+\fpart{ ID: 8, Content-type: text/plain
+And this message is signed.
+
+-Carl
+\fpart}
+\fpart}
+\fpart{ ID: 9, Content-type: application/pgp-signature
+Non-text part: application/pgp-signature
+\fpart}
+\fpart}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --part=2, multipart/mixed"
+notmuch show --format=text --part=2 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fpart{ ID: 2, Content-type: multipart/mixed
+\fpart{ ID: 3, Content-type: message/rfc822
+\fheader{
+Subject: html message
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Date: Fri, 05 Jan 2001 15:42:57 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 4, Content-type: multipart/alternative
+\fpart{ ID: 5, Content-type: text/html
+Non-text part: text/html
+\fpart}
+\fpart{ ID: 6, Content-type: text/plain
+This is an embedded message, with a multipart/alternative part.
+\fpart}
+\fpart}
+\fbody}
+\fpart}
+\fattachment{ ID: 7, Filename: attachment, Content-type: text/plain
+This is a text attachment.
+\fattachment}
+\fpart{ ID: 8, Content-type: text/plain
+And this message is signed.
+
+-Carl
+\fpart}
+\fpart}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --part=3, rfc822 part"
+notmuch show --format=text --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fpart{ ID: 3, Content-type: message/rfc822
+\fheader{
+Subject: html message
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Date: Fri, 05 Jan 2001 15:42:57 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 4, Content-type: multipart/alternative
+\fpart{ ID: 5, Content-type: text/html
+Non-text part: text/html
+\fpart}
+\fpart{ ID: 6, Content-type: text/plain
+This is an embedded message, with a multipart/alternative part.
+\fpart}
+\fpart}
+\fbody}
+\fpart}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --part=4, rfc822's multipart"
+notmuch show --format=text --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fpart{ ID: 4, Content-type: multipart/alternative
+\fpart{ ID: 5, Content-type: text/html
+Non-text part: text/html
+\fpart}
+\fpart{ ID: 6, Content-type: text/plain
+This is an embedded message, with a multipart/alternative part.
+\fpart}
+\fpart}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --part=5, rfc822's html part"
+notmuch show --format=text --part=5 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fpart{ ID: 5, Content-type: text/html
+Non-text part: text/html
+\fpart}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --include-html --part=5, rfc822's html part"
+notmuch show --format=text --include-html --part=5 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fpart{ ID: 5, Content-type: text/html
+<p>This is an embedded message, with a multipart/alternative part.</p>
+\fpart}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --part=6, rfc822's text part"
+notmuch show --format=text --part=6 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fpart{ ID: 6, Content-type: text/plain
+This is an embedded message, with a multipart/alternative part.
+\fpart}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --part=7, inline attachment"
+notmuch show --format=text --part=7 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fattachment{ ID: 7, Filename: attachment, Content-type: text/plain
+This is a text attachment.
+\fattachment}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --part=8, plain text part"
+notmuch show --format=text --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fpart{ ID: 8, Content-type: text/plain
+And this message is signed.
+
+-Carl
+\fpart}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=text --part=9, pgp signature (unverified)"
+notmuch show --format=text --part=9 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+\fpart{ ID: 9, Content-type: application/pgp-signature
+Non-text part: application/pgp-signature
+\fpart}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+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' | notmuch_json_show_sanitize >OUTPUT
+cat <<EOF >EXPECTED
+{"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": "GENERATED_DATE"}, "body": [
+{"id": 4, "content-type": "multipart/alternative", "content": [
+{"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": "NONZERO"}]}]}
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--format=json --part=1, message body"
+notmuch show --format=json --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+{"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": 4, "content-type": "multipart/alternative", "content": [
+{"id": 5, "content-type": "text/html", "content-length": 71},
+{"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}]}
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--format=json --part=2, multipart/mixed"
+notmuch show --format=json --part=2 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+{"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": 4, "content-type": "multipart/alternative", "content": [
+{"id": 5, "content-type": "text/html", "content-length": 71},
+{"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"}]}
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--format=json --part=3, rfc822 part"
+notmuch show --format=json --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+{"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": 4, "content-type": "multipart/alternative", "content": [
+{"id": 5, "content-type": "text/html", "content-length": 71},
+{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--format=json --part=4, rfc822's multipart/alternative"
+notmuch show --format=json --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+{"id": 4, "content-type": "multipart/alternative", "content": [
+{"id": 5, "content-type": "text/html", "content-length": 71},
+{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--format=json --part=5, rfc822's html part"
+notmuch show --format=json --part=5 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+{"id": 5, "content-type": "text/html", "content-length": 71}
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--format=json --part=6, rfc822's text part"
+notmuch show --format=json --part=6 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--format=json --part=7, inline attachment"
+notmuch show --format=json --part=7 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+{"id": 7,
+ "content-type": "text/plain",
+ "filename": "attachment",
+ "content": "This is a text attachment.\n",
+ "content-disposition": "attachment"}
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--format=json --part=8, plain text part"
+notmuch show --format=json --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+{"id": 8, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"}
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--format=json --part=9, pgp signature (unverified)"
+notmuch show --format=json --part=9 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+{"id": 9, "content-type": "application/pgp-signature", "content-length": 197}
+EOF
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED)"
+
+test_begin_subtest "--format=json --part=10, no part, expect error"
+test_expect_success "notmuch show --format=json --part=10 'id:87liy5ap00.fsf@yoom.home.cworth.org'"
+
+test_begin_subtest "--format=raw"
+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' | notmuch_json_show_sanitize >OUTPUT
+test_expect_equal_file "${MAIL_DIR}"/multipart OUTPUT
+
+test_begin_subtest "--format=raw --part=1, message body"
+notmuch show --format=raw --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+test_expect_equal_file multipart_body OUTPUT
+
+test_begin_subtest "--format=raw --part=2, multipart/mixed"
+notmuch show --format=raw --part=2 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: inline
+
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Subject: html message
+Date: Fri, 05 Jan 2001 15:42:57 +0000
+User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu)
+Message-ID: <87liy5ap01.fsf@yoom.home.cworth.org>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="==-=-=="
+
+--==-=-==
+Content-Type: text/html
+
+<p>This is an embedded message, with a multipart/alternative part.</p>
+
+--==-=-==
+Content-Type: text/plain
+
+This is an embedded message, with a multipart/alternative part.
+
+--==-=-==--
+
+--=-=-=
+Content-Disposition: attachment; filename=attachment
+
+This is a text attachment.
+
+--=-=-=
+
+And this message is signed.
+
+-Carl
+
+--=-=-=--
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=raw --part=3, rfc822 part"
+notmuch show --format=raw --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+test_expect_equal_file embedded_message OUTPUT
+
+test_begin_subtest "--format=raw --part=4, rfc822's multipart"
+notmuch show --format=raw --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+test_expect_equal_file embedded_message_body OUTPUT
+
+test_begin_subtest "--format=raw --part=5, rfc822's html part"
+notmuch show --format=raw --part=5 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+<p>This is an embedded message, with a multipart/alternative part.</p>
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=raw --part=6, rfc822's text part"
+notmuch show --format=raw --part=6 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+This is an embedded message, with a multipart/alternative part.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=raw --part=7, inline attachment"
+notmuch show --format=raw --part=7 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+This is a text attachment.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=raw --part=8, plain text part"
+notmuch show --format=raw --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+And this message is signed.
+
+-Carl
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=raw --part=9, pgp signature (unverified)"
+notmuch show --format=raw --part=9 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+# output should *not* include newline
+echo >>OUTPUT
+cat <<EOF >EXPECTED
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.11 (GNU/Linux)
+
+iEYEARECAAYFAk3SA/gACgkQ6JDdNq8qSWj0sACghqVJEQJUs3yV8zbTzhgnSIcD
+W6cAmQE4dcYrx/LPLtYLZm1jsGauE5hE
+=zkga
+-----END PGP SIGNATURE-----
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=raw --part=10, no part, expect error"
+test_expect_success "notmuch show --format=raw --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org'"
+
+test_begin_subtest "--format=mbox"
+notmuch show --format=mbox 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+printf "From cworth@cworth.org Fri Jan  5 15:43:57 2001\n" >EXPECTED
+cat "${MAIL_DIR}"/multipart >>EXPECTED
+# mbox output is expected to include a blank line
+echo >>EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--format=mbox --part=1, incompatible, expect error"
+test_expect_success "! notmuch show --format=mbox --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org'"
+
+test_begin_subtest "'notmuch reply' to a multipart message"
+notmuch reply 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: Multipart message
+To: Carl Worth <cworth@cworth.org>, cworth@cworth.org
+In-Reply-To: <87liy5ap00.fsf@yoom.home.cworth.org>
+References: <87liy5ap00.fsf@yoom.home.cworth.org>
+
+On Fri, 05 Jan 2001 15:43:57 +0000, Carl Worth <cworth@cworth.org> wrote:
+> From: Carl Worth <cworth@cworth.org>
+> To: cworth@cworth.org
+> Subject: html message
+> Date: Fri, 05 Jan 2001 15:42:57 +0000
+>
+Non-text part: text/html
+> This is an embedded message, with a multipart/alternative part.
+> This is a text attachment.
+> And this message is signed.
+> 
+> -Carl
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "'notmuch reply' to a multipart message with json format"
+notmuch reply --format=json 'id:87liy5ap00.fsf@yoom.home.cworth.org' | notmuch_json_show_sanitize >OUTPUT
+notmuch_json_show_sanitize <<EOF >EXPECTED
+{"reply-headers": {"Subject": "Re: Multipart message",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Carl Worth <cworth@cworth.org>, cworth@cworth.org",
+ "In-reply-to": "<87liy5ap00.fsf@yoom.home.cworth.org>",
+ "References": "<87liy5ap00.fsf@yoom.home.cworth.org>"},
+ "original": {"id": "XXXXX",
+ "crypto": {},
+ "match": false,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "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": 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": 4,
+ "content-type": "multipart/alternative",
+ "content": [{"id": 5,
+ "content-type": "text/html",
+ "content-length": 71},
+ {"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}]}]}}
+EOF
+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
+printf "\xEF\x0D\x0A" > crlf.expected
+test_expect_equal_file crlf.out crlf.expected
+
+
+# The ISO-8859-1 encoding of U+00BD is a single byte: octal 275
+# (Portability note: Dollar-Single ($'...', ANSI C-style escape sequences)
+# quoting works on bash, ksh, zsh, *BSD sh but not on dash, ash nor busybox sh)
+readonly u_00bd_latin1=$'\275'
+
+# The Unicode fraction symbol 1/2 is U+00BD and is encoded
+# in UTF-8 as two bytes: octal 302 275
+readonly u_00bd_utf8=$'\302\275'
+
+cat <<EOF > ${MAIL_DIR}/include-html
+From: A <a@example.com>
+To: B <b@example.com>
+Subject: html message
+Date: Sat, 01 January 2000 00:00:00 +0000
+Message-ID: <htmlmessage>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="==-=="
+
+--==-==
+Content-Type: text/html; charset=UTF-8
+
+<p>0.5 equals ${u_00bd_utf8}</p>
+
+--==-==
+Content-Type: text/html; charset=ISO-8859-1
+
+<p>0.5 equals ${u_00bd_latin1}</p>
+
+--==-==
+Content-Type: text/plain; charset=UTF-8
+
+0.5 equals ${u_00bd_utf8}
+
+--==-==--
+EOF
+
+notmuch new > /dev/null
+
+cat_expected_head () {
+        cat <<EOF
+[[[{"id": "XXXXX", "match":true, "excluded": false, "date_relative":"2000-01-01",
+   "crypto": {},
+   "timestamp": 946684800,
+   "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>"},
+   "body": [{
+     "content-type": "multipart/alternative", "id": 1,
+EOF
+}
+
+cat_expected_head > EXPECTED.nohtml
+cat <<EOF >> EXPECTED.nohtml
+"content": [
+  { "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
+
+# Both the UTF-8 and ISO-8859-1 part should have U+00BD
+cat_expected_head > EXPECTED.withhtml
+cat <<EOF >> EXPECTED.withhtml
+"content": [
+  { "id": 2, "content-type": "text/html", "content": "<p>0.5 equals \\u00bd</p>\\n"},
+  { "id": 3, "content-type": "text/html", "content": "<p>0.5 equals \\u00bd</p>\\n"},
+  { "id": 4, "content-type": "text/plain", "content": "0.5 equals \\u00bd\\n"}
+]}]},[]]]]
+EOF
+
+test_begin_subtest "html parts excluded by default"
+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 | notmuch_json_show_sanitize > OUTPUT
+test_expect_equal_json "$(cat OUTPUT)" "$(cat EXPECTED.withhtml)"
+
+test_begin_subtest "indexes mime-type #1"
+output=$(notmuch search mimetype:application/unique_identifier | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2014-01-12 [1/1] Todd; odd content types (inbox unread)"
+
+test_begin_subtest "indexes mime-type #2"
+output=$(notmuch search mimetype:text/some_other_identifier | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2014-01-12 [1/1] Todd; odd content types (inbox unread)"
+
+test_begin_subtest "indexes mime-type #3"
+output=$(notmuch search from:todd and mimetype:multipart/alternative | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2014-01-12 [1/1] Todd; odd content types (inbox unread)"
+
+test_begin_subtest "case of Content-Disposition doesn't matter for indexing"
+cat <<EOF > ${MAIL_DIR}/content-disposition
+Return-path: <david@tethera.net>
+Envelope-to: david@tethera.net
+Delivery-date: Sun, 04 Oct 2015 09:16:03 -0300
+Received: from gitolite.debian.net ([87.98.215.224])
+       by yantan.tethera.net with esmtps (TLS1.2:DHE_RSA_AES_128_CBC_SHA1:128)
+       (Exim 4.80)
+       (envelope-from <david@tethera.net>)
+       id 1ZiiCx-0007iz-RK
+       for david@tethera.net; Sun, 04 Oct 2015 09:16:03 -0300
+Received: from remotemail by gitolite.debian.net with local (Exim 4.80)
+       (envelope-from <david@tethera.net>)
+       id 1ZiiC8-0002Rz-Uf; Sun, 04 Oct 2015 12:15:12 +0000
+Received: (nullmailer pid 28621 invoked by uid 1000); Sun, 04 Oct 2015
+ 12:14:53 -0000
+From: David Bremner <david@tethera.net>
+To: David Bremner <david@tethera.net>
+Subject: test attachment
+User-Agent: Notmuch/0.20.2+93~g33c8777 (http://notmuchmail.org) Emacs/24.5.1
+ (x86_64-pc-linux-gnu)
+Date: Sun, 04 Oct 2015 09:14:53 -0300
+Message-ID: <87io6m96f6.fsf@zancas.localnet>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+Content-Disposition: ATTACHMENT; filename=hello.txt
+Content-Description: this is a very exciting file
+
+hello
+
+--=-=-=
+Content-Type: text/plain
+
+
+world
+
+--=-=-=--
+
+EOF
+NOTMUCH_NEW
+
+cat <<EOF > EXPECTED
+attachment
+inbox
+unread
+EOF
+
+notmuch search --output=tags id:87io6m96f6.fsf@zancas.localnet > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+test_done
diff --git a/test/T200-thread-naming.sh b/test/T200-thread-naming.sh
new file mode 100755 (executable)
index 0000000..594d301
--- /dev/null
@@ -0,0 +1,212 @@
+#!/usr/bin/env bash
+test_description="naming of threads with changing subject"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "Initial thread name (oldest-first search)"
+add_message '[subject]="thread-naming: Initial thread subject"' \
+           '[date]="Fri, 05 Jan 2001 15:43:56 -0000"'
+first=${gen_msg_cnt}
+parent=${gen_msg_id}
+add_message '[subject]="thread-naming: Older changed subject"' \
+           '[date]="Sat, 06 Jan 2001 15:43:56 -0000"' \
+           "[in-reply-to]=\<$parent\>"
+add_message '[subject]="thread-naming: Newer changed subject"' \
+           '[date]="Sun, 07 Jan 2001 15:43:56 -0000"' \
+           "[in-reply-to]=\<$parent\>"
+add_message '[subject]="thread-naming: Final thread subject"' \
+           '[date]="Mon, 08 Jan 2001 15:43:56 -0000"' \
+           "[in-reply-to]=\<$parent\>"
+final=${gen_msg_id}
+output=$(notmuch search --sort=oldest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [4/4] Notmuch Test Suite; thread-naming: Initial thread subject (inbox unread)"
+
+test_begin_subtest "Initial thread name (newest-first search)"
+output=$(notmuch search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-08 [4/4] Notmuch Test Suite; thread-naming: Final thread subject (inbox unread)"
+
+# Remove oldest and newest messages from search results
+notmuch tag -inbox id:$parent or id:$final
+
+test_begin_subtest "Changed thread name (oldest-first search)"
+output=$(notmuch search --sort=oldest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-06 [2/4] Notmuch Test Suite; thread-naming: Older changed subject (inbox unread)"
+
+test_begin_subtest "Changed thread name (newest-first search)"
+output=$(notmuch search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-07 [2/4] Notmuch Test Suite; thread-naming: Newer changed subject (inbox unread)"
+
+test_begin_subtest "Ignore added reply prefix (Re:)"
+add_message '[subject]="Re: thread-naming: Initial thread subject"' \
+           '[date]="Tue, 09 Jan 2001 15:43:45 -0000"' \
+           "[in-reply-to]=\<$parent\>"
+output=$(notmuch search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-09 [3/5] Notmuch Test Suite; thread-naming: Initial thread subject (inbox unread)"
+
+test_begin_subtest "Ignore added reply prefix (Aw:)"
+add_message '[subject]="Aw: thread-naming: Initial thread subject"' \
+           '[date]="Wed, 10 Jan 2001 15:43:45 -0000"' \
+           "[in-reply-to]=\<$parent\>"
+output=$(notmuch search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-10 [4/6] Notmuch Test Suite; thread-naming: Initial thread subject (inbox unread)"
+
+test_begin_subtest "Ignore added reply prefix (Vs:)"
+add_message '[subject]="Vs: thread-naming: Initial thread subject"' \
+           '[date]="Thu, 11 Jan 2001 15:43:45 -0000"' \
+           "[in-reply-to]=\<$parent\>"
+output=$(notmuch search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-11 [5/7] Notmuch Test Suite; thread-naming: Initial thread subject (inbox unread)"
+
+test_begin_subtest "Ignore added reply prefix (Sv:)"
+add_message '[subject]="Sv: thread-naming: Initial thread subject"' \
+           '[date]="Fri, 12 Jan 2001 15:43:45 -0000"' \
+           "[in-reply-to]=\<$parent\>"
+output=$(notmuch search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-12 [6/8] Notmuch Test Suite; thread-naming: Initial thread subject (inbox unread)"
+
+test_begin_subtest "Use empty subjects if necessary."
+add_message '[subject]="@FORCE_EMPTY"' \
+           '[date]="Sat, 13 Jan 2001 15:43:45 -0000"' \
+            '[from]="Empty Sender <empty_test@notmuchmail.org>"'
+empty_parent=${gen_msg_id}
+add_message '[subject]="@FORCE_EMPTY"' \
+           '[date]="Sun, 14 Jan 2001 15:43:45 -0000"' \
+            '[from]="Empty Sender <empty_test@notmuchmail.org>"' \
+            "[in-reply-to]=\<$empty_parent\>"
+output=$(notmuch search --sort=newest-first from:empty_test@notmuchmail.org | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-14 [2/2] Empty Sender;  (inbox unread)"
+
+test_begin_subtest "Avoid empty subjects if possible (newest-first)."
+add_message '[subject]="Non-empty subject (1)"' \
+           '[date]="Mon, 15 Jan 2001 15:43:45 -0000"' \
+            '[from]="Empty Sender <empty_test@notmuchmail.org>"' \
+            "[in-reply-to]=\<$empty_parent\>"
+add_message '[subject]="Non-empty subject (2)"' \
+           '[date]="Mon, 16 Jan 2001 15:43:45 -0000"' \
+            '[from]="Empty Sender <empty_test@notmuchmail.org>"' \
+            "[in-reply-to]=\<$empty_parent\>"
+add_message '[subject]="@FORCE_EMPTY"' \
+           '[date]="Tue, 17 Jan 2001 15:43:45 -0000"' \
+            '[from]="Empty Sender <empty_test@notmuchmail.org>"' \
+            "[in-reply-to]=\<$empty_parent\>"
+output=$(notmuch search --sort=newest-first from:Empty | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-17 [5/5] Empty Sender; Non-empty subject (2) (inbox unread)"
+
+test_begin_subtest "Avoid empty subjects if possible (oldest-first)."
+output=$(notmuch search --sort=oldest-first from:Empty | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-13 [5/5] Empty Sender; Non-empty subject (1) (inbox unread)"
+
+test_begin_subtest 'Test order of messages in "notmuch show"'
+output=$(notmuch show thread-naming | notmuch_show_sanitize)
+test_expect_equal "$output" "\fmessage{ id:msg-$(printf "%03d" $first)@notmuch-test-suite depth:0 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $first)
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (unread)
+Subject: thread-naming: Initial thread subject
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Fri, 05 Jan 2001 15:43:56 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This is just a test message (#$first)
+\fpart}
+\fbody}
+\fmessage}
+\fmessage{ id:msg-$(printf "%03d" $((first + 1)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 1)))
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-06) (inbox unread)
+Subject: thread-naming: Older changed subject
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Sat, 06 Jan 2001 15:43:56 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This is just a test message (#$((first + 1)))
+\fpart}
+\fbody}
+\fmessage}
+\fmessage{ id:msg-$(printf "%03d" $((first + 2)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 2)))
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-07) (inbox unread)
+Subject: thread-naming: Newer changed subject
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Sun, 07 Jan 2001 15:43:56 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This is just a test message (#$((first + 2)))
+\fpart}
+\fbody}
+\fmessage}
+\fmessage{ id:msg-$(printf "%03d" $((first + 3)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 3)))
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-08) (unread)
+Subject: thread-naming: Final thread subject
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Mon, 08 Jan 2001 15:43:56 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This is just a test message (#$((first + 3)))
+\fpart}
+\fbody}
+\fmessage}
+\fmessage{ id:msg-$(printf "%03d" $((first + 4)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 4)))
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-09) (inbox unread)
+Subject: Re: thread-naming: Initial thread subject
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Tue, 09 Jan 2001 15:43:45 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This is just a test message (#$((first + 4)))
+\fpart}
+\fbody}
+\fmessage}
+\fmessage{ id:msg-$(printf "%03d" $((first + 5)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 5)))
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-10) (inbox unread)
+Subject: Aw: thread-naming: Initial thread subject
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Wed, 10 Jan 2001 15:43:45 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This is just a test message (#$((first + 5)))
+\fpart}
+\fbody}
+\fmessage}
+\fmessage{ id:msg-$(printf "%03d" $((first + 6)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 6)))
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-11) (inbox unread)
+Subject: Vs: thread-naming: Initial thread subject
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Thu, 11 Jan 2001 15:43:45 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This is just a test message (#$((first + 6)))
+\fpart}
+\fbody}
+\fmessage}
+\fmessage{ id:msg-$(printf "%03d" $((first + 7)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 7)))
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-12) (inbox unread)
+Subject: Sv: thread-naming: Initial thread subject
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Fri, 12 Jan 2001 15:43:45 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This is just a test message (#$((first + 7)))
+\fpart}
+\fbody}
+\fmessage}"
+test_done
diff --git a/test/T205-author-naming.sh b/test/T205-author-naming.sh
new file mode 100755 (executable)
index 0000000..68b85ce
--- /dev/null
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+test_description="naming of authors with unusual addresses"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "Add author with empty quoted real name"
+add_message '[subject]="author-naming: Initial thread subject"' \
+           '[date]="Fri, 05 Jan 2001 15:43:56 -0000"' \
+           '[from]="\"\" <address@example.com>"'
+output=$(notmuch search --sort=oldest-first author-naming and tag:inbox | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] address@example.com; author-naming: Initial thread subject (inbox unread)"
+
+test_done
diff --git a/test/T210-raw.sh b/test/T210-raw.sh
new file mode 100755 (executable)
index 0000000..4408202
--- /dev/null
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+
+test_description='notmuch show --format=raw'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_message
+add_message
+
+test_begin_subtest "Attempt to show multiple raw messages"
+output=$(notmuch show --format=raw "*" 2>&1)
+test_expect_equal "$output" "Error: search term did not match precisely one message (matched 2 messages)."
+
+test_begin_subtest "Show a raw message"
+output=$(notmuch show --format=raw id:msg-001@notmuch-test-suite | notmuch_date_sanitize)
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Message-Id: <msg-001@notmuch-test-suite>
+Subject: Test message #1
+Date: GENERATED_DATE
+
+This is just a test message (#1)"
+
+test_begin_subtest "Show another raw message"
+output=$(notmuch show --format=raw id:msg-002@notmuch-test-suite | notmuch_date_sanitize)
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Message-Id: <msg-002@notmuch-test-suite>
+Subject: Test message #2
+Date: GENERATED_DATE
+
+This is just a test message (#2)"
+
+test_python <<EOF
+from email.message import EmailMessage
+for pow in range(10,21):
+    size = 2 ** pow
+    msg = EmailMessage()
+    msg['Subject'] = 'message with {:07d} bytes'.format(size)
+    msg['From'] = 'Notmuch Test Suite <test_suite@notmuchmail.org>'
+    msg['To'] = msg['From']
+    msg['Message-Id'] = 'size-{:07d}@notmuch-test-suite'.format(size)
+    content = ""
+    msg.set_content("\n")
+    padding = size - len(bytes(msg))
+    lines = []
+    while padding > 0:
+        line = '.' * min(padding, 72)
+        lines.append(line)
+        padding = padding - len(line) - 1
+    content ='\n'.join(lines)
+    msg.set_content(content)
+    with open('mail/size-{:07d}'.format(size), 'wb') as f:
+        f.write(bytes(msg))
+EOF
+
+notmuch new --quiet
+
+for pow in {10..20}; do
+    printf -v size "%07d" $((2**$pow))
+    test_begin_subtest "content, message of size $size"
+    notmuch show --format=raw subject:$size > OUTPUT
+    test_expect_equal_file mail/size-$size OUTPUT
+    test_begin_subtest "return value, message of size $size"
+    test_expect_success "notmuch show --format=raw subject:$size > /dev/null"
+done
+
+add_email_corpus duplicate
+ID=87r2ecrr6x.fsf@zephyr.silentflame.com
+test_begin_subtest "raw content, duplicate files"
+rm -f OUTPUT.raw
+for dup in {1..5}; do
+    notmuch show --format=raw --duplicate=${dup} --format=raw id:${ID} | md5sum | cut -f1 -d' '  >> OUTPUT.raw
+done
+sort OUTPUT.raw > OUTPUT
+notmuch search --output=files id:${ID} | xargs md5sum | cut -f1 -d ' ' | sort > EXPECTED
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_done
diff --git a/test/T220-reply.sh b/test/T220-reply.sh
new file mode 100755 (executable)
index 0000000..120d713
--- /dev/null
@@ -0,0 +1,392 @@
+#!/usr/bin/env bash
+test_description="\"notmuch reply\" in several variations"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+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"'
+
+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}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Sender <sender@example.com> wrote:
+> basic reply test
+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>"' \
+           '[to]="test_suite@notmuchmail.org, Someone Else <someone@example.com>"' \
+            [subject]=notmuch-reply-test \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="Multiple recipients"'
+
+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: notmuch-reply-test
+To: Sender <sender@example.com>, Someone Else <someone@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:
+> Multiple recipients
+OK"
+
+test_begin_subtest "Reply with CC"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=test_suite@notmuchmail.org \
+           '[cc]="Other Parties <cc@example.com>"' \
+            [subject]=notmuch-reply-test \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="reply with CC"'
+
+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: notmuch-reply-test
+To: Sender <sender@example.com>
+Cc: Other Parties <cc@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:
+> reply with CC
+OK"
+
+test_begin_subtest "Reply from alternate address"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=test_suite_other@notmuchmail.org \
+            [subject]=notmuch-reply-test \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="reply from alternate address"'
+
+output=$(notmuch reply id:${gen_msg_id} 2>&1 && echo OK)
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@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:
+> reply from alternate address
+OK"
+
+test_begin_subtest "Reply from address in named group list"
+add_message '[from]="Sender <sender@example.com>"' \
+            '[to]=group:test_suite@notmuchmail.org,someone@example.com\;' \
+             [cc]=test_suite_other@notmuchmail.org \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="Reply from address in named group list"'
+
+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: notmuch-reply-test
+To: Sender <sender@example.com>, someone@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:
+> Reply from address in named group list
+OK"
+
+test_begin_subtest "Support for Reply-To"
+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]="support for reply-to"' \
+           '[reply-to]="Sender <elsewhere@example.com>"'
+
+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: notmuch-reply-test
+To: Sender <elsewhere@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:
+> support for reply-to
+OK"
+
+test_begin_subtest "Un-munging Reply-To"
+add_message '[from]="Sender <sender@example.com>"' \
+           '[to]="Some List <list@example.com>"' \
+            [subject]=notmuch-reply-test \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="Un-munging Reply-To"' \
+           '[reply-to]="Evil Munging List <list@example.com>"'
+
+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: notmuch-reply-test
+To: Sender <sender@example.com>, Some List <list@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:
+> Un-munging Reply-To
+OK"
+
+test_begin_subtest "Un-munging Reply-To With Exact Match"
+add_message '[from]="Sender <sender@example.com>"' \
+           '[to]="Some List <list@example.com>"' \
+            [subject]=notmuch-reply-test \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="Un-munging Reply-To"' \
+           '[reply-to]="Some List <list@example.com>"'
+
+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: notmuch-reply-test
+To: Sender <sender@example.com>, Some List <list@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:
+> Un-munging Reply-To
+OK"
+
+test_begin_subtest "Un-munging Reply-To With Raw addr-spec"
+add_message '[from]="Sender <sender@example.com>"' \
+           '[to]="Some List <list@example.com>"' \
+            [subject]=notmuch-reply-test \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="Un-munging Reply-To"' \
+           '[reply-to]="list@example.com"'
+
+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: notmuch-reply-test
+To: Sender <sender@example.com>, Some List <list@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:
+> Un-munging Reply-To
+OK"
+
+test_begin_subtest "Message with header of exactly 200 bytes"
+add_message '[subject]="This subject is exactly 200 bytes in length. Other than its length there is not much of note here. Note that the length of 200 bytes includes the Subject: and Re: prefixes with two spaces"' \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="200-byte header"'
+output=$(notmuch reply id:${gen_msg_id} 2>&1 && echo OK)
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: This subject is exactly 200 bytes in length. Other than its
+ length there is not much of note here. Note that the length of 200 bytes
+ includes the Subject: and Re: prefixes with two spaces
+To: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+> 200-byte header
+OK"
+
+test_begin_subtest "From guessing: Envelope-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]="Envelope-To: test_suite_other@notmuchmail.org"'
+
+output=$(notmuch reply id:${gen_msg_id} 2>&1 && echo OK)
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite_other@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 "From guessing: X-Original-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]="X-Original-To: test_suite@otherdomain.org"'
+
+output=$(notmuch reply id:${gen_msg_id} 2>&1 && echo OK)
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@otherdomain.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 "From guessing: 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"'
+
+output=$(notmuch reply id:${gen_msg_id} 2>&1 && echo OK)
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite_other@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 "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>"' \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="Encoding"'
+
+# GMime happens to change from Q- to B-encoding.  We canonicalize the
+# case of the encoding and charset because different versions of GMime
+# capitalize the encoding differently.
+output=$( (notmuch reply id:${gen_msg_id} 2>&1 && echo OK) | perl -pe 's/=\?[^?]+\?[bB]\?/lc($&)/ge')
+test_expect_equal "$output" "\
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: =?iso-8859-1?b?4N/n?=
+To: =?utf-8?b?4piD?= <snowman@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, ☃ <snowman@example.com> wrote:
+> Encoding
+OK"
+
+test_begin_subtest "Reply with RFC 2047-encoded headers (JSON)"
+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": {
+        "body": [
+            {
+                "content": "Encoding\n",
+                "content-type": "text/plain",
+                "id": 1
+            }
+        ],
+        "crypto": {},
+        "date_relative": "2010-01-05",
+        "excluded": false,
+        "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": "XXXXX",
+        "match": false,
+        "tags": [
+            "inbox",
+            "unread"
+        ],
+        "timestamp": 1262706236
+    },
+    "reply-headers": {
+        "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+        "In-reply-to": "<'${gen_msg_id}'>",
+        "References": "<'${gen_msg_id}'>",
+        "Subject": "Re: \u00e0\u00df\u00e7",
+        "To": "\u2603 <snowman@example.com>"
+    }
+  },
+  "success": "OK"
+}'
+
+test_begin_subtest "Reply to a message with multiple Cc headers"
+add_email_corpus broken
+output=$(notmuch reply id:multiple-cc@example.org 2>&1 && echo OK)
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: wowsers!
+To: Alice <alice@example.org>, Daniel <daniel@example.org>
+Cc: Bob <bob@example.org>, Charles <charles@example.org>
+In-Reply-To: <multiple-cc@example.org>
+References: <multiple-cc@example.org>
+
+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
diff --git a/test/T230-reply-to-sender.sh b/test/T230-reply-to-sender.sh
new file mode 100755 (executable)
index 0000000..38fbe96
--- /dev/null
@@ -0,0 +1,212 @@
+#!/usr/bin/env bash
+test_description="\"notmuch reply --reply-to=sender\" in several variations"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "Basic reply-to-sender"
+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-to-sender test"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@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:
+> basic reply-to-sender test"
+
+test_begin_subtest "From Us, Basic reply to message"
+add_message '[from]="Notmuch Test Suite <test_suite@notmuchmail.org>"' \
+            '[to]="Recipient <recipient@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="basic reply-to-from-us test"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Recipient <recipient@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+> basic reply-to-from-us test"
+
+test_begin_subtest "Multiple recipients"
+add_message '[from]="Sender <sender@example.com>"' \
+            '[to]="test_suite@notmuchmail.org, Someone Else <someone@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="Multiple recipients"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@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:
+> Multiple recipients"
+
+test_begin_subtest "From Us, Multiple TO recipients"
+add_message '[from]="Notmuch Test Suite <test_suite@notmuchmail.org>"' \
+            '[to]="Recipient <recipient@example.com>, Someone Else <someone@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="From Us, Multiple TO recipients"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Recipient <recipient@example.com>, Someone Else <someone@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+> From Us, Multiple TO recipients"
+
+test_begin_subtest "Reply with CC"
+add_message '[from]="Sender <sender@example.com>"' \
+             [to]=test_suite@notmuchmail.org \
+            '[cc]="Other Parties <cc@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="reply with CC"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@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:
+> reply with CC"
+
+test_begin_subtest "From Us, Reply with CC"
+add_message '[from]="Notmuch Test Suite <test_suite@notmuchmail.org>"' \
+            '[to]="Recipient <recipient@example.com>"' \
+            '[cc]="Other Parties <cc@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="reply with CC"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Recipient <recipient@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+> reply with CC"
+
+test_begin_subtest "From Us, Reply no TO but with CC"
+add_message '[from]="Notmuch Test Suite <test_suite@notmuchmail.org>"' \
+            '[cc]="Other Parties <cc@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="reply with CC"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+Cc: Other Parties <cc@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+> reply with CC"
+
+test_begin_subtest "Reply from alternate address"
+add_message '[from]="Sender <sender@example.com>"' \
+             [to]=test_suite_other@notmuchmail.org \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="reply from alternate address"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@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:
+> reply from alternate address"
+
+test_begin_subtest "Support for Reply-To"
+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]="support for reply-to"' \
+            '[reply-to]="Sender <elsewhere@example.com>"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <elsewhere@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:
+> support for reply-to"
+
+test_begin_subtest "Support for Reply-To with multiple recipients"
+add_message '[from]="Sender <sender@example.com>"' \
+            '[to]="test_suite@notmuchmail.org, Someone Else <someone@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="support for reply-to with multiple recipients"' \
+            '[reply-to]="Sender <elsewhere@example.com>"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <elsewhere@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:
+> support for reply-to with multiple recipients"
+
+test_begin_subtest "Un-munging Reply-To"
+add_message '[from]="Sender <sender@example.com>"' \
+            '[to]="Some List <list@example.com>"' \
+             [subject]=notmuch-reply-test \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="Un-munging Reply-To"' \
+            '[reply-to]="Evil Munging List <list@example.com>"'
+
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@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:
+> Un-munging Reply-To"
+
+test_begin_subtest "Message with header of exactly 200 bytes"
+add_message '[subject]="This subject is exactly 200 bytes in length. Other than its length there is not much of note here. Note that the length of 200 bytes includes the Subject: and Re: prefixes with two spaces"' \
+            '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+            '[body]="200-byte header"'
+output=$(notmuch reply --reply-to=sender id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: This subject is exactly 200 bytes in length. Other than its
+ length there is not much of note here. Note that the length of 200 bytes
+ includes the Subject: and Re: prefixes with two spaces
+To: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+> 200-byte header"
+test_done
diff --git a/test/T240-dump-restore.sh b/test/T240-dump-restore.sh
new file mode 100755 (executable)
index 0000000..c3f1883
--- /dev/null
@@ -0,0 +1,360 @@
+#!/usr/bin/env bash
+test_description="\"notmuch dump\" and \"notmuch restore\""
+. $(dirname "$0")/test-lib.sh || exit 1
+
+NOTMUCH_NEW > /dev/null
+test_begin_subtest "dump header"
+cat <<EOF > EXPECTED
+#notmuch-dump batch-tag:3 config,properties,tags
+EOF
+notmuch dump > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+add_email_corpus
+
+test_begin_subtest "Dumping all tags"
+test_expect_success 'generate_message && notmuch new && notmuch dump > dump.expected'
+
+# The use of from:cworth is rather arbitrary: it matches some of the
+# email corpus' messages, but not all of them.
+
+test_begin_subtest "Dumping all tags II"
+test_expect_success \
+  'notmuch tag +ABC +DEF -- from:cworth &&
+  notmuch dump > dump-ABC_DEF.expected &&
+  ! cmp dump.expected dump-ABC_DEF.expected'
+
+test_begin_subtest "Clearing all tags"
+test_expect_success \
+  'sed -e "s/(\([^(]*\))$/()/" < dump.expected > clear.expected &&
+  notmuch restore --input=clear.expected &&
+  notmuch dump > clear.actual &&
+  test_cmp clear.expected clear.actual'
+
+test_begin_subtest "Clearing all tags"
+test_expect_success \
+  'notmuch tag +ABC +DEF -- from:cworth &&
+  notmuch restore --accumulate < dump.expected &&
+  notmuch dump > dump.actual &&
+  test_cmp dump-ABC_DEF.expected dump.actual'
+
+test_begin_subtest "Restoring original tags"
+test_expect_success \
+  'notmuch restore --input=dump.expected &&
+  notmuch dump > dump.actual &&
+  test_cmp dump.expected dump.actual'
+
+test_begin_subtest "Restore with nothing to do"
+test_expect_success \
+  'notmuch restore < dump.expected &&
+  notmuch dump > dump.actual &&
+  test_cmp dump.expected dump.actual'
+
+test_begin_subtest "Accumulate with existing tags"
+test_expect_success \
+  'notmuch restore --accumulate --input=dump.expected &&
+  notmuch dump > dump.actual &&
+  test_cmp dump.expected dump.actual'
+
+test_begin_subtest "Accumulate with no tags"
+test_expect_success \
+  'notmuch restore --accumulate < clear.expected &&
+  notmuch dump > dump.actual &&
+  test_cmp dump.expected dump.actual'
+
+test_begin_subtest "Accumulate with new tags"
+test_expect_success \
+  'notmuch restore --input=dump.expected &&
+  notmuch restore --accumulate --input=dump-ABC_DEF.expected &&
+  notmuch dump > OUTPUT.$test_count &&
+  notmuch restore --input=dump.expected &&
+  test_cmp dump-ABC_DEF.expected OUTPUT.$test_count'
+
+# notmuch restore currently only considers the first argument.
+test_begin_subtest "Invalid restore invocation"
+test_expect_success \
+  'test_must_fail notmuch restore --input=dump.expected another_one'
+
+test_begin_subtest "dump --output=outfile"
+notmuch dump --output=dump-outfile.actual
+test_expect_equal_file dump.expected dump-outfile.actual
+
+test_begin_subtest "dump --output=outfile --"
+notmuch dump --output=dump-1-arg-dash.actual --
+test_expect_equal_file dump.expected dump-1-arg-dash.actual
+
+# gzipped output
+
+test_begin_subtest "dump --gzip"
+notmuch dump --gzip > dump-gzip.gz
+gunzip dump-gzip.gz
+test_expect_equal_file dump.expected dump-gzip
+
+test_begin_subtest "dump --gzip --output=outfile"
+notmuch dump --gzip --output=dump-gzip-outfile.gz
+gunzip dump-gzip-outfile.gz
+test_expect_equal_file dump.expected dump-gzip-outfile
+
+test_begin_subtest "restoring gzipped stdin"
+notmuch dump --gzip --output=backup.gz
+notmuch tag +new_tag '*'
+notmuch restore < backup.gz
+notmuch dump --output=dump.actual
+test_expect_equal_file dump.expected dump.actual
+
+test_begin_subtest "restoring gzipped file"
+notmuch dump --gzip --output=backup.gz
+notmuch tag +new_tag '*'
+notmuch restore --input=backup.gz
+notmuch dump --output=dump.actual
+test_expect_equal_file dump.expected dump.actual
+
+# Note, we assume all messages from cworth have a message-id
+# containing cworth.org
+
+{ head -1 dump.expected ; grep 'cworth[.]org' dump.expected; } > dump-cworth.expected
+
+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
+
+test_begin_subtest "dump --output=outfile -- from:cworth"
+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
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "format=batch-tag, dump sanity check."
+NOTMUCH_DUMP_TAGS --format=sup from:cworth | cut -f1 -d' ' | \
+    sort > EXPECTED.$test_count
+NOTMUCH_DUMP_TAGS --format=batch-tag from:cworth | sed 's/^.*-- id://' | \
+    sort > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "format=batch-tag, missing newline"
+printf "+a_tag_without_newline -- id:20091117232137.GA7669@griffis1.net" > IN
+notmuch restore --accumulate < IN
+NOTMUCH_DUMP_TAGS id:20091117232137.GA7669@griffis1.net > OUT
+cat <<EOF > EXPECTED
++a_tag_without_newline +inbox +unread -- id:20091117232137.GA7669@griffis1.net
+EOF
+test_expect_equal_file EXPECTED OUT
+
+test_begin_subtest "format=batch-tag, # round-trip"
+notmuch dump --format=sup | sort > EXPECTED.$test_count
+notmuch dump --format=batch-tag > DUMPFILE
+notmuch restore --format=batch-tag < DUMPFILE
+notmuch dump --format=sup | sort > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "format=batch-tag, # blank lines and comments"
+notmuch dump --format=batch-tag| sort > EXPECTED.$test_count
+notmuch restore <<EOF
+# this line is a comment; the next has only white space
+        
+
+# the previous line is empty
+EOF
+notmuch dump --format=batch-tag | sort > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "format=batch-tag, # reverse-round-trip empty tag"
+cat <<EOF >EXPECTED.$test_count
++ -- id:20091117232137.GA7669@griffis1.net
+EOF
+notmuch restore --format=batch-tag < EXPECTED.$test_count
+NOTMUCH_DUMP_TAGS --format=batch-tag id:20091117232137.GA7669@griffis1.net > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+tag1='comic_swear=$&^%$^%\\//-+$^%$'
+enc1=$($TEST_DIRECTORY/hex-xcode --direction=encode "$tag1")
+
+tag2=$(printf 'this\n tag\t has\n spaces')
+enc2=$($TEST_DIRECTORY/hex-xcode --direction=encode "$tag2")
+
+enc3='%c3%91%c3%a5%c3%b0%c3%a3%c3%a5%c3%a9-%c3%8f%c3%8a'
+tag3=$($TEST_DIRECTORY/hex-xcode --direction=decode $enc3)
+
+notmuch dump --format=batch-tag > BACKUP
+
+notmuch tag +"$tag1" +"$tag2" +"$tag3" -inbox -unread "*"
+
+# initial segment of file used for several tests below.
+cat <<EOF > comments-and-blanks
+# this is a comment
+
+# next line has leading whitespace
+       
+
+EOF
+
+test_begin_subtest 'restoring empty file is not an error'
+notmuch restore < /dev/null 2>OUTPUT.$test_count
+cp /dev/null EXPECTED
+test_expect_equal_file EXPECTED OUTPUT.$test_count
+
+test_begin_subtest 'file of comments and blank lines is not an error'
+notmuch restore --input=comments-and-blanks
+ret_val=$?
+test_expect_equal "$ret_val" "0"
+
+cp comments-and-blanks leading-comments-blanks-batch-tag
+echo "+some_tag -- id:yun1vjwegii.fsf@aiko.keithp.com" \
+    >> leading-comments-blanks-batch-tag
+
+test_begin_subtest 'detect format=batch-tag with leading comments and blanks'
+notmuch restore --input=leading-comments-blanks-batch-tag
+notmuch search --output=tags id:yun1vjwegii.fsf@aiko.keithp.com > OUTPUT.$test_count
+echo "some_tag" > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT.$test_count
+
+cp comments-and-blanks leading-comments-blanks-sup
+echo "yun1vjwegii.fsf@aiko.keithp.com (another_tag)" \
+    >> leading-comments-blanks-sup
+
+test_begin_subtest 'detect format=sup with leading comments and blanks'
+notmuch restore --input=leading-comments-blanks-sup
+notmuch search --output=tags id:yun1vjwegii.fsf@aiko.keithp.com > OUTPUT.$test_count
+echo "another_tag" > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT.$test_count
+
+test_begin_subtest 'format=batch-tag, round trip with strange tags'
+notmuch dump --format=batch-tag > EXPECTED.$test_count
+notmuch dump --format=batch-tag > DUMPFILE
+notmuch restore --format=batch-tag < DUMPFILE
+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
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'restoring sane tags'
+notmuch restore --format=batch-tag < BACKUP
+notmuch dump --format=batch-tag > OUTPUT.$test_count
+test_expect_equal_file BACKUP OUTPUT.$test_count
+
+test_begin_subtest 'format=batch-tag, restore=auto'
+notmuch dump --format=batch-tag > EXPECTED.$test_count
+notmuch tag -inbox -unread "*"
+notmuch restore --format=auto < EXPECTED.$test_count
+notmuch dump --format=batch-tag > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'format=sup, restore=auto'
+notmuch dump --format=sup > EXPECTED.$test_count
+notmuch tag -inbox -unread "*"
+notmuch restore --format=auto < EXPECTED.$test_count
+notmuch dump --format=sup > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'format=batch-tag, restore=default'
+notmuch dump --format=batch-tag > EXPECTED.$test_count
+notmuch tag -inbox -unread "*"
+notmuch restore < EXPECTED.$test_count
+notmuch dump --format=batch-tag > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'format=sup, restore=default'
+notmuch dump --format=sup > EXPECTED.$test_count
+notmuch tag -inbox -unread "*"
+notmuch restore < EXPECTED.$test_count
+notmuch dump --format=sup > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest 'restore: checking error messages'
+notmuch restore <<EOF 2>OUTPUT
+# the next line has a space
+a
++0
++a +b
+# trailing whitespace
++a +b 
++c +d --
+# this is a harmless comment, do not yell about it.
+
+# the previous line was blank; also no yelling please
++%zz -- id:whatever
++e +f id:"
++e +f tag:abc
+# the next non-comment line should report an an empty tag error for
+# batch tagging, but not for restore
++ +e -- id:20091117232137.GA7669@griffis1.net
+# valid id, but warning about missing message
++e id:missing_message_id
+# exercise parser
++e -- id:some)stuff
++e -- id:some stuff
++e -- id:some"stuff
++e -- id:"a_message_id_with""_a_quote"
++e -- id:"a message id with spaces"
++e --  id:an_id_with_leading_and_trailing_ws \
+
+EOF
+
+cat <<EOF > EXPECTED
+Warning: cannot parse query: a (skipping)
+Warning: no query string [+0]
+Warning: no query string [+a +b]
+Warning: missing query string [+a +b ]
+Warning: no query string after -- [+c +d --]
+Warning: hex decoding of tag %zz failed [+%zz -- id:whatever]
+Warning: cannot parse query: id:" (skipping)
+Warning: not an id query: tag:abc (skipping)
+Warning: cannot apply tags to missing message: missing_message_id
+Warning: cannot parse query: id:some)stuff (skipping)
+Warning: cannot parse query: id:some stuff (skipping)
+Warning: cannot apply tags to missing message: some"stuff
+Warning: cannot apply tags to missing message: a_message_id_with"_a_quote
+Warning: cannot apply tags to missing message: a message id with spaces
+Warning: cannot apply tags to missing message: an_id_with_leading_and_trailing_ws
+EOF
+
+test_expect_equal_file EXPECTED OUTPUT
+
+backup_database
+test_begin_subtest 'roundtripping random message-ids and tags'
+
+    ${TEST_DIRECTORY}/random-corpus --config-path=${NOTMUCH_CONFIG} \
+                       --num-messages=100
+
+     notmuch dump --format=batch-tag| \
+        sort > EXPECTED.$test_count
+
+     notmuch tag +this_tag_is_very_unlikely_to_be_random '*'
+
+     notmuch restore --format=batch-tag < EXPECTED.$test_count
+
+     notmuch dump --format=batch-tag| \
+        sort > OUTPUT.$test_count
+
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+restore_database
+
+test_done
+
diff --git a/test/T250-uuencode.sh b/test/T250-uuencode.sh
new file mode 100755 (executable)
index 0000000..251c0b4
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+test_description="handling of uuencoded data"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_message [subject]=uuencodetest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' \
+'[body]="This message is used to ensure that notmuch correctly handles a
+message containing a block of uuencoded data. First, we have a marker
+this content beforeuudata . Then we begin the uuencoded data itself:
+
+begin 644 bogus-uuencoded-data
+M0123456789012345678901234567890123456789012345678901234567890
+MOBVIOUSLY, THIS IS NOT ANY SORT OF USEFUL UUENCODED DATA.    
+MINSTEAD THIS IS JUST A WAY TO ENSURE THAT THIS BLOCK OF DATA 
+MIS CORRECTLY IGNORED WHEN NOTMUCH CREATES ITS INDEX. SO WE   
+MINCLUDE A DURINGUUDATA MARKER THAT SHOULD NOT RESULT IN ANY  
+MSEARCH RESULT.                                               
+\\\`
+end
+
+Finally, we have our afteruudata marker as well."'
+
+test_begin_subtest "Ensure content before uu data is indexed"
+output=$(notmuch search beforeuudata | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; uuencodetest (inbox unread)"
+
+test_begin_subtest "Ensure uu data is not indexed"
+output=$(notmuch search DURINGUUDATA | notmuch_search_sanitize)
+test_expect_equal "$output" ""
+
+test_begin_subtest "Ensure content after uu data is indexed"
+output=$(notmuch search afteruudata | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; uuencodetest (inbox unread)"
+
+test_done
diff --git a/test/T260-thread-order.sh b/test/T260-thread-order.sh
new file mode 100755 (executable)
index 0000000..fea6127
--- /dev/null
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+test_description="threading when messages received out of order"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+# Generate all single-root four message thread structures.  We'll use
+# this for multiple tests below.
+THREADS=$($NOTMUCH_PYTHON ${NOTMUCH_SRCDIR}/test/gen-threads.py 4)
+nthreads=$(wc -l <<< "$THREADS")
+
+test_begin_subtest "Messages with one parent get linked in all delivery orders"
+# In the first variant, this delivers messages that reference only
+# their immediate parent.  Hence, we should only expect threads to be
+# fully joined at the end.
+for ((n = 0; n < 4; n++)); do
+    # Deliver the n'th message of every thread
+    thread=0
+    while read -a parents; do
+        parent=${parents[$n]}
+        generate_message \
+            [id]=m$n@t$thread [in-reply-to]="\<m$parent@t$thread\>" \
+            [subject]=p$thread [from]=m$n
+        thread=$((thread + 1))
+    done <<< "$THREADS"
+    notmuch new > /dev/null
+done
+output=$(notmuch search --sort=newest-first '*' | notmuch_search_sanitize)
+expected=$(for ((i = 0; i < $nthreads; i++)); do
+        echo "thread:XXX   2001-01-05 [4/4] m3, m2, m1, m0; p$i (inbox unread)"
+    done)
+test_expect_equal "$output" "$expected"
+
+test_begin_subtest "Messages with all parents get linked in all delivery orders"
+# Here we do the same thing as the previous test, but each message
+# references all of its parents.  Since every message references the
+# root of the thread, each thread should always be fully joined.  This
+# is currently broken because of the bug detailed in
+# id:8738h7kv2q.fsf@qmul.ac.uk.
+rm ${MAIL_DIR}/*
+notmuch new > /dev/null
+output=""
+expected=""
+for ((n = 0; n < 4; n++)); do
+    # Deliver the n'th message of every thread
+    thread=0
+    while read -a parents; do
+        references=""
+        parent=${parents[$n]}
+        while [[ ${parent:-None} != None ]]; do
+            references="<m$parent@t$thread> $references"
+            pp=$parent
+            parent=${parents[$parent]}
+            # Avoid looping over broken input (if ever)
+            parents[$pp]="None"
+        done
+
+        generate_message \
+            [id]=m$n@t$thread [references]="'$references'" \
+            [subject]=p$thread [from]=m$n
+        thread=$((thread + 1))
+    done <<< "$THREADS"
+    notmuch new > /dev/null
+
+    output="$output
+$(notmuch search --sort=newest-first '*' | notmuch_search_sanitize)"
+
+    # Construct expected output
+    template="thread:XXX   2001-01-05 [$((n+1))/$((n+1))]"
+    for ((m = n; m > 0; m--)); do
+        template="$template m$m,"
+    done
+    expected="$expected
+$(for ((i = 0; i < $nthreads; i++)); do
+        echo "$template m0; p$i (inbox unread)"
+    done)"
+done
+test_expect_equal "$output" "$expected"
+
+test_done
diff --git a/test/T270-author-order.sh b/test/T270-author-order.sh
new file mode 100755 (executable)
index 0000000..c28ecb0
--- /dev/null
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+test_description="author reordering;"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "Adding parent message"
+generate_message [body]=findme [id]=new-parent-id [subject]=author-reorder-threadtest '[from]="User <user@example.com>"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Adding initial child message"
+generate_message [body]=findme "[in-reply-to]=\<new-parent-id\>" [subject]=author-reorder-threadtest '[from]="User1 <user1@example.com>"' '[date]="Sat, 01 Jan 2000 12:01:00 -0000"'
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Adding second child message"
+generate_message [body]=findme "[in-reply-to]=\<new-parent-id\>" [subject]=author-reorder-threadtest '[from]="User2 <user2@example.com>"' '[date]="Sat, 01 Jan 2000 12:02:00 -0000"'
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Searching when all three messages match"
+output=$(notmuch search findme | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [3/3] User, User1, User2; author-reorder-threadtest (inbox unread)"
+
+test_begin_subtest "Searching when two messages match"
+output=$(notmuch search User1 or User2 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [2/3] User1, User2| User; author-reorder-threadtest (inbox unread)"
+
+test_begin_subtest "Searching when only one message matches"
+output=$(notmuch search User2 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/3] User2| User, User1; author-reorder-threadtest (inbox unread)"
+
+test_begin_subtest "Searching when only first message matches"
+output=$(notmuch search User | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/3] User| User1, User2; author-reorder-threadtest (inbox unread)"
+
+test_begin_subtest "Adding duplicate author"
+generate_message [body]=findme "[in-reply-to]=\<new-parent-id\>" [subject]=author-reorder-threadtest '[from]="User1 <user1@example.com>"' '[date]="Sat, 01 Jan 2000 12:03:00 -0000"'
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Searching when all four messages match"
+output=$(notmuch search findme | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [4/4] User, User1, User2; author-reorder-threadtest (inbox unread)"
+
+test_begin_subtest "Adding non-monotonic child message"
+generate_message [body]=findme "[in-reply-to]=\<new-parent-id\>" [subject]=author-reorder-threadtest '[from]="User0 <user0@example.com>"' '[date]="Sat, 01 Jan 2000 11:00:00 -0000"'
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Searching non-monotonic messages (oldest-first)"
+output=$(notmuch search --sort=oldest-first findme | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [5/5] User0, User, User1, User2; author-reorder-threadtest (inbox unread)"
+
+test_begin_subtest "Searching non-monotonic messages (newest-first)"
+output=$(notmuch search --sort=newest-first findme | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [5/5] User0, User, User1, User2; author-reorder-threadtest (inbox unread)"
+
+test_done
diff --git a/test/T280-from-guessing.sh b/test/T280-from-guessing.sh
new file mode 100755 (executable)
index 0000000..b871823
--- /dev/null
@@ -0,0 +1,217 @@
+#!/usr/bin/env bash
+test_description="From line heuristics (with multiple configured addresses)"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "Magic from guessing (nothing to go on)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-test \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_begin_subtest "Magic from guessing (Envelope-to:)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-test \
+           '[header]="Envelope-To: test_suite_other@notmuchmail.org"' \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_begin_subtest "Magic from guessing (X-Original-To:)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-test \
+           '[header]="X-Original-To: test_suite_other@notmuchmail.org"' \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_begin_subtest "Magic from guessing (Received: .. for ..)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-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)\"" \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_begin_subtest "Magic from guessing (Received: domain)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-test \
+           "[header]=\"Received: from mail.example.com (mail.example.com [1.1.1.1])
+       by mail.otherdomain.org (some MTA) with ESMTP id 12345678
+       Sat, 10 Apr 2010 07:54:51 -0400 (EDT)\"" \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@otherdomain.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_begin_subtest "Magic from guessing (multiple Received: headers)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-test \
+           "[header]=\"Received: from extraneous.example.com (extraneous.example.com [1.1.1.1])
+Received: from mail.example.com (mail.example.com [1.1.1.1])
+       by mail.otherdomain.org (some MTA) with ESMTP id 12345678
+       for <test_suite_other@notmuchmail.org>; Sat, 10 Apr 2010 07:54:51 -0400 (EDT)
+Received: from extraneous.example.com (extraneous.example.com [1.1.1.1])\"" \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output="$(notmuch reply id:${gen_msg_id})"
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_begin_subtest "Testing From line heuristics (with single configured address)"
+sed -i -e "s/^other_email.*//" "${NOTMUCH_CONFIG}"
+test_expect_equal '' ''
+
+test_begin_subtest "Magic from guessing (nothing to go on)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-test \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_begin_subtest "Magic from guessing (Envelope-to:)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-test \
+           '[header]="Envelope-To: test_suite_other@notmuchmail.org"' \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_begin_subtest "Magic from guessing (X-Original-To:)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-test \
+           '[header]="X-Original-To: test_suite_other@notmuchmail.org"' \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_begin_subtest "Magic from guessing (Received: .. for ..)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-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)\"" \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_begin_subtest "Magic from guessing (Received: domain)"
+add_message '[from]="Sender <sender@example.com>"' \
+            [to]=mailinglist@notmuchmail.org \
+            [subject]=notmuch-reply-test \
+           "[header]=\"Received: from mail.example.com (mail.example.com [1.1.1.1])
+       by mail.otherdomain.org (some MTA) with ESMTP id 12345678
+       Sat, 10 Apr 2010 07:54:51 -0400 (EDT)\"" \
+           '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+           '[body]="from guessing test"'
+
+output=$(notmuch reply id:${gen_msg_id})
+test_expect_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, mailinglist@notmuchmail.org
+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 test"
+
+test_done
diff --git a/test/T290-long-id.sh b/test/T290-long-id.sh
new file mode 100755 (executable)
index 0000000..5e3879f
--- /dev/null
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+test_description="messages with ridiculously-long message IDs"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "Referencing long ID before adding"
+generate_message '[subject]="Reference of ridiculously-long message ID"' \
+                "[references]=\<abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-\>"
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Adding message with long ID"
+generate_message '[subject]="A ridiculously-long message ID"' \
+                "[id]=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Referencing long ID after adding"
+generate_message '[subject]="Reply to ridiculously-long message ID"' \
+                "[in-reply-to]=\<abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-\>"
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Ensure all messages were threaded together"
+output=$(notmuch search 'subject:"a ridiculously-long message ID"' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/3] Notmuch Test Suite; A ridiculously-long message ID (inbox unread)"
+
+test_done
diff --git a/test/T300-encoding.sh b/test/T300-encoding.sh
new file mode 100755 (executable)
index 0000000..6fcd10c
--- /dev/null
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+test_description="encoding issues"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "Message with text of unknown charset"
+add_message '[content-type]="text/plain; charset=unknown-8bit"' \
+           "[body]=irrelevant"
+output=$(notmuch show id:${gen_msg_id} 2>&1 | notmuch_show_sanitize_all)
+test_expect_equal "$output" "\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox unread)
+Subject: Message with text of unknown charset
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: GENERATED_DATE
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+irrelevant
+\fpart}
+\fbody}
+\fmessage}"
+
+test_begin_subtest "Search for ISO-8859-2 encoded message"
+add_message '[content-type]="text/plain; charset=iso-8859-2"' \
+            '[content-transfer-encoding]=8bit' \
+            '[subject]="ISO-8859-2 encoded message"' \
+            "[body]=$'Czech word tu\350\362\341\350\350\355 means pinguin\'s.'" # ISO-8859-2 characters are generated by shell's escape sequences
+output=$(notmuch search tučňáččí 2>&1 | notmuch_show_sanitize_all)
+test_expect_equal "$output" "thread:0000000000000002   2001-01-05 [1/1] Notmuch Test Suite; ISO-8859-2 encoded message (inbox unread)"
+
+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_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_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_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"'                           \
+            "[body]=$'This text contains \x93Windows-1252\x94 character codes.'"
+cat <<EOF > EXPECTED
+\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox unread)
+Subject: Mislabeled Windows-1252 encoding
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: GENERATED_DATE
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This text contains “Windows-1252” character codes.
+\fpart}
+\fbody}
+\fmessage}
+EOF
+notmuch show id:${gen_msg_id} 2>&1 | notmuch_show_sanitize_all > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T310-emacs.sh b/test/T310-emacs.sh
new file mode 100755 (executable)
index 0000000..d3aa2e7
--- /dev/null
@@ -0,0 +1,1111 @@
+#!/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
+
+# syntax errors in test-lib.el cause mysterious failures
+test_begin_subtest "Syntax of emacs test library"
+test_expect_success "${TEST_EMACS} -Q --batch --load $NOTMUCH_SRCDIR/test/test-lib.el"
+
+test_begin_subtest "Basic notmuch-hello view in emacs"
+test_emacs '(notmuch-hello)
+           (test-output)'
+test_expect_equal_file $EXPECTED/notmuch-hello OUTPUT
+
+test_begin_subtest "Saved search with 0 results"
+test_emacs '(let ((notmuch-show-empty-saved-searches t)
+                 (notmuch-saved-searches
+                  '\''(("inbox" . "tag:inbox")
+                       ("unread" . "tag:unread")
+                       ("empty" . "tag:doesnotexist"))))
+             (notmuch-hello)
+             (test-output))'
+test_expect_equal_file $EXPECTED/notmuch-hello-with-empty OUTPUT
+
+test_begin_subtest "No saved searches displayed (all with 0 results)"
+test_emacs '(let ((notmuch-saved-searches
+                  '\''(("empty" . "tag:doesnotexist"))))
+             (notmuch-hello)
+             (test-output))'
+test_expect_equal_file $EXPECTED/notmuch-hello-no-saved-searches OUTPUT
+
+test_begin_subtest "Basic notmuch-search view in emacs"
+test_emacs '(notmuch-search "tag:inbox")
+           (notmuch-test-wait)
+           (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))
+                      ((symbol-function 'notmuch-search-process-filter)
+                       (lambda (proc string)
+                         (cl-loop for char across string
+                                  do (orig proc (char-to-string char))))))
+             (notmuch-search \"tag:inbox\")
+             (notmuch-test-wait))
+           (test-output)"
+test_expect_equal_file $EXPECTED/notmuch-search-tag-inbox OUTPUT
+
+test_begin_subtest "Navigation of notmuch-hello to search results"
+test_emacs '(notmuch-hello)
+           (goto-char (point-min))
+           (re-search-forward "inbox")
+           (widget-button-press (1- (point)))
+           (notmuch-test-wait)
+           (test-output)'
+test_expect_equal_file $EXPECTED/notmuch-hello-view-inbox OUTPUT
+
+test_begin_subtest "Basic notmuch-show view in emacs"
+maildir_storage_thread=$(notmuch search --output=threads id:20091117190054.GU3165@dottiness.seas.harvard.edu)
+test_emacs "(notmuch-show \"$maildir_storage_thread\")
+           (test-output)"
+test_expect_equal_file $EXPECTED/notmuch-show-thread-maildir-storage OUTPUT
+
+test_begin_subtest "Basic notmuch-show view in emacs default indentation"
+maildir_storage_thread=$(notmuch search --output=threads id:20091117190054.GU3165@dottiness.seas.harvard.edu)
+test_emacs "(let ((notmuch-show-indent-messages-width 1))
+             (notmuch-show \"$maildir_storage_thread\")
+             (test-output))"
+test_expect_equal_file $EXPECTED/notmuch-show-thread-maildir-storage OUTPUT
+
+test_begin_subtest "Basic notmuch-show view in emacs without indentation"
+maildir_storage_thread=$(notmuch search --output=threads id:20091117190054.GU3165@dottiness.seas.harvard.edu)
+test_emacs "(let ((notmuch-show-indent-messages-width 0))
+             (notmuch-show \"$maildir_storage_thread\")
+             (test-output))"
+test_expect_equal_file $EXPECTED/notmuch-show-thread-maildir-storage-without-indentation OUTPUT
+
+test_begin_subtest "Basic notmuch-show view in emacs with fourfold indentation"
+maildir_storage_thread=$(notmuch search --output=threads id:20091117190054.GU3165@dottiness.seas.harvard.edu)
+test_emacs "(let ((notmuch-show-indent-messages-width 4))
+             (notmuch-show \"$maildir_storage_thread\")
+             (test-output))"
+test_expect_equal_file $EXPECTED/notmuch-show-thread-maildir-storage-with-fourfold-indentation OUTPUT
+
+test_begin_subtest "notmuch-show for message with invalid From"
+add_message "[subject]=\"message-with-invalid-from\"" \
+           "[from]=\"\\\"Invalid \\\" From\\\" <test_suite@notmuchmail.org>\""
+thread=$(notmuch search --output=threads subject:message-with-invalid-from)
+test_emacs "(notmuch-show \"$thread\")
+           (test-output \"OUTPUT.raw\")"
+cat <<EOF >EXPECTED
+Invalid " From <test_suite@notmuchmail.org> (2001-01-05) (inbox)
+Subject: message-with-invalid-from
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: GENERATED_DATE
+
+This is just a test message (#1)
+EOF
+notmuch_date_sanitize < OUTPUT.raw > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Navigation of notmuch-search to thread view"
+test_emacs '(notmuch-search "tag:inbox")
+           (notmuch-test-wait)
+           (goto-char (point-min))
+           (re-search-forward "Working with Maildir")
+           (notmuch-search-show-thread)
+           (notmuch-test-wait)
+           (test-output)'
+test_expect_equal_file $EXPECTED/notmuch-show-thread-maildir-storage OUTPUT
+
+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\"")
+           (notmuch-test-wait)
+           (execute-kbd-macro "+search-add")
+           (execute-kbd-macro "+search-remove")
+           (execute-kbd-macro "-search-remove")
+           (notmuch-show "id:\"123..456@example\"")
+           (notmuch-test-wait)
+           (execute-kbd-macro "+show-add")
+           (execute-kbd-macro "+show-remove")
+           (execute-kbd-macro "-show-remove")'
+output=$(notmuch search 'id:"123..456@example"' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Message with .. in Message-Id (inbox search-add show-add)"
+
+test_begin_subtest "Message with quote in Message-Id:"
+add_message '[id]="\"quote\"@example"' '[subject]="Message with quote in Message-Id"'
+test_emacs '(notmuch-search "subject:\"Message with quote\"")
+           (notmuch-test-wait)
+           (execute-kbd-macro "+search-add")
+            (notmuch-search-show-thread)
+           (notmuch-test-wait)
+           (execute-kbd-macro "+show-add")'
+output=$(notmuch search 'id:"""quote""@example"' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Message with quote in Message-Id (inbox search-add show-add)"
+
+test_begin_subtest "Sending a message via (fake) SMTP"
+emacs_deliver_message \
+    'Testing message sent via SMTP' \
+    'This is a test that messages are sent via SMTP' \
+    '(message-goto-to)
+     (kill-whole-line)
+     (insert "To: user@example.com\n")'
+sed \
+    -e s',^Message-ID: <.*>$,Message-ID: <XXX>,' \
+    -e s',^\(Content-Type: text/plain\); charset=us-ascii$,\1,' < sent_message >OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: user@example.com
+Subject: Testing message sent via SMTP
+Date: 01 Jan 2000 12:00:00 -0000
+Message-ID: <XXX>
+MIME-Version: 1.0
+Content-Type: text/plain
+
+This is a test that messages are sent via SMTP
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Folding a long header when sending via (fake) SMTP"
+long_subject="This is a long subject `echo {1..1000}`"
+emacs_deliver_message \
+    "${long_subject}" \
+    'This is a test that long headers are folded when messages are sent via SMTP' \
+    '(message-goto-to)
+     (kill-whole-line)
+     (insert "To: user@example.com\n")'
+sed \
+    -e s',^Message-ID: <.*>$,Message-ID: <XXX>,' \
+    -e s',^\(Content-Type: text/plain\); charset=us-ascii$,\1,' < sent_message >OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: user@example.com
+Subject: This is a long subject 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
+ 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
+ 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
+ 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
+ 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
+ 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
+ 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
+ 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
+ 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
+ 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
+ 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
+ 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
+ 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
+ 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
+ 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
+ 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
+ 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
+ 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
+ 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
+ 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
+ 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
+ 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
+ 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
+ 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
+ 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
+ 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
+ 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509
+ 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
+ 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
+ 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
+ 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
+ 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
+ 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617
+ 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
+ 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
+ 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671
+ 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689
+ 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707
+ 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725
+ 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743
+ 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761
+ 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779
+ 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797
+ 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815
+ 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833
+ 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851
+ 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
+ 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887
+ 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905
+ 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923
+ 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941
+ 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959
+ 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977
+ 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995
+ 996 997 998 999 1000
+Date: 01 Jan 2000 12:00:00 -0000
+Message-ID: <XXX>
+MIME-Version: 1.0
+Content-Type: text/plain
+
+This is a test that long headers are folded when messages are sent via SMTP
+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)"
+
+test_begin_subtest "notmuch-fcc-dirs set to nil"
+test_emacs "(let ((notmuch-fcc-dirs nil))
+             (notmuch-mua-mail)
+             (test-output))"
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: 
+Subject: 
+--text follows this line--
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+# Make another FCC maildir specific for the next test
+mkdir -p mail/sent-string/cur
+mkdir -p mail/sent-string/new
+mkdir -p mail/sent-string/tmp
+
+test_begin_subtest "notmuch-fcc-dirs set to a string"
+test_emacs "(let ((notmuch-fcc-dirs \"sent-string\"))
+             (notmuch-mua-mail)
+             (test-output))"
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: 
+Subject: 
+Fcc: ${MAIL_DIR}/sent-string
+--text follows this line--
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+# Make more FCC maildirs specific for the next test
+mkdir -p mail/sent-list-match/cur
+mkdir -p mail/sent-list-match/new
+mkdir -p mail/sent-list-match/tmp
+mkdir -p mail/failure/cur
+mkdir -p mail/failure/new
+mkdir -p mail/failure/tmp
+
+test_begin_subtest "notmuch-fcc-dirs set to a list (with match)"
+test_emacs "(let ((notmuch-fcc-dirs
+                  '((\"notmuchmail.org\" . \"sent-list-match\")
+                    (\".*\" . \"failure\"))))
+             (notmuch-mua-mail)
+             (test-output))"
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: 
+Subject: 
+Fcc: ${MAIL_DIR}/sent-list-match
+--text follows this line--
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+# Make another FCC maildir specific for the next test
+mkdir -p mail/sent-list-catch-all/cur
+mkdir -p mail/sent-list-catch-all/new
+mkdir -p mail/sent-list-catch-all/tmp
+
+test_begin_subtest "notmuch-fcc-dirs set to a list (catch-all)"
+test_emacs "(let ((notmuch-fcc-dirs
+                  '((\"example.com\" . \"failure\")
+                    (\".*\" . \"sent-list-catch-all\"))))
+             (notmuch-mua-mail)
+             (test-output))"
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: 
+Subject: 
+Fcc: ${MAIL_DIR}/sent-list-catch-all
+--text follows this line--
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch-fcc-dirs set to a list (no match)"
+test_emacs "(let ((notmuch-fcc-dirs
+                  '((\"example.com\" . \"failure\")
+                    (\"nomatchhere.net\" . \"failure\"))))
+             (notmuch-mua-mail)
+             (test-output))"
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: 
+Subject: 
+--text follows this line--
+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)
+           (notmuch-search-reply-to-thread)
+           (test-output))'
+sed -i -e 's/^In-Reply-To: <.*>$/In-Reply-To: <XXX>/' OUTPUT
+sed -i -e 's/^References: <.*>$/References: <XXX>/' OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: user@example.com
+Subject: Re: Testing message sent via SMTP
+In-Reply-To: <XXX>
+Fcc: ${MAIL_DIR}/sent
+References: <XXX>
+--text follows this line--
+Notmuch Test Suite <test_suite@notmuchmail.org> writes:
+
+> This is a test that messages are sent via SMTP
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Reply within emacs to a message with TAB in subject"
+test_emacs '(let ((message-hidden-headers ''()))
+           (notmuch-search "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net")
+           (notmuch-test-wait)
+           (notmuch-search-show-thread)
+           (notmuch-test-wait)
+           (notmuch-show-reply-sender)
+           (test-output))'
+sed -i -e 's/^In-Reply-To: <.*>$/In-Reply-To: <XXX>/' OUTPUT
+sed -i -e 's/^References: <.*>$/References: <XXX>/' OUTPUT
+sed -i -e '/^--text follows this line--$/q' OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Mikhail Gusarov <dottedmag@dottedmag.net>
+Subject: Re: [notmuch] [PATCH 1/2] Close message file after parsing message headers
+In-Reply-To: <XXX>
+Fcc: ${MAIL_DIR}/sent
+References: <XXX>
+--text follows this line--
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Reply from alternate address within emacs"
+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
+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\;' \
+             [cc]=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@notmuchmail.org>
+To: Sender <sender@example.com>, someone@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
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Reply within emacs to a multipart/mixed message"
+test_emacs '(let ((message-hidden-headers ''()))
+           (notmuch-show "id:20091118002059.067214ed@hikari")
+               (notmuch-show-reply)
+               (test-output))'
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Adrian Perez de Castro <aperez@igalia.com>, notmuch@notmuchmail.org
+Subject: Re: [notmuch] Introducing myself
+In-Reply-To: <20091118002059.067214ed@hikari>
+Fcc: ${MAIL_DIR}/sent
+References: <20091118002059.067214ed@hikari>
+--text follows this line--
+Adrian Perez de Castro <aperez@igalia.com> writes:
+
+> Hello to all,
+>
+> I have just heard about Not Much today in some random Linux-related news
+> site (LWN?), my name is Adrian Perez and I work as systems administrator
+> (although I can do some code as well :P). I have always thought that the
+> ideas behind Sup were great, but after some time using it, I got tired of
+> the oddities that it has. I also do not like doing things like having to
+> install Ruby just for reading and sorting mails. Some time ago I thought
+> about doing something like Not Much and in fact I played a bit with the
+> Python+Xapian and the Python+Whoosh combinations, because I find relaxing
+> to code things in Python when I am not working and also it is installed
+> by default on most distribution. I got to have some mailboxes indexed and
+> basic searching working a couple of months ago. Lately I have been very
+> busy and had no time for coding, and them... boom! Not Much appears -- and
+> it is almost exactly what I was trying to do, but faster. I have been
+> playing a bit with Not Much today, and I think it has potential.
+>
+> Also, I would like to share one idea I had in mind, that you might find
+> interesting: One thing I have found very annoying is having to re-tag my
+> mail when the indexes get b0rked (it happened a couple of times to me while
+> using Sup), so I was planning to mails as read/unread and adding the tags
+> not just to the index, but to the mail text itself, e.g. by adding a
+> "X-Tags" header field or by reusing the "Keywords" one. This way, the index
+> could be totally recreated by re-reading the mail directories, and this
+> would also allow to a tools like OfflineIMAP [1] to get the mails into a
+> local maildir, tagging and indexing the mails with the e-mail reader and
+> then syncing back the messages with the "X-Tags" header to the IMAP server.
+> This would allow to use the mail reader from a different computer and still
+> have everything tagged finely.
+>
+> Best regards,
+>
+>
+> ---
+> [1] http://software.complete.org/software/projects/show/offlineimap
+>
+> -- 
+> Adrian Perez de Castro <aperez@igalia.com>
+> Igalia - Free Software Engineering
+> _______________________________________________
+> notmuch mailing list
+> notmuch@notmuchmail.org
+> http://notmuchmail.org/mailman/listinfo/notmuch
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Reply within emacs to a multipart/alternative message"
+test_emacs '(let ((message-hidden-headers ''()))
+           (notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com")
+               (notmuch-show-reply)
+               (test-output))'
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Alex Botero-Lowry <alex.boterolowry@gmail.com>, notmuch@notmuchmail.org
+Subject: Re: [notmuch] preliminary FreeBSD support
+In-Reply-To: <cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com>
+Fcc: ${MAIL_DIR}/sent
+References: <cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com>
+--text follows this line--
+Alex Botero-Lowry <alex.boterolowry@gmail.com> writes:
+
+> 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.
+> 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
+>
+> _______________________________________________
+> notmuch mailing list
+> notmuch@notmuchmail.org
+> http://notmuchmail.org/mailman/listinfo/notmuch
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Reply within emacs to an html-only message"
+add_message '[content-type]="text/html"' \
+           '[body]="Hi,<br />This is an <b>HTML</b> test message.<br /><br />OK?"'
+test_emacs "(let ((message-hidden-headers '()))
+           (notmuch-show \"id:${gen_msg_id}\")
+           (notmuch-show-reply)
+           (test-output))"
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: test_suite@notmuchmail.org
+Subject: Re: Reply within emacs to an html-only message
+In-Reply-To: <${gen_msg_id}>
+Fcc: ${MAIL_DIR}/sent
+References: <${gen_msg_id}>
+--text follows this line--
+Notmuch Test Suite <test_suite@notmuchmail.org> writes:
+
+> Hi,This is an HTML test message.OK?
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Reply within emacs to message from self"
+add_message '[from]="test_suite@notmuchmail.org"' \
+           '[to]="test_suite@notmuchmail.org"'
+test_emacs "(let ((message-hidden-headers '()))
+           (notmuch-show \"id:${gen_msg_id}\")
+           (notmuch-show-reply)
+           (test-output))"
+sed -i -e 's/^In-Reply-To: <.*>$/In-Reply-To: <XXX>/' OUTPUT
+sed -i -e 's/^References: <.*>$/References: <XXX>/' OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: test_suite@notmuchmail.org
+Subject: Re: Reply within emacs to message from self
+In-Reply-To: <XXX>
+Fcc: ${MAIL_DIR}/sent
+References: <XXX>
+--text follows this line--
+test_suite@notmuchmail.org writes:
+
+> This is just a test message (#${gen_msg_cnt})
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Quote MML tags in reply"
+message_id='test-emacs-mml-quoting@message.id'
+add_message [id]="$message_id" \
+           "[subject]='$test_subtest_name'" \
+           '[body]="<#part disposition=inline>"'
+test_emacs "(let ((message-hidden-headers '()))
+             (notmuch-show \"id:$message_id\")
+             (notmuch-show-reply)
+             (test-output))"
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: test_suite@notmuchmail.org
+Subject: Re: Quote MML tags in reply
+In-Reply-To: <test-emacs-mml-quoting@message.id>
+Fcc: ${MAIL_DIR}/sent
+References: <test-emacs-mml-quoting@message.id>
+--text follows this line--
+Notmuch Test Suite <test_suite@notmuchmail.org> writes:
+
+> <#!part disposition=inline>
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Save attachment from within emacs using notmuch-show-save-attachments"
+# save as archive to test that Emacs does not re-compress .gz
+test_emacs '(let ((standard-input "\"attachment1.gz\""))
+             (notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com")
+             (notmuch-show-save-attachments))'
+test_expect_equal_file attachment1.gz "$EXPECTED/attachment"
+
+test_begin_subtest "Save attachment from within emacs using notmuch-show-save-part"
+# save as archive to test that Emacs does not re-compress .gz
+test_emacs '(let ((standard-input "\"attachment2.gz\""))
+             (notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com")
+             (search-forward "0001-Deal-with")
+             (notmuch-show-save-part))'
+test_expect_equal_file attachment2.gz "$EXPECTED/attachment"
+
+test_begin_subtest "Save 8bit attachment from within emacs using notmuch-show-save-attachments"
+
+add_message '[subject]="Attachment with 8bit chars"' \
+       '[header]="MIME-Version: 1.0"' \
+       '[content-type]="multipart/mixed; boundary=\"abcd\""' \
+       '[body]="--abcd
+Content-Type: text/plain
+
+Attachment follows:
+
+--abcd
+Content-Type: application/octet-stream; name=\"sample\"
+Content-Transfer-Encoding: 8bit
+Content-Disposition: attachment; filename=\"sample\"
+
+“¡ Hey ! It compiles ¡ Ship it !”
+
+--abcd--
+"'
+test_emacs '(notmuch-show "id:'"${gen_msg_id}"'")
+           (delete-file "OUTPUT")
+           (let ((standard-input "\"OUTPUT\""))
+             (notmuch-show-save-attachments))'
+
+test_expect_equal "$(cat OUTPUT)" '“¡ Hey ! It compiles ¡ Ship it !”'
+
+test_begin_subtest "View raw message within emacs"
+test_emacs '(notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com")
+           (notmuch-show-view-raw-message)
+           (test-output)'
+test_expect_equal_file $EXPECTED/raw-message-cf0c4d-52ad0a OUTPUT
+
+test_begin_subtest "Hiding/showing signature in notmuch-show view"
+maildir_storage_thread=$(notmuch search --output=threads id:20091117190054.GU3165@dottiness.seas.harvard.edu)
+test_emacs "(notmuch-show \"$maildir_storage_thread\")
+           (search-forward \"Click/Enter to show.\")
+           (button-activate (button-at (point)))
+           (search-backward \"Click/Enter to hide.\")
+           (button-activate (button-at (point)))
+           (test-output)"
+test_expect_equal_file $EXPECTED/notmuch-show-thread-maildir-storage OUTPUT
+
+test_begin_subtest "Detection and hiding of top-post quoting of message"
+add_message '[subject]="The problem with top-posting"' \
+           [id]=top-post-target \
+           '[body]="A: Because it messes up the order in which people normally read text.
+Q: Why is top-posting such a bad thing?
+A: Top-posting.
+Q: What is the most annoying thing in e-mail?"'
+add_message '[from]="Top Poster <top@poster.com>"' \
+           [in-reply-to]=top-post-target \
+           [references]=top-post-target \
+           '[subject]="Re: The problem with top-posting"' \
+           '[body]="Thanks for the advice! I will be sure to put it to good use.
+
+-Top Poster
+
+----- Original Message -----
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmai.org>
+Sent: Fri, 05 Jan 2001 15:43:57 +0000
+Subject: The problem with top-posting
+
+Q: Why is top-posting such a bad thing?
+A: Top-posting.
+Q: What is the most annoying thing in e-mail?"'
+test_emacs "(notmuch-show \"top-posting\")
+           (test-visible-output \"OUTPUT.raw\")"
+echo "Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox)
+Subject: The problem with top-posting
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: GENERATED_DATE
+
+A: Because it messes up the order in which people normally read text.
+Q: Why is top-posting such a bad thing?
+A: Top-posting.
+Q: What is the most annoying thing in e-mail?
+Top Poster <top@poster.com> (2001-01-05) (inbox unread)
+Subject: Re: The problem with top-posting
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: GENERATED_DATE
+
+Thanks for the advice! I will be sure to put it to good use.
+
+-Top Poster
+
+[ 9-line hidden original message. Click/Enter to show. ]" > EXPECTED
+notmuch_date_sanitize < OUTPUT.raw > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Hiding message in notmuch-show view"
+test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com")
+           (notmuch-show-toggle-message)
+           (test-visible-output)'
+test_expect_equal_file $EXPECTED/notmuch-show-thread-with-hidden-messages OUTPUT
+
+test_begin_subtest "Hiding message with visible citation in notmuch-show view"
+test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com")
+           (search-forward "Click/Enter to show.")
+           (button-activate (button-at (point)))
+           (notmuch-show-toggle-message)
+           (test-visible-output)'
+test_expect_equal_file $EXPECTED/notmuch-show-thread-with-hidden-messages OUTPUT
+
+test_begin_subtest "notmuch-show: show message headers"
+test_emacs \
+       '(let ((notmuch-message-headers '\''("Subject" "To" "Cc" "Date"))
+              (notmuch-message-headers-visible t))
+          (notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com")
+          (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-message-with-headers-visible OUTPUT
+
+test_begin_subtest "notmuch-show: hide message headers"
+test_emacs \
+       '(let ((notmuch-message-headers '\''("Subject" "To" "Cc" "Date"))
+              (notmuch-message-headers-visible nil))
+          (notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com")
+          (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-message-with-headers-hidden OUTPUT
+
+test_begin_subtest "notmuch-show: hide message headers (w/ notmuch-show-toggle-visibility-headers)"
+test_emacs \
+       '(let ((notmuch-message-headers '\''("Subject" "To" "Cc" "Date"))
+              (notmuch-message-headers-visible t))
+          (notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com")
+          (notmuch-show-toggle-visibility-headers)
+          (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-message-with-headers-hidden OUTPUT
+
+test_begin_subtest "notmuch-show: collapse all messages in thread"
+test_emacs '(notmuch-show "id:f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com")
+       (let ((current-prefix-arg t))
+         (notmuch-show-open-or-close-all)
+         (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-thread-with-all-messages-collapsed OUTPUT
+
+test_begin_subtest "notmuch-show: uncollapse all messages in thread"
+test_emacs '(notmuch-show "id:f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com")
+       (notmuch-show-open-or-close-all)
+       (test-visible-output)'
+test_expect_equal_file $EXPECTED/notmuch-show-thread-with-all-messages-uncollapsed OUTPUT
+
+test_begin_subtest "Stashing in notmuch-show"
+add_message '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' \
+    '[from]="Some One <someone@somewhere.org>"' \
+    '[to]="Some One Else <notsomeone@somewhere.org>"' \
+    '[cc]="Notmuch <notmuch@notmuchmail.org>"' \
+    '[subject]="Stash my stashables"' \
+    '[id]="bought"' \
+    '[body]="Unable to stash body. Where did you get it in the first place?!?"'
+notmuch tag +stashtest id:${gen_msg_id}
+test_emacs '(notmuch-show "id:\"bought\"")
+       (notmuch-show-stash-date)
+       (notmuch-show-stash-from)
+       (notmuch-show-stash-to)
+       (notmuch-show-stash-cc)
+       (notmuch-show-stash-subject)
+       (notmuch-show-stash-message-id)
+       (notmuch-show-stash-message-id-stripped)
+       (notmuch-show-stash-tags)
+       (notmuch-show-stash-filename)
+       (notmuch-show-stash-mlarchive-link "Notmuch")
+       (notmuch-show-stash-mlarchive-link "MARC")
+       (notmuch-show-stash-mlarchive-link "Mail Archive, The")
+       (switch-to-buffer
+         (generate-new-buffer "*test-stashing*"))
+       (dotimes (i 12)
+         (yank)
+         (insert "\n")
+         (rotate-yank-pointer 1))
+       (reverse-region (point-min) (point-max))
+           (test-output)'
+cat <<EOF >EXPECTED
+Sat, 01 Jan 2000 12:00:00 +0000
+Some One <someone@somewhere.org>
+Some One Else <notsomeone@somewhere.org>
+Notmuch <notmuch@notmuchmail.org>
+Stash my stashables
+id:bought
+bought
+inbox,stashtest
+${gen_msg_filename}
+https://nmbug.notmuchmail.org/nmweb/show/bought
+https://marc.info/?i=bought
+https://mid.mail-archive.com/bought
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Stashing in notmuch-search"
+test_emacs '(notmuch-search "id:\"bought\"")
+       (notmuch-test-wait)
+       (notmuch-search-stash-thread-id)
+       (switch-to-buffer
+         (generate-new-buffer "*test-stashing*"))
+       (yank)
+           (test-output)'
+sed -i -e 's/^thread:.*$/thread:XXX/' OUTPUT
+test_expect_equal "$(cat OUTPUT)" "thread:XXX"
+
+test_begin_subtest 'notmuch-show-advance-and-archive with invisible signature'
+message1='id:20091118010116.GC25380@dottiness.seas.harvard.edu'
+message2='id:1258491078-29658-1-git-send-email-dottedmag@dottedmag.net'
+test_emacs "(notmuch-show \"$message2\")
+           (test-output \"EXPECTED\")"
+test_emacs "(notmuch-search \"$message1 or $message2\")
+           (notmuch-test-wait)
+           (notmuch-search-show-thread)
+           (goto-char (point-max))
+           (redisplay)
+           (notmuch-show-advance-and-archive)
+           (test-output)"
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Refresh show buffer"
+test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com")
+           (test-visible-output "EXPECTED")
+           (notmuch-show-refresh-view)
+           (test-visible-output)'
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Refresh modified show buffer"
+test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com")
+           (notmuch-show-toggle-message)
+           (notmuch-show-next-message)
+           (notmuch-show-toggle-message)
+           (test-visible-output "EXPECTED")
+           (notmuch-show-refresh-view)
+           (test-visible-output)'
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Do not call notmuch for non-inlinable application/mpeg parts"
+id='message-with-application/mpeg-attachment@notmuchmail.org'
+emacs_fcc_message \
+    'Message with application/mpeg attachment' \
+    '' \
+    "(message-goto-eoh)
+     (insert \"Message-ID: <$id>\n\")
+     (message-goto-body)
+     (mml-insert-part \"application/mpeg\")
+     (insert \"a fake mp3 file\")"
+notmuch_counter_reset
+test_emacs "(let ((notmuch-command \"$notmuch_counter_command\"))
+             (notmuch-show \"id:$id\"))"
+test_expect_equal $(notmuch_counter_value) 1
+
+test_begin_subtest "Do not call notmuch for non-inlinable audio/mpeg parts"
+id='message-with-audio/mpeg-attachment@notmuchmail.org'
+emacs_fcc_message \
+    'Message with audio/mpeg attachment' \
+    '' \
+    "(message-goto-eoh)
+     (insert \"Message-ID: <$id>\n\")
+     (message-goto-body)
+     (mml-insert-part \"audio/mpeg\")
+     (insert \"a fake mp3 file\")"
+notmuch_counter_reset
+test_emacs "(let ((notmuch-command \"$notmuch_counter_command\"))
+             (notmuch-show \"id:$id\"))"
+test_expect_equal $(notmuch_counter_value) 1
+
+test_begin_subtest "notmuch-hello-mode hook is called"
+counter=$(test_emacs \
+    '(let ((notmuch-hello-mode-hook-counter 0))
+       (kill-buffer "*notmuch-hello*")
+       (notmuch-hello)
+       notmuch-hello-mode-hook-counter)'
+)
+test_expect_equal "$counter" 1
+
+test_begin_subtest "notmuch-hello-mode hook is not called on updates"
+counter=$(test_emacs \
+    '(let ((notmuch-hello-mode-hook-counter 0))
+       (kill-buffer "*notmuch-hello*")
+       (notmuch-hello)
+       (notmuch-hello-update)
+       notmuch-hello-mode-hook-counter)'
+)
+test_expect_equal "$counter" 1
+
+test_begin_subtest "notmuch-hello-refresh hook is called"
+counter=$(test_emacs \
+    '(let ((notmuch-hello-refresh-hook-counter 0))
+       (kill-buffer "*notmuch-hello*")
+       (notmuch-hello)
+       notmuch-hello-refresh-hook-counter)'
+)
+test_expect_equal "$counter" 1
+
+test_begin_subtest "notmuch-hello-refresh hook is called on updates"
+counter=$(test_emacs \
+    '(let ((notmuch-hello-refresh-hook-counter 0))
+       (kill-buffer "*notmuch-hello*")
+       (notmuch-hello)
+       (notmuch-hello-update)
+       notmuch-hello-refresh-hook-counter)'
+)
+test_expect_equal "$counter" 2
+
+
+add_message '[subject]="HTML mail with images"' \
+    '[content-type]="multipart/related; boundary=abcd"' \
+    '[body]="--abcd
+Content-Type: text/html
+
+<img src="cid:330@goomoji.gmail"> smiley
+
+--abcd
+Content-Type: image/gif
+Content-Transfer-Encoding: base64
+Content-ID: <330@goomoji.gmail>
+
+R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMl
+WLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7
+--abcd--"'
+test_emacs "(let ((mm-text-html-renderer
+                  (if (assq 'shr mm-text-html-renderer-alist)
+                      'shr 'html2text)))
+             (notmuch-show \"id:${gen_msg_id}\"))
+           (test-output)" > /dev/null
+# Different Emacs versions and renderers give very different results,
+# so just check that something reasonable showed up.  We first cat the
+# output so the test framework will print it if the test fails.
+test_begin_subtest "Rendering HTML mail with images"
+test_expect_success 'cat OUTPUT && grep -q smiley OUTPUT'
+
+test_begin_subtest "Search handles subprocess error exit codes"
+cat > notmuch_fail <<EOF
+#!/bin/sh
+echo '()'
+exit 1
+EOF
+chmod a+x notmuch_fail
+test_emacs "(let ((notmuch-command \"$PWD/notmuch_fail\"))
+              (with-current-buffer \"*Messages*\"
+                 (let ((inhibit-read-only t)) (erase-buffer)))
+              (with-current-buffer (get-buffer-create \"*Notmuch errors*\")
+                 (erase-buffer))
+              (notmuch-search \"tag:inbox\")
+              (notmuch-test-wait)
+              (with-current-buffer \"*Messages*\"
+                 (test-output \"MESSAGES\"))
+              (with-current-buffer \"*Notmuch errors*\"
+                 (test-output \"ERROR\"))
+              (test-output))"
+
+test_expect_equal "$(notmuch_emacs_error_sanitize notmuch_fail OUTPUT MESSAGES ERROR)" "\
+=== OUTPUT ===
+End of search results.
+=== MESSAGES ===
+YYY/notmuch_fail exited with status 1 (see *Notmuch errors* for more details)
+=== ERROR ===
+YYY/notmuch_fail exited with status 1
+command: YYY/notmuch_fail search --format\=sexp --format-version\=5 --sort\=newest-first tag\:inbox
+exit status: 1"
+
+test_begin_subtest "Search handles subprocess warnings"
+cat > notmuch_fail <<EOF
+#!/bin/sh
+echo '()'
+echo This is a warning >&2
+echo This is another warning >&2
+exit 0
+EOF
+chmod a+x notmuch_fail
+test_emacs "(let ((notmuch-command \"$PWD/notmuch_fail\"))
+              (with-current-buffer \"*Messages*\"
+                 (let ((inhibit-read-only t)) (erase-buffer)))
+              (with-current-buffer (get-buffer-create \"*Notmuch errors*\")
+                 (erase-buffer))
+              (notmuch-search \"tag:inbox\")
+              (notmuch-test-wait)
+              (with-current-buffer \"*Messages*\"
+                 (test-output \"MESSAGES\"))
+              (with-current-buffer \"*Notmuch errors*\"
+                 (test-output \"ERROR\"))
+              (test-output))"
+sed -i -e 's/^\[.*\]$/[XXX]/' ERROR
+test_expect_equal "$(cat OUTPUT; echo ---; cat MESSAGES; echo ---; cat ERROR)" "\
+End of search results.
+---
+This is a warning (see *Notmuch errors* for more details)
+---
+[XXX]
+This is a warning
+This is another warning"
+
+test_begin_subtest "Term escaping"
+output=$(test_emacs "(mapcar 'notmuch-escape-boolean-term (list
+       \"\"
+       \"abc\`~\!@#\$%^&*-=_+123\"
+       \"(abc\"
+       \")abc\"
+       \"\\\"abc\"
+       \"\x01xyz\"
+       \"\\x201cxyz\\x201d\"))")
+test_expect_equal "$output" '("\"\"" "abc`~!@#$%^&*-=_+123" "\"(abc\"" "\")abc\"" "\"\"\"abc\"" "\"'$'\x01''xyz\"" "\"“xyz”\"")'
+
+test_begin_subtest "Sending a message calls the send message hooks"
+emacs_deliver_message \
+    'Testing message sending hooks' \
+    'This is a test of the message sending hooks.' \
+    "(message-goto-to)
+     (kill-whole-line)
+     (insert \"To: user@example.com\n\")
+     (add-hook 'notmuch-mua-send-hook (lambda () (goto-char (point-max)) (insert \"\nThis text added by the hook.\")))"
+sed \
+    -e s',^Message-ID: <.*>$,Message-ID: <XXX>,' \
+    -e s',^\(Content-Type: text/plain\); charset=us-ascii$,\1,' < sent_message >OUTPUT
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: user@example.com
+Subject: Testing message sending hooks
+Date: 01 Jan 2000 12:00:00 -0000
+Message-ID: <XXX>
+MIME-Version: 1.0
+Content-Type: text/plain
+
+This is a test of the message sending hooks.
+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
diff --git a/test/T320-emacs-large-search-buffer.sh b/test/T320-emacs-large-search-buffer.sh
new file mode 100755 (executable)
index 0000000..617985e
--- /dev/null
@@ -0,0 +1,35 @@
+#!/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.
+n=30
+for i in $(seq 1 $n); do
+  # Roughly 100B2 KiB per message.  That is, we need two messages in order to
+  # exceed the typical size of the pipe buffer (4 KiB on commodity systems).
+  generate_message '[subject]="$x $i of $n"'
+done
+
+notmuch new > /dev/null
+
+test_begin_subtest "Ensure that emacs doesn't drop results"
+notmuch search '*' > EXPECTED
+sed -i -e 's/^thread:[0-9a-f]*  //' -e 's/;//' -e 's/xx*/[BLOB]/' EXPECTED
+echo 'End of search results.' >> EXPECTED
+
+test_emacs '(notmuch-search "*")
+           (notmuch-test-wait)
+           (test-output)'
+sed -i -e s',  *, ,g' -e 's/xxx*/[BLOB]/g' OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T330-emacs-subject-to-filename.sh b/test/T330-emacs-subject-to-filename.sh
new file mode 100755 (executable)
index 0000000..405b063
--- /dev/null
@@ -0,0 +1,141 @@
+#!/usr/bin/env bash
+
+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
+
+# test notmuch-wash-subject-to-patch-sequence-number (subject)
+test_begin_subtest "no patch sequence number"
+output=$(test_emacs '(format "%S" (notmuch-wash-subject-to-patch-sequence-number
+      "[PATCH] A normal patch subject without numbers"))'
+)
+test_expect_equal "$output" '"nil"'
+
+test_begin_subtest "patch sequence number #1"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-sequence-number
+      "[PATCH 2/3] A most regular patch subject")'
+)
+test_expect_equal "$output" 2
+
+test_begin_subtest "patch sequence number #2"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-sequence-number
+      "  [dummy list prefix]  [RFC PATCH v2 13/42]  Special prefixes")'
+)
+test_expect_equal "$output" 13
+
+test_begin_subtest "patch sequence number #3"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-sequence-number
+      "[PATCH 2/3] [PATCH 032/037] use the last prefix")'
+)
+test_expect_equal "$output" 32
+
+test_begin_subtest "patch sequence number #4"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-sequence-number
+      "[dummy list prefix] [PATCH 2/3] PATCH 3/3] do not use a broken prefix")'
+)
+test_expect_equal "$output" 2
+
+test_begin_subtest "patch sequence number #5"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-sequence-number
+      "[RFC][PATCH 3/5][PATCH 4/5][PATCH 5/5] A made up test")'
+)
+test_expect_equal "$output" 5
+
+test_begin_subtest "patch sequence number #6"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-sequence-number
+      "[PATCH 2/3] this -> [PATCH 3/3] is not a prefix anymore [nor this 4/4]")'
+)
+test_expect_equal "$output" 2
+
+test_begin_subtest "patch sequence number #7"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-sequence-number
+      "[liberally accept crapola right before123/456and after] the numbers")'
+)
+test_expect_equal "$output" 123
+
+# test notmuch-wash-subject-to-filename (subject &optional maxlen)
+test_begin_subtest "filename #1"
+output=$(test_emacs '(notmuch-wash-subject-to-filename
+      "just a subject line")'
+)
+test_expect_equal "$output" '"just-a-subject-line"'
+
+test_begin_subtest "filename #2"
+output=$(test_emacs '(notmuch-wash-subject-to-filename
+      " [any]  [prefixes are ] [removed!] from the subject")'
+)
+test_expect_equal "$output" '"from-the-subject"'
+
+test_begin_subtest "filename #3"
+output=$(test_emacs '(notmuch-wash-subject-to-filename
+      "  leading and trailing space  ")'
+)
+test_expect_equal "$output" '"leading-and-trailing-space"'
+
+test_begin_subtest "filename #4"
+output=$(test_emacs '(notmuch-wash-subject-to-filename
+      "!#  leading ()// &%, and in between_and_trailing garbage ()(&%%")'
+)
+test_expect_equal "$output" '"-leading-and-in-between_and_trailing-garbage"'
+
+test_begin_subtest "filename #5"
+output=$(test_emacs '(notmuch-wash-subject-to-filename
+      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-_01234567890")'
+)
+test_expect_equal "$output" '"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-_01234567890"'
+
+test_begin_subtest "filename #6"
+output=$(test_emacs '(notmuch-wash-subject-to-filename
+      "sequences of ... are squashed and trailing are removed ...")'
+)
+test_expect_equal "$output" '"sequences-of-.-are-squashed-and-trailing-are-removed"'
+
+test_begin_subtest "filename #7"
+output=$(test_emacs '(notmuch-wash-subject-to-filename
+      "max length test" 1)'
+)
+test_expect_equal "$output" '"m"'
+
+test_begin_subtest "filename #8"
+output=$(test_emacs '(notmuch-wash-subject-to-filename
+      "max length test /&(/%&/%%&¤%¤" 20)'
+)
+test_expect_equal "$output" '"max-length-test"'
+
+test_begin_subtest "filename #9"
+output=$(test_emacs '(notmuch-wash-subject-to-filename
+      "[a prefix] [is only separated] by [spaces], so \"by\" is not okay!")'
+)
+test_expect_equal "$output" '"by-spaces-so-by-is-not-okay"'
+
+# test notmuch-wash-subject-to-patch-filename (subject)
+test_begin_subtest "patch filename #1"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-filename
+      "[RFC][PATCH 099/100] rewrite notmuch")'
+)
+test_expect_equal "$output" '"0099-rewrite-notmuch.patch"'
+
+test_begin_subtest "patch filename #2"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-filename
+      "[RFC PATCH v1] has no patch number, default to 1")'
+)
+test_expect_equal "$output" '"0001-has-no-patch-number-default-to-1.patch"'
+
+test_begin_subtest "patch filename #3"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-filename
+      "[PATCH 4/5] the maximum length of a patch filename is 52 + patch sequence number + .patch extension")'
+)
+test_expect_equal "$output" '"0004-the-maximum-length-of-a-patch-filename-is-52-patch-s.patch"'
+
+test_begin_subtest "patch filename #4"
+output=$(test_emacs '(notmuch-wash-subject-to-patch-filename
+      "[PATCH 4/5] the maximum length of a patch filename is 52 + patchh ! sequence number + .patch extension, *before* trimming trailing - and .")'
+)
+test_expect_equal "$output" '"0004-the-maximum-length-of-a-patch-filename-is-52-patchh.patch"'
+
+test_done
diff --git a/test/T340-maildir-sync.sh b/test/T340-maildir-sync.sh
new file mode 100755 (executable)
index 0000000..a697317
--- /dev/null
@@ -0,0 +1,210 @@
+#!/usr/bin/env bash
+
+test_description="maildir synchronization"
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+# Create the expected maildir structure
+mkdir $MAIL_DIR/cur
+mkdir $MAIL_DIR/new
+mkdir $MAIL_DIR/tmp
+
+test_begin_subtest "Adding 'S' flag to existing filename removes 'unread' tag"
+add_message [subject]='"Adding S flag"' [filename]='adding-s-flag:2,' [dir]=cur
+output=$(notmuch search subject:"Adding S flag" | notmuch_search_sanitize)
+output+="
+"
+mv "${gen_msg_filename}" "${gen_msg_filename}S"
+output+=$(NOTMUCH_NEW)
+output+="
+"
+output+=$(notmuch search subject:"Adding S flag" | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Adding S flag (inbox unread)
+No new mail. Detected 1 file rename.
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Adding S flag (inbox)"
+
+test_begin_subtest "Adding message with 'S' flag prevents 'unread' tag"
+add_message [subject]='"Adding message with S"' [filename]='adding-with-s-flag:2,S' [dir]=cur
+output=$(notmuch search subject:"Adding message with S" | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Adding message with S (inbox)"
+
+test_begin_subtest "Adding message with 'S' w/o 'unread' in new.tags prevents 'unread' tag"
+OLDCONFIG=$(notmuch config get new.tags)
+notmuch config set new.tags "inbox"
+add_message [subject]='"Adding message with S 2"' [filename]='adding-with-s-flag2:2,S' [dir]=cur
+notmuch config set new.tags $OLDCONFIG
+output=$(notmuch search subject:Adding-message-with-S-2 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Adding message with S 2 (inbox)"
+
+test_begin_subtest "Adding 'replied' tag adds 'R' flag to filename"
+add_message [subject]='"Adding replied tag"' [filename]='adding-replied-tag:2,S' [dir]=cur
+notmuch tag +replied subject:"Adding replied tag"
+output=$(cd ${MAIL_DIR}/cur; ls -1 adding-replied*)
+test_expect_equal "$output" "adding-replied-tag:2,RS"
+
+test_begin_subtest "notmuch show works with renamed file (without notmuch new)"
+output=$(notmuch show --format=json id:${gen_msg_id} | notmuch_json_show_sanitize)
+test_expect_equal_json "$output" '[[[{"id": "XXXXX",
+"crypto": {},
+"match": true,
+"excluded": false,
+"filename": ["YYYYY"],
+"timestamp": 42,
+"date_relative": "2001-01-05",
+"tags": ["inbox","replied"],
+"headers": {"Subject": "Adding replied tag",
+"From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+"To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+"Date": "GENERATED_DATE"},
+"body": [{"id": 1,
+"content-type": "text/plain",
+"content": "This is just a test message (#4)\n"}]},
+[]]]]'
+
+test_begin_subtest "notmuch reply works with renamed file (without notmuch new)"
+test_expect_success 'notmuch reply id:${gen_msg_id}'
+
+test_begin_subtest "notmuch new detects no file rename after tag->flag synchronization"
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "No new mail."
+
+test_begin_subtest "When read, message moved from new to cur"
+add_message [subject]='"Message to move to cur"' [date]='"Sat, 01 Jan 2000 12:00:00 -0000"' [filename]='message-to-move-to-cur' [dir]=new
+notmuch tag -unread subject:"Message to move to cur"
+output=$(cd "$MAIL_DIR/cur"; ls message-to-move*)
+test_expect_equal "$output" "message-to-move-to-cur:2,S"
+
+test_begin_subtest "No rename should be detected by notmuch new"
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "No new mail."
+# (*) If notmuch new was not run we've got "Processed 1 file in almost
+# no time" here. The reason is that removing unread tag in a previous
+# test created directory document in the database but this document
+# was not linked as subdirectory of $MAIL_DIR. Therefore notmuch new
+# could not reach the cur/ directory and its files in it during
+# recursive traversal.
+#
+# XXX: The above sounds like a bug that should be fixed. If notmuch is
+# creating new directories in the mail store, then it should be
+# creating all necessary database state for those directories.
+
+test_begin_subtest "Adding non-maildir tags does not move message from new to cur"
+add_message [subject]='"Message to stay in new"' \
+    [date]='"Sat, 01 Jan 2000 12:00:00 -0000"' \
+    [filename]='message-to-stay-in-new' [dir]=new
+notmuch tag +donotmove subject:"Message to stay in new"
+output=$(cd "$MAIL_DIR"; ls */message-to-stay-in-new*)
+test_expect_equal "$output" "new/message-to-stay-in-new"
+
+test_begin_subtest "Message in cur lacking maildir info gets one on any tag change"
+add_message [filename]='message-to-get-maildir-info' [dir]=cur
+notmuch tag +anytag id:$gen_msg_id
+output=$(cd "$MAIL_DIR"; ls */message-to-get-maildir-info*)
+test_expect_equal "$output" "cur/message-to-get-maildir-info:2,"
+
+test_begin_subtest "Message in new with maildir info is moved to cur on any tag change"
+add_message [filename]='message-with-info-to-be-moved-to-cur:2,' [dir]=new
+notmuch tag +anytag id:$gen_msg_id
+output=$(cd "$MAIL_DIR"; ls */message-with-info-to-be-moved-to-cur*)
+test_expect_equal "$output" "cur/message-with-info-to-be-moved-to-cur:2,"
+
+test_begin_subtest "Removing 'S' flag from existing filename adds 'unread' tag"
+add_message [subject]='"Removing S flag"' [filename]='removing-s-flag:2,S' [dir]=cur
+output=$(notmuch search subject:"Removing S flag" | notmuch_search_sanitize)
+output+="
+"
+mv "${gen_msg_filename}" "${gen_msg_filename%S}"
+output+=$(NOTMUCH_NEW)
+output+="
+"
+output+=$(notmuch search subject:"Removing S flag" | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Removing S flag (inbox)
+No new mail. Detected 1 file rename.
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Removing S flag (inbox unread)"
+
+test_begin_subtest "Removing info from filename leaves tags unchanged"
+add_message [subject]='"Message to lose maildir info"' [filename]='message-to-lose-maildir-info' [dir]=cur
+notmuch tag -unread subject:"Message to lose maildir info"
+mv "$MAIL_DIR/cur/message-to-lose-maildir-info:2,S" "$MAIL_DIR/cur/message-without-maildir-info"
+output=$(NOTMUCH_NEW)
+output+="
+"
+output+=$(notmuch search subject:"Message to lose maildir info" | notmuch_search_sanitize)
+test_expect_equal "$output" "No new mail. Detected 1 file rename.
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Message to lose maildir info (inbox)"
+
+test_begin_subtest "Can remove unread tag from message in non-maildir directory"
+add_message [subject]='"Non-maildir message"' [dir]=notmaildir [filename]='non-maildir-message'
+expected=$(notmuch search --output=files subject:"Non-maildir message")
+test_expect_success 'notmuch tag -unread subject:"Non-maildir message"'
+
+test_begin_subtest "Message in non-maildir directory does not get renamed"
+output=$(notmuch search --output=files subject:"Non-maildir message")
+test_expect_equal "$output" "$expected"
+
+test_begin_subtest "notmuch dump/restore re-synchronizes maildir tags with flags"
+# Capture current filename state
+expected=$(ls $MAIL_DIR/cur)
+# Add/remove some flags from filenames
+mv $MAIL_DIR/cur/adding-replied-tag:2,RS $MAIL_DIR/cur/adding-replied-tag:2,S
+mv $MAIL_DIR/cur/adding-s-flag:2,S $MAIL_DIR/cur/adding-s-flag:2,
+mv $MAIL_DIR/cur/adding-with-s-flag:2,S $MAIL_DIR/cur/adding-with-s-flag:2,RS
+mv $MAIL_DIR/cur/message-to-move-to-cur:2,S $MAIL_DIR/cur/message-to-move-to-cur:2,DS
+notmuch dump --output=dump.txt
+NOTMUCH_NEW >/dev/null
+notmuch restore --input=dump.txt
+output=$(ls $MAIL_DIR/cur)
+test_expect_equal "$output" "$expected"
+
+test_begin_subtest 'Adding flags to duplicate message tags the mail'
+add_message [subject]='"Duplicated message"' [dir]=cur [filename]='duplicated-message:2,'
+cp "$MAIL_DIR/cur/duplicated-message:2," "$MAIL_DIR/cur/duplicated-message-copy:2,RS"
+NOTMUCH_NEW > output
+notmuch search subject:"Duplicated message" | notmuch_search_sanitize >> output
+test_expect_equal "$(< output)" "No new mail.
+thread:XXX   2001-01-05 [1/1(2)] Notmuch Test Suite; Duplicated message (inbox replied)"
+
+test_begin_subtest "Adding duplicate message without flags does not remove tags"
+cp "$MAIL_DIR/cur/duplicated-message-copy:2,RS" "$MAIL_DIR/cur/duplicated-message-another-copy:2,"
+NOTMUCH_NEW > output
+notmuch search subject:"Duplicated message" | notmuch_search_sanitize >> output
+test_expect_equal "$(< output)" "No new mail.
+thread:XXX   2001-01-05 [1/1(3)] Notmuch Test Suite; Duplicated message (inbox replied)"
+
+test_begin_subtest "Tag changes modify flags of multiple files"
+notmuch tag -replied subject:"Duplicated message"
+(cd $MAIL_DIR/cur/; ls duplicated*) > actual
+test_expect_equal "$(< actual)" "duplicated-message-another-copy:2,S
+duplicated-message-copy:2,S
+duplicated-message:2,S"
+
+test_begin_subtest "Synchronizing tag changes preserves unsupported maildir flags"
+add_message [subject]='"Unsupported maildir flags"' [dir]=cur [filename]='unsupported-maildir-flags:2,FSZxyz'
+notmuch tag +unread +draft -flagged subject:"Unsupported maildir flags"
+test_expect_equal "$(cd $MAIL_DIR/cur/; ls unsupported*)" "unsupported-maildir-flags:2,DZxyz"
+
+test_begin_subtest "A file with non-compliant maildir info will not be renamed"
+add_message [subject]='"Non-compliant maildir info"' [dir]=cur [filename]='non-compliant-maildir-info:2,These-are-not-flags-in-ASCII-order-donottouch'
+notmuch tag +unread +draft -flagged subject:"Non-compliant maildir info"
+test_expect_equal "$(cd $MAIL_DIR/cur/; ls non-compliant*)" "non-compliant-maildir-info:2,These-are-not-flags-in-ASCII-order-donottouch"
+
+test_begin_subtest "Files in new/ get default synchronized tags"
+OLDCONFIG=$(notmuch config get new.tags)
+notmuch config set new.tags "test;unread"
+add_message [subject]='"File in new/"' [dir]=new [filename]='file-in-new'
+notmuch config set new.tags $OLDCONFIG
+notmuch search 'subject:"File in new"' | notmuch_search_sanitize > output
+test_expect_equal "$(< output)" \
+"thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; File in new/ (test unread)"
+
+for tag in draft flagged passed replied; do
+    test_begin_subtest "$tag is valid in new.tags"
+    OLDCONFIG=$(notmuch config get new.tags)
+    notmuch config set new.tags "$tag;unread"
+    add_message [subject]="\"$tag sync in new\"" [dir]=new
+    notmuch config set new.tags $OLDCONFIG
+    notmuch search "subject:\"$tag sync in new\"" | notmuch_search_sanitize > output
+    test_expect_equal "$(< output)" \
+                     "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; $tag sync in new ($tag unread)"
+done
+test_done
diff --git a/test/T350-crypto.sh b/test/T350-crypto.sh
new file mode 100755 (executable)
index 0000000..27c0e86
--- /dev/null
@@ -0,0 +1,485 @@
+#!/usr/bin/env bash
+
+# TODO:
+# - decryption/verification with signer key not available
+# - verification of signatures from expired/revoked keys
+
+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 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"
+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" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|g')
+expected='[[[{"id": "XXXXX",
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["inbox","signed"],
+ "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",
+ "Date": "Sat, 01 Jan 2000 12:00:00 +0000"},
+ "body": [{"id": 1,
+ "sigstatus": [{"status": "good",
+ "fingerprint": "'$FINGERPRINT'",
+ "created": 946728000,
+ "email": "'"$SELF_EMAIL"'",
+ "userid": "'"$SELF_USERID"'"}],
+ "content-type": "multipart/signed",
+ "content": [{"id": 2,
+ "content-type": "text/plain",
+ "content": "This is a test signed message.\n"},
+ {"id": 3,
+ "content-type": "application/pgp-signature",
+ "content-length": "NONZERO"}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "detection of modified signed contents"
+emacs_fcc_message \
+    "bad signed message 001" \
+    "Incriminating stuff. This is a test signed message." \
+    "(mml-secure-message-sign)"
+
+file=$(notmuch search --output=files subject:"bad signed message 001")
+
+sed -i 's/Incriminating stuff. //' ${file}
+
+output=$(notmuch show --format=json --verify subject:"bad signed message 001" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|')
+expected='[[[{"id": "XXXXX",
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["inbox","signed"],
+ "crypto": {"signed": {"status": [{ "status": "bad", "keyid": "'$(echo $FINGERPRINT | cut -c 25-)'"}]}},
+ "headers": {"Subject": "bad signed message 001",
+ "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,
+ "sigstatus": [{"status": "bad",
+ "keyid": "'$(echo $FINGERPRINT | cut -c 25-)'"}],
+ "content-type": "multipart/signed",
+ "content": [{"id": 2,
+ "content-type": "text/plain",
+ "content": "This is a test signed message.\n"},
+ {"id": 3,
+ "content-type": "application/pgp-signature",
+ "content-length": "NONZERO"}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "corrupted pgp/mime signature"
+emacs_fcc_message \
+    "bad signed message 002" \
+    "Incriminating stuff. This is a test signed message." \
+    "(mml-secure-message-sign)"
+
+file=$(notmuch search --output=files subject:"bad signed message 002")
+
+awk '/-----BEGIN PGP SIGNATURE-----/{flag=1;print;next} \
+     /-----END PGP SIGNATURE-----/{flag=0;print;next} \
+     flag{gsub(/[A-Za-z]/,"0");print}!flag{print}' $file > $file.new
+
+rm $file
+mv $file.new $file
+
+output=$(notmuch show --format=json --verify subject:"bad signed message 002" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|')
+expected='[[[{"id": "XXXXX",
+ "crypto": {},
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["inbox","signed"],
+ "headers": {"Subject": "bad signed message 002",
+ "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,
+ "sigstatus": [],
+ "content-type": "multipart/signed",
+ "content": [{"id": 2,
+ "content-type": "text/plain",
+ "content": "Incriminating stuff. This is a test signed message.\n"},
+ {"id": 3,
+ "content-type": "application/pgp-signature",
+ "content-length": "NONZERO"}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "signature verification without full user ID validity"
+# give the key no owner trust, removes validity on all user IDs of the
+# certificate in the absence of other trusted certifiers:
+gpg --quiet --batch --no-tty --export-ownertrust > "$GNUPGHOME/ownertrust.bak"
+echo "${FINGERPRINT}:3:" | gpg --quiet --batch --no-tty --import-ownertrust
+output=$(notmuch show --format=json --verify subject:"test signed message 001" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|g')
+expected='[[[{"id": "XXXXX",
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["inbox","signed"],
+ "crypto": {"signed": {"status": [{ "status": "good", "created": 946728000, "fingerprint": "'$FINGERPRINT'"}]}},
+ "headers": {"Subject": "test signed message 001",
+ "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,
+ "sigstatus": [{"status": "good",
+ "fingerprint": "'$FINGERPRINT'",
+ "created": 946728000}],
+ "content-type": "multipart/signed",
+ "content": [{"id": 2,
+ "content-type": "text/plain",
+ "content": "This is a test signed message.\n"},
+ {"id": 3,
+ "content-type": "application/pgp-signature",
+ "content-length": "NONZERO"}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+gpg --quiet --batch --no-tty --import-ownertrust < "$GNUPGHOME/ownertrust.bak"
+
+test_begin_subtest "signature verification with signer key unavailable"
+# move the gnupghome temporarily out of the way
+mv "${GNUPGHOME}"{,.bak}
+output=$(notmuch show --format=json --verify subject:"test signed message 001" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|g')
+expected='[[[{"id": "XXXXX",
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["inbox","signed"],
+ "crypto": {"signed": {"status": [{"errors": {"key-missing": true}, "keyid": "'$(echo $FINGERPRINT | cut -c 25-)'", "status": "error"}]}},
+ "headers": {"Subject": "test signed message 001",
+ "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,
+ "sigstatus": [{"status": "error",
+ "keyid": "'$(echo $FINGERPRINT | cut -c 25-)'",
+ "errors": {"key-missing": true}}],
+ "content-type": "multipart/signed",
+ "content": [{"id": 2,
+ "content-type": "text/plain",
+ "content": "This is a test signed message.\n"},
+ {"id": 3,
+ "content-type": "application/pgp-signature",
+ "content-length": "NONZERO"}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+mv "${GNUPGHOME}"{.bak,}
+
+test_begin_subtest "emacs delivery of encrypted message with attachment"
+# create a test encrypted message with attachment
+cat <<EOF >TESTATTACHMENT
+This is a test file.
+EOF
+test_expect_success \
+'emacs_fcc_message \
+    "test encrypted message 001" \
+    "This is a test encrypted message.\n" \
+    "(mml-attach-file \"TESTATTACHMENT\") (mml-secure-message-encrypt)"'
+
+test_begin_subtest "encrypted part content-type indexing"
+output=$(notmuch search mimetype:multipart/encrypted and mimetype:application/pgp-encrypted and mimetype:application/octet-stream | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message 001 (encrypted inbox)"
+
+test_begin_subtest "decryption, --format=text"
+output=$(notmuch show --format=text --decrypt=true subject:"test encrypted message 001" \
+    | notmuch_show_sanitize_all \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|')
+expected='\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2000-01-01) (encrypted inbox)
+Subject: test encrypted message 001
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: test_suite@notmuchmail.org
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: multipart/encrypted
+\fpart{ ID: 2, Content-type: application/pgp-encrypted
+Non-text part: application/pgp-encrypted
+\fpart}
+\fpart{ ID: 3, Content-type: multipart/mixed
+\fpart{ ID: 4, Content-type: text/plain
+This is a test encrypted message.
+\fpart}
+\fattachment{ ID: 5, Filename: TESTATTACHMENT, Content-type: application/octet-stream
+Non-text part: application/octet-stream
+\fattachment}
+\fpart}
+\fpart}
+\fbody}
+\fmessage}'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "decryption, --format=json"
+output=$(notmuch show --format=json --decrypt=true subject:"test encrypted message 001" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|')
+expected='[[[{"id": "XXXXX",
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["encrypted","inbox"],
+ "crypto": {"decrypted": {"status": "full"}},
+ "headers": {"Subject": "test encrypted message 001",
+ "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,
+ "encstatus": [{"status": "good"}],
+ "content-type": "multipart/encrypted",
+ "content": [{"id": 2,
+ "content-type": "application/pgp-encrypted",
+ "content-length": "NONZERO"},
+ {"id": 3,
+ "content-type": "multipart/mixed",
+ "content": [{"id": 4,
+ "content-type": "text/plain",
+ "content": "This is a test encrypted message.\n"},
+ {"id": 5,
+ "content-type": "application/octet-stream",
+ "content-disposition": "attachment",
+ "content-length": "NONZERO",
+ "content-transfer-encoding": "base64",
+ "filename": "TESTATTACHMENT"}]}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "decryption, --format=json, --part=4"
+output=$(notmuch show --format=json --part=4 --decrypt=true subject:"test encrypted message 001" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|')
+expected='{"id": 4,
+ "content-type": "text/plain",
+ "content": "This is a test encrypted message.\n"}'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "decrypt attachment (--part=5 --format=raw)"
+notmuch show \
+    --format=raw \
+    --part=5 \
+    --decrypt=true \
+    subject:"test encrypted message 001" >OUTPUT
+test_expect_equal_file TESTATTACHMENT OUTPUT
+
+test_begin_subtest "decryption failure with missing key"
+mv "${GNUPGHOME}"{,.bak}
+output=$(notmuch show --format=json --decrypt=true subject:"test encrypted message 001" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|')
+expected='[[[{"id": "XXXXX",
+ "crypto": {},
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["encrypted","inbox"],
+ "headers": {"Subject": "test encrypted message 001",
+ "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,
+ "encstatus": [{"status": "bad"}],
+ "content-type": "multipart/encrypted",
+ "content": [{"id": 2,
+ "content-type": "application/pgp-encrypted",
+ "content-length": "NONZERO"},
+ {"id": 3,
+ "content-type": "application/octet-stream",
+ "content-length": "NONZERO"}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+mv "${GNUPGHOME}"{.bak,}
+
+test_begin_subtest "emacs delivery of encrypted + signed message"
+test_expect_success \
+'emacs_fcc_message \
+    "test encrypted message 002" \
+    "This is another test encrypted message.\n" \
+    "(mml-secure-message-sign-encrypt)"'
+
+test_begin_subtest "decryption + signature verification"
+output=$(notmuch show --format=json --decrypt=true subject:"test encrypted message 002" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|g')
+expected='[[[{"id": "XXXXX",
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["encrypted","inbox"],
+ "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",
+ "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,
+ "encstatus": [{"status": "good"}],
+ "sigstatus": [{"status": "good",
+ "fingerprint": "'$FINGERPRINT'",
+ "created": 946728000,
+ "email": "'"$SELF_EMAIL"'",
+ "userid": "'"$SELF_USERID"'"}],
+ "content-type": "multipart/encrypted",
+ "content": [{"id": 2,
+ "content-type": "application/pgp-encrypted",
+ "content-length": "NONZERO"},
+ {"id": 3,
+ "content-type": "text/plain",
+ "content": "This is another test encrypted message.\n"}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "reply to encrypted message"
+output=$(notmuch reply --decrypt=true subject:"test encrypted message 002" \
+    | notmuch_drop_mail_headers In-Reply-To References)
+expected='From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: test encrypted message 002
+To: test_suite@notmuchmail.org
+
+On 01 Jan 2000 12:00:00 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+> This is another test encrypted message.'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "Reply within emacs to an encrypted message"
+test_emacs "(let ((message-hidden-headers '())
+      (notmuch-crypto-process-mime 't))
+  (notmuch-show \"subject:test.encrypted.message.002\")
+  (notmuch-show-reply)
+  (test-output))"
+grep -v -e '^In-Reply-To:' -e '^References:' -e '^Fcc:' < OUTPUT > OUTPUT.clean
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: test_suite@notmuchmail.org
+Subject: Re: test encrypted message 002
+--text follows this line--
+<#secure method=pgpmime mode=signencrypt>
+Notmuch Test Suite <test_suite@notmuchmail.org> writes:
+
+> This is another test encrypted message.
+EOF
+test_expect_equal_file EXPECTED OUTPUT.clean
+
+test_begin_subtest "signature verification with revoked key"
+# generate revocation certificate and load it to revoke key
+echo "y
+1
+Notmuch Test Suite key revocation (automated) $(date '+%F_%T%z')
+
+y
+
+" \
+    | gpg --no-tty --quiet --command-fd 0 --armor --gen-revoke "0x${FINGERPRINT}!" 2>/dev/null \
+    | gpg --no-tty --quiet --import
+output=$(notmuch show --format=json --verify subject:"test signed message 001" \
+    | notmuch_json_show_sanitize \
+    | sed -e 's|"created": [1234567890]*|"created": 946728000|')
+expected='[[[{"id": "XXXXX",
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["inbox","signed"],
+ "crypto": {"signed": {"status": [{"errors": {"key-revoked": true}, "keyid": "'$(echo $FINGERPRINT | cut -c 25-)'", "status": "error"}]}},
+ "headers": {"Subject": "test signed message 001",
+ "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,
+ "sigstatus": [{"status": "error",
+ "keyid": "'$(echo $FINGERPRINT | cut -c 25-)'",
+ "errors": {"key-revoked": true}}],
+ "content-type": "multipart/signed",
+ "content": [{"id": 2,
+ "content-type": "text/plain",
+ "content": "This is a test signed message.\n"},
+ {"id": 3,
+ "content-type": "application/pgp-signature",
+ "content-length": "NONZERO"}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+
+test_done
diff --git a/test/T351-pgpmime-mangling.sh b/test/T351-pgpmime-mangling.sh
new file mode 100755 (executable)
index 0000000..71a68c0
--- /dev/null
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+test_description='PGP/MIME message mangling'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_gnupg_home
+add_email_corpus mangling
+
+bodytext='["body"][0]["content"][1]["content"]="The password is \"abcd1234!\", please do not tell anyone.\n"'
+
+test_begin_subtest "show 'Mixed-Up' mangled PGP/MIME message correctly"
+output=$(notmuch show --format=json --decrypt=true id:mixed-up@mangling.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'body:[0][0][0]'"$bodytext"
+
+test_begin_subtest "reply to 'Mixed-Up' mangled PGP/MIME message correctly"
+output=$(notmuch reply --format=json --decrypt=true id:mixed-up@mangling.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'body:["original"]'"$bodytext"
+
+test_begin_subtest "repaired 'Mixed-up' messages can be found with index.repaired=mixedup"
+output=$(notmuch search --output=messages property:index.repaired=mixedup)
+test_expect_equal "$output" id:mixed-up@mangling.notmuchmail.org
+
+test_begin_subtest "index cleartext of 'Mixed-Up' mangled PGP/MIME message"
+test_expect_success 'notmuch reindex --decrypt=true id:mixed-up@mangling.notmuchmail.org'
+
+test_begin_subtest "search cleartext of 'Mixed-Up' mangled PGP/MIME message"
+output=$(notmuch search --output=messages body:password)
+test_expect_equal "$output" id:mixed-up@mangling.notmuchmail.org
+
+test_done
diff --git a/test/T355-smime.sh b/test/T355-smime.sh
new file mode 100755 (executable)
index 0000000..d2118c0
--- /dev/null
@@ -0,0 +1,207 @@
+#!/usr/bin/env bash
+
+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
+
+FINGERPRINT=$(openssl x509 -sha1 -fingerprint -in "$NOTMUCH_SRCDIR/test/smime/key+cert.pem" -noout | sed -e 's/^.*=//' -e s/://g)
+
+add_gpgsm_home
+
+test_begin_subtest "emacs delivery of S/MIME signed message"
+test_expect_success \
+     'emacs_fcc_message \
+     "test signed message 001" \
+     "This is a test signed message." \
+     "(mml-secure-message-sign \"smime\")"'
+
+test_begin_subtest "emacs delivery of S/MIME encrypted + signed message"
+# Hard code the MML to avoid several interactive questions
+test_expect_success \
+'emacs_fcc_message \
+    "test encrypted message 001" \
+    "<#secure method=smime mode=signencrypt>\nThis is a test encrypted message.\n"'
+
+test_begin_subtest "Signature verification (openssl)"
+notmuch show --format=raw subject:"test signed message 001" |\
+    openssl smime -verify -CAfile $NOTMUCH_SRCDIR/test/smime/test.crt 2>OUTPUT
+cat <<EOF > EXPECTED
+Verification successful
+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' \
+         -e 's|"expires": [-1234567890]*|"expires": 424242424|g' )
+expected='[[[{"id": "XXXXX",
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 946728000,
+ "date_relative": "2000-01-01",
+ "tags": ["inbox","signed"],
+ "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",
+ "Date": "Sat, 01 Jan 2000 12:00:00 +0000"},
+ "body": [{"id": 1,
+ "sigstatus": [{"fingerprint": "'$FINGERPRINT'",
+ "status": "good",
+ "userid": "CN=Notmuch Test Suite",
+ "email": "'$EXPECTED_EMAIL_ADDR'",
+ "expires": 424242424,
+ "created": 946728000}],
+ "content-type": "multipart/signed",
+ "content": [{"id": 2,
+ "content-type": "text/plain",
+ "content": "This is a test signed message.\n"},
+ {"id": 3,
+  "content-disposition": "attachment",
+  "content-length": "NONZERO",
+  "content-transfer-encoding": "base64",
+  "content-type": "application/pkcs7-signature",
+  "filename": "smime.p7s"}]}]},
+ []]]]'
+test_expect_equal_json \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "Decryption and signature verification (openssl)"
+notmuch show --format=raw subject:"test encrypted message 001" |\
+    openssl smime -decrypt -recip $NOTMUCH_SRCDIR/test/smime/key+cert.pem |\
+    openssl smime -verify -CAfile $NOTMUCH_SRCDIR/test/smime/test.crt 2>OUTPUT
+cat <<EOF > EXPECTED
+Verification successful
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Decryption (notmuch CLI)"
+notmuch show --decrypt=true subject:"test encrypted message 001" |\
+    grep "^This is a" > OUTPUT
+cat <<EOF > EXPECTED
+This is a test encrypted message.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Cryptographic message status (encrypted+signed)"
+output=$(notmuch show --format=json --decrypt=true subject:"test encrypted message 001")
+test_json_nodes <<<"$output" \
+                'crypto_encrypted:[0][0][0]["crypto"]["decrypted"]["status"]="full"' \
+                'crypto_sigok:[0][0][0]["crypto"]["signed"]["status"][0]["status"]="good"' \
+                'crypto_fpr:[0][0][0]["crypto"]["signed"]["status"][0]["fingerprint"]="616F46CD73834C63847756AF0DFB64A6E0972A47"' \
+                'crypto_uid:[0][0][0]["crypto"]["signed"]["status"][0]["userid"]="CN=Notmuch Test Suite"'
+
+test_begin_subtest "encrypted+signed message is known to be encrypted, but signature is unknown"
+output=$(notmuch search subject:"test encrypted message 001")
+test_expect_equal "$output" "thread:0000000000000002   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message 001 (encrypted inbox)"
+
+test_begin_subtest "Encrypted body is not indexed"
+output=$(notmuch search 'this is a test encrypted message')
+test_expect_equal "$output" ""
+
+test_begin_subtest "Reindex cleartext"
+test_expect_success "notmuch reindex --decrypt=true subject:'test encrypted message 001'"
+
+test_begin_subtest "signature is now known"
+output=$(notmuch search subject:"test encrypted message 001")
+test_expect_equal "$output" "thread:0000000000000002   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message 001 (encrypted inbox signed)"
+
+test_begin_subtest "Encrypted body is indexed"
+output=$(notmuch search 'this is a test encrypted message')
+test_expect_equal "$output" "thread:0000000000000002   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message 001 (encrypted inbox signed)"
+
+add_email_corpus pkcs7
+
+test_begin_subtest "index PKCS#7 SignedData message"
+output=$(notmuch search --output=messages Thanks)
+expected=id:smime-onepart-signed@protected-headers.example
+test_expect_equal "$expected" "$output"
+
+test_begin_subtest "do not index embedded certificates from PKCS#7 SignedData"
+output=$(notmuch search --output=messages 'LAMPS Certificate')
+expected=''
+test_expect_equal "$expected" "$output"
+
+test_begin_subtest "know the MIME type of the embedded part in PKCS#7 SignedData"
+output=$(notmuch search --output=messages 'mimetype:text/plain')
+expected=id:smime-onepart-signed@protected-headers.example
+test_expect_equal "$expected" "$output"
+
+test_begin_subtest "PKCS#7 SignedData message is tagged 'signed'"
+output=$(notmuch dump id:smime-onepart-signed@protected-headers.example)
+expected='#notmuch-dump batch-tag:3 config,properties,tags
++inbox +signed +unread -- id:smime-onepart-signed@protected-headers.example'
+test_expect_equal "$expected" "$output"
+
+test_begin_subtest "show contents of PKCS#7 SignedData message"
+output=$(notmuch show --format=raw --part=2 id:smime-onepart-signed@protected-headers.example)
+whitespace=' '
+expected="Bob, we need to cancel this contract.
+
+Please start the necessary processes to make that happen today.
+
+Thanks, Alice
+--${whitespace}
+Alice Lovelace
+President
+OpenPGP Example Corp"
+test_expect_equal "$expected" "$output"
+
+test_begin_subtest "reply to PKCS#7 SignedData message with proper quoting and attribution"
+output=$(notmuch reply id:smime-onepart-signed@protected-headers.example)
+expected="From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: The FooCorp contract
+To: Alice Lovelace <alice@smime.example>, Bob Babbage <bob@smime.example>
+In-Reply-To: <smime-onepart-signed@protected-headers.example>
+References: <smime-onepart-signed@protected-headers.example>
+
+On Tue, 26 Nov 2019 20:11:29 -0400, Alice Lovelace <alice@smime.example> wrote:
+> Bob, we need to cancel this contract.
+>${whitespace}
+> Please start the necessary processes to make that happen today.
+>${whitespace}
+> Thanks, Alice
+> --${whitespace}
+> Alice Lovelace
+> President
+> OpenPGP Example Corp"
+test_expect_equal "$expected" "$output"
+
+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-0}" != "1" ]; then
+    test_subtest_known_broken
+fi
+output=$(notmuch show --format=json id:smime-onepart-signed@protected-headers.example)
+
+test_json_nodes <<<"$output" \
+                'created:[0][0][0]["crypto"]["signed"]["status"][0]["created"]=1574813489' \
+                '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
+    test_subtest_known_broken
+fi
+test_json_nodes <<<"$output" \
+                'userid:[0][0][0]["crypto"]["signed"]["status"][0]["userid"]="CN=Alice Lovelace"'
+
+test_done
diff --git a/test/T356-protected-headers.sh b/test/T356-protected-headers.sh
new file mode 100755 (executable)
index 0000000..9f64033
--- /dev/null
@@ -0,0 +1,208 @@
+#!/usr/bin/env bash
+
+test_description='Message decryption with protected headers'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+##################################################
+
+test_require_external_prereq gpgsm
+
+add_gnupg_home
+add_gpgsm_home
+
+add_email_corpus protected-headers
+
+test_begin_subtest "verify protected header is not visible without decryption"
+output=$(notmuch show --format=json id:protected-header@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'no_crypto_info:[0][0][0]["crypto"]={}' \
+                'subject:[0][0][0]["headers"]["Subject"]="Subject Unavailable"'
+
+test_begin_subtest "verify protected header is visible with decryption"
+output=$(notmuch show --decrypt=true --format=json id:protected-header@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full", "header-mask": {"Subject": "Subject Unavailable"}}}' \
+                'subject:[0][0][0]["headers"]["Subject"]="This is a protected header"'
+
+test_begin_subtest "when no external header is present, show masked subject as null"
+output=$(notmuch show --decrypt=true --format=json id:subjectless-protected-header@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full", "header-mask": {"Subject": null}}}' \
+                'subject:[0][0][0]["headers"]["Subject"]="This is a protected header"'
+
+test_begin_subtest "misplaced protected headers should not be made visible during decryption"
+output=$(notmuch show --decrypt=true --format=json id:misplaced-protected-header@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full"}}' \
+                'subject:[0][0][0]["headers"]["Subject"]="Subject Unavailable"'
+
+test_begin_subtest "verify double-wrapped phony protected header is not visible when inner decryption fails"
+output=$(notmuch show --decrypt=true --format=json id:double-wrapped-with-phony-protected-header@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full"}}' \
+                'subject:[0][0][0]["headers"]["Subject"]="Subject Unavailable"'
+
+test_begin_subtest "cleartext phony protected headers should not be made visible when decryption fails"
+output=$(notmuch show --decrypt=true --format=json id:phony-protected-header-bad-encryption@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'no_crypto_info:[0][0][0]["crypto"]={}' \
+                'subject:[0][0][0]["headers"]["Subject"]="Subject Unavailable"'
+
+test_begin_subtest "wrapped protected headers should not be made visible during decryption"
+output=$(notmuch show --decrypt=true --format=json id:wrapped-protected-header@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "partial"}}' \
+                'subject:[0][0][0]["headers"]["Subject"]="[mailing-list] Subject Unavailable"'
+
+test_begin_subtest "internal headers without protected-header attribute should be skipped"
+output=$(notmuch show --decrypt=true --format=json id:no-protected-header-attribute@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full"}}' \
+                'subject:[0][0][0]["headers"]["Subject"]="Subject Unavailable"'
+
+test_begin_subtest "verify nested message/rfc822 protected header is visible"
+output=$(notmuch show --decrypt=true --format=json id:nested-rfc822-message@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full", "header-mask": {"Subject": "Subject Unavailable"}}}' \
+                'subject:[0][0][0]["headers"]["Subject"]="This is a message using draft-melnikov-smime-header-signing"'
+
+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": 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": 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)
+test_json_nodes <<<"$output" \
+                'crypto:["original"]["crypto"]={"decrypted": {"status": "full", "header-mask": {"Subject": "Subject Unavailable"}}}' \
+                'subject:["original"]["headers"]["Subject"]="This is a protected header"' \
+                'reply-subject:["reply-headers"]["Subject"]="Re: Subject Unavailable"'
+
+test_begin_subtest "protected subject is not indexed by default"
+output=$(notmuch search --output=messages 'subject:"This is a protected header"')
+test_expect_equal "$output" ''
+
+test_begin_subtest "reindex message with protected header"
+test_expect_success 'notmuch reindex --decrypt=true id:protected-header@crypto.notmuchmail.org'
+
+test_begin_subtest "protected subject is indexed when cleartext is indexed"
+output=$(notmuch search --output=messages 'subject:"This is a protected header"')
+test_expect_equal "$output" 'id:protected-header@crypto.notmuchmail.org'
+
+test_begin_subtest "indexed protected subject is visible in search"
+output=$(notmuch search --format=json 'id:protected-header@crypto.notmuchmail.org')
+test_json_nodes <<<"$output" \
+                'subject:[0]["subject"]="This is a protected header"'
+
+test_begin_subtest "indexed protected subject is not visible in reply header"
+output=$(notmuch reply --format=json 'id:protected-header@crypto.notmuchmail.org')
+test_json_nodes <<<"$output" \
+                'subject:["original"]["headers"]["Subject"]="This is a protected header"' \
+                'reply-subject:["reply-headers"]["Subject"]="Re: Subject Unavailable"'
+
+test_begin_subtest "verify correct protected header when submessage exists"
+output=$(notmuch show --decrypt=true --format=json id:encrypted-message-with-forwarded-attachment@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full", "header-mask": {"Subject": "Subject Unavailable"}}}' \
+                'subject:[0][0][0]["headers"]["Subject"]="This is the cryptographic envelope subject"'
+
+test_begin_subtest "verify protected header is both signed and encrypted"
+output=$(notmuch show --decrypt=true --format=json id:encrypted-signed@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'crypto:[0][0][0]["crypto"]={
+                   "signed":{"status": [{"status": "good", "fingerprint": "'$FINGERPRINT'", "email": "'"$SELF_EMAIL"'", "userid": "'"$SELF_USERID"'", "created": 1662550328}],
+                   "encrypted": true, "headers": ["Subject"]},"decrypted": {"status": "full", "header-mask": {"Subject": "Subject Unavailable"}}}' \
+                'subject:[0][0][0]["headers"]["Subject"]="Rhinoceros dinner"'
+
+test_begin_subtest "verify protected header is signed even when not masked"
+output=$(notmuch show --decrypt=true --format=json id:encrypted-signed-not-masked@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'crypto:[0][0][0]["crypto"]={
+                   "signed":{"status": [{"status": "good", "fingerprint": "'$FINGERPRINT'", "userid": "'"$SELF_USERID"'", "email": "'"$SELF_EMAIL"'", "created": 1662550328}],
+                   "encrypted": true, "headers": ["Subject"]},"decrypted": {"status": "full"}}' \
+                'subject:[0][0][0]["headers"]["Subject"]="Rhinoceros dinner"'
+
+test_begin_subtest "reindex everything, ensure headers are as expected"
+notmuch reindex --decrypt=true from:test_suite@notmuchmail.org
+output=$(notmuch search --output=messages 'subject:"protected header" or subject:"Rhinoceros" or subject:"draft-melnikov-smime-header-signing" or subject:"valid"' | sort)
+test_expect_equal "$output" 'id:encrypted-signed-not-masked@crypto.notmuchmail.org
+id:encrypted-signed@crypto.notmuchmail.org
+id:nested-rfc822-message@crypto.notmuchmail.org
+id:protected-header@crypto.notmuchmail.org
+id:subjectless-protected-header@crypto.notmuchmail.org'
+
+test_begin_subtest "when rendering protected headers, avoid rendering legacy-display part"
+output=$(notmuch show --format=json id:protected-with-legacy-display@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'subject:[0][0][0]["headers"]["Subject"]="Interrupting Cow"' \
+                'no_legacy_display:[0][0][0]["body"][0]["content"][1]["content-type"]="text/plain"'
+
+test_begin_subtest "when replying, avoid rendering legacy-display part"
+output=$(notmuch reply --format=json id:protected-with-legacy-display@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" \
+                'no_legacy_display:["original"]["body"][0]["content"][1]["content-type"]="text/plain"'
+
+test_begin_subtest "do not treat legacy-display part as body when indexing"
+output=$(notmuch search --output=messages body:interrupting)
+test_expect_equal "$output" ''
+
+test_begin_subtest "identify message that had a legacy display part skipped during indexing"
+output=$(notmuch search --output=messages property:index.repaired=skip-protected-headers-legacy-display)
+test_expect_equal "$output" id:protected-with-legacy-display@crypto.notmuchmail.org
+
+for variant in multipart-signed onepart-signed; do
+    test_begin_subtest "verify signed PKCS#7 subject ($variant)"
+    output=$(notmuch show --verify --format=json "id:smime-${variant}@protected-headers.example")
+    test_json_nodes <<<"$output" \
+                    'signed_subject:[0][0][0]["crypto"]["signed"]["headers"]=["Subject"]' \
+                    'sig_good:[0][0][0]["crypto"]["signed"]["status"][0]["status"]="good"' \
+                    'sig_fpr:[0][0][0]["crypto"]["signed"]["status"][0]["fingerprint"]="702BA4B157F1E2B7D16B0C6A5FFC8A7DE2057DEB"' \
+                    'not_encrypted:[0][0][0]["crypto"]!"decrypted"'
+    test_begin_subtest "verify signed PKCS#7 subject ($variant) signer User ID"
+    if [ $NOTMUCH_GMIME_X509_CERT_VALIDITY -ne 1 ]; then
+        test_subtest_known_broken
+    fi
+    test_json_nodes <<<"$output" \
+                    'sig_uid:[0][0][0]["crypto"]["signed"]["status"][0]["userid"]="CN=Alice Lovelace"'
+done
+
+for variant in sign+enc sign+enc+legacy-disp; do
+    test_begin_subtest "confirm signed and encrypted PKCS#7 subject ($variant)"
+    output=$(notmuch show --decrypt=true --format=json "id:smime-${variant}@protected-headers.example")
+    test_json_nodes <<<"$output" \
+                    'signed_subject:[0][0][0]["crypto"]["signed"]["headers"]=["Subject"]' \
+                    'sig_good:[0][0][0]["crypto"]["signed"]["status"][0]["status"]="good"' \
+                    'sig_fpr:[0][0][0]["crypto"]["signed"]["status"][0]["fingerprint"]="702BA4B157F1E2B7D16B0C6A5FFC8A7DE2057DEB"' \
+                    'encrypted:[0][0][0]["crypto"]["decrypted"]={"status":"full","header-mask":{"Subject":"..."}}'
+    test_begin_subtest "confirm signed and encrypted PKCS#7 subject ($variant) signer User ID"
+    if [ $NOTMUCH_GMIME_X509_CERT_VALIDITY -ne 1 ]; then
+        test_subtest_known_broken
+    fi
+    test_json_nodes <<<"$output" \
+                    'sig_uid:[0][0][0]["crypto"]["signed"]["status"][0]["userid"]="CN=Alice Lovelace"'
+
+done
+
+test_begin_subtest "confirm encryption-protected PKCS#7 subject (enc+legacy-disp)"
+output=$(notmuch show --decrypt=true --format=json "id:smime-enc+legacy-disp@protected-headers.example")
+test_json_nodes <<<"$output" \
+                'encrypted:[0][0][0]["crypto"]["decrypted"]={"status":"full","header-mask":{"Subject":"..."}}' \
+                'no_sig:[0][0][0]["crypto"]!"signed"'
+
+
+# TODO: test that a part that looks like a legacy-display in
+# multipart/signed, but not encrypted, is indexed and not stripped.
+
+# TODO: test that a legacy-display in a decrypted subpart (not in the
+# cryptographic payload) is indexed and not stripped.
+
+# TODO: test that a legacy-display inside multiple MIME layers that
+# include an encryption layer (e.g. multipart/encrypted around
+# multipart/signed) is stripped and not indexed.
+
+test_done
diff --git a/test/T357-index-decryption.sh b/test/T357-index-decryption.sh
new file mode 100755 (executable)
index 0000000..a749748
--- /dev/null
@@ -0,0 +1,325 @@
+#!/usr/bin/env bash
+
+# TODO: test index.decryption=failed
+
+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
+test_begin_subtest 'emacs delivery of encrypted message'
+test_expect_success \
+'emacs_fcc_message \
+    "test encrypted message for cleartext index 001" \
+    "This is a test encrypted message with a wumpus.\n" \
+    "(mml-secure-message-encrypt)"'
+
+test_begin_subtest "search for unindexed cleartext"
+output=$(notmuch search wumpus)
+expected=''
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# create a test encrypted message that is indexed in the clear
+test_begin_subtest 'emacs delivery of encrypted message'
+test_expect_success \
+'emacs_fcc_message --decrypt=true \
+    "test encrypted message for cleartext index 002" \
+    "This is a test encrypted message with a wumpus.\n" \
+    "(mml-secure-message-encrypt)"'
+
+test_begin_subtest "emacs delivery of encrypted message, indexed cleartext"
+output=$(notmuch search wumpus)
+expected='thread:0000000000000002   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (encrypted inbox)'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# and the same search, but by property ($expected is untouched):
+test_begin_subtest "emacs search by property for one message"
+output=$(notmuch search property:index.decryption=success)
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "show the message body of the encrypted message"
+notmuch dump wumpus
+output=$(notmuch show wumpus | notmuch_show_part 3)
+expected='This is a test encrypted message with a wumpus.'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+
+test_begin_subtest "message should go away after deletion"
+# cache the message in an env var and remove it:
+fname=$(notmuch search --output=files wumpus)
+contents="$(notmuch show --format=raw wumpus)"
+rm -f "$fname"
+notmuch new
+output=$(notmuch search wumpus)
+expected=''
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# try reinserting it without decryption, should stay the same:
+test_begin_subtest "message cleartext not present after insert"
+notmuch insert --folder=sent <<<"$contents"
+output=$(notmuch search wumpus)
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# show the message using stashing decryption
+test_begin_subtest "stash decryption during show"
+output=$(notmuch show --decrypt=stash tag:encrypted subject:002 | notmuch_show_part 3)
+expected='This is a test encrypted message with a wumpus.'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "search should now find the contents"
+output=$(notmuch search wumpus)
+expected='thread:0000000000000003   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (encrypted inbox unread)'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# try reinserting it with decryption, should appear again, but now we
+# have two copies of the message:
+test_begin_subtest "message cleartext is present after reinserting with --decrypt=true"
+notmuch insert --folder=sent --decrypt=true <<<"$contents"
+output=$(notmuch search wumpus)
+expected='thread:0000000000000003   2000-01-01 [1/1(2)] Notmuch Test Suite; test encrypted message for cleartext index 002 (encrypted inbox unread)'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# remove all copies
+test_begin_subtest "delete all copies of the message"
+mid="$(notmuch search --output=messages wumpus)"
+rm -f $(notmuch search --output=files wumpus)
+notmuch new
+output=$(notmuch search "id:$mid")
+expected=''
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# try inserting it with decryption, should appear as a single copy
+test_begin_subtest "message cleartext is present with insert --decrypt=true"
+notmuch insert --folder=sent --decrypt=true <<<"$contents"
+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"
+
+
+# add a tag to all messages to ensure that it stays after reindexing
+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 | 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"
+
+# see if first message shows up after reindexing with --decrypt=true (same $expected, untouched):
+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 | 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 | notmuch_search_sanitize)
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# try a simple reindex
+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 | notmuch_search_sanitize)
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# try to remove cleartext indexing
+test_begin_subtest 'reindex without cleartext'
+test_expect_success 'notmuch reindex --decrypt=false tag:encrypted and property:index.decryption=success'
+test_begin_subtest "reindexed encrypted messages, without cleartext"
+output=$(notmuch search wumpus)
+expected=''
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# ensure no session keys are present:
+test_begin_subtest 'reindex using only session keys'
+test_expect_success 'notmuch reindex tag:encrypted and property:index.decryption=success'
+test_begin_subtest "reindexed encrypted messages, decrypting only with session keys"
+output=$(notmuch search wumpus)
+expected=''
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# and the same search, but by property ($expected is untouched):
+test_begin_subtest "emacs search by property with both messages unindexed"
+output=$(notmuch search property:index.decryption=success)
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+# 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 | 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"
+
+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 | notmuch_search_sanitize)
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "show one of the messages with --decrypt=true"
+output=$(notmuch show --decrypt=true thread:0000000000000001 | notmuch_show_part 3)
+expected='This is a test encrypted message with a wumpus.'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "Ensure that we cannot show the message with --decrypt=auto"
+output=$(notmuch show thread:0000000000000001 | notmuch_show_part 3)
+expected='Non-text part: application/octet-stream'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+add_email_corpus crypto
+
+test_begin_subtest "indexing message fails when secret key not available"
+notmuch reindex --decrypt=true id:simple-encrypted@crypto.notmuchmail.org
+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 \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "cannot find cleartext index"
+output=$(notmuch search sekrit)
+expected=''
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "cleartext index recovery on reindexing with stashed session keys"
+notmuch restore <<EOF
+#notmuch-dump batch-tag:3 config,properties,tags
+#= simple-encrypted@crypto.notmuchmail.org session-key=9%3AFC09987F5F927CC0CC0EE80A96E4C5BBF4A499818FB591207705DFDDD6112CF9
+EOF
+notmuch reindex id:simple-encrypted@crypto.notmuchmail.org
+output=$(notmuch search sekrit | notmuch_search_sanitize)
+expected='thread:XXX   2016-12-22 [1/1] Daniel Kahn Gillmor; encrypted message (encrypted inbox unread)'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "notmuch reply should show cleartext if session key is present"
+output=$(notmuch reply id:simple-encrypted@crypto.notmuchmail.org | grep '^>')
+expected='> This is a top sekrit message.'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "notmuch show should show cleartext if session key is present"
+output=$(notmuch show id:simple-encrypted@crypto.notmuchmail.org | notmuch_show_part 3)
+expected='This is a top sekrit message.'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "notmuch show should show nothing if decryption is explicitly disallowed"
+output=$(notmuch show --decrypt=false id:simple-encrypted@crypto.notmuchmail.org | notmuch_show_part 3)
+expected='Non-text part: application/octet-stream'
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "purging stashed session keys should lose access to the cleartext"
+notmuch reindex --decrypt=false id:simple-encrypted@crypto.notmuchmail.org
+output=$(notmuch search sekrit)
+expected=''
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+test_begin_subtest "and cleartext should be unrecoverable now that there are no stashed session keys"
+notmuch dump
+notmuch reindex --decrypt=true id:simple-encrypted@crypto.notmuchmail.org
+output=$(notmuch search sekrit)
+expected=''
+test_expect_equal \
+    "$output" \
+    "$expected"
+
+goodsig='good_sig:[0][0][0]["crypto"]["signed"]["status"][0]["status"]="good"'
+nosig='no_sig:[0][0][0]["crypto"]!"signed"'
+
+test_begin_subtest "verify signature without a session key stashed when --decrypt=true"
+output=$(notmuch show --format=json --decrypt=true id:encrypted-signed@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" "$goodsig"
+
+test_begin_subtest "do not verify sig without a session key stashed if --decrypt=auto"
+output=$(notmuch show --format=json id:encrypted-signed@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" "$nosig"
+
+test_begin_subtest "verify signature when --decrypt=stash"
+output=$(notmuch show --format=json --decrypt=stash id:encrypted-signed@crypto.notmuchmail.org)
+test_json_nodes <<<"$output" "$goodsig"
+
+test_begin_subtest "verify signature with stashed session key"
+output=$(notmuch show --format=json id:encrypted-signed@crypto.notmuchmail.org)
+if [ $NOTMUCH_GMIME_VERIFY_WITH_SESSION_KEY -ne 1 ]; then
+    test_subtest_known_broken
+fi
+test_json_nodes <<<"$output" "$goodsig"
+
+# TODO: test removal of a message from the message store between
+# indexing and reindexing.
+
+# TODO: insert the same message into the message store twice, index,
+# remove one of them from the message store, and then reindex.
+# reindexing should return a failure but the message should still be
+# present? -- or what should the semantics be if you ask to reindex a
+# message whose underlying files have been renamed or moved or
+# removed?
+
+test_done
diff --git a/test/T358-emacs-protected-headers.sh b/test/T358-emacs-protected-headers.sh
new file mode 100755 (executable)
index 0000000..96e42bf
--- /dev/null
@@ -0,0 +1,116 @@
+#!/usr/bin/env bash
+
+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
+
+test_begin_subtest "notmuch-search should show not unindexed protected subject header in emacs"
+test_emacs '(notmuch-search "id:protected-header@crypto.notmuchmail.org")
+           (notmuch-test-wait)
+           (test-output)'
+cat <<EOF >EXPECTED
+  2000-01-01 [1/1]   test_suite@notmuchmail.org  Subject Unavailable (encrypted inbox unread)
+End of search results.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch-show should not show unindexed protected subject header in emacs when nm-c-process-mime is nil"
+test_emacs '(let ((notmuch-crypto-process-mime nil))
+             (notmuch-show "id:protected-header@crypto.notmuchmail.org")
+             (test-output))'
+cat <<EOF >EXPECTED
+test_suite@notmuchmail.org (2000-01-01) (encrypted inbox)
+Subject: Subject Unavailable
+To: test_suite@notmuchmail.org
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+
+[ multipart/encrypted ]
+[ Unknown encryption status ]
+[ application/pgp-encrypted ]
+[ application/octet-stream ]
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch-show should show protected subject header in emacs"
+test_emacs '(notmuch-show "id:protected-header@crypto.notmuchmail.org")
+           (test-output)'
+cat <<EOF >EXPECTED
+test_suite@notmuchmail.org (2000-01-01) (encrypted inbox)
+Subject: This is a protected header
+To: test_suite@notmuchmail.org
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+
+[ multipart/encrypted ]
+[ Decryption successful ]
+[ application/pgp-encrypted ]
+[ text/plain ]
+This is the sekrit message
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+# notmuch-emacs still leaks the subject line; as long as it leaks the
+# subject line, it should emit the external subject, not the protected
+# subject, even if it knows what the true subject is:
+test_begin_subtest "Reply within emacs to a message with protected headers, not leaking subject"
+test_emacs "(let ((message-hidden-headers '()))
+           (notmuch-show \"id:protected-header@crypto.notmuchmail.org\")
+           (notmuch-show-reply)
+           (test-output))"
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: test_suite@notmuchmail.org
+Subject: Re: Subject Unavailable
+In-Reply-To: <protected-header@crypto.notmuchmail.org>
+Fcc: ${MAIL_DIR}/sent
+References: <protected-header@crypto.notmuchmail.org>
+--text follows this line--
+<#secure method=pgpmime mode=signencrypt>
+test_suite@notmuchmail.org writes:
+
+> This is the sekrit message
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+# protected headers should behave differently after re-indexing
+test_begin_subtest 'defaulting to indexing cleartext'
+test_expect_success 'notmuch config set index.decrypt true'
+test_begin_subtest 'try reindexing protected header message'
+test_expect_success 'notmuch reindex id:protected-header@crypto.notmuchmail.org'
+
+test_begin_subtest "notmuch-search should show indexed protected subject header in emacs"
+test_emacs '(notmuch-search "id:protected-header@crypto.notmuchmail.org")
+           (notmuch-test-wait)
+           (test-output)'
+cat <<EOF >EXPECTED
+  2000-01-01 [1/1]   test_suite@notmuchmail.org  This is a protected header (encrypted inbox)
+End of search results.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+# notmuch-emacs still leaks the subject line:
+test_begin_subtest "don't leak protected subject during reply, even if indexed"
+test_emacs "(let ((message-hidden-headers '()))
+           (notmuch-show \"id:protected-header@crypto.notmuchmail.org\")
+           (notmuch-show-reply)
+           (test-output))"
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: test_suite@notmuchmail.org
+Subject: Re: Subject Unavailable
+In-Reply-To: <protected-header@crypto.notmuchmail.org>
+Fcc: ${MAIL_DIR}/sent
+References: <protected-header@crypto.notmuchmail.org>
+--text follows this line--
+<#secure method=pgpmime mode=signencrypt>
+test_suite@notmuchmail.org writes:
+
+> This is the sekrit message
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T360-symbol-hiding.sh b/test/T360-symbol-hiding.sh
new file mode 100755 (executable)
index 0000000..ff06ff6
--- /dev/null
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2011 David Bremner
+#
+
+# This test tests whether hiding Xapian::Error symbols in libnotmuch
+# also hides them for other users of libxapian. This is motivated by
+# the discussion in https://gcc.gnu.org/wiki/Visibility'
+
+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 \
+       | notmuch_dir_sanitize | sed -e "s,\`,\',g" -e "s,No [^[:space:]]* database,No XXXXXX database,g" > OUTPUT
+
+cat <<EOF > EXPECTED
+Cannot open Xapian database at CWD/fakedb/.notmuch/xapian: Couldn't stat 'CWD/fakedb/.notmuch/xapian'
+caught No XXXXXX database found at path 'CWD/nonexistent'
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'checking output'
+test_expect_equal "$result" "$output"
+
+test_begin_subtest 'comparing existing to exported symbols'
+readelf -Ws $NOTMUCH_BUILDDIR/lib/libnotmuch.so | sed -e 's/\[[^]]*\]//' |\
+    awk '$4 == "FUNC" && $5 == "GLOBAL" && $7 != "UND" {print $8}' | sort -u > ACTUAL
+sed -n 's/^\(notmuch_[a-zA-Z0-9_]*\)[[:blank:]]*(.*/\1/p' $NOTMUCH_SRCDIR/lib/notmuch.h | sort -u > EXPORTED
+test_expect_equal_file EXPORTED ACTUAL
+
+test_done
diff --git a/test/T370-search-folder-coherence.sh b/test/T370-search-folder-coherence.sh
new file mode 100755 (executable)
index 0000000..cf202bb
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+test_description='folder tags removed and added through file renames remain consistent'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest "No new messages"
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "No new mail."
+
+
+test_begin_subtest "Single new message"
+generate_message
+file_x=$gen_msg_filename
+id_x=$gen_msg_id
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Add second folder for same message"
+dir=$(dirname $file_x)
+mkdir $dir/spam
+cp $file_x $dir/spam
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "No new mail."
+
+
+test_begin_subtest "Multiple files for same message"
+cat <<EOF >EXPECTED
+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
+
+test_begin_subtest "Test matches folder:spam"
+output=$(notmuch search folder:spam)
+test_expect_equal "$output" "thread:0000000000000001   2001-01-05 [1/1(2)] Notmuch Test Suite; Single new message (inbox unread)"
+
+test_begin_subtest "Remove folder:spam copy of email"
+rm $dir/spam/$(basename $file_x)
+output=$(NOTMUCH_NEW)
+test_expect_equal "$output" "No new mail. Detected 1 file rename."
+
+test_begin_subtest "No mails match the folder:spam search"
+output=$(notmuch search folder:spam)
+test_expect_equal "$output" ""
+
+test_done
diff --git a/test/T380-atomicity.sh b/test/T380-atomicity.sh
new file mode 100755 (executable)
index 0000000..0f9e6d2
--- /dev/null
@@ -0,0 +1,101 @@
+#!/usr/bin/env bash
+test_description='atomicity'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+# This script tests the effects of killing and restarting "notmuch
+# new" at arbitrary points.  If notmuch new is properly atomic, the
+# final database contents should be the same regardless of when (or
+# if) it is killed and restarted.
+
+if test_require_external_prereq gdb; then
+
+# Create a maildir structure to also stress flag synchronization
+    mkdir $MAIL_DIR/cur
+    mkdir $MAIL_DIR/new
+    mkdir $MAIL_DIR/tmp
+    mkdir $MAIL_DIR/.remove-dir
+
+    # Prepare the initial database
+    generate_message [subject]='Duplicate' [filename]='duplicate:2,' [dir]=cur
+    generate_message [subject]='Remove' [filename]='remove:2,' [dir]=cur
+    generate_message [subject]='"Remove duplicate"' [filename]='remove-duplicate:2,' [dir]=cur
+    cp $MAIL_DIR/cur/remove-duplicate:2, $MAIL_DIR/cur/remove-duplicate-copy:2,
+    generate_message [subject]='Rename' [filename]='rename:2,' [dir]=cur
+    generate_message [subject]='"Rename duplicate"' [filename]='rename-duplicate:2,' [dir]=cur
+    generate_message [subject]='"Move 1"' [filename]='move1:2,' [dir]=cur
+    generate_message [subject]='"Move 2"' [filename]='move2:2,' [dir]=new
+    generate_message [subject]='Flag' [filename]='flag:2,' [dir]=cur
+    generate_message [subject]='"Flag duplicate"' [filename]='flag-duplicate:2,' [dir]=cur
+    cp $MAIL_DIR/cur/flag-duplicate:2, $MAIL_DIR/cur/flag-duplicate-copy:2,F
+    generate_message [subject]='"Remove directory"' [filename]='remove-directory:2,' [dir]=.remove-dir
+    generate_message [subject]='"Remove directory duplicate"' [filename]='remove-directory-duplicate:2,' [dir]=.remove-dir
+    cp $MAIL_DIR/.remove-dir/remove-directory-duplicate:2, $MAIL_DIR/cur/
+    notmuch new > /dev/null
+
+    # Make all maildir changes, but *don't* update the database
+    generate_message [subject]='Added' [filename]='added:2,' [dir]=cur
+    cp $MAIL_DIR/cur/duplicate:2, $MAIL_DIR/cur/duplicate-copy:2,
+    generate_message [subject]='"Add duplicate"' [filename]='add-duplicate:2,' [dir]=cur
+    generate_message [subject]='"Add duplicate copy"' [filename]='add-duplicate-copy:2,' [dir]=cur
+    rm $MAIL_DIR/cur/remove:2,
+    rm $MAIL_DIR/cur/remove-duplicate-copy:2,
+    mv $MAIL_DIR/cur/rename:2, $MAIL_DIR/cur/renamed:2,
+    mv $MAIL_DIR/cur/rename-duplicate:2, $MAIL_DIR/cur/renamed-duplicate:2,
+    mv $MAIL_DIR/cur/move1:2, $MAIL_DIR/new/move1:2,
+    mv $MAIL_DIR/new/move2:2, $MAIL_DIR/cur/move2:2,
+    mv $MAIL_DIR/cur/flag:2, $MAIL_DIR/cur/flag:2,F
+    rm $MAIL_DIR/cur/flag-duplicate-copy:2,F
+    rm $MAIL_DIR/.remove-dir/remove-directory:2,
+    rm $MAIL_DIR/.remove-dir/remove-directory-duplicate:2,
+    rmdir $MAIL_DIR/.remove-dir
+
+    # Prepare a snapshot of the updated maildir.  The gdb script will
+    # update the database in this snapshot as it goes.
+    cp -a $MAIL_DIR $MAIL_DIR.snap
+    cp ${NOTMUCH_CONFIG} ${NOTMUCH_CONFIG}.snap
+    NOTMUCH_CONFIG=${NOTMUCH_CONFIG}.snap notmuch config set database.path $MAIL_DIR.snap
+
+
+
+    # Execute notmuch new and, at every call to rename, snapshot the
+    # database, run notmuch new again on the snapshot, and capture the
+    # results of search.
+    #
+    # -tty /dev/null works around a conflict between the 'timeout' wrapper
+    # and gdb's attempt to control the TTY.
+    export MAIL_DIR
+    ${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 '*' 2>/dev/null > expected
+
+    # Check output against golden output
+    outcount=$(cat outcount)
+    : > 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
+           for ((end = $i + 1 ; end < $outcount; end++)); do
+               if ! cmp -s search.$i search.$end; then
+                   break
+               fi
+           done
+           echo "When interrupted after $test/backtrace.$(expr $i - 1) (abort points $i-$(expr $end - 1))" >> searchall
+           cat search.$i >> searchall
+           cat expected >> expectall
+           echo >> searchall
+           echo >> expectall
+
+           i=$(expr $end - 1)
+       fi
+    done
+fi
+
+test_begin_subtest '"notmuch new" is idempotent under arbitrary aborts'
+test_expect_equal_file searchall expectall
+
+test_begin_subtest "detected $outcount>10 abort points"
+test_expect_success "test $outcount -gt 10"
+
+test_done
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
diff --git a/test/T390-python.sh b/test/T390-python.sh
new file mode 100755 (executable)
index 0000000..2191243
--- /dev/null
@@ -0,0 +1,201 @@
+#!/usr/bin/env bash
+test_description="python bindings"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_require_external_prereq ${NOTMUCH_PYTHON}
+
+if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then
+    test_done
+fi
+
+add_email_corpus
+add_gnupg_home
+
+test_begin_subtest "compare thread ids"
+test_python <<EOF
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+q_new = notmuch.Query(db, 'tag:inbox')
+q_new.set_sort(notmuch.Query.SORT.OLDEST_FIRST)
+for t in q_new.search_threads():
+    print (t.get_thread_id())
+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"
+test_python <<EOF
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+q_new = notmuch.Query(db, 'tag:inbox')
+q_new.set_sort(notmuch.Query.SORT.OLDEST_FIRST)
+for m in q_new.search_messages():
+    print (m.get_message_id())
+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"
+test_python <<EOF
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+print (db.find_message_by_filename("i-dont-exist"))
+EOF
+test_expect_equal "$(cat OUTPUT)" "None"
+
+test_begin_subtest "get revision"
+test_python ${MAIL_DIR} <<'EOF'
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+(revision, uuid) = db.get_revision()
+print ("%s\t%lu" % (uuid, revision))
+EOF
+notmuch_uuid_sanitize < OUTPUT > CLEAN
+cat <<'EOF' >EXPECTED
+UUID   53
+EOF
+test_expect_equal_file EXPECTED CLEAN
+
+grep '^[0-9a-f]' OUTPUT > INITIAL_OUTPUT
+
+test_begin_subtest "output of count matches test code"
+notmuch count --lastmod '*' | cut -f2-3 > OUTPUT
+test_expect_equal_file INITIAL_OUTPUT OUTPUT
+add_message '[content-type]="text/plain; charset=iso-8859-2"' \
+            '[content-transfer-encoding]=8bit' \
+            '[subject]="ISO-8859-2 encoded message"' \
+            "[body]=$'Czech word tu\350\362\341\350\350\355 means pinguin\'s.'" # ISO-8859-2 characters are generated by shell's escape sequences
+test_begin_subtest "Add ISO-8859-2 encoded message, call get_message_parts"
+test_python <<EOF
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+q_new = notmuch.Query(db, 'ISO-8859-2 encoded message')
+for m in q_new.search_messages():
+    for mp in m.get_message_parts():
+      continue
+    print(m.get_message_id())
+EOF
+
+notmuch search --sort=oldest-first --output=messages "tučňáččí" | sed s/^id:// > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+# TODO currently these tests for setting and getting config values are
+# somewhat interdependent.  This is because the config values stored in the
+# database are not cleaned up after each test, so they remain there for the
+# next test.  The ./README file states that this can happen so it seems kind
+# of ok.
+
+test_begin_subtest "set and get config values"
+test_python <<'EOF'
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE)
+db.set_config('testkey1', 'testvalue1')
+db.set_config('testkey2', 'testvalue2')
+v1 = db.get_config('testkey1')
+v2 = db.get_config('testkey2')
+print('testkey1 = ' + v1)
+print('testkey2 = ' + v2)
+EOF
+cat <<'EOF' >EXPECTED
+testkey1 = testvalue1
+testkey2 = testvalue2
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get_configs with no match returns empty generator"
+test_python <<'EOF'
+import notmuch
+db = notmuch.Database()
+v = db.get_configs('nonexistent')
+print(list(v) == [])
+EOF
+test_expect_equal "$(cat OUTPUT)" "True"
+
+test_begin_subtest "get_configs with no arguments returns all pairs"
+test_python <<'EOF'
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE)
+db.set_config("zzzafter", "afterval")
+db.set_config("aaabefore", "beforeval")
+v = db.get_configs()
+for index, keyval in enumerate(v):
+    key, val = keyval
+    print('{}: {} => {}'.format(index, key, val))
+EOF
+cat <<'EOF' >EXPECTED
+0: aaabefore => beforeval
+1: testkey1 => testvalue1
+2: testkey2 => testvalue2
+3: zzzafter => afterval
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get_configs prefix is used to match keys"
+test_python <<'EOF'
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE)
+db.set_config('testkey1', 'testvalue1')
+db.set_config('testkey2', 'testvalue2')
+v = db.get_configs('testkey')
+for index, keyval in enumerate(v):
+    key, val = keyval
+    print('{}: {} => {}'.format(index, key, val))
+EOF
+cat <<'EOF' >EXPECTED
+0: testkey1 => testvalue1
+1: testkey2 => testvalue2
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "set_config with no value will unset config entries"
+test_python <<'EOF'
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE)
+db.set_config('testkey1', '')
+db.set_config('testkey2', '')
+db.set_config("zzzafter", '')
+db.set_config("aaabefore", '')
+v = db.get_configs()
+print(list(v) == [])
+EOF
+test_expect_equal "$(cat OUTPUT)" "True"
+
+mkdir -p "${MAIL_DIR}/cur"
+fname="${MAIL_DIR}/cur/simplemsg.eml"
+cat <<EOF > "$fname"
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: encrypted message
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <simplemsg@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+$(printf 'Content-Type: text/plain\n\nThis is the sekrit message\n' | gpg --no-tty --batch --quiet --trust-model=always --encrypt --armor --recipient test_suite@notmuchmail.org)
+--=-=-=--
+EOF
+
+test_begin_subtest "index message with decryption"
+test_python <<EOF
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE)
+(m, status) = db.index_file('$fname', decrypt_policy=notmuch.Database.DECRYPTION_POLICY.TRUE)
+if status == notmuch.errors.STATUS.DUPLICATE_MESSAGE_ID:
+   print("got duplicate message")
+q_new = notmuch.Query(db, 'sekrit')
+for m in q_new.search_messages():
+    print(m.get_filename())
+EOF
+echo "$fname" > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T391-python-cffi.sh b/test/T391-python-cffi.sh
new file mode 100755 (executable)
index 0000000..0059b05
--- /dev/null
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+test_description="python bindings (pytest)"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+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 (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
diff --git a/test/T395-ruby.sh b/test/T395-ruby.sh
new file mode 100755 (executable)
index 0000000..d0c6bb1
--- /dev/null
@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+test_description="ruby bindings"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+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"
+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
+
+test_begin_subtest "compare message ids"
+notmuch search --sort=oldest-first --output=messages tag:inbox > EXPECTED
+test_ruby <<"EOF"
+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
+
+test_begin_subtest "get non-existent file"
+echo nil > EXPECTED
+test_ruby <<"EOF"
+p db.find_message_by_filename('i-dont-exist')
+EOF
+
+test_begin_subtest "count messages"
+notmuch count --output=messages tag:inbox > EXPECTED
+test_ruby <<"EOF"
+puts db.query('tag:inbox').count_messages()
+EOF
+
+test_begin_subtest "count threads"
+notmuch count --output=threads tag:inbox > EXPECTED
+test_ruby <<"EOF"
+puts db.query('tag:inbox').count_threads()
+EOF
+
+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"
+q = db.query('tag:inbox', sort: Notmuch::SORT_OLDEST_FIRST)
+q.search_threads.each do |t|
+  puts 'thread:%s' % t.thread_id
+end
+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
+
+test_done
diff --git a/test/T400-hooks.sh b/test/T400-hooks.sh
new file mode 100755 (executable)
index 0000000..35bf70c
--- /dev/null
@@ -0,0 +1,236 @@
+#!/usr/bin/env bash
+test_description='hooks'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_require_external_prereq xapian-delve
+
+create_echo_hook () {
+    local TOKEN="${RANDOM}"
+    mkdir -p ${HOOK_DIR}
+    cat <<EOF >"${HOOK_DIR}/${1}"
+#!/bin/sh
+echo "${TOKEN}" > ${3}
+EOF
+    chmod +x "${HOOK_DIR}/${1}"
+    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}
+    cat <<EOF >"${HOOK_DIR}/${1}"
+#!/bin/sh
+if xapian-delve ${MAIL_DIR}/.notmuch/xapian | grep -q "writing = false"; then
+   echo "${TOKEN}" > ${3}
+fi
+EOF
+    chmod +x "${HOOK_DIR}/${1}"
+    echo "${TOKEN}" > ${2}
+}
+
+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}
+    cat <<EOF >"${HOOK_DIR}/${1}"
+#!/bin/sh
+exit 13
+EOF
+    chmod +x "${HOOK_DIR}/${1}"
+}
+
+# add a message to generate mail dir and database
+add_message
+# create maildir structure for notmuch-insert
+mkdir -p "$MAIL_DIR"/{cur,new,tmp}
+
+ORIG_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+for config in traditional profile explicit relative XDG split; do
+    unset NOTMUCH_PROFILE
+    export NOTMUCH_CONFIG=${ORIG_NOTMUCH_CONFIG}
+    EXPECTED_CONFIG=${NOTMUCH_CONFIG}
+    notmuch config set database.hook_dir
+    notmuch config set database.path ${MAIL_DIR}
+    case $config in
+       traditional)
+           HOOK_DIR=${MAIL_DIR}/.notmuch/hooks
+           ;;
+       profile)
+           dir=${HOME}/.config/notmuch/other
+           mkdir -p ${dir}
+           HOOK_DIR=${dir}/hooks
+           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
+           ;;
+       split)
+           dir="$TMP_DIRECTORY/database.$test_count"
+           notmuch config set database.path $dir
+           notmuch config set database.mail_root $MAIL_DIR
+           HOOK_DIR=${dir}/hooks
+           ;;
+    esac
+
+    test_begin_subtest "pre-new is run [${config}]"
+    rm -rf ${HOOK_DIR}
+    generate_message
+    create_echo_hook "pre-new" expected output $HOOK_DIR
+    notmuch new > /dev/null
+    test_expect_equal_file expected output
+
+    test_begin_subtest "post-new is run [${config}]"
+    rm -rf ${HOOK_DIR}
+    generate_message
+    create_echo_hook "post-new" expected output $HOOK_DIR
+    notmuch new > /dev/null
+    test_expect_equal_file expected output
+
+    test_begin_subtest "post-insert hook is run [${config}]"
+    rm -rf ${HOOK_DIR}
+    generate_message
+    create_echo_hook "post-insert" expected output $HOOK_DIR
+    notmuch insert < "$gen_msg_filename"
+    test_expect_equal_file expected output
+
+    test_begin_subtest "pre-new is run before post-new [${config}]"
+    rm -rf ${HOOK_DIR}
+    generate_message
+    create_echo_hook "pre-new" pre-new.expected pre-new.output $HOOK_DIR
+    create_echo_hook "post-new" post-new.expected post-new.output $HOOK_DIR
+    notmuch new > /dev/null
+    test_expect_equal_file post-new.expected post-new.output
+
+    test_begin_subtest "pre-new non-zero exit status (hook status) [${config}]"
+    rm -rf ${HOOK_DIR}
+    generate_message
+    create_failing_hook "pre-new" $HOOK_DIR
+    output=`notmuch new 2>&1`
+    test_expect_equal "$output" "Error: pre-new hook failed with status 13"
+
+    # depends on the previous subtest leaving broken hook behind
+    test_begin_subtest "pre-new non-zero exit status (notmuch status) [${config}]"
+    test_expect_code 1 "notmuch new"
+
+    # depends on the previous subtests leaving 1 new message behind
+    test_begin_subtest "pre-new non-zero exit status aborts new [${config}]"
+    rm -rf ${HOOK_DIR}
+    output=$(NOTMUCH_NEW)
+    test_expect_equal "$output" "Added 1 new message to the database."
+
+    test_begin_subtest "post-new non-zero exit status (hook status) [${config}]"
+    rm -rf ${HOOK_DIR}
+    generate_message
+    create_failing_hook "post-new" $HOOK_DIR
+    NOTMUCH_NEW 2>output.stderr >output
+    cat output.stderr >> output
+    echo "Added 1 new message to the database." > expected
+    echo "Error: post-new hook failed with status 13" >> expected
+    test_expect_equal_file expected output
+
+    # depends on the previous subtest leaving broken hook behind
+    test_begin_subtest "post-new non-zero exit status (notmuch status) [${config}]"
+    test_expect_code 1 "notmuch new"
+
+    test_begin_subtest "post-insert hook does not affect insert status [${config}]"
+    rm -rf ${HOOK_DIR}
+    generate_message
+    create_failing_hook "post-insert" $HOOK_DIR
+    test_expect_success "notmuch insert < \"$gen_msg_filename\" > /dev/null"
+
+    test_begin_subtest "hook without executable permissions [${config}]"
+    rm -rf ${HOOK_DIR}
+    mkdir -p ${HOOK_DIR}
+    cat <<EOF >"${HOOK_DIR}/pre-new"
+    #!/bin/sh
+    echo foo
+EOF
+    output=`notmuch new 2>&1`
+    test_expect_code 1 "notmuch new"
+
+    test_begin_subtest "hook execution failure [${config}]"
+    rm -rf ${HOOK_DIR}
+    mkdir -p ${HOOK_DIR}
+    cat <<EOF >"${HOOK_DIR}/pre-new"
+    no hashbang, execl fails
+EOF
+    chmod +x "${HOOK_DIR}/pre-new"
+    test_expect_code 1 "notmuch new"
+
+    test_begin_subtest "post-new with write access [${config}]"
+    rm -rf ${HOOK_DIR}
+    create_write_hook "post-new" write.expected write.output $HOOK_DIR
+    NOTMUCH_NEW
+    test_expect_equal_file write.expected write.output
+
+    test_begin_subtest "pre-new with write access [${config}]"
+    rm -rf ${HOOK_DIR}
+    create_write_hook "pre-new" write.expected write.output $HOOK_DIR
+    NOTMUCH_NEW
+    test_expect_equal_file write.expected write.output
+
+    test_begin_subtest "add message in pre-new [${config}]"
+    rm -rf ${HOOK_DIR}
+    generate_message '[subject]="add msg in pre-new"'
+    id1=$gen_msg_id
+    create_change_hook "pre-new" $gen_msg_filename $HOOK_DIR
+    generate_message '[subject]="add msg in new"'
+    NOTMUCH_NEW
+    notmuch search id:$id1 or id:$gen_msg_id | notmuch_search_sanitize > OUTPUT
+    cat <<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
diff --git a/test/T410-argument-parsing.sh b/test/T410-argument-parsing.sh
new file mode 100755 (executable)
index 0000000..40b625f
--- /dev/null
@@ -0,0 +1,104 @@
+#!/usr/bin/env bash
+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
+boolean 1
+keyword 1
+flags 5
+int 7
+string foo
+positional arg 1 pos1
+positional arg 2 pos2
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "sanity check zero values"
+$TEST_DIRECTORY/arg-test --keyword=zero --boolean=false --int=0 > OUTPUT
+cat <<EOF > EXPECTED
+boolean 0
+keyword 0
+int 0
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "space instead of = between parameter name and value"
+# Note: spaces aren't allowed for booleans. false turns into a positional arg!
+$TEST_DIRECTORY/arg-test --keyword one --boolean false --string foo --int 7 --flag one --flag three > OUTPUT
+cat <<EOF > EXPECTED
+boolean 1
+keyword 1
+flags 5
+int 7
+string foo
+positional arg 1 false
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--boolean=true"
+$TEST_DIRECTORY/arg-test --boolean=true > OUTPUT
+cat <<EOF > EXPECTED
+boolean 1
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--boolean=false"
+$TEST_DIRECTORY/arg-test --boolean=false > OUTPUT
+cat <<EOF > EXPECTED
+boolean 0
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--no-boolean"
+$TEST_DIRECTORY/arg-test --no-boolean > OUTPUT
+cat <<EOF > EXPECTED
+boolean 0
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--no-flag"
+$TEST_DIRECTORY/arg-test --flag=one --flag=three --no-flag=three > OUTPUT
+cat <<EOF > EXPECTED
+flags 1
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "test keyword arguments without value"
+$TEST_DIRECTORY/arg-test --boolkeyword bananas > OUTPUT
+cat <<EOF > EXPECTED
+boolkeyword 1
+positional arg 1 bananas
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "test keyword arguments with non-default value separated by a space"
+$TEST_DIRECTORY/arg-test --boolkeyword false bananas > OUTPUT
+cat <<EOF > EXPECTED
+boolkeyword 0
+positional arg 1 bananas
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "test keyword arguments without value at the end"
+$TEST_DIRECTORY/arg-test bananas --boolkeyword > OUTPUT
+cat <<EOF > EXPECTED
+boolkeyword 1
+positional arg 1 bananas
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "test keyword arguments without value but with = (should be an error)"
+$TEST_DIRECTORY/arg-test bananas --boolkeyword= > OUTPUT 2>&1
+cat <<EOF > EXPECTED
+Unknown keyword argument "" for option "boolkeyword".
+Unrecognized option: --boolkeyword=
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T420-emacs-test-functions.sh b/test/T420-emacs-test-functions.sh
new file mode 100755 (executable)
index 0000000..2dfd7c5
--- /dev/null
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+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'
+
+test_done
diff --git a/test/T430-emacs-address-cleaning.sh b/test/T430-emacs-address-cleaning.sh
new file mode 100755 (executable)
index 0000000..7d3d61f
--- /dev/null
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+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)'
+
+test_begin_subtest "notmuch-test-address-clean part 2"
+test_emacs_expect_t '(notmuch-test-address-cleaning-2)'
+
+test_begin_subtest "notmuch-test-address-clean part 3"
+test_emacs_expect_t '(notmuch-test-address-cleaning-3)'
+
+test_done
diff --git a/test/T440-emacs-hello.sh b/test/T440-emacs-hello.sh
new file mode 100755 (executable)
index 0000000..842781a
--- /dev/null
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+
+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"
+test_emacs "(let ((notmuch-hello-sections
+                   (list (lambda () (notmuch-hello-insert-searches
+                                     \"Test\" '((\"inbox\" . \"tag:inbox\")))))))
+           (notmuch-hello)
+           (test-output))"
+test_expect_equal_file $EXPECTED/notmuch-hello-new-section OUTPUT
+
+test_begin_subtest "User-defined section with empty, hidden entry"
+test_emacs "(let ((notmuch-hello-sections
+                   (list (lambda () (notmuch-hello-insert-searches
+                                     \"Test-with-empty\"
+                                     '((\"inbox\" . \"tag:inbox\")
+                                       (\"doesnotexist\" . \"tag:doesnotexist\"))
+                                     :hide-empty-searches t)))))
+             (notmuch-hello)
+             (test-output))"
+test_expect_equal_file $EXPECTED/notmuch-hello-section-with-empty OUTPUT
+
+test_begin_subtest "User-defined section, unread tag filtered out"
+test_emacs "(let ((notmuch-hello-sections
+                   (list (lambda () (notmuch-hello-insert-tags-section
+                                     \"Test-with-filtered\"
+                                     :hide-tags '(\"unread\"))))))
+             (notmuch-hello)
+             (test-output))"
+test_expect_equal_file $EXPECTED/notmuch-hello-section-hidden-tag OUTPUT
+
+test_begin_subtest "User-defined section, different query for counts"
+test_emacs "(let ((notmuch-hello-sections
+                   (list (lambda () (notmuch-hello-insert-tags-section
+                                     \"Test-with-counts\"
+                                     :filter-count \"tag:signed\")))))
+             (notmuch-hello)
+             (test-output))"
+test_expect_equal_file $EXPECTED/notmuch-hello-section-counts OUTPUT
+
+test_begin_subtest "Empty custom tags section"
+test_emacs "(let* ((widget (widget-create 'notmuch-hello-tags-section))
+                   (notmuch-hello-sections (list (widget-value widget))))
+             (notmuch-hello)
+             (test-output))"
+test_expect_equal_file $EXPECTED/notmuch-hello-empty-custom-tags-section OUTPUT
+
+test_begin_subtest "Empty custom queries section"
+test_emacs "(let* ((widget (widget-create 'notmuch-hello-query-section))
+                   (notmuch-hello-sections (list (widget-value widget))))
+             (notmuch-hello)
+             (test-output))"
+test_expect_equal_file $EXPECTED/notmuch-hello-empty-custom-queries-section OUTPUT
+
+test_begin_subtest "Column alignment for tag/queries with long names"
+tag=a-very-long-tag # length carefully calculated for 80 characters window width
+notmuch tag +$tag '*'
+test_emacs '(notmuch-hello)
+            (test-output)'
+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
diff --git a/test/T450-emacs-show.sh b/test/T450-emacs-show.sh
new file mode 100755 (executable)
index 0000000..559df8a
--- /dev/null
@@ -0,0 +1,415 @@
+#!/usr/bin/env bash
+
+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"
+message_id='OriginalMessageHiding.1@notmuchmail.org'
+add_message \
+    [id]="$message_id" \
+    '[subject]="Hiding Original Message region at beginning of a message"' \
+    '[body]="-----Original Message-----
+Text here."'
+
+cat <<EOF >EXPECTED
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox)
+Subject: Hiding Original Message region at beginning of a message
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: GENERATED_DATE
+
+[ 2-line hidden original message. Click/Enter to show. ]
+EOF
+
+test_emacs "(notmuch-show \"id:$message_id\")
+           (test-visible-output \"OUTPUT.raw\")"
+notmuch_date_sanitize < OUTPUT.raw > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Bare subject #1"
+output=$(test_emacs '(notmuch-show-strip-re "Re: subject")')
+test_expect_equal "$output" '"subject"'
+
+test_begin_subtest "Bare subject #2"
+output=$(test_emacs '(notmuch-show-strip-re "re:Re: re:  Re:  re:subject")')
+test_expect_equal "$output" '"subject"'
+
+test_begin_subtest "Bare subject #3"
+output=$(test_emacs '(notmuch-show-strip-re "the cure: fix the regexp")')
+test_expect_equal "$output" '"the cure: fix the regexp"'
+
+test_begin_subtest "don't process cryptographic MIME parts"
+test_emacs '(let ((notmuch-crypto-process-mime nil))
+       (notmuch-show "id:20091117203301.GV3165@dottiness.seas.harvard.edu")
+       (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-process-crypto-mime-parts-off OUTPUT
+
+test_begin_subtest "process cryptographic MIME parts"
+test_emacs '(let ((notmuch-crypto-process-mime t))
+       (notmuch-show "id:20091117203301.GV3165@dottiness.seas.harvard.edu")
+       (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-process-crypto-mime-parts-on OUTPUT
+
+test_begin_subtest "process cryptographic MIME parts (w/ notmuch-show-toggle-process-crypto)"
+test_emacs '(let ((notmuch-crypto-process-mime nil))
+       (notmuch-show "id:20091117203301.GV3165@dottiness.seas.harvard.edu")
+       (notmuch-show-toggle-process-crypto)
+       (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\"")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-elide-non-matching-messages-off OUTPUT
+
+test_begin_subtest "notmuch-show: elide non-matching messages"
+test_emacs '(let ((notmuch-show-only-matching-messages t))
+       (notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (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\"")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (notmuch-show-toggle-elide-non-matching)
+       (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-elide-non-matching-messages-on OUTPUT
+
+test_begin_subtest "notmuch-show: elide non-matching messages (w/ prefix arg to notmuch-show)"
+test_emacs '(let ((notmuch-show-only-matching-messages nil))
+       (notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread t)
+       (notmuch-test-wait)
+       (test-visible-output))'
+test_expect_equal_file $EXPECTED/notmuch-show-elide-non-matching-messages-on OUTPUT
+
+test_begin_subtest "notmuch-show: disable indentation of thread content (w/ notmuch-show-toggle-thread-indentation)"
+test_emacs '(notmuch-search "from:lars@seas.harvard.edu and subject:\"Maildir storage\"")
+       (notmuch-test-wait)
+       (notmuch-search-show-thread)
+       (notmuch-test-wait)
+       (notmuch-show-toggle-thread-indentation)
+       (test-visible-output)'
+test_expect_equal_file $EXPECTED/notmuch-show-indent-thread-content-off OUTPUT
+
+test_begin_subtest "id buttonization"
+add_message '[body]="
+id:abc
+id:abc.def. id:abc,def, id:abc;def; id:abc:def:
+id:foo@bar.?baz? id:foo@bar!.baz!
+(id:foo@bar.baz) [id:foo@bar.baz]
+id:foo@bar.baz...
+id:2+2=5
+id:=_-:/.[]@$%+
+id:abc)def
+id:ab\"c def
+id:\"abc\"def
+id:\"ab\"\"c\"def
+id:\"ab c\"def
+id:\"abc\".def
+id:\"abc
+\"
+id:)
+id:
+cid:xxx
+mid:abc mid:abc/def
+mid:abc%20def
+mid:abc. mid:abc, mid:abc;"'
+test_emacs '(notmuch-show "id:'$gen_msg_id'")
+       (notmuch-test-mark-links)
+       (test-visible-output "OUTPUT.raw")'
+cat <<EOF >EXPECTED
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox)
+Subject: id buttonization
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: GENERATED_DATE
+
+<<id:abc>>
+<<id:abc.def>>. <<id:abc,def>>, <<id:abc;def>>; <<id:abc:def>>:
+<<id:foo@bar.?baz>>? <<id:foo@bar!.baz>>!
+(<<id:foo@bar.baz>>) [<<id:foo@bar.baz>>]
+<<id:foo@bar.baz>>...
+<<id:2+2=5>>
+<<id:=_-:/.[]@$%+>>
+<<id:abc>>)def
+<<id:ab"c>> def
+<<id:"abc">>def
+<<id:"ab""c">>def
+<<id:"ab c">>def
+<<id:"abc">>.def
+id:"abc
+"
+id:)
+id:
+cid:xxx
+<<mid:abc>> <<mid:abc/def>>
+<<mid:abc%20def>>
+<<mid:abc>>. <<mid:abc>>, <<mid:abc>>;
+EOF
+notmuch_date_sanitize < OUTPUT.raw > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_begin_subtest "Show handles subprocess errors"
+cat > notmuch_fail <<EOF
+#!/bin/sh
+echo This is output
+echo This is an error >&2
+exit 1
+EOF
+chmod a+x notmuch_fail
+test_emacs "(let ((notmuch-command \"$PWD/notmuch_fail\"))
+              (with-current-buffer \"*Messages*\"
+                  (let ((inhibit-read-only t)) (erase-buffer)))
+              (condition-case err
+                  (notmuch-show \"*\")
+                (error (message \"%s\" (cadr err))))
+              (notmuch-test-wait)
+              (with-current-buffer \"*Messages*\"
+                 (test-output \"MESSAGES\"))
+              (with-current-buffer \"*Notmuch errors*\"
+                 (test-output \"ERROR\"))
+              (test-output))"
+test_expect_equal "$(notmuch_emacs_error_sanitize notmuch_fail OUTPUT MESSAGES ERROR)" "\
+=== OUTPUT ===
+=== MESSAGES ===
+This is an error (see *Notmuch errors* for more details)
+=== ERROR ===
+This is an error
+command: YYY/notmuch_fail show --format\\=sexp --format-version\\=5 --decrypt\\=true --exclude\\=false \\' \\* \\'
+exit status: 1
+stderr:
+This is an error
+stdout:
+This is output"
+
+test_begin_subtest "text/enriched exploit mitigation"
+add_message '[content-type]="text/enriched"
+             [body]="
+<x-display><param>(when (progn (read-only-mode -1) (insert ?p ?0 ?w ?n ?e ?d)) nil)</param>test</x-display>
+"'
+test_emacs '(notmuch-show "id:'$gen_msg_id'")
+       (test-visible-output "OUTPUT.raw")'
+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
+add_email_corpus crypto
+
+test_begin_subtest "show decrypted message"
+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)'
+test_expect_equal_file $EXPECTED/notmuch-show-undecryptable-message OUTPUT
+
+test_begin_subtest "show encrypted message when not processing crypto"
+test_emacs '(let ((notmuch-crypto-process-mime nil))
+             (notmuch-show "id:basic-encrypted@crypto.notmuchmail.org")
+             (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
diff --git a/test/T455-emacs-charsets.sh b/test/T455-emacs-charsets.sh
new file mode 100755 (executable)
index 0000000..db03bb6
--- /dev/null
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+
+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
+generate_message \
+    [id]=test-plain@example.com \
+    '[content-type]="text/plain; charset=big5"' \
+    '[content-transfer-encoding]=quoted-printable' \
+    '[body]="Yen: =A2=44"'
+
+# 2) text/plain in 8bit big5
+generate_message \
+    [id]=test-plain-8bit@example.com \
+    '[content-type]="text/plain; charset=big5"' \
+    '[content-transfer-encoding]=8bit' \
+    '[body]="Yen: '$BIG5_YEN'"'
+
+# 3) text/html in quoted-printable big5
+generate_message \
+    [id]=test-html@example.com \
+    '[content-type]="text/html; charset=big5"' \
+    '[content-transfer-encoding]=quoted-printable' \
+    '[body]="<html><body>Yen: =A2=44</body></html>"'
+
+# 4) application/octet-stream in quoted-printable of big5 text
+generate_message \
+    [id]=test-binary@example.com \
+    '[content-type]="application/octet-stream"' \
+    '[content-transfer-encoding]=quoted-printable' \
+    '[body]="Yen: =A2=44"'
+
+notmuch new > /dev/null
+
+# Test rendering
+
+test_begin_subtest "Text parts are decoded when rendering"
+test_emacs '(notmuch-show "id:test-plain@example.com")
+           (test-visible-output "OUTPUT.raw")'
+awk 'show {print} /^$/ {show=1}' < OUTPUT.raw > OUTPUT
+cat <<EOF >EXPECTED
+Yen: $UTF8_YEN
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "8bit text parts are decoded when rendering"
+test_emacs '(notmuch-show "id:test-plain-8bit@example.com")
+           (test-visible-output "OUTPUT.raw")'
+awk 'show {print} /^$/ {show=1}' < OUTPUT.raw > OUTPUT
+cat <<EOF >EXPECTED
+Yen: $UTF8_YEN
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "HTML parts are decoded when rendering"
+test_emacs '(notmuch-show "id:test-html@example.com")
+           (test-visible-output "OUTPUT.raw")'
+awk 'show {print} /^$/ {show=1}' < OUTPUT.raw > OUTPUT
+cat <<EOF >EXPECTED
+[ text/html ]
+Yen: $UTF8_YEN
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+# Test saving
+
+test_begin_subtest "Text parts are not decoded when saving"
+rm -f part
+test_emacs '(notmuch-show "id:test-plain@example.com")
+           (search-forward "Yen")
+           (let ((standard-input "\"part\""))
+              (notmuch-show-save-part))'
+cat <<EOF >EXPECTED
+Yen: $BIG5_YEN
+EOF
+test_expect_equal_file part EXPECTED
+
+test_begin_subtest "8bit text parts are not decoded when saving"
+rm -f part
+test_emacs '(notmuch-show "id:test-plain-8bit@example.com")
+           (search-forward "Yen")
+           (let ((standard-input "\"part\""))
+              (notmuch-show-save-part))'
+cat <<EOF >EXPECTED
+Yen: $BIG5_YEN
+EOF
+test_expect_equal_file part EXPECTED
+
+test_begin_subtest "HTML parts are not decoded when saving"
+rm -f part
+test_emacs '(notmuch-show "id:test-html@example.com")
+           (search-forward "Yen")
+           (let ((standard-input "\"part\""))
+              (notmuch-show-save-part))'
+cat <<EOF >EXPECTED
+<html><body>Yen: $BIG5_YEN</body></html>
+EOF
+test_expect_equal_file part EXPECTED
+
+test_begin_subtest "Binary parts are not decoded when saving"
+rm -f part
+test_emacs '(notmuch-show "id:test-binary@example.com")
+           (search-forward "application/")
+           (let ((standard-input "\"part\""))
+              (notmuch-show-save-part))'
+cat <<EOF >EXPECTED
+Yen: $BIG5_YEN
+EOF
+test_expect_equal_file part EXPECTED
+
+# Test message viewing
+
+test_begin_subtest "Text message are not decoded when viewing"
+test_emacs '(notmuch-show "id:test-plain@example.com")
+           (notmuch-show-view-raw-message)
+           (test-visible-output "OUTPUT.raw")'
+awk 'show {print} /^$/ {show=1}' < OUTPUT.raw > OUTPUT
+cat <<EOF >EXPECTED
+Yen: =A2=44
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "8bit text message are not decoded when viewing"
+test_emacs '(notmuch-show "id:test-plain-8bit@example.com")
+           (notmuch-show-view-raw-message)
+           (test-visible-output "OUTPUT.raw")'
+awk 'show {print} /^$/ {show=1}' < OUTPUT.raw > OUTPUT
+cat <<EOF >EXPECTED
+Yen: $BIG5_YEN
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T460-emacs-tree.sh b/test/T460-emacs-tree.sh
new file mode 100755 (executable)
index 0000000..8e07144
--- /dev/null
@@ -0,0 +1,249 @@
+#!/usr/bin/env bash
+
+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"
+test_emacs '(notmuch-tree "tag:inbox")
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox OUTPUT
+
+test_begin_subtest "Refreshed notmuch-tree view in emacs"
+test_emacs '(notmuch-tree "tag:inbox")
+           (notmuch-test-wait)
+           (notmuch-tree-refresh-view)
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox OUTPUT
+
+# In the following tag tests we make sure the display is updated
+# correctly and, in a separate test, that the database is updated
+# correctly.
+
+test_begin_subtest "Tag message in notmuch tree view (display)"
+test_emacs '(notmuch-tree "tag:inbox")
+           (notmuch-test-wait)
+           (forward-line)
+           (notmuch-tree-tag (list "+test_tag"))
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox-tagged OUTPUT
+
+test_begin_subtest "Tag message in notmuch tree view (database)"
+output=$(notmuch search --output=messages 'tag:test_tag')
+test_expect_equal "$output" "id:877h1wv7mg.fsf@inf-8657.int-evry.fr"
+
+test_begin_subtest "Untag message in notmuch tree view"
+test_emacs '(notmuch-tree "tag:inbox")
+           (notmuch-test-wait)
+           (forward-line)
+           (notmuch-tree-tag (list "-test_tag"))
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox OUTPUT
+
+test_begin_subtest "Untag message in notmuch tree view (database)"
+output=$(notmuch search --output=messages 'tag:test_tag')
+test_expect_equal "$output" ""
+
+test_begin_subtest "Tag thread in notmuch tree view"
+test_emacs '(notmuch-tree "tag:inbox")
+           (notmuch-test-wait)
+           ;; move to a sizable thread
+           (forward-line 26)
+           (notmuch-tree-tag-thread (list "+test_thread_tag"))
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox-thread-tagged OUTPUT
+
+test_begin_subtest "Tag message in notmuch tree view (database)"
+output=$(notmuch search --output=messages 'tag:test_thread_tag')
+test_expect_equal "$output" \
+"id:87ocn0qh6d.fsf@yoom.home.cworth.org
+id:20091118005040.GA25380@dottiness.seas.harvard.edu
+id:yunaayketfm.fsf@aiko.keithp.com
+id:87fx8can9z.fsf@vertex.dottedmag
+id:20091117203301.GV3165@dottiness.seas.harvard.edu
+id:87iqd9rn3l.fsf@vertex.dottedmag
+id:20091117190054.GU3165@dottiness.seas.harvard.edu"
+
+test_begin_subtest "Untag thread in notmuch tree view"
+test_emacs '(notmuch-tree "tag:inbox")
+           (notmuch-test-wait)
+           ;; move to the same sizable thread as above
+           (forward-line 26)
+           (notmuch-tree-tag-thread (list "-test_thread_tag"))
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file $EXPECTED/notmuch-tree-tag-inbox OUTPUT
+
+test_begin_subtest "Untag message in notmuch tree view (database)"
+output=$(notmuch search --output=messages 'tag:test_thread_tag')
+test_expect_equal "$output" ""
+
+test_begin_subtest "Navigation of notmuch-hello to search results"
+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 OUTPUT
+
+test_begin_subtest "Tree view of a single thread (from 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-thread)
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file $EXPECTED/notmuch-tree-single-thread OUTPUT
+
+test_begin_subtest "Tree view of a single thread (from show)"
+test_emacs '(notmuch-hello)
+           (goto-char (point-min))
+           (re-search-forward "inbox")
+           (widget-button-press (1- (point)))
+           (notmuch-test-wait)
+           (notmuch-search-show-thread)
+           (notmuch-tree-from-show-current-query)
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file $EXPECTED/notmuch-tree-single-thread OUTPUT
+
+test_begin_subtest "Message window of tree view"
+test_emacs '(notmuch-hello)
+           (goto-char (point-min))
+           (re-search-forward "inbox")
+           (widget-button-press (1- (point)))
+           (notmuch-test-wait)
+           (notmuch-search-next-thread)
+           (notmuch-tree-from-search-thread)
+           (notmuch-test-wait)
+           (select-window notmuch-tree-message-window)
+           (test-output)
+           (delete-other-windows)'
+test_expect_equal_file $EXPECTED/notmuch-tree-show-window OUTPUT
+
+test_begin_subtest "Stash id"
+output=$(test_emacs '(notmuch-tree "id:1258498485-sup-142@elly")
+                    (notmuch-test-wait)
+                    (notmuch-show-stash-message-id)')
+test_expect_equal "$output" "\"Stashed: id:1258498485-sup-142@elly\""
+
+test_begin_subtest "Move to next matching message"
+output=$(test_emacs '(notmuch-tree "from:cworth")
+                    (notmuch-test-wait)
+                    (notmuch-tree-next-matching-message)
+                    (notmuch-show-stash-message-id)')
+test_expect_equal "$output" "\"Stashed: id:878we4qdqf.fsf@yoom.home.cworth.org\""
+
+test_begin_subtest "Move to next thread"
+output=$(test_emacs '(notmuch-tree "tag:inbox")
+                    (notmuch-test-wait)
+                    (forward-line 26)
+                    (notmuch-tree-next-thread)
+                    (notmuch-show-stash-message-id)')
+test_expect_equal "$output" "\"Stashed: id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net\""
+
+test_begin_subtest "Move to previous thread"
+output=$(test_emacs '(notmuch-tree "tag:inbox")
+                    (notmuch-test-wait)
+                    (forward-line 26)
+                    (notmuch-tree-prev-thread)
+                    (notmuch-show-stash-message-id)')
+test_expect_equal "$output" "\"Stashed: id:20091117190054.GU3165@dottiness.seas.harvard.edu\""
+
+test_begin_subtest "Move to previous previous thread"
+output=$(test_emacs '(notmuch-tree "tag:inbox")
+                    (notmuch-test-wait)
+                    (forward-line 26)
+                    (notmuch-tree-prev-thread)
+                    (notmuch-tree-prev-thread)
+                    (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/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
diff --git a/test/T470-missing-headers.sh b/test/T470-missing-headers.sh
new file mode 100755 (executable)
index 0000000..32b070e
--- /dev/null
@@ -0,0 +1,166 @@
+#!/usr/bin/env bash
+test_description='messages with missing headers'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+# Notmuch requires at least one of from, subject, or to or it will
+# ignore the file.  Generate two messages so that together they cover
+# all possible missing headers.  We also give one of the messages a
+# date to ensure stable result ordering.
+
+cat <<EOF > "${MAIL_DIR}/msg-2"
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Fri, 05 Jan 2001 15:43:57 +0000
+
+Body
+EOF
+
+cat <<EOF > "${MAIL_DIR}/msg-1"
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+
+Body
+EOF
+
+NOTMUCH_NEW >/dev/null
+
+test_begin_subtest "Search: text"
+output=$(notmuch search '*' | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] ;  (inbox unread)
+thread:XXX   1970-01-01 [1/1] Notmuch Test Suite;  (inbox unread)"
+
+test_begin_subtest "Search: json"
+output=$(notmuch search --format=json '*' | notmuch_search_sanitize)
+test_expect_equal_json "$output" '
+[
+    {
+        "authors": "",
+        "date_relative": "2001-01-05",
+        "matched": 1,
+        "subject": "",
+        "tags": [
+            "inbox",
+            "unread"
+        ],
+        "thread": "XXX",
+        "timestamp": 978709437,
+        "total": 1,
+        "query": ["id:notmuch-sha1-7a6e4eac383ef958fcd3ebf2143db71b8ff01161", null]
+    },
+    {
+        "authors": "Notmuch Test Suite",
+        "date_relative": "1970-01-01",
+        "matched": 1,
+        "subject": "",
+        "tags": [
+            "inbox",
+            "unread"
+        ],
+        "thread": "XXX",
+        "timestamp": 0,
+        "total": 1,
+        "query": ["id:notmuch-sha1-ca55943aff7a72baf2ab21fa74fab3d632401334", null]
+    }
+]'
+
+test_begin_subtest "Show: text"
+output=$(notmuch show '*' | notmuch_show_sanitize)
+test_expect_equal "$output" "\
+\fmessage{ id:notmuch-sha1-7a6e4eac383ef958fcd3ebf2143db71b8ff01161 depth:0 match:1 excluded:0 filename:/XXX/mail/msg-2
+\fheader{
+ (2001-01-05) (inbox unread)
+Subject: (null)
+From: (null)
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Fri, 05 Jan 2001 15:43:57 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+Body
+\fpart}
+\fbody}
+\fmessage}
+\fmessage{ id:notmuch-sha1-ca55943aff7a72baf2ab21fa74fab3d632401334 depth:0 match:1 excluded:0 filename:/XXX/mail/msg-1
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (1970-01-01) (inbox unread)
+Subject: (null)
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Thu, 01 Jan 1970 00:00:00 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+Body
+\fpart}
+\fbody}
+\fmessage}"
+
+test_begin_subtest "Show: json"
+output=$(notmuch show --format=json '*' | notmuch_json_show_sanitize)
+expected=$(notmuch_json_show_sanitize <<EOF
+[
+    [
+        [
+            {
+                "body": [
+                    {
+                        "content": "Body\n",
+                        "content-type": "text/plain",
+                        "id": 1
+                    }
+                ],
+                "crypto": {},
+                "date_relative": "2001-01-05",
+                "excluded": false,
+                "filename": ["YYYYY"],
+                "headers": {
+                    "Date": "Fri, 05 Jan 2001 15:43:57 +0000",
+                    "From": "",
+                    "Subject": "",
+                    "To": "Notmuch Test Suite <test_suite@notmuchmail.org>"
+                },
+                "id": "XXXXX",
+                "match": true,
+                "tags": [
+                    "inbox",
+                    "unread"
+                ],
+                "timestamp": 978709437
+            },
+            []
+        ]
+    ],
+    [
+        [
+            {
+                "body": [
+                    {
+                        "content": "Body\n",
+                        "content-type": "text/plain",
+                        "id": 1
+                    }
+                ],
+                "crypto": {},
+                "date_relative": "1970-01-01",
+                "excluded": false,
+                "filename": ["YYYYY"],
+                "headers": {
+                    "Date": "Thu, 01 Jan 1970 00:00:00 +0000",
+                    "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+                    "Subject": ""
+                },
+                "id": "XXXXX",
+                "match": true,
+                "tags": [
+                    "inbox",
+                    "unread"
+                ],
+                "timestamp": 0
+            },
+            []
+        ]
+    ]
+]
+EOF
+)
+test_expect_equal_json "$output" "$expected"
+
+test_done
diff --git a/test/T480-hex-escaping.sh b/test/T480-hex-escaping.sh
new file mode 100755 (executable)
index 0000000..8bddf3e
--- /dev/null
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+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
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "punctuation"
+tag1='comic_swear=$&^%$^%\\//-+$^%$'
+tag_enc1=$($TEST_DIRECTORY/hex-xcode --direction=encode "$tag1")
+test_expect_equal "$tag_enc1" "comic_swear=%24%26%5e%25%24%5e%25%5c%5c%2f%2f-+%24%5e%25%24"
+
+test_begin_subtest "round trip newlines"
+printf 'this\n tag\t has\n spaces\n' > EXPECTED.$test_count
+$TEST_DIRECTORY/hex-xcode --direction=encode < EXPECTED.$test_count |\
+       $TEST_DIRECTORY/hex-xcode --direction=decode > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "round trip 8bit chars"
+echo '%c3%91%c3%a5%c3%b0%c3%a3%c3%a5%c3%a9-%c3%8f%c3%8a' > EXPECTED.$test_count
+$TEST_DIRECTORY/hex-xcode --direction=decode < EXPECTED.$test_count |\
+    $TEST_DIRECTORY/hex-xcode --direction=encode > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "round trip (in-place)"
+find $NOTMUCH_SRCDIR/test/corpora/default -type f -print | sort | xargs cat > EXPECTED
+$TEST_DIRECTORY/hex-xcode --in-place --direction=encode < EXPECTED |\
+     $TEST_DIRECTORY/hex-xcode --in-place --direction=decode > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "punctuation (in-place)"
+tag1='comic_swear=$&^%$^%\\//-+$^%$'
+tag_enc1=$($TEST_DIRECTORY/hex-xcode --in-place --direction=encode "$tag1")
+test_expect_equal "$tag_enc1" "comic_swear=%24%26%5e%25%24%5e%25%5c%5c%2f%2f-+%24%5e%25%24"
+
+test_begin_subtest "round trip newlines (in-place)"
+printf 'this\n tag\t has\n spaces\n' > EXPECTED.$test_count
+$TEST_DIRECTORY/hex-xcode --in-place --direction=encode < EXPECTED.$test_count |\
+    $TEST_DIRECTORY/hex-xcode --in-place --direction=decode > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_begin_subtest "round trip 8bit chars (in-place)"
+echo '%c3%91%c3%a5%c3%b0%c3%a3%c3%a5%c3%a9-%c3%8f%c3%8a' > EXPECTED.$test_count
+$TEST_DIRECTORY/hex-xcode --in-place --direction=decode < EXPECTED.$test_count |\
+    $TEST_DIRECTORY/hex-xcode --in-place --direction=encode > OUTPUT.$test_count
+test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+
+test_done
diff --git a/test/T490-parse-time-string.sh b/test/T490-parse-time-string.sh
new file mode 100755 (executable)
index 0000000..3b6e48c
--- /dev/null
@@ -0,0 +1,96 @@
+#!/usr/bin/env bash
+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 -d "$*" +%s
+}
+
+_parse_time () {
+    ${TEST_DIRECTORY}/parse-time --format=%s "$*"
+}
+
+test_begin_subtest "date(1) default format without TZ code"
+test_expect_equal "$(_parse_time Fri Aug 3 23:06:06 2012)" "$(_date Fri Aug 3 23:06:06 2012)"
+
+test_begin_subtest "date(1) --rfc-2822 format"
+test_expect_equal "$(_parse_time Fri, 03 Aug 2012 23:07:46 +0100)" "$(_date Fri, 03 Aug 2012 23:07:46 +0100)"
+
+test_begin_subtest "date(1) --rfc=3339=seconds format"
+test_expect_equal "$(_parse_time 2012-08-03 23:09:37+03:00)" "$(_date 2012-08-03 23:09:37+03:00)"
+
+test_begin_subtest "Date parser tests"
+REFERENCE=$(_date Tue Jan 11 12:13:14 +0000 2011)
+cat <<EOF > INPUT
+now          ==> Tue Jan 11 12:13:14 +0000 2011
+2010-1-1     ==> ERROR: DATEFORMAT
+Jan 2        ==> Sun Jan 02 12:13:14 +0000 2011
+Mon          ==> Mon Jan 10 12:13:14 +0000 2011
+last Friday  ==> ERROR: FORMAT
+2 hours ago  ==> Tue Jan 11 10:13:14 +0000 2011
+last month   ==> Sat Dec 11 12:13:14 +0000 2010
+month ago    ==> Sat Dec 11 12:13:14 +0000 2010
+two mo       ==> Thu Nov 11 12:13:14 +0000 2010
+3M           ==> Mon Oct 11 12:13:14 +0000 2010
+4-mont       ==> Sat Sep 11 12:13:14 +0000 2010
+5m           ==> Tue Jan 11 12:08:14 +0000 2011
+dozen mi     ==> Tue Jan 11 12:01:14 +0000 2011
+8am          ==> Tue Jan 11 08:00:00 +0000 2011
+monday       ==> Mon Jan 10 12:13:14 +0000 2011
+yesterday    ==> Mon Jan 10 12:13:14 +0000 2011
+tomorrow     ==> ERROR: KEYWORD
+             ==> Tue Jan 11 12:13:14 +0000 2011 # empty string is reference time
+
+Aug 3 23:06:06 2012             ==> Fri Aug 03 23:06:06 +0000 2012 # date(1) default format without TZ code
+Fri, 03 Aug 2012 23:07:46 +0100 ==> Fri Aug 03 22:07:46 +0000 2012 # rfc-2822
+2012-08-03 23:09:37+03:00       ==> Fri Aug 03 20:09:37 +0000 2012 # rfc-3339 seconds
+
+10:30:40     ==> Tue Jan 11 10:30:40 +0000 2011
+10:30:40     ==^> Tue Jan 11 10:30:40 +0000 2011
+10:30:40     ==^^> Tue Jan 11 10:30:40 +0000 2011
+10:30:40     ==_> Tue Jan 11 10:30:40 +0000 2011
+
+10s           ==> Tue Jan 11 12:13:04 +0000 2011
+19701223s     ==> Fri May 28 11:39:31 +0000 2010
+19701223      ==> Wed Dec 23 12:13:14 +0000 1970
+
+19701223 +0100 ==> Wed Dec 23 12:13:14 +0000 1970 # Timezone is ignored without an error
+
+today ==^^> Wed Jan 12 00:00:00 +0000 2011
+today ==^> Tue Jan 11 23:59:59 +0000 2011
+today ==_> Tue Jan 11 00:00:00 +0000 2011
+
+this week ==^^> Sun Jan 16 00:00:00 +0000 2011
+this week ==^> Sat Jan 15 23:59:59 +0000 2011
+this week ==_> Sun Jan 09 00:00:00 +0000 2011
+
+two months ago ==> Thu Nov 11 12:13:14 +0000 2010
+two months ==> Thu Nov 11 12:13:14 +0000 2010
+
+@1348569850 ==> Tue Sep 25 10:44:10 +0000 2012
+@10 ==> Thu Jan 01 00:00:10 +0000 1970
+EOF
+
+${TEST_DIRECTORY}/parse-time --ref=${REFERENCE} < INPUT > OUTPUT
+test_expect_equal_file INPUT OUTPUT
+
+test_begin_subtest "Second rounding tests"
+REFERENCE=$(_date Tue Jan 11 12:13:14 +0000 2011)
+cat <<EOF > INPUT
+9:15         ==> Tue Jan 11 09:15:14 +0000 2011
+12:34        ==> Tue Jan 11 12:34:14 +0000 2011
+10:30        ==> Tue Jan 11 10:30:14 +0000 2011
+10:30        ==^> Tue Jan 11 10:30:59 +0000 2011
+10:30        ==^^> Tue Jan 11 10:31:00 +0000 2011
+10:30        ==_> Tue Jan 11 10:30:00 +0000 2011
+EOF
+${TEST_DIRECTORY}/parse-time --ref=${REFERENCE} < INPUT > OUTPUT
+test_expect_equal_file INPUT OUTPUT
+
+test_done
diff --git a/test/T500-search-date.sh b/test/T500-search-date.sh
new file mode 100755 (executable)
index 0000000..85ff831
--- /dev/null
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+test_description="date:since..until queries"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "Absolute date range"
+output=$(notmuch search date:2010-12-16..12/16/2010 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2010-12-16 [1/1] Olivier Berger; Essai accentué (inbox unread)"
+
+test_begin_subtest "Absolute date range with 'same' operator"
+output=$(notmuch search date:2010-12-16..! | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2010-12-16 [1/1] Olivier Berger; Essai accentué (inbox unread)"
+
+test_begin_subtest "Absolute date field"
+output=$(notmuch search date:2010-12-16 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2010-12-16 [1/1] Olivier Berger; Essai accentué (inbox unread)"
+
+test_begin_subtest "Absolute time range with TZ"
+notmuch search date:18-Nov-2009_02:19:26-0800..2009-11-18_04:49:52-06:00 | 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)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Jan Janak; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+thread:XXX   2009-11-18 [1/3(4)] Carl Worth| Aron Griffis, Keith Packard; [notmuch] archive (inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Keith Packard; [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T510-thread-replies.sh b/test/T510-thread-replies.sh
new file mode 100755 (executable)
index 0000000..35f3ff8
--- /dev/null
@@ -0,0 +1,237 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2013 Aaron Ecay
+#
+
+test_description='test of proper handling of in-reply-to and references headers'
+
+# This test makes sure that the thread structure in the notmuch
+# database is constructed properly, even in the presence of
+# 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"' \
+    '[subject]=one'
+add_message '[in-reply-to]="mumble"' \
+    '[references]="<foo@one.com>"' \
+    '[subject]="Re: one"'
+output=$(notmuch show --format=json 'subject:one' | notmuch_json_show_sanitize)
+expected='[[[{"id": "foo@one.com",
+ "crypto": {},
+ "match": true,
+ "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 978709437,
+ "date_relative": "2001-01-05",
+ "tags": ["inbox", "unread"],
+ "headers": {"Subject": "one",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "Fri, 05 Jan 2001 15:43:57 +0000"},
+ "body": [{"id": 1,
+ "content-type": "text/plain",
+ "content": "This is just a test message (#1)\n"}]},
+ [[{"id": "msg-002@notmuch-test-suite",
+ "crypto": {},
+ "match": true, "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 978709437, "date_relative": "2001-01-05",
+ "tags": ["inbox", "unread"], "headers": {"Subject": "Re: one",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "Fri, 05 Jan 2001 15:43:57 +0000"},
+ "body": [{"id": 1, "content-type": "text/plain",
+ "content": "This is just a test message (#2)\n"}]}, []]]]]]'
+expected=`echo "$expected" | notmuch_json_show_sanitize`
+test_expect_equal_json "$output" "$expected"
+
+test_begin_subtest "Prefer References to dodgy In-Reply-To"
+add_message '[id]="foo@two.com"' \
+    '[subject]=two'
+add_message '[in-reply-to]="Your message of December 31 1999 <bar@baz.com>"' \
+    '[references]="<foo@two.com>"' \
+    '[subject]="Re: two"'
+output=$(notmuch show --format=json 'subject:two' | notmuch_json_show_sanitize)
+expected='[[[{"id": "foo@two.com",
+ "crypto": {},
+ "match": true, "excluded": false,
+ "filename": ["YYYYY"],
+ "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"],
+ "headers": {"Subject": "two",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "Fri, 05 Jan 2001 15:43:57 +0000"},
+ "body": [{"id": 1, "content-type": "text/plain",
+ "content": "This is just a test message (#3)\n"}]},
+ [[{"id": "msg-004@notmuch-test-suite", "match": true, "excluded": false,
+ "crypto": {},
+ "filename": ["YYYYY"],
+ "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"],
+ "headers": {"Subject": "Re: two",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "Fri, 05 Jan 2001 15:43:57 +0000"},
+ "body": [{"id": 1,
+ "content-type": "text/plain", "content": "This is just a test message (#4)\n"}]},
+ []]]]]]'
+expected=`echo "$expected" | notmuch_json_show_sanitize`
+test_expect_equal_json "$output" "$expected"
+
+test_begin_subtest "Use In-Reply-To when no References"
+add_message '[id]="foo@three.com"' \
+    '[subject]="three"'
+add_message '[in-reply-to]="<foo@three.com>"' \
+    '[subject]="Re: three"'
+output=$(notmuch show --format=json 'subject:three' | notmuch_json_show_sanitize)
+expected='[[[{"id": "foo@three.com", "match": true, "excluded": false,
+ "crypto": {},
+ "filename": ["YYYYY"],
+ "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"],
+ "headers": {"Subject": "three",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [{"id": 1,
+ "content-type": "text/plain", "content": "This is just a test message (#5)\n"}]},
+ [[{"id": "msg-006@notmuch-test-suite", "match": true, "excluded": false,
+ "crypto": {},
+ "filename": ["YYYYY"],
+ "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"],
+ "headers": {"Subject": "Re: three",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [{"id": 1,
+ "content-type": "text/plain", "content": "This is just a test message (#6)\n"}]},
+ []]]]]]'
+expected=`echo "$expected" | notmuch_json_show_sanitize`
+test_expect_equal_json "$output" "$expected"
+
+test_begin_subtest "Use last Reference when In-Reply-To is dodgy"
+add_message '[id]="foo@four.com"' \
+    '[subject]="four"'
+add_message '[id]="bar@four.com"' \
+    '[subject]="not-four"'
+add_message '[in-reply-to]="<baz@four.com> (RFC822 4lyfe)"' \
+    '[references]="<baz@four.com> <foo@four.com>"' \
+    '[subject]="neither"'
+output=$(notmuch show --format=json 'subject:four' | notmuch_json_show_sanitize)
+expected='[[[{"id": "foo@four.com", "match": true, "excluded": false,
+ "crypto": {},
+ "filename": ["YYYYY"],
+ "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"],
+ "headers": {"Subject": "four",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [{"id": 1,
+ "content-type": "text/plain", "content": "This is just a test message (#7)\n"}]},
+ [[{"id": "msg-009@notmuch-test-suite", "match": false, "excluded": false,
+ "crypto": {},
+ "filename": ["YYYYY"],
+ "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"],
+ "headers": {"Subject": "neither",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [{"id": 1,
+ "content-type": "text/plain", "content": "This is just a test message (#9)\n"}]},
+ []]]]], [[{"id": "bar@four.com", "match": true, "excluded": false,
+ "crypto": {},
+ "filename": ["YYYYY"],
+ "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["inbox", "unread"],
+ "headers": {"Subject": "not-four",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [{"id": 1,
+ "content-type": "text/plain", "content": "This is just a test message (#8)\n"}]}, []]]]'
+expected=`echo "$expected" | notmuch_json_show_sanitize`
+test_expect_equal_json "$output" "$expected"
+
+test_begin_subtest "Ignore garbage at the end of References"
+add_message '[id]="foo@five.com"' \
+    '[subject]="five"'
+add_message '[id]="bar@five.com"' \
+    '[references]="<foo@five.com> (garbage)"' \
+    '[subject]="not-five"'
+output=$(notmuch show --format=json 'subject:five' | notmuch_json_show_sanitize)
+expected='[[[{"id": "XXXXX", "match": true, "excluded": false,
+ "crypto": {},
+ "filename": ["YYYYY"], "timestamp": 42, "date_relative": "2001-01-05",
+ "tags": ["inbox", "unread"], "headers": {"Subject": "five",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "GENERATED_DATE"}, "body": [{"id": 1,
+ "content-type": "text/plain",
+ "content": "This is just a test message (#10)\n"}]},
+ [[{"id": "XXXXX", "match": true, "excluded": false,
+ "crypto": {},
+ "filename": ["YYYYY"], "timestamp": 42, "date_relative": "2001-01-05",
+ "tags": ["inbox", "unread"],
+ "headers": {"Subject": "not-five",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "Date": "GENERATED_DATE"},
+ "body": [{"id": 1, "content-type": "text/plain",
+ "content": "This is just a test message (#11)\n"}]}, []]]]]]'
+expected=`echo "$expected" | notmuch_json_show_sanitize`
+test_expect_equal_json "$output" "$expected"
+
+add_email_corpus threading
+
+test_begin_subtest "reply to ghost"
+notmuch show --entire-thread=true id:000-real-root@example.org | grep ^Subject: | head -1 > OUTPUT
+cat <<EOF > EXPECTED
+Subject: root message
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "reply to ghost (tree view)"
+test_emacs '(notmuch-tree "id:000-real-root@example.org")
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+cat <<EOF > EXPECTED
+  2016-06-17  Alice                 ┬►root message                                        (inbox unread)
+  2016-06-18  Alice                 ╰┬►child message                                      (inbox unread)
+  2016-06-17  Mallory                ├─►fake root message                                 (inbox unread)
+  2016-06-18  Alice                  ├┬►grand-child message                               (inbox unread)
+  2016-06-18  Alice                  │╰─►great grand-child message                        (inbox unread)
+  2016-06-18  Daniel                 ╰─►grand-child message 2                             (inbox unread)
+End of search results.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "reply to ghost (RT)"
+notmuch show --entire-thread=true id:87bmc6lp3h.fsf@len.workgroup | grep ^Subject: | head -1  > OUTPUT
+cat <<EOF > EXPECTED
+Subject: FYI: xxxx  xxxxxxx  xxxxxxxxxxxx xxx
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "reply to ghost (RT/tree view)"
+test_emacs '(notmuch-tree "id:87bmc6lp3h.fsf@len.workgroup")
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+cat <<EOF > EXPECTED
+  2016-06-19  Gregor Zattler       ┬┬►FYI: xxxx  xxxxxxx  xxxxxxxxxxxx xxx                (inbox unread)
+  2016-06-19   via RT              │╰─►[support.xxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de #33575] AutoReply: FYI: xxxx  xxxxxxx  xxxxxxxxxxxx xxx (inbox unread)
+  2016-06-26   via RT              ╰─►[support.xxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de #33575] Resolved: FYI: xxxx  xxxxxxx  xxxxxxxxxxxx xxx (inbox unread)
+End of search results.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "trusting reply-to (tree view)"
+test_emacs '(notmuch-tree "id:B00-root@example.org")
+           (notmuch-test-wait)
+           (test-output)
+           (delete-other-windows)'
+cat <<EOF > EXPECTED
+  2016-06-17  Alice                 ┬►root message                                        (inbox unread)
+  2016-06-18  Alice                 ╰┬►child message                                      (inbox unread)
+  2016-06-18  Alice                  ╰─►grand-child message                               (inbox unread)
+End of search results.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T520-show.sh b/test/T520-show.sh
new file mode 100755 (executable)
index 0000000..6bcf109
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+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"
+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
diff --git a/test/T530-upgrade.sh b/test/T530-upgrade.sh
new file mode 100755 (executable)
index 0000000..5f0de2e
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+test_description='database upgrades'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_require_external_prereq xapian-metadata
+
+XAPIAN_PATH=$MAIL_DIR/.notmuch/xapian
+BACKUP_PATH=$MAIL_DIR/.notmuch/backups
+
+delete_feature () {
+    local key=$1
+    features=$(xapian-metadata get $XAPIAN_PATH features | grep -v "^$key")
+    xapian-metadata set $XAPIAN_PATH features "$features"
+}
+
+add_email_corpus
+
+for key in 'multiple paths per message' \
+              'relative directory paths' \
+              'exact folder:/path: search' \
+              'mail documents for missing messages' \
+              'modification tracking'; do
+    backup_database
+    test_begin_subtest "upgrade is triggered by missing '$key'"
+    delete_feature "$key"
+    output=$(notmuch new | grep Welcome)
+    test_expect_equal \
+       "$output" \
+       "Welcome to a new version of notmuch! Your database will now be upgraded."
+
+    restore_database
+
+    backup_database
+    test_begin_subtest "backup can be restored ['$key']"
+    notmuch dump > BEFORE
+    delete_feature "$key"
+    notmuch new
+    notmuch tag -inbox '*'
+    dump_file=$(echo ${BACKUP_PATH}/dump*)
+    notmuch restore --input=$dump_file
+    notmuch dump > AFTER
+    test_expect_equal_file BEFORE AFTER
+    restore_database
+done
+
+for key in 'from/subject/message-ID in database' \
+              'indexed MIME types' \
+              'index body and headers separately'; do
+    backup_database
+    test_begin_subtest "upgrade not triggered by missing '$key'"
+    delete_feature "$key"
+    output=$(notmuch new | grep Welcome)
+    test_expect_equal "$output" ""
+    restore_database
+done
+
+test_begin_subtest "upgrade with configured backup dir"
+notmuch config set database.backup_dir ${HOME}/backups
+delete_feature 'modification tracking'
+notmuch new | grep Backing | notmuch_dir_sanitize | sed 's/dump-[0-9T]*/dump-XXX/' > OUTPUT
+cat <<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
diff --git a/test/T550-db-features.sh b/test/T550-db-features.sh
new file mode 100755 (executable)
index 0000000..3048c7c
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+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/')
+rm -rf ${MAIL_DIR}/.notmuch
+test_expect_equal "$output" "\
+Error: Notmuch database at FILENAME
+       has a newer database format version (9999) than supported by this
+       version of notmuch (3)."
+
+test_begin_subtest "unknown 'rw' feature aborts read/write open"
+${TEST_DIRECTORY}/make-db-version ${MAIL_DIR} 3 $'test feature\trw'
+output=$(notmuch new 2>&1 | sed 's/\(database at\) .*/\1 FILENAME/')
+rm -rf ${MAIL_DIR}/.notmuch
+test_expect_equal "$output" "\
+Error: Notmuch database at FILENAME
+       requires features (test feature)
+       not supported by this version of notmuch."
+
+test_begin_subtest "unknown 'rw' feature aborts read-only open"
+${TEST_DIRECTORY}/make-db-version ${MAIL_DIR} 3 $'test feature\trw'
+output=$(notmuch search x 2>&1 | sed 's/\(database at\) .*/\1 FILENAME/')
+rm -rf ${MAIL_DIR}/.notmuch
+test_expect_equal "$output" "\
+Error: Notmuch database at FILENAME
+       requires features (test feature)
+       not supported by this version of notmuch."
+
+test_begin_subtest "unknown 'w' feature aborts read/write open"
+${TEST_DIRECTORY}/make-db-version ${MAIL_DIR} 3 $'test feature\tw'
+output=$(notmuch new 2>&1 | sed 's/\(database at\) .*/\1 FILENAME/')
+rm -rf ${MAIL_DIR}/.notmuch
+test_expect_equal "$output" "\
+Error: Notmuch database at FILENAME
+       requires features (test feature)
+       not supported by this version of notmuch."
+
+test_begin_subtest "unknown 'w' feature does not abort read-only open"
+${TEST_DIRECTORY}/make-db-version ${MAIL_DIR} 3 $'test feature\tw'
+output=$(notmuch search x 2>&1 | sed 's/\(database at\) .*/\1 FILENAME/')
+rm -rf ${MAIL_DIR}/.notmuch
+test_expect_equal "$output" ""
+
+test_done
diff --git a/test/T560-lib-error.sh b/test/T560-lib-error.sh
new file mode 100755 (executable)
index 0000000..78cae1c
--- /dev/null
@@ -0,0 +1,334 @@
+#!/usr/bin/env bash
+test_description="error reporting for library"
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "building database"
+test_expect_success "NOTMUCH_NEW"
+
+test_begin_subtest "Open null pointer"
+test_C <<'EOF'
+#include <stdio.h>
+#include <notmuch.h>
+int main (int argc, char** argv)
+{
+    notmuch_database_t *db;
+    notmuch_status_t stat;
+    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: could not locate database.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Open relative path"
+test_C <<'EOF'
+#include <stdio.h>
+#include <notmuch.h>
+int main (int argc, char** argv)
+{
+    notmuch_database_t *db;
+    notmuch_status_t stat;
+    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
+== stdout ==
+== stderr ==
+Error: Database path must be absolute.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Create database in relative path"
+test_C <<'EOF'
+#include <stdio.h>
+#include <notmuch.h>
+int main (int argc, char** argv)
+{
+    notmuch_database_t *db;
+    notmuch_status_t stat;
+    char *msg = NULL;
+
+    stat = notmuch_database_create_with_config ("./nonexistent/foo", "", NULL, &db, &msg);
+    if (msg) fputs (msg, stderr);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+Error: Database path must be absolute.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Open nonexistent database"
+test_C ${PWD}/nonexistent/foo <<'EOF'
+#include <stdio.h>
+#include <notmuch.h>
+int main (int argc, char** argv)
+{
+    notmuch_database_t *db;
+    notmuch_status_t stat;
+    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: database path 'CWD/nonexistent/foo' does not exist or is not a directory.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "create NULL path"
+test_C <<'EOF'
+#include <stdio.h>
+#include <notmuch.h>
+int main (int argc, char** argv)
+{
+    notmuch_status_t stat;
+    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 ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Create database in nonexistent directory"
+test_C ${PWD}/nonexistent/foo<<'EOF'
+#include <stdio.h>
+#include <notmuch.h>
+int main (int argc, char** argv)
+{
+    notmuch_database_t *db;
+    notmuch_status_t stat;
+    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 ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Write to read-only database"
+test_C ${MAIL_DIR} <<'EOF'
+#include <stdio.h>
+#include <notmuch.h>
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   notmuch_status_t stat;
+   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);
+   }
+   stat = notmuch_database_index_file (db, "/dev/null", NULL, NULL);
+   if (stat)
+       fputs (notmuch_database_status_string (db), stderr);
+
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+Cannot write to a read-only database.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Add non-existent file"
+test_C ${MAIL_DIR} <<'EOF'
+#include <stdio.h>
+#include <notmuch.h>
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   notmuch_status_t stat;
+   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);
+   }
+   stat = notmuch_database_index_file (db, "./nonexistent", NULL, NULL);
+   if (stat) {
+       char *status_string = notmuch_database_status_string (db);
+       if (status_string) fputs (status_string, stderr);
+   }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+Error opening ./nonexistent: No such file or directory
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "compact, overwriting existing backup"
+test_C ${MAIL_DIR} <<'EOF'
+#include <stdio.h>
+#include <notmuch.h>
+static void
+status_cb (const char *msg, void *closure)
+{
+    printf ("%s\n", msg);
+}
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   notmuch_status_t stat;
+   stat = notmuch_database_compact (argv[1], argv[1], status_cb, NULL);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+Path already exists: MAIL_DIR
+
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+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: %d %s\n", stat, msg ? msg : "");
+     exit (1);
+   }
+   fd = open(argv[2],O_WRONLY|O_TRUNC);
+   if (fd < 0) {
+       fprintf (stderr, "error opening %s\n", argv[1]);
+       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 finding message"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH}
+   {
+       notmuch_message_t *message = NULL;
+       stat = notmuch_database_find_message (db, "id:nonexistent", &message);
+   }
+EOF
+sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+A Xapian exception occurred finding message
+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}
+   {
+       notmuch_directory_t *directory = NULL;
+       stat = notmuch_database_get_directory (db, "none/existing", &directory);
+   }
+EOF
+sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+A Xapian exception occurred finding/creating a directory
+EOF
+test_expect_equal_file EXPECTED OUTPUT.clean
+restore_database
+
+backup_database
+test_begin_subtest "Xapian exception searching messages"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH}
+   {
+       notmuch_messages_t *messages = NULL;
+       notmuch_query_t *query=notmuch_query_create (db, "*");
+       stat = notmuch_query_search_messages (query, &messages);
+   }
+EOF
+sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+A Xapian exception occurred performing query
+Query string was: *
+EOF
+test_expect_equal_file EXPECTED OUTPUT.clean
+restore_database
+
+backup_database
+test_begin_subtest "Xapian exception counting messages"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${POSTLIST_PATH}
+   {
+       int count;
+       notmuch_query_t *query=notmuch_query_create (db, "id:87ocn0qh6d.fsf@yoom.home.cworth.org");
+       stat = notmuch_query_count_messages (query, &count);
+   }
+EOF
+sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+A Xapian exception occurred performing query
+Query string was: id:87ocn0qh6d.fsf@yoom.home.cworth.org
+EOF
+test_expect_equal_file EXPECTED OUTPUT.clean
+restore_database
+
+test_done
diff --git a/test/T562-lib-database.sh b/test/T562-lib-database.sh
new file mode 100755 (executable)
index 0000000..fedfc9e
--- /dev/null
@@ -0,0 +1,427 @@
+#!/usr/bin/env bash
+test_description="notmuch_database_* API"
+
+. $(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 <notmuch-test.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_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);
+   }
+EOF
+
+cat <<'EOF' > c_tail
+   if (stat) {
+       const char *stat_str = notmuch_database_status_string (db);
+       if (stat_str)
+           fputs (stat_str, stderr);
+    }
+
+}
+EOF
+
+test_begin_subtest "get status_string with closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        const char *str;
+        EXPECT0(notmuch_database_close (db));
+        str = notmuch_database_status_string (db);
+        printf("%d\n", str == NULL);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get path with closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        const char *path;
+        EXPECT0(notmuch_database_close (db));
+        path = notmuch_database_get_path (db);
+        printf("%s\n", path);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+MAIL_DIR
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get version with closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        unsigned int version;
+        EXPECT0(notmuch_database_close (db));
+        version = notmuch_database_get_version (db);
+        printf ("%u\n", version);
+        stat = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+0
+== stderr ==
+A Xapian exception occurred at database.cc:XXX: Database has been closed
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "re-close a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_database_close (db);
+        printf ("%d\n", stat);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+0
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "destroy a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        unsigned int version;
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_database_destroy (db);
+        printf ("%d\n", stat);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+0
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "destroy an open db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        unsigned int version;
+        stat = notmuch_database_destroy (db);
+        printf ("%d\n", stat);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+0
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "check a closed db for upgrade"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_bool_t ret;
+
+        EXPECT0(notmuch_database_close (db));
+        ret = notmuch_database_needs_upgrade (db);
+        printf ("%d\n", ret == FALSE);
+        stat = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred at database.cc:XXX: Database has been closed
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "upgrade a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_database_upgrade (db, NULL, NULL);
+        printf ("%d\n", stat == NOTMUCH_STATUS_SUCCESS);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "begin atomic section for a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_database_begin_atomic (db);
+        printf ("%d\n", stat == NOTMUCH_STATUS_SUCCESS ||
+                        stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+        stat = NOTMUCH_STATUS_SUCCESS;
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "end atomic section for a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        EXPECT0(notmuch_database_close (db));
+        EXPECT0(notmuch_database_begin_atomic (db));
+        stat = notmuch_database_end_atomic (db);
+        printf ("%d\n", stat == NOTMUCH_STATUS_SUCCESS ||
+                        stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+        stat = NOTMUCH_STATUS_SUCCESS;
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get revision for a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        const char *uuid;
+        unsigned long rev;
+
+        EXPECT0(notmuch_database_close (db));
+        rev = notmuch_database_get_revision (db, &uuid);
+        printf ("%d\n", rev, uuid);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+53
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get directory for a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_directory_t *dir;
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_database_get_directory (db, "/nonexistent", &dir);
+        printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred finding/creating a directory: Database has been closed.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "index file with a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_message_t *msg;
+        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_CLOSED_DATABASE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+Cannot write to a closed database.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+generate_message '[filename]=relative_path'
+test_begin_subtest "index file (relative path)"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_message_t *msg;
+        stat = notmuch_database_index_file (db, "relative_path", NULL, &msg);
+        printf ("%d\n", stat == NOTMUCH_STATUS_SUCCESS);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "index file (absolute path outside mail root)"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_message_t *msg;
+        stat = notmuch_database_index_file (db, "/dev/zero", NULL, &msg);
+        printf ("%d\n", stat == NOTMUCH_STATUS_FILE_ERROR);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+Error opening /dev/zero: path outside mail root
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "remove message file with a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_database_remove_message (db, "01:2,");
+        printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred finding/creating a directory: Database has been closed.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "find message by filename with a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_message_t *msg;
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_database_find_message_by_filename (db, "01:2,", &msg);
+        printf ("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred finding/creating a directory: Database has been closed.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle getting tags from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_tags_t *result;
+        EXPECT0(notmuch_database_close (db));
+        result = notmuch_database_get_all_tags (db);
+        printf("%d\n", result == NULL);
+        stat = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred getting tags: Database has been closed.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get config from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        char *result;
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_database_get_config (db, "foo", &result);
+        printf("%d\n", stat == NOTMUCH_STATUS_SUCCESS);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "set config in closed database"
+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_CLOSED_DATABASE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+Cannot write to a closed database.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get indexopts from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_indexopts_t *result;
+        EXPECT0(notmuch_database_close (db));
+        result = notmuch_database_get_default_indexopts (db);
+        printf("%d\n", result != NULL);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get decryption policy from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_indexopts_t *result;
+        result = notmuch_database_get_default_indexopts (db);
+        EXPECT0(notmuch_database_close (db));
+        notmuch_decryption_policy_t policy = notmuch_indexopts_get_decrypt_policy (result);
+        printf ("%d\n",  policy == NOTMUCH_DECRYPT_AUTO);
+        notmuch_indexopts_destroy (result);
+        printf ("SUCCESS\n");
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+SUCCESS
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "set decryption policy with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_indexopts_t *result;
+        result = notmuch_database_get_default_indexopts (db);
+        EXPECT0(notmuch_database_close (db));
+        notmuch_decryption_policy_t policy = notmuch_indexopts_get_decrypt_policy (result);
+        stat = notmuch_indexopts_set_decrypt_policy (result, policy);
+        printf("%d\n%d\n", policy == NOTMUCH_DECRYPT_AUTO, stat == NOTMUCH_STATUS_SUCCESS);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T563-lib-directory.sh b/test/T563-lib-directory.sh
new file mode 100755 (executable)
index 0000000..4711fcd
--- /dev/null
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+test_description="notmuch_directory_* API"
+
+. $(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 <notmuch-test.h>
+
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   notmuch_directory_t *dir;
+   notmuch_status_t stat = NOTMUCH_STATUS_SUCCESS;
+   char *msg = NULL;
+
+   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);
+   }
+
+   EXPECT0(notmuch_database_get_directory (db, "bar", &dir));
+   EXPECT0(notmuch_database_close (db));
+EOF
+
+cat <<'EOF' > c_tail
+   if (stat) {
+       const char *stat_str = notmuch_database_status_string (db);
+       if (stat_str)
+           fputs (stat_str, stderr);
+    }
+
+}
+EOF
+
+test_begin_subtest "get child directories for a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_filenames_t *children;
+        children = notmuch_directory_get_child_directories (dir);
+        printf ("%d\n", children == NULL);
+        stat = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred at directory.cc:XXX: Database has been closed
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get child filenames for a closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_filenames_t *children;
+        children = notmuch_directory_get_child_files (dir);
+        printf ("%d\n", children == NULL);
+        stat = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred at directory.cc:XXX: Database has been closed
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+backup_database
+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_CLOSED_DATABASE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+Cannot write to a closed database.
+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_CLOSED_DATABASE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+Cannot write to a closed database.
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+test_done
diff --git a/test/T564-lib-query.sh b/test/T564-lib-query.sh
new file mode 100755 (executable)
index 0000000..53a63bf
--- /dev/null
@@ -0,0 +1,254 @@
+#!/usr/bin/env bash
+test_description="notmuch_query_* API"
+
+. $(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 <notmuch-test.h>
+
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   notmuch_status_t stat;
+   char *msg = NULL;
+
+   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);
+   }
+EOF
+
+cat <<'EOF' > c_tail
+   if (stat) {
+       const char *stat_str = notmuch_database_status_string (db);
+       if (stat_str)
+           fputs (stat_str, stderr);
+    }
+
+}
+EOF
+
+test_begin_subtest "roundtrip query string with closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_query_t *query;
+        const char *str = "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+        const char *ret;
+
+        EXPECT0(notmuch_database_close (db));
+        query = notmuch_query_create (db, str);
+        ret = notmuch_query_get_query_string (query);
+
+        printf("%s\n%s\n", str, ret);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net
+id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "retrieve closed db from query"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_query_t *query;
+        const char *str = "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+        notmuch_database_t *db2;
+
+        query = notmuch_query_create (db, str);
+        EXPECT0(notmuch_database_close (db));
+        db2 = notmuch_query_get_database (query);
+
+        printf("%d\n", db == db2);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "set omit_excluded on closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_query_t *query;
+        const char *str = "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+
+        query = notmuch_query_create (db, str);
+        EXPECT0(notmuch_database_close (db));
+        notmuch_query_set_omit_excluded (query, NOTMUCH_EXCLUDE_ALL);
+
+        printf("SUCCESS\n");
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+SUCCESS
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "roundtrip sort on closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_query_t *query;
+        const char *str = "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+        notmuch_sort_t sort;
+
+        query = notmuch_query_create (db, str);
+        EXPECT0(notmuch_database_close (db));
+        notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);
+        sort = notmuch_query_get_sort (query);
+        printf("%d\n", sort == NOTMUCH_SORT_UNSORTED);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "add tag_exclude on closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_query_t *query;
+        const char *str = "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+
+        query = notmuch_query_create (db, str);
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_query_add_tag_exclude (query, "spam");
+        printf("%d\n", stat == NOTMUCH_STATUS_SUCCESS);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search threads on closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_query_t *query;
+        const char *str = "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+        notmuch_threads_t *threads;
+
+        query = notmuch_query_create (db, str);
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_query_search_threads (query, &threads);
+
+        printf("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred performing query: Database has been closed
+Query string was: id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search messages on closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_query_t *query;
+        const char *str = "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+        notmuch_messages_t *messages;
+
+        query = notmuch_query_create (db, str);
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_query_search_messages (query, &messages);
+
+        printf("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred performing query: Database has been closed
+Query string was: id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "count messages on closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_query_t *query;
+        const char *str = "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+        unsigned int count;
+
+        query = notmuch_query_create (db, str);
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_query_count_messages (query, &count);
+
+        printf("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred performing query: Database has been closed
+Query string was: id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "count threads on closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_query_t *query;
+        const char *str = "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+        unsigned int count;
+
+        query = notmuch_query_create (db, str);
+        EXPECT0(notmuch_database_close (db));
+        stat = notmuch_query_count_threads (query, &count);
+
+        printf("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+== stderr ==
+A Xapian exception occurred performing query: Database has been closed
+Query string was: id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "destroy query with closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_query_t *query;
+        const char *str = "id:1258471718-6781-1-git-send-email-dottedmag@dottedmag.net";
+
+        query = notmuch_query_create (db, str);
+        EXPECT0(notmuch_database_close (db));
+        notmuch_query_destroy (query);
+
+        printf("SUCCESS\n");
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+SUCCESS
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
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
diff --git a/test/T566-lib-message.sh b/test/T566-lib-message.sh
new file mode 100755 (executable)
index 0000000..6905193
--- /dev/null
@@ -0,0 +1,550 @@
+#!/usr/bin/env bash
+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"
+test_expect_success "NOTMUCH_NEW"
+
+cat <<'EOF' > c_tail
+   if (stat) {
+       const char *stat_str = notmuch_database_status_string (db);
+       if (stat_str)
+           fputs (stat_str, stderr);
+    }
+
+}
+EOF
+
+cat <<EOF > c_head0
+#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 = "87pr7gqidx.fsf@yoom.home.cworth.org";
+
+   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);
+   }
+   EXPECT0(notmuch_database_find_message (db, id, &message));
+EOF
+
+cp c_head0 c_head
+echo "   EXPECT0(notmuch_database_close (db));" >> c_head
+
+test_begin_subtest "Handle getting message-id from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        const char *id2;
+        id2=notmuch_message_get_message_id (message);
+        printf("%d\n%d\n", message != NULL, id2==NULL);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle getting thread-id from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        const char *id2;
+        id2=notmuch_message_get_thread_id (message);
+        printf("%d\n%d\n", message != NULL, id2==NULL);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle getting header from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        const char *from;
+        from=notmuch_message_get_header (message, "from");
+        printf("%s\n%d\n", id, from == NULL);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+87pr7gqidx.fsf@yoom.home.cworth.org
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+# XXX this test only tests the trivial code path
+test_begin_subtest "Handle getting replies from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_messages_t *replies;
+        replies = notmuch_message_get_replies (message);
+        printf("%d\n%d\n", message != NULL, replies==NULL);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle getting message filename from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        const char *filename;
+        filename = notmuch_message_get_filename (message);
+        printf("%d\n%d\n", message != NULL, filename == NULL);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle getting all message filenames from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_filenames_t *filenames;
+        filenames = notmuch_message_get_filenames (message);
+        printf("%d\n%d\n", message != NULL, filenames == NULL);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "iterate over all message filenames from closed database"
+cat c_head0 - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_filenames_t *filenames;
+        filenames = notmuch_message_get_filenames (message);
+        EXPECT0(notmuch_database_close (db));
+        for (; notmuch_filenames_valid (filenames);
+               notmuch_filenames_move_to_next (filenames)) {
+            const char *filename = notmuch_filenames_get (filenames);
+            printf("%s\n", filename);
+        }
+        notmuch_filenames_destroy (filenames);
+        printf("SUCCESS\n");
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+MAIL_DIR/cur/40:2,
+SUCCESS
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle getting ghost flag from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_bool_t result;
+        result = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_GHOST);
+        printf("%d\n%d\n", message != NULL, result == FALSE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle getting date from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        time_t result;
+        result = notmuch_message_get_date (message);
+        printf("%d\n%d\n", message != NULL, result == 0);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle getting tags from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_tags_t *result;
+        result = notmuch_message_get_tags (message);
+        printf("%d\n%d\n", message != NULL, result == NULL);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle counting files from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        int result;
+        result = notmuch_message_count_files (message);
+        printf("%d\n%d\n", message != NULL, result < 0);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle adding tag with closed database"
+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_CLOSED_DATABASE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle removing tag with closed database"
+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_CLOSED_DATABASE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle read maildir flag with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_bool_t is_set = -1;
+        is_set = notmuch_message_has_maildir_flag (message, 'S');
+        printf("%d\n%d\n", message != NULL, is_set == FALSE || is_set == TRUE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle checking maildir flag with closed db (new API)"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_status_t status;
+        notmuch_bool_t out;
+        status = notmuch_message_has_maildir_flag_st (message, 'S', &out);
+        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle converting maildir flags to tags with closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_status_t status;
+        status = notmuch_message_maildir_flags_to_tags (message);
+        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+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_CLOSED_DATABASE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle freezing message with closed db"
+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_CLOSED_DATABASE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle thawing message with closed db"
+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_CLOSED_DATABASE);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle destroying message with closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_message_destroy (message);
+        printf("%d\n%d\n", message != NULL, 1);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle retrieving closed db from message"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_database_t *db2;
+        db2 = notmuch_message_get_database (message);
+        printf("%d\n%d\n", message != NULL, db == db2);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Handle reindexing message with closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_status_t status;
+        status = notmuch_message_reindex (message, NULL);
+        printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+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
diff --git a/test/T568-lib-thread.sh b/test/T568-lib-thread.sh
new file mode 100755 (executable)
index 0000000..b4c24ca
--- /dev/null
@@ -0,0 +1,341 @@
+#!/usr/bin/env bash
+test_description="API tests for notmuch_thread_*"
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "building database"
+test_expect_success "NOTMUCH_NEW"
+
+test_begin_subtest "finding thread"
+THREAD=$(notmuch search --output=threads id:20091117190054.GU3165@dottiness.seas.harvard.edu)
+count=$(notmuch count $THREAD)
+test_expect_equal "$count" "7"
+
+cat <<'EOF' > c_tail
+   if (stat) {
+       const char *stat_str = notmuch_database_status_string (db);
+       if (stat_str)
+           fputs (stat_str, stderr);
+    }
+
+}
+EOF
+
+cat <<EOF > c_head
+#include <notmuch-test.h>
+
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   notmuch_status_t stat;
+   char *msg = NULL;
+   notmuch_thread_t *thread = NULL;
+   notmuch_threads_t *threads = NULL;
+   notmuch_query_t *query = NULL;
+   const char *id = "${THREAD}";
+
+   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);
+   }
+
+   query = notmuch_query_create (db, id);
+   EXPECT0(notmuch_query_search_threads (query, &threads));
+   thread = notmuch_threads_get (threads);
+   EXPECT0(notmuch_database_close (db));
+EOF
+
+test_begin_subtest "get thread-id from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        const char *id2;
+        id2 = notmuch_thread_get_thread_id (thread);
+        printf("%d\n%s\n", thread != NULL, id2);
+    }
+EOF
+thread_num=${THREAD#thread:}
+cat <<EOF > EXPECTED
+== stdout ==
+1
+${thread_num}
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get total messages with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        int count;
+        count = notmuch_thread_get_total_messages (thread);
+        printf("%d\n%d\n", thread != NULL, count);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+7
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get total files with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        int count;
+        count = notmuch_thread_get_total_files (thread);
+        printf("%d\n%d\n", thread != NULL, count);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+7
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get top level messages with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        notmuch_messages_t *messages;
+        messages = notmuch_thread_get_toplevel_messages (thread);
+        printf("%d\n%d\n", thread != NULL, messages != NULL);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "iterate over level messages with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+      notmuch_messages_t *messages;
+      for (messages = notmuch_thread_get_toplevel_messages (thread);
+           notmuch_messages_valid (messages);
+           notmuch_messages_move_to_next (messages)) {
+        notmuch_message_t *message = notmuch_messages_get (messages);
+        const char *mid = notmuch_message_get_message_id (message);
+        printf("%s\n", mid);
+      }
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+20091117190054.GU3165@dottiness.seas.harvard.edu
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "iterate over level messages with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+      notmuch_messages_t *messages;
+      for (messages = notmuch_thread_get_toplevel_messages (thread);
+           notmuch_messages_valid (messages);
+           notmuch_messages_move_to_next (messages)) {
+        notmuch_message_t *message = notmuch_messages_get (messages);
+        const char *mid = notmuch_message_get_message_id (message);
+        printf("%s\n", mid);
+      }
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+20091117190054.GU3165@dottiness.seas.harvard.edu
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "iterate over replies with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+      notmuch_messages_t *messages = notmuch_thread_get_toplevel_messages (thread);
+      notmuch_message_t *message = notmuch_messages_get (messages);
+      notmuch_messages_t *replies;
+      for (replies = notmuch_message_get_replies (message);
+           notmuch_messages_valid (replies);
+           notmuch_messages_move_to_next (replies)) {
+        notmuch_message_t *message = notmuch_messages_get (replies);
+        const char *mid = notmuch_message_get_message_id (message);
+
+        printf("%s\n", mid);
+      }
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+87iqd9rn3l.fsf@vertex.dottedmag
+87ocn0qh6d.fsf@yoom.home.cworth.org
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "iterate over all messages with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+      notmuch_messages_t *messages;
+      for (messages = notmuch_thread_get_messages (thread);
+           notmuch_messages_valid (messages);
+           notmuch_messages_move_to_next (messages)) {
+        notmuch_message_t *message = notmuch_messages_get (messages);
+        const char *mid = notmuch_message_get_message_id (message);
+        printf("%s\n", mid);
+      }
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+20091117190054.GU3165@dottiness.seas.harvard.edu
+87iqd9rn3l.fsf@vertex.dottedmag
+20091117203301.GV3165@dottiness.seas.harvard.edu
+87fx8can9z.fsf@vertex.dottedmag
+yunaayketfm.fsf@aiko.keithp.com
+20091118005040.GA25380@dottiness.seas.harvard.edu
+87ocn0qh6d.fsf@yoom.home.cworth.org
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get authors from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        const char *authors;
+        authors = notmuch_thread_get_authors (thread);
+        printf("%d\n%s\n", thread != NULL, authors);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "get subject from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        const char *subject;
+        subject = notmuch_thread_get_subject (thread);
+        printf("%d\n%s\n", thread != NULL, subject);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+[notmuch] Working with Maildir storage?
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "oldest date from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        time_t stamp;
+        stamp = notmuch_thread_get_oldest_date (thread);
+        printf("%d\n%d\n", thread != NULL, stamp > 0);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "newest date from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        time_t stamp;
+        stamp = notmuch_thread_get_newest_date (thread);
+        printf("%d\n%d\n", thread != NULL, stamp > 0);
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+1
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "iterate tags from closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+      notmuch_tags_t *tags;
+      const char *tag;
+      for (tags = notmuch_thread_get_tags (thread);
+           notmuch_tags_valid (tags);
+           notmuch_tags_move_to_next (tags))
+        {
+          tag = notmuch_tags_get (tags);
+          printf ("%s\n", tag);
+        }
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+inbox
+signed
+unread
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "collect tags with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+      notmuch_messages_t *messages = notmuch_thread_get_messages (thread);
+
+      notmuch_tags_t *tags = notmuch_messages_collect_tags (messages);
+
+      const char *tag;
+      for (tags = notmuch_thread_get_tags (thread);
+           notmuch_tags_valid (tags);
+           notmuch_tags_move_to_next (tags))
+        {
+          tag = notmuch_tags_get (tags);
+          printf ("%s\n", tag);
+        }
+      notmuch_tags_destroy (tags);
+      notmuch_messages_destroy (messages);
+
+      printf("SUCCESS\n");
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+inbox
+signed
+unread
+SUCCESS
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "destroy thread with closed database"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+    {
+        time_t stamp;
+        notmuch_thread_destroy (thread);
+        printf("SUCCESS\n");
+    }
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+SUCCESS
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T570-revision-tracking.sh b/test/T570-revision-tracking.sh
new file mode 100755 (executable)
index 0000000..bcc97dd
--- /dev/null
@@ -0,0 +1,125 @@
+#!/usr/bin/env bash
+test_description="database revision tracking"
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "notmuch_database_get_revision"
+test_C ${MAIL_DIR} <<'EOF'
+#include <stdio.h>
+#include <string.h>
+#include <notmuch.h>
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   notmuch_status_t stat;
+   unsigned long revision;
+   const char *uuid;
+
+   unsigned long rev;
+
+   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);
+   printf("%s\t%lu\n", uuid, revision);
+}
+EOF
+notmuch_uuid_sanitize < OUTPUT > CLEAN
+cat <<'EOF' >EXPECTED
+== stdout ==
+UUID   53
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED CLEAN
+
+grep '^[0-9a-f]' OUTPUT > INITIAL_OUTPUT
+
+test_begin_subtest "output of count matches test code"
+notmuch count --lastmod '*' | cut -f2-3 > OUTPUT
+test_expect_equal_file INITIAL_OUTPUT OUTPUT
+
+test_begin_subtest "modification count increases"
+before=$(notmuch count --lastmod '*' | cut -f3)
+notmuch tag +a-random-tag-8743632 '*'
+after=$(notmuch count --lastmod '*' | cut -f3)
+result=$(($before < $after))
+test_expect_equal 1 ${result}
+
+notmuch count --lastmod '*' | cut -f2 > UUID
+
+test_begin_subtest "search succeeds with correct uuid"
+test_expect_success "notmuch search --uuid=$(cat UUID) '*'"
+
+test_begin_subtest "uuid works as global option"
+test_expect_success "notmuch --uuid=$(cat UUID) search '*'"
+
+test_begin_subtest "uuid works as global option II"
+test_expect_code 1 "notmuch --uuid=this-is-no-uuid search '*'"
+
+test_begin_subtest "search fails with incorrect uuid"
+test_expect_code 1 "notmuch search --uuid=this-is-no-uuid '*'"
+
+test_begin_subtest "show succeeds with correct uuid"
+test_expect_success "notmuch show --uuid=$(cat UUID) '*'"
+
+test_begin_subtest "show fails with incorrect uuid"
+test_expect_code 1 "notmuch show --uuid=this-is-no-uuid '*'"
+
+test_begin_subtest "tag succeeds with correct uuid"
+test_expect_success "notmuch tag --uuid=$(cat UUID) +test '*'"
+
+test_begin_subtest "tag fails with incorrect uuid"
+test_expect_code 1 "notmuch tag --uuid=this-is-no-uuid '*' +test2"
+
+test_begin_subtest 'lastmod:0.. matches everything'
+total=$(notmuch count '*')
+modtotal=$(notmuch count lastmod:0..)
+test_expect_equal "$total" "$modtotal"
+
+test_begin_subtest 'lastmod:1000000.. matches nothing'
+modtotal=$(notmuch count lastmod:1000000..)
+test_expect_equal 0 "$modtotal"
+
+test_begin_subtest 'exclude one message using lastmod'
+lastmod=$(notmuch count --lastmod '*' | cut -f3)
+total=$(notmuch count '*')
+notmuch tag +4EFC743A.3060609@april.org id:4EFC743A.3060609@april.org
+subtotal=$(notmuch count lastmod:..$lastmod)
+result=$(($subtotal == $total-1))
+test_expect_equal 1 "$result"
+
+if [ "${NOTMUCH_HAVE_SFSEXP-0}" = "1" ]; then
+    test_begin_subtest 'exclude one message using negative lastmod (sexp)'
+    total=$(notmuch count '*')
+    notmuch tag +${RANDOM} id:4EFC743A.3060609@april.org
+    count=$(notmuch count --query=sexp '(lastmod -1 *)')
+    test_expect_equal 1 "$count"
+fi
+
+test_begin_subtest 'exclude one message using negative lastmod'
+total=$(notmuch count '*')
+notmuch tag +${RANDOM} id:4EFC743A.3060609@april.org
+count=$(notmuch count lastmod:-1..)
+test_expect_equal 1 "$count"
+
+test_begin_subtest 'exclude one message using negative lastmod (second param)'
+total=$(notmuch count '*')
+notmuch tag +${RANDOM} id:4EFC743A.3060609@april.org
+count=$(notmuch count lastmod:..-1)
+test_expect_equal 51 "$count"
+
+test_begin_subtest 'negative lastmod (two parameters)'
+notmuch tag +${RANDOM} '*'
+before=$(notmuch count --lastmod '*' | cut -f3)
+notmuch tag +${RANDOM} id:4EFC743A.3060609@april.org
+count=$(notmuch count lastmod:-100..$before)
+test_expect_equal 51 "$count"
+
+test_done
diff --git a/test/T580-thread-search.sh b/test/T580-thread-search.sh
new file mode 100755 (executable)
index 0000000..01aa3ef
--- /dev/null
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2015 David Bremner
+#
+
+test_description='test of searching by thread-id'
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "Every message is found in exactly one thread"
+
+count=0
+success=0
+for id in $(notmuch search --output=messages '*'); do
+    count=$((count +1))
+    matches=$((`notmuch search --output=threads "$id" | wc -l`))
+    if [ "$matches" = 1 ]; then
+       success=$((success + 1))
+    fi
+done
+
+test_expect_equal "$count" "$success"
+
+test_begin_subtest "roundtripping message-ids via thread-ids"
+
+count=0
+success=0
+for id in $(notmuch search --output=messages '*'); do
+    count=$((count +1))
+    thread=$(notmuch search --output=threads "$id")
+    matched=$(notmuch search --output=messages "$thread" | grep "$id")
+    if [ "$matched" = "$id" ]; then
+       success=$((success + 1))
+    fi
+done
+
+test_expect_equal "$count" "$success"
+
+
+test_done
diff --git a/test/T585-thread-subquery.sh b/test/T585-thread-subquery.sh
new file mode 100755 (executable)
index 0000000..71ced14
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2018 David Bremner
+#
+
+test_description='test of searching by using thread subqueries'
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+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 thread:{from:keithp} and thread:{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 thread:{from:keithp} and 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 'thread:"{from:keithp and date:2009}" and thread:{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 "Syntax/quoting error in subquery"
+notmuch search 'thread:{from:keithp and date:2009} and thread:{to:keithp}' 1>OUTPUT 2>&1
+cat<<EOF > EXPECTED
+notmuch search: A Xapian exception occurred
+A Xapian exception occurred parsing query: missing } in '{from:keithp'
+Query string was: thread:{from:keithp and date:2009} and thread:{to:keithp}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T590-libconfig.sh b/test/T590-libconfig.sh
new file mode 100755 (executable)
index 0000000..9326ba3
--- /dev/null
@@ -0,0 +1,1045 @@
+#!/usr/bin/env bash
+test_description="library config API"
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+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 <notmuch-test.h>
+
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   char *val;
+   notmuch_status_t stat;
+   char *msg = NULL;
+
+   for (int i = 1; i < argc; i++)
+      if (strcmp (argv[i], "%NULL%") == 0) argv[i] = NULL;
+
+   stat = notmuch_database_open_with_config (argv[1],
+                                              NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                              argv[2],
+                                              argv[3],
+                                              &db,
+                                              &msg);
+   if (stat != NOTMUCH_STATUS_SUCCESS) {
+     fprintf (stderr, "error opening database\n%s\n%s\n", notmuch_status_to_string (stat), msg ? msg : "");
+     exit (1);
+   }
+EOF
+
+cat <<EOF > c_tail
+   EXPECT0(notmuch_database_destroy(db));
+}
+EOF
+
+test_begin_subtest "notmuch_database_{set,get}_config"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG}
+{
+   EXPECT0(notmuch_database_set_config (db, "test.key1", "testvalue1"));
+   EXPECT0(notmuch_database_set_config (db, "test.key2", "testvalue2"));
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = testvalue1
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_begin_subtest "notmuch_database_get_config_list: empty list"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+   notmuch_config_list_t *list;
+   EXPECT0(notmuch_database_get_config_list (db, "nonexistent", &list));
+   printf("valid = %d\n", notmuch_config_list_valid (list));
+   notmuch_config_list_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+valid = 0
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_database_get_config_list: closed db"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+{
+   notmuch_config_list_t *list;
+   EXPECT0(notmuch_database_close (db));
+   stat = notmuch_database_get_config_list (db, "nonexistent", &list);
+   printf("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_database_get_config_list: all pairs"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+   notmuch_config_list_t *list;
+   EXPECT0(notmuch_database_set_config (db, "zzzafter", "afterval"));
+   EXPECT0(notmuch_database_set_config (db, "aaabefore", "beforeval"));
+   EXPECT0(notmuch_database_get_config_list (db, "", &list));
+   for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+      printf("%s %s\n", notmuch_config_list_key (list), notmuch_config_list_value(list));
+   }
+   notmuch_config_list_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+aaabefore beforeval
+test.key1 testvalue1
+test.key2 testvalue2
+zzzafter afterval
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_database_get_config_list: all pairs (closed db)"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+{
+   notmuch_config_list_t *list;
+   EXPECT0(notmuch_database_get_config_list (db, "", &list));
+   EXPECT0(notmuch_database_close (db));
+   for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+      printf("%s %d\n", notmuch_config_list_key (list), NULL == notmuch_config_list_value(list));
+   }
+   notmuch_config_list_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+aaabefore 1
+test.key1 1
+test.key2 1
+zzzafter 1
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_database_get_config_list: one prefix"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+   notmuch_config_list_t *list;
+   EXPECT0(notmuch_database_get_config_list (db, "test.key", &list));
+   for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+      printf("%s %s\n", notmuch_config_list_key (list), notmuch_config_list_value(list));
+   }
+   notmuch_config_list_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 testvalue1
+test.key2 testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "dump config"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+    EXPECT0(notmuch_database_set_config (db, "key with spaces", "value, with, spaces!"));
+}
+EOF
+notmuch dump --include=config >OUTPUT
+cat <<'EOF' >EXPECTED
+#notmuch-dump batch-tag:3 config
+#@ aaabefore beforeval
+#@ key%20with%20spaces value,%20with,%20spaces%21
+#@ test.key1 testvalue1
+#@ test.key2 testvalue2
+#@ zzzafter afterval
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "restore config"
+notmuch dump --include=config >EXPECTED
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+    EXPECT0(notmuch_database_set_config (db, "test.key1", "mutatedvalue"));
+}
+EOF
+notmuch restore --include=config <EXPECTED
+notmuch dump --include=config >OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+backup_database
+test_begin_subtest "override config from file"
+notmuch config set test.key1 overridden
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG}
+{
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+test_begin_subtest "NOTMUCH_CONFIG_HOOK_DIR: traditional"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+   const char *val = notmuch_config_get (db, NOTMUCH_CONFIG_HOOK_DIR);
+   printf("database.hook_dir = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+database.hook_dir = MAIL_DIR/.notmuch/hooks
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "NOTMUCH_CONFIG_HOOK_DIR: xdg"
+dir="${HOME}/.config/notmuch/default/hooks"
+mkdir -p $dir
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+   const char *val = notmuch_config_get (db, NOTMUCH_CONFIG_HOOK_DIR);
+   printf("database.hook_dir = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+database.hook_dir = CWD/home/.config/notmuch/default/hooks
+== stderr ==
+EOF
+rmdir $dir
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_config_get_values"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+    notmuch_config_values_t *values;
+    EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_NEW_TAGS, "a;b;c"));
+    for (values = notmuch_config_get_values (db, NOTMUCH_CONFIG_NEW_TAGS);
+        notmuch_config_values_valid (values);
+        notmuch_config_values_move_to_next (values))
+    {
+         puts (notmuch_config_values_get (values));
+    }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+a
+b
+c
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+test_begin_subtest "notmuch_config_get_values (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%
+{
+    notmuch_config_values_t *values;
+    EXPECT0(notmuch_database_set_config (db, "test.list", "x;y;z"));
+    for (values = notmuch_config_get_values_string (db, "test.list");
+        notmuch_config_values_valid (values);
+        notmuch_config_values_move_to_next (values))
+    {
+         puts (notmuch_config_values_get (values));
+    }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+x
+y
+z
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+test_begin_subtest "notmuch_config_get_values (restart)"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+    notmuch_config_values_t *values;
+    EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_NEW_TAGS, "a;b;c"));
+    for (values = notmuch_config_get_values (db, NOTMUCH_CONFIG_NEW_TAGS);
+        notmuch_config_values_valid (values);
+        notmuch_config_values_move_to_next (values))
+    {
+         puts (notmuch_config_values_get (values));
+    }
+    for (notmuch_config_values_start (values);
+        notmuch_config_values_valid (values);
+        notmuch_config_values_move_to_next (values))
+    {
+         puts (notmuch_config_values_get (values));
+    }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+a
+b
+c
+a
+b
+c
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "notmuch_config_get_values, trailing ;"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+    notmuch_config_values_t *values;
+    EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_NEW_TAGS, "a;b;c"));
+    for (values = notmuch_config_get_values (db, NOTMUCH_CONFIG_NEW_TAGS);
+        notmuch_config_values_valid (values);
+        notmuch_config_values_move_to_next (values))
+    {
+         puts (notmuch_config_values_get (values));
+    }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+a
+b
+c
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "get config by key"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG}
+{
+   printf("before = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS));
+   EXPECT0(notmuch_database_set_config (db, "maildir.synchronize_flags", "false"));
+   printf("after = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS));
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+before = true
+after = false
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "set config by key"
+notmuch config set test.key1 overridden
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG}
+{
+   printf("before = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS));
+   EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS, "false"));
+   printf("after = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS));
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+before = true
+after = false
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+test_begin_subtest "load default values"
+export MAILDIR=${MAIL_DIR}
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} '' %NULL%
+{
+    notmuch_config_key_t key;
+    for (key = NOTMUCH_CONFIG_FIRST;
+        key < NOTMUCH_CONFIG_LAST;
+        key = (notmuch_config_key_t)(key + 1)) {
+       const char *val = notmuch_config_get (db, key);
+       printf("%02d: '%s'\n", key, val ? val : "NULL" );
+    }
+}
+EOF
+
+_libconfig_sanitize < OUTPUT > OUTPUT.clean
+
+cat <<'EOF' >EXPECTED
+== stdout ==
+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
+test_expect_equal_file EXPECTED OUTPUT.clean
+
+backup_database
+test_begin_subtest "override config from \${NOTMUCH_CONFIG}"
+notmuch config set test.key1 overridden
+# second argument omitted to make argv[2] == NULL
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+{
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+notmuch config set test.key1
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "override config from \${HOME}/.notmuch-config"
+ovconfig=${HOME}/.notmuch-config
+cp ${NOTMUCH_CONFIG} ${ovconfig}
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+notmuch --config=${ovconfig} config set test.key1 overridden-home
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
+{
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+rm -f ${ovconfig}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden-home
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "override config from \${XDG_CONFIG_HOME}/notmuch"
+ovconfig=${HOME}/.config/notmuch/default/config
+mkdir -p $(dirname ${ovconfig})
+cp ${NOTMUCH_CONFIG} ${ovconfig}
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+notmuch --config=${ovconfig} config set test.key1 overridden-xdg
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
+{
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+rm -f ${ovconfig}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden-xdg
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "override config from \${XDG_CONFIG_HOME}/notmuch with profile"
+ovconfig=${HOME}/.config/notmuch/work/config
+mkdir -p $(dirname ${ovconfig})
+cp ${NOTMUCH_CONFIG} ${ovconfig}
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+notmuch --config=${ovconfig} config set test.key1 overridden-xdg-profile
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% work
+{
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+rm -f ${ovconfig}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden-xdg-profile
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "override config from \${HOME}/.notmuch-config.work (via args)"
+ovconfig=${HOME}/.notmuch-config.work
+cp ${NOTMUCH_CONFIG} ${ovconfig}
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+notmuch --config=${ovconfig} config set test.key1 overridden-profile
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% work
+{
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+#rm -f ${ovconfig}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden-profile
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+test_begin_subtest "no config, fail to open database"
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+cat c_head - c_tail <<'EOF' | test_C %NULL% '' %NULL%
+{
+   printf("NOT RUN");
+}
+EOF
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+error opening database
+No database found
+Error: could not locate database.
+
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "open database from NOTMUCH_DATABASE"
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+export NOTMUCH_DATABASE=${MAIL_DIR}
+cat c_head - c_tail <<'EOF' | test_C %NULL% '' %NULL%
+{
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+unset NOTMUCH_DATABASE
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = testvalue1
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "NOTMUCH_DATABASE overrides config"
+cp notmuch-config notmuch-config.bak
+notmuch config set database.path /nonexistent
+export NOTMUCH_DATABASE=${MAIL_DIR}
+cat c_head - c_tail <<'EOF' | test_C %NULL% '' %NULL%
+{
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+unset NOTMUCH_DATABASE
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = testvalue1
+test.key2 = testvalue2
+== stderr ==
+EOF
+cp notmuch-config.bak notmuch-config
+test_expect_equal_file EXPECTED OUTPUT
+
+cat <<EOF > c_head2
+#include <notmuch-test.h>
+
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   char *val;
+   notmuch_status_t stat;
+   char *msg = NULL;
+
+   for (int i = 1; i < argc; i++)
+      if (strcmp (argv[i], "%NULL%") == 0) argv[i] = NULL;
+
+   stat = notmuch_database_load_config (argv[1],
+                                        argv[2],
+                                        argv[3],
+                                        &db,
+                                        &msg);
+   if (stat != NOTMUCH_STATUS_SUCCESS  && stat != NOTMUCH_STATUS_NO_CONFIG) {
+     fprintf (stderr, "error opening database\n%d: %s\n%s\n", stat,
+             notmuch_status_to_string (stat), msg ? msg : "");
+     exit (1);
+   }
+EOF
+
+
+test_begin_subtest "notmuch_database_get_config (ndlc)"
+cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
+{
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = testvalue1
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_begin_subtest "notmuch_database_get_config_list: all pairs (ndlc)"
+cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+   notmuch_config_list_t *list;
+   EXPECT0(notmuch_database_get_config_list (db, "", &list));
+   for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+      printf("%s %s\n", notmuch_config_list_key (list), notmuch_config_list_value(list));
+   }
+   notmuch_config_list_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+aaabefore beforeval
+key with spaces value, with, spaces!
+test.key1 testvalue1
+test.key2 testvalue2
+zzzafter afterval
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_database_get_config_list: one prefix (ndlc)"
+cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+   notmuch_config_list_t *list;
+   EXPECT0(notmuch_database_get_config_list (db, "test.key", &list));
+   for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+      printf("%s %s\n", notmuch_config_list_key (list), notmuch_config_list_value(list));
+   }
+   notmuch_config_list_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 testvalue1
+test.key2 testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "list by keys (ndlc)"
+notmuch config set search.exclude_tags "foo;bar;fub"
+notmuch config set new.ignore "sekrit_junk"
+notmuch config set index.as_text "text/"
+cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
+{
+    notmuch_config_key_t key;
+    for (key = NOTMUCH_CONFIG_FIRST;
+        key < NOTMUCH_CONFIG_LAST;
+        key = (notmuch_config_key_t)(key + 1)) {
+       const char *val = notmuch_config_get (db, key);
+       printf("%02d: '%s'\n", key, val ? val : "NULL" );
+    }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+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
+
+test_begin_subtest "load default values (ndlc, nonexistent config)"
+cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} /nonexistent %NULL%
+{
+    notmuch_config_key_t key;
+    for (key = NOTMUCH_CONFIG_FIRST;
+        key < NOTMUCH_CONFIG_LAST;
+        key = (notmuch_config_key_t)(key + 1)) {
+       const char *val = notmuch_config_get (db, key);
+       printf("%02d: '%s'\n", key, val ? val : "NULL" );
+    }
+}
+EOF
+
+_libconfig_sanitize < OUTPUT > OUTPUT.clean
+
+cat <<'EOF' >EXPECTED
+== stdout ==
+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
+
+backup_database
+test_begin_subtest "override config from \${HOME}/.notmuch-config (ndlc)"
+ovconfig=${HOME}/.notmuch-config
+cp ${NOTMUCH_CONFIG} ${ovconfig}
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+notmuch --config=${ovconfig} config set test.key1 overridden-home
+cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
+{
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+rm -f ${ovconfig}
+export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden-home
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+test_begin_subtest "notmuch_config_get_pairs: prefix (ndlc)"
+cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+   notmuch_config_pairs_t *list;
+   for (list =  notmuch_config_get_pairs (db, "user.");
+        notmuch_config_pairs_valid (list);
+        notmuch_config_pairs_move_to_next (list)) {
+     printf("%s %s\n", notmuch_config_pairs_key (list), notmuch_config_pairs_value(list));
+   }
+   notmuch_config_pairs_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+user.name Notmuch Test Suite
+user.other_email test_suite_other@notmuchmail.org;test_suite@otherdomain.org
+user.primary_email test_suite@notmuchmail.org
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_config_get_pairs: all pairs (ndlc)"
+cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+   notmuch_config_pairs_t *list;
+   for (list =  notmuch_config_get_pairs (db, "");
+        notmuch_config_pairs_valid (list);
+        notmuch_config_pairs_move_to_next (list)) {
+     printf("%s %s\n", notmuch_config_pairs_key (list), notmuch_config_pairs_value(list));
+   }
+   notmuch_config_pairs_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+aaabefore beforeval
+database.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
+user.other_email test_suite_other@notmuchmail.org;test_suite@otherdomain.org
+user.primary_email test_suite@notmuchmail.org
+zzzafter afterval
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+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/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
diff --git a/test/T595-reopen.sh b/test/T595-reopen.sh
new file mode 100755 (executable)
index 0000000..1a51742
--- /dev/null
@@ -0,0 +1,123 @@
+#!/usr/bin/env bash
+test_description="library reopen API"
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+cat <<EOF > c_head
+#include <notmuch-test.h>
+
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   char *val;
+   notmuch_status_t stat;
+   notmuch_database_mode_t mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
+
+   char *msg = NULL;
+
+   for (int i = 1; i < argc; i++)
+      if (strcmp (argv[i], "%NULL%") == 0) argv[i] = NULL;
+
+   if (argv[2] && (argv[2][0] == 'w' || argv[2][0] == 'W'))
+     mode = NOTMUCH_DATABASE_MODE_READ_WRITE;
+
+   stat = notmuch_database_open_with_config (argv[1],
+                                            mode,
+                                            argv[3],
+                                            argv[4],
+                                            &db,
+                                            &msg);
+   if (stat != NOTMUCH_STATUS_SUCCESS) {
+     fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : "");
+     exit (1);
+   }
+EOF
+
+cat <<EOF > c_tail
+   EXPECT0(notmuch_database_destroy(db));
+}
+EOF
+
+# The sequence of tests is important here
+
+test_begin_subtest "notmuch_database_reopen (read=>write)"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} read ${NOTMUCH_CONFIG}
+{
+   EXPECT0(notmuch_database_reopen (db, NOTMUCH_DATABASE_MODE_READ_WRITE));
+   EXPECT0(notmuch_database_set_config (db, "test.key1", "testvalue1"));
+   EXPECT0(notmuch_database_set_config (db, "test.key2", "testvalue2"));
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = testvalue1
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_database_reopen (read=>read)"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} read ${NOTMUCH_CONFIG}
+{
+   EXPECT0(notmuch_database_reopen (db, NOTMUCH_DATABASE_MODE_READ_ONLY));
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = testvalue1
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_database_reopen (write=>read)"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} write ${NOTMUCH_CONFIG}
+{
+   EXPECT0(notmuch_database_reopen (db, NOTMUCH_DATABASE_MODE_READ_ONLY));
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = testvalue1
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_database_reopen (write=>write)"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} write ${NOTMUCH_CONFIG}
+{
+   EXPECT0(notmuch_database_reopen (db, NOTMUCH_DATABASE_MODE_READ_WRITE));
+   EXPECT0(notmuch_database_set_config (db, "test.key3", "testvalue3"));
+   EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+   printf("test.key1 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+   printf("test.key2 = %s\n", val);
+   EXPECT0(notmuch_database_get_config (db, "test.key3", &val));
+   printf("test.key3 = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = testvalue1
+test.key2 = testvalue2
+test.key3 = testvalue3
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T600-named-queries.sh b/test/T600-named-queries.sh
new file mode 100755 (executable)
index 0000000..a7b8499
--- /dev/null
@@ -0,0 +1,80 @@
+#!/usr/bin/env bash
+test_description='named queries'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+QUERYSTR="date:2009-11-18..2009-11-18 and tag:unread"
+
+test_begin_subtest "error adding named query to DB before initialization"
+test_expect_code 1 "notmuch config set --database query.test \"$QUERYSTR\""
+
+add_email_corpus
+
+test_begin_subtest "adding named query (database)"
+test_expect_success "notmuch config set --database query.test \"$QUERYSTR\""
+
+test_begin_subtest "adding nested named query"
+QUERYSTR2="query:test and subject:Maildir"
+test_expect_success "notmuch config set query.test2 \"$QUERYSTR2\""
+
+test_begin_subtest "retrieve named query"
+output=$(notmuch config get query.test)
+test_expect_equal "$QUERYSTR" "$output"
+
+test_begin_subtest "List all queries"
+notmuch config list | grep ^query | notmuch_config_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+query.test=date:2009-11-18..2009-11-18 and tag:unread
+query.test2=query:test and subject:Maildir
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "dump named queries"
+notmuch dump | grep '^#@' > OUTPUT
+cat<<EOF > QUERIES.BEFORE
+#@ query.test date%3a2009-11-18..2009-11-18%20and%20tag%3aunread
+EOF
+test_expect_equal_file QUERIES.BEFORE OUTPUT
+
+test_begin_subtest 'dumping large queries'
+# This value is just large enough to trigger a limitation of gzprintf
+# to 8191 bytes in total (by default).
+repeat=1329
+notmuch config set --database query.big "$(seq -s' ' $repeat)"
+notmuch dump --include=config > OUTPUT
+notmuch config set --database query.big
+printf "#notmuch-dump batch-tag:3 config\n#@ query.big " > EXPECTED
+seq -s'%20' $repeat >> EXPECTED
+cat <<EOF >> EXPECTED
+#@ query.test date%3a2009-11-18..2009-11-18%20and%20tag%3aunread
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "delete named queries"
+notmuch dump > BEFORE
+notmuch config set --database query.test
+notmuch dump | grep '^#@' > OUTPUT
+cat<<EOF > EXPECTED
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "restore named queries"
+notmuch restore < BEFORE
+notmuch dump | grep '^#@' > OUTPUT
+test_expect_equal_file QUERIES.BEFORE OUTPUT
+
+test_begin_subtest "search named query"
+notmuch search query:test > OUTPUT
+notmuch search $QUERYSTR > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search named query with other terms"
+notmuch search query:test and 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:test2 > OUTPUT
+notmuch search $QUERYSTR2 > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T610-message-property.sh b/test/T610-message-property.sh
new file mode 100755 (executable)
index 0000000..a7cbe04
--- /dev/null
@@ -0,0 +1,404 @@
+#!/usr/bin/env bash
+test_description="message property API"
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+cat <<EOF > c_head
+#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 = %s\n", notmuch_message_properties_key(list), notmuch_message_properties_value(list));
+    }
+    notmuch_message_properties_destroy (list);
+}
+
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   notmuch_message_t *message = NULL;
+   const char *val;
+   notmuch_status_t stat;
+   char* msg = NULL;
+
+   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);
+   }
+EOF
+
+cat <<EOF > c_tail
+   EXPECT0(notmuch_database_destroy(db));
+}
+EOF
+
+test_begin_subtest "notmuch_message_{add,get,remove}_property"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+{
+   EXPECT0(notmuch_message_add_property (message, "testkey1", "testvalue1"));
+   EXPECT0(notmuch_message_get_property (message, "testkey1", &val));
+   printf("testkey1[1] = %s\n", val);
+   EXPECT0(notmuch_message_add_property (message, "testkey2", "this value has spaces and = sign"));
+   EXPECT0(notmuch_message_get_property (message, "testkey1", &val));
+   printf("testkey1[2] = %s\n", val);
+   EXPECT0(notmuch_message_get_property (message, "testkey1", &val));
+
+   EXPECT0(notmuch_message_get_property (message, "testkey2", &val));
+   printf("testkey2 = %s\n", val);
+
+   /* Add second value for key */
+   EXPECT0(notmuch_message_add_property (message, "testkey2", "zztestvalue3"));
+   EXPECT0(notmuch_message_get_property (message, "testkey2", &val));
+   printf("testkey2 = %s\n", val);
+
+   /* remove first value for key */
+   EXPECT0(notmuch_message_remove_property (message, "testkey2", "this value has spaces and = sign"));
+   EXPECT0(notmuch_message_get_property (message, "testkey2", &val));
+   printf("testkey2 = %s\n", val);
+
+   /* remove non-existent value for key */
+   EXPECT0(notmuch_message_remove_property (message, "testkey2", "this value has spaces and = sign"));
+   EXPECT0(notmuch_message_get_property (message, "testkey2", &val));
+   printf("testkey2 = %s\n", val);
+
+   /* remove only value for key */
+   EXPECT0(notmuch_message_remove_property (message, "testkey2", "zztestvalue3"));
+   EXPECT0(notmuch_message_get_property (message, "testkey2", &val));
+   printf("testkey2 = %s\n", val == NULL ? "NULL" : val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+testkey1[1] = testvalue1
+testkey1[2] = testvalue1
+testkey2 = this value has spaces and = sign
+testkey2 = this value has spaces and = sign
+testkey2 = zztestvalue3
+testkey2 = zztestvalue3
+testkey2 = NULL
+== 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}
+{
+   char *keys[] = {"a", "b", "c", "d", "e", NULL};
+   for (int i=0; keys[i]; i++)
+       EXPECT0(notmuch_message_add_property (message, keys[i], keys[i]));
+
+   for (int i=0; keys[i]; i++) {
+      EXPECT0(notmuch_message_get_property (message, keys[i], &val));
+      printf("%s = %s\n", keys[i], val);
+   }
+
+   for (int i=0; keys[i]; i++) {
+      EXPECT0(notmuch_message_remove_property (message, keys[i], keys[i]));
+      EXPECT0(notmuch_message_get_property (message, keys[i], &val));
+      printf("%s = %s\n", keys[i], val == NULL ? "NULL" : val);
+   }
+}
+EOF
+cat <<EOF > EXPECTED
+== stdout ==
+a = a
+b = b
+c = c
+d = d
+e = e
+a = NULL
+b = NULL
+c = NULL
+d = NULL
+e = NULL
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_message_get_properties: empty list"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+{
+   notmuch_message_properties_t *list;
+   list = notmuch_message_get_properties (message, "nonexistent", TRUE);
+   printf("valid = %d\n", notmuch_message_properties_valid (list));
+   notmuch_message_properties_destroy (list);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+valid = 0
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_message_properties: one value"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+print_properties (message, "testkey1", TRUE);
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+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
+
+test_begin_subtest "notmuch_message_properties: multiple values"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+EXPECT0(notmuch_message_add_property (message, "testkey1", "bob"));
+EXPECT0(notmuch_message_add_property (message, "testkey1", "testvalue2"));
+EXPECT0(notmuch_message_add_property (message, "testkey1", "alice"));
+print_properties (message, "testkey1", TRUE);
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+testkey1 = alice
+testkey1 = bob
+testkey1 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_message_properties: prefix"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+EXPECT0(notmuch_message_add_property (message, "testkey3", "bob3"));
+EXPECT0(notmuch_message_add_property (message, "testkey3", "testvalue3"));
+EXPECT0(notmuch_message_add_property (message, "testkey3", "alice3"));
+print_properties (message, "testkey", FALSE);
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+testkey1 = alice
+testkey1 = bob
+testkey1 = testvalue2
+testkey3 = alice3
+testkey3 = bob3
+testkey3 = testvalue3
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_message_properties: modify during iteration"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+{
+    const char *keys[1000] = {NULL};
+    const char *vals[1000] = {NULL};
+    notmuch_message_properties_t *properties;
+    int i;
+
+    for (properties = notmuch_message_get_properties (message, "", FALSE), i=0;
+        notmuch_message_properties_valid (properties);
+        notmuch_message_properties_move_to_next (properties), i++)
+    {
+       const char *key, *value;
+
+       keys[i]=talloc_strdup(message,
+                   notmuch_message_properties_key (properties));
+        vals[i]=talloc_strdup(message,
+                   notmuch_message_properties_value (properties));
+
+       EXPECT0(notmuch_message_remove_property (message, keys[i], vals[i]));
+    }
+
+    print_properties (message, "", FALSE);
+
+    for (i = 0; keys[i] && vals[i]; i++) {
+        EXPECT0(notmuch_message_add_property (message, keys[i], vals[i]));
+    }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+EOF
+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=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 ="));
+EOF
+notmuch dump | grep '^#=' > OUTPUT
+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=testvalue2 testkey3=alice3 testkey3=bob3 testkey3=testvalue3
+EOF
+notmuch dump --include=properties > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_begin_subtest "restore missing message property (single line)"
+notmuch dump | grep '^#=' > BEFORE1
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+EXPECT0(notmuch_message_remove_property (message, "testkey1", "bob"));
+EOF
+notmuch restore < BEFORE1
+notmuch dump | grep '^#=' > OUTPUT
+test_expect_equal_file PROPERTIES OUTPUT
+
+
+test_begin_subtest "restore missing message property (full dump)"
+notmuch dump > BEFORE2
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+EXPECT0(notmuch_message_remove_property (message, "testkey1", "bob"));
+EOF
+notmuch restore < BEFORE2
+notmuch dump | grep '^#=' > OUTPUT
+test_expect_equal_file PROPERTIES OUTPUT
+
+test_begin_subtest "restore clear extra message property"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+EXPECT0(notmuch_message_add_property (message, "testkey1", "charles"));
+EOF
+notmuch restore < BEFORE2
+notmuch dump | grep '^#=' > OUTPUT
+test_expect_equal_file PROPERTIES OUTPUT
+
+test_begin_subtest "test 'property:' queries: empty"
+notmuch search property:testkey1=charles > OUTPUT
+test_expect_equal_file /dev/null OUTPUT
+
+test_begin_subtest "test 'property:' queries: single message"
+notmuch search --output=messages property:testkey1=alice > OUTPUT
+cat <<EOF >EXPECTED
+id:4EFC743A.3060609@april.org
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "msg.get_property (python)"
+test_python <<'EOF'
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE)
+msg = db.find_message("4EFC743A.3060609@april.org")
+print("testkey1 = {0}".format(msg.get_property("testkey1")))
+print("testkey3 = {0}".format(msg.get_property("testkey3")))
+EOF
+cat <<'EOF' > EXPECTED
+testkey1 = alice
+testkey3 = alice3
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "msg.get_properties (python)"
+test_python <<'EOF'
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+msg = db.find_message("4EFC743A.3060609@april.org")
+for (key,val) in msg.get_properties("testkey1"):
+        print("{0} = {1}".format(key,val))
+EOF
+cat <<'EOF' > EXPECTED
+testkey1 = alice
+testkey1 = bob
+testkey1 = testvalue2
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "msg.get_properties (python, prefix)"
+test_python <<'EOF'
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+msg = db.find_message("4EFC743A.3060609@april.org")
+for (key,val) in msg.get_properties("testkey"):
+        print("{0} = {1}".format(key,val))
+EOF
+cat <<'EOF' > EXPECTED
+testkey1 = alice
+testkey1 = bob
+testkey1 = testvalue2
+testkey3 = alice3
+testkey3 = bob3
+testkey3 = testvalue3
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "msg.get_properties (python, exact)"
+test_python <<'EOF'
+import notmuch
+db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+msg = db.find_message("4EFC743A.3060609@april.org")
+for (key,val) in msg.get_properties("testkey",True):
+        print("{0} = {1}".format(key,val))
+EOF
+test_expect_equal_file /dev/null OUTPUT
+
+test_begin_subtest "notmuch_message_remove_all_properties_with_prefix"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+EXPECT0(notmuch_message_remove_all_properties_with_prefix (message, "testkey3"));
+print_properties (message, "", FALSE);
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+fancy key with áccènts = import value with =
+testkey1 = alice
+testkey1 = bob
+testkey1 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "edit property on removed message without uncaught exception"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+EXPECT0(notmuch_database_remove_message (db, notmuch_message_get_filename (message)));
+stat = notmuch_message_remove_property (message, "example", "example");
+if (stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION)
+    fprintf (stderr, "unable to remove properties on message");
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+unable to remove properties on message
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+add_email_corpus
+
+test_begin_subtest "remove all properties on removed message without uncaught exception"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+EXPECT0(notmuch_database_remove_message (db, notmuch_message_get_filename (message)));
+stat = notmuch_message_remove_all_properties_with_prefix (message, "");
+if (stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION)
+    fprintf (stderr, "unable to remove properties on message");
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+unable to remove properties on message
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T620-lock.sh b/test/T620-lock.sh
new file mode 100755 (executable)
index 0000000..99cc701
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+test_description="locking"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "blocking open"
+if [ $NOTMUCH_HAVE_XAPIAN_DB_RETRY_LOCK -ne 1 ]; then
+    test_subtest_known_broken
+fi
+test_C ${MAIL_DIR} <<'EOF'
+#include <notmuch-test.h>
+
+void
+taggit (notmuch_database_t *db, const char *tag)
+{
+    notmuch_message_t *message;
+
+    EXPECT0 (notmuch_database_find_message (db, "4EFC743A.3060609@april.org", &message));
+    if (message == NULL) {
+       fprintf (stderr, "unable to find message");
+       exit (1);
+    }
+
+    EXPECT0 (notmuch_message_add_tag (message, tag));
+    notmuch_message_destroy (message);
+}
+
+int
+main (int argc, char **argv)
+{
+    pid_t child;
+    const char *path = argv[1];
+
+    child = fork ();
+    if (child == -1) {
+       fprintf (stderr, "fork failed\n");
+       exit (1);
+    }
+
+    if (child == 0) {
+       notmuch_database_t *db2;
+       char* msg = NULL;
+
+       sleep (1);
+
+        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_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));
+       wait (NULL);
+    }
+}
+
+EOF
+notmuch search --output=tags id:4EFC743A.3060609@april.org >> OUTPUT
+cat <<'EOF' >EXPECTED
+== stdout ==
+== stderr ==
+child
+inbox
+parent
+unread
+EOF
+if [ $NOTMUCH_HAVE_XAPIAN_DB_RETRY_LOCK -ne 1 ]; then
+    test_subtest_known_broken
+fi
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T630-emacs-draft.sh b/test/T630-emacs-draft.sh
new file mode 100755 (executable)
index 0000000..c443f41
--- /dev/null
@@ -0,0 +1,74 @@
+#!/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
+
+test_begin_subtest "Saving a draft indexes it"
+test_emacs '(notmuch-mua-mail)
+           (message-goto-subject)
+           (insert "draft-test-0001")
+           (notmuch-draft-save)
+           (test-output)'
+count1=$(notmuch count tag:draft)
+count2=$(notmuch count subject:draft-test-0001)
+test_expect_equal "$count1=$count2" "1=1"
+
+test_begin_subtest "Saving a draft tags previous draft as deleted"
+test_emacs '(notmuch-mua-mail)
+           (message-goto-subject)
+           (insert "draft-test-0002")
+           (notmuch-draft-save)
+           (notmuch-draft-save)
+           (test-output)'
+count1=$(notmuch count tag:draft)
+count2=$(notmuch count subject:draft-test-0002)
+
+test_expect_equal "$count1,$count2" "2,1"
+
+test_begin_subtest "Saving a signed draft adds header"
+test_emacs '(notmuch-mua-mail)
+           (message-goto-subject)
+           (insert "draft-test-0003")
+            ;; We would use (mml-secure-message-sign) but on emacs23
+            ;; that only signs the part, not the whole message.
+            (mml-secure-message mml-secure-method '\''sign)
+           (notmuch-draft-save)
+           (test-output)'
+header_count=$(notmuch show --format=raw subject:draft-test-0003 | grep -c ^X-Notmuch-Emacs-Secure)
+body_count=$(notmuch notmuch show --format=raw subject:draft-test-0003 | grep -c '^\<#secure')
+test_expect_equal "$header_count,$body_count" "1,0"
+
+test_begin_subtest "Refusing to save an encrypted draft"
+test_emacs '(notmuch-mua-mail)
+           (message-goto-subject)
+           (insert "draft-test-0004")
+           (mml-secure-message-sign-encrypt)
+           (let ((notmuch-draft-save-plaintext nil))
+                    (notmuch-draft-save))
+           (test-output)'
+count1=$(notmuch count tag:draft)
+count2=$(notmuch count subject:draft-test-0004)
+
+test_expect_equal "$count1,$count2" "3,0"
+
+test_begin_subtest "Resuming a signed draft"
+
+test_emacs '(notmuch-show "subject:draft-test-0003")
+           (notmuch-show-resume-message)
+           (test-output)'
+notmuch_dir_sanitize OUTPUT > OUTPUT.clean
+cat <<EOF | notmuch_dir_sanitize >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: 
+Subject: draft-test-0003
+Fcc: MAIL_DIR/sent
+--text follows this line--
+<#secure method=pgpmime mode=sign>
+EOF
+test_expect_equal_file EXPECTED OUTPUT.clean
+test_done
diff --git a/test/T640-database-modified.sh b/test/T640-database-modified.sh
new file mode 100755 (executable)
index 0000000..2c3fa73
--- /dev/null
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+test_description="DatabaseModifiedError handling"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+# add enough messages to trigger the exception
+add_email_corpus
+
+test_begin_subtest "catching DatabaseModifiedError in _notmuch_message_ensure_metadata"
+# it seems to need to be an early document to trigger the exception
+first_id=$(notmuch search --output=messages '*'| head -1 | sed s/^id://)
+
+test_C ${MAIL_DIR} <<EOF
+#include <notmuch-test.h>
+
+int
+main (int argc, char **argv)
+{
+    const char *path = argv[1];
+
+    notmuch_database_t *rw_db, *ro_db;
+    notmuch_messages_t *messages;
+    notmuch_message_t *message, *ro_message;
+    notmuch_query_t *query;
+    notmuch_tags_t *tags;
+    int i;
+    char* msg = NULL;
+
+    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_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));
+
+    for (;
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages)) {
+       message = notmuch_messages_get (messages);
+       for (i=0; i<200; i++) {
+           char *tag_str = talloc_asprintf(rw_db, "%d", i);
+           EXPECT0 (notmuch_message_add_tag (message, tag_str));
+           talloc_free (tag_str);
+       }
+    }
+
+    notmuch_database_close (rw_db);
+
+    tags = notmuch_message_get_tags (ro_message);
+    if (tags)
+       printf("SUCCESS\n");
+    return 0;
+}
+EOF
+
+cat <<'EOF' >EXPECTED
+== stdout ==
+SUCCESS
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T650-regexp-query.sh b/test/T650-regexp-query.sh
new file mode 100755 (executable)
index 0000000..a984450
--- /dev/null
@@ -0,0 +1,206 @@
+#!/usr/bin/env bash
+test_description='regular expression searches'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+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 "empty path:// search"
+notmuch search 'path:""' > EXPECTED
+notmuch search 'path:/^$/' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "empty folder:// search"
+notmuch search 'folder:""' > EXPECTED
+notmuch search 'folder:/^$/' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "unanchored folder:// specification"
+output=$(notmuch search folder:/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 "anchored folder:// search"
+output=$(notmuch search 'folder:/^bad$/' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread)"
+
+test_begin_subtest "unanchored path:// specification"
+output=$(notmuch search path:/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 "anchored path:// search"
+output=$(notmuch search 'path:/^bad$/' | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread)"
+
+# Use "standard" corpus from here on.
+add_email_corpus
+
+notmuch search --output=messages from:cworth > cworth.msg-ids
+
+# these headers will generate no document terms
+add_message '[from]="-" [subject]="empty from"'
+add_message '[subject]="-"'
+
+test_begin_subtest "null from: search"
+notmuch search 'from:""' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1] -; empty from (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "null subject: search"
+notmuch search 'subject:""' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+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
+
+test_begin_subtest "xapian wildcard search for subject:"
+test_expect_equal $(notmuch count 'subject:count*') 1
+
+add_message '[from]="and"' '[subject]="and-and-and"'
+printf "id:$gen_msg_id\n" > EXPECTED
+
+test_begin_subtest "quoted xapian keyword search for from:"
+notmuch search --output=messages 'from:"and"' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "quoted xapian keyword search for subject:"
+notmuch search --output=messages 'subject:"and-and-and"' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp from search, case sensitive"
+notmuch search --output=messages from:/carl/ > OUTPUT
+test_expect_equal_file /dev/null OUTPUT
+
+test_begin_subtest "empty regexp or query"
+notmuch search --output=messages from:/carl/ or from:/cworth/ > OUTPUT
+test_expect_equal_file cworth.msg-ids OUTPUT
+
+test_begin_subtest "non-empty regexp and query"
+notmuch search  from:/cworth@cworth.org/ and subject:patch | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-18 [1/2] Carl Worth| Alex Botero-Lowry; [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Ingmar Vanhassel; [notmuch] [PATCH] Typsos (inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Jan Janak; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+thread:XXX   2009-11-18 [1/2] Carl Worth| Keith Packard; [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread)
+thread:XXX   2009-11-18 [2/5] Carl Worth| Mikhail Gusarov, Keith Packard; [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp from search, duplicate term search"
+notmuch search --output=messages from:/cworth/ > OUTPUT
+test_expect_equal_file cworth.msg-ids OUTPUT
+
+test_begin_subtest "long enough regexp matches only desired senders"
+notmuch search --output=messages 'from:"/C.* Wo/"' > OUTPUT
+test_expect_equal_file cworth.msg-ids OUTPUT
+
+test_begin_subtest "shorter regexp matches one more sender"
+notmuch search --output=messages 'from:"/C.* W/"' > OUTPUT
+{ echo id:1258544095-16616-1-git-send-email-chris@chris-wilson.co.uk; cat cworth.msg-ids; } > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp subject search, non-ASCII"
+notmuch search --output=messages subject:/accentué/ > OUTPUT
+echo id:877h1wv7mg.fsf@inf-8657.int-evry.fr > EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp subject search, punctuation"
+notmuch search subject:/\'X\'/ | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   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)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp subject search, no punctuation"
+notmuch search  subject:/X/ | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   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)
+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 "combine regexp from and subject"
+notmuch search  subject:/-C/ and from:/.an.k/ | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-17 [1/2] Jan Janak| Carl Worth; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "regexp error reporting"
+notmuch search 'from:/unbalanced[/' 2>&1 | sed -e '/^A Xapian/ s/[^:]*$//' > OUTPUT
+cat <<EOF > EXPECTED
+notmuch search: A Xapian exception occurred
+A Xapian exception occurred parsing query: Regexp error:
+Query string was: from:/unbalanced[/
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "empty mid search"
+notmuch search --output=messages mid:yoom > OUTPUT
+cp /dev/null EXPECTED
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "non-empty mid regex search"
+notmuch search --output=messages mid:/yoom/ > OUTPUT
+test_expect_equal_file cworth.msg-ids OUTPUT
+
+test_begin_subtest "combine regexp mid and subject"
+notmuch search  subject:/-C/ and mid:/y..m/ | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-18 [1/2] Carl Worth| Jan Janak; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "unanchored tag search"
+notmuch search tag:signed or tag:inbox > EXPECTED
+notmuch search tag:/i/ > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+notmuch tag +testsi '*'
+test_begin_subtest "anchored tag search"
+notmuch search tag:signed > EXPECTED
+notmuch search tag:/^si/ > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T660-bad-date.sh b/test/T660-bad-date.sh
new file mode 100755 (executable)
index 0000000..f65544b
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+test_description="parsing of bad dates"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_message [date]='"()"'
+
+test_begin_subtest 'Bad dates translate to a date after the Unix epoch'
+cat <<EOF >EXPECTED
+thread:0000000000000001   1970-01-01 [1/1] Notmuch Test Suite; Test message #1 (inbox unread)
+EOF
+notmuch search '*' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T670-duplicate-mid.sh b/test/T670-duplicate-mid.sh
new file mode 100755 (executable)
index 0000000..8fec291
--- /dev/null
@@ -0,0 +1,128 @@
+#!/usr/bin/env bash
+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)
+EOF
+notmuch search id:duplicate | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'First subject preserved in notmuch-show (json)'
+test_subtest_known_broken
+output=$(notmuch show --body=false --format=json id:duplicate | notmuch_json_show_sanitize)
+expected='[[[{
+    "id": "XXXXX",
+    "match": true,
+    "excluded": false,
+    "filename": [
+        "'"${MAIL_DIR}"/copy0'",
+        "'"${MAIL_DIR}"/copy1'",
+        "'"${MAIL_DIR}"/copy2'"
+    ],
+    "timestamp": 42,
+    "date_relative": "2001-01-05",
+    "tags": ["inbox","unread"],
+    "headers": {
+        "Subject": "message 1",
+        "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+        "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+        "Date": "GENERATED_DATE"
+    }
+ },
+[]]]]'
+test_expect_equal_json "$output" "$expected"
+
+test_begin_subtest 'Search for second subject'
+cat <<EOF >EXPECTED
+MAIL_DIR/copy0
+MAIL_DIR/copy1
+MAIL_DIR/copy2
+EOF
+notmuch search --output=files subject:'"message 2"' | notmuch_dir_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'Regexp search for second subject'
+test_subtest_known_broken
+cat <<EOF >EXPECTED
+MAIL_DIR/copy0
+MAIL_DIR/copy1
+MAIL_DIR/copy2
+EOF
+notmuch search --output=files 'subject:"/message 2/"' | notmuch_dir_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+add_message '[id]="duplicate"' '[body]="sekrit" [filename]=copy3'
+test_begin_subtest 'search for body in duplicate file'
+cat <<EOF >EXPECTED
+MAIL_DIR/copy0
+MAIL_DIR/copy1
+MAIL_DIR/copy2
+MAIL_DIR/copy3
+EOF
+notmuch search --output=files "sekrit" | notmuch_dir_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+rm ${MAIL_DIR}/copy3
+test_begin_subtest 'reindex drops terms in duplicate file'
+cp /dev/null EXPECTED
+notmuch reindex '*'
+notmuch search --output=files "sekrit" | notmuch_dir_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'reindex choses subject from first filename'
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1(3)] Notmuch Test Suite; message 0 (inbox unread)
+EOF
+notmuch search id:duplicate | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+rm ${MAIL_DIR}/copy0
+test_begin_subtest 'Deleted first duplicate file does not stop notmuch show from working'
+output=$(notmuch show --body=false --format=json id:duplicate |
+            notmuch_json_show_sanitize | sed 's/message [0-9]/A_SUBJECT/')
+expected='[[[{
+    "id": "XXXXX",
+    "crypto": {},
+    "match": true,
+    "excluded": false,
+    "filename": [
+        "'"${MAIL_DIR}"/copy0'",
+        "'"${MAIL_DIR}"/copy1'",
+        "'"${MAIL_DIR}"/copy2'"
+    ],
+    "timestamp": 42,
+    "date_relative": "2001-01-05",
+    "tags": ["inbox","unread"],
+    "headers": {
+        "Subject": "A_SUBJECT",
+        "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+        "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+        "Date": "GENERATED_DATE"
+    }
+ },
+[]]]]'
+
+test_expect_equal_json "$output" "$expected"
+
+test_done
diff --git a/test/T680-html-indexing.sh b/test/T680-html-indexing.sh
new file mode 100755 (executable)
index 0000000..62ba849
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+test_description="indexing of html parts"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus html
+
+test_begin_subtest 'embedded images should not be indexed'
+notmuch search kwpza7svrgjzqwi8fhb2msggwtxtwgqcxp4wbqr4wjddstqmeqa7 > OUTPUT
+test_expect_equal_file /dev/null OUTPUT
+
+test_begin_subtest 'ignore > in attribute text'
+notmuch search swordfish | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file /dev/null OUTPUT
+
+test_begin_subtest 'non tag text should be indexed'
+notmuch search hunter2 | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2009-11-17 [1/1] David Bremner; test html attachment (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T690-command-line-args.sh b/test/T690-command-line-args.sh
new file mode 100755 (executable)
index 0000000..9aa4761
--- /dev/null
@@ -0,0 +1,85 @@
+#!/usr/bin/env bash
+
+test_description="command line arguments"
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_message
+
+test_begin_subtest 'bad option to show'
+notmuch show --frobnicate >& OUTPUT
+cat <<EOF > EXPECTED
+Unrecognized option: --frobnicate
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'string option with space'
+cp /dev/null EXPECTED
+notmuch dump --output foo.txt '*' >& OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'string option with ='
+cp /dev/null EXPECTED
+notmuch dump --output=foo.txt '*' >& OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'string option with :'
+cp /dev/null EXPECTED
+notmuch dump --output:foo.txt '*' >& OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'single keyword option with space'
+cat <<EOF > EXPECTED
+id:msg-001@notmuch-test-suite
+EOF
+notmuch search --output messages '*' >& OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'single keyword option with ='
+cat <<EOF > EXPECTED
+id:msg-001@notmuch-test-suite
+EOF
+notmuch search --output=messages '*' >& OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'single keyword option with :'
+cat <<EOF > EXPECTED
+id:msg-001@notmuch-test-suite
+EOF
+notmuch search --output:messages '*' >& OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'multiple keyword options with space'
+cat <<EOF > EXPECTED
+["msg-001@notmuch-test-suite"]
+EOF
+notmuch search --output messages --format json '*' >& OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'multiple keyword options with ='
+cat <<EOF > EXPECTED
+["msg-001@notmuch-test-suite"]
+EOF
+notmuch search --output=messages --format=json '*' >& OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'mixed space and = delimiters'
+cat <<EOF > EXPECTED
+["msg-001@notmuch-test-suite"]
+EOF
+notmuch search --output messages --format=json '*' >& OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'mixed space and : delimiters'
+cat <<EOF > EXPECTED
+["msg-001@notmuch-test-suite"]
+EOF
+notmuch search --output:messages --format json '*' >& OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'show --entire-thread'
+test_expect_success 'notmuch show --entire-thread tag:test > /dev/null'
+
+test_begin_subtest 'show --exclude'
+test_expect_success 'notmuch show --exclude tag:test > /dev/null'
+
+test_done
diff --git a/test/T700-reindex.sh b/test/T700-reindex.sh
new file mode 100755 (executable)
index 0000000..af34ad7
--- /dev/null
@@ -0,0 +1,122 @@
+#!/usr/bin/env bash
+test_description='reindexing messages'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+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 '*' 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'
+notmuch reindex '*'
+notmuch search '*' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file initial-threads OUTPUT
+
+test_begin_subtest 'reindex after removing duplicate file preserves threads'
+# remove one copy
+sed 's,3/3(4),3/3,' < initial-threads > EXPECTED
+mv $MAIL_DIR/bar/18:2, duplicate-msg-1.eml
+notmuch reindex '*'
+notmuch search '*' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'reindex preserves message-ids'
+notmuch reindex '*'
+notmuch search --output=messages '*' > OUTPUT
+test_expect_equal_file initial-message-ids OUTPUT
+
+test_begin_subtest 'reindex preserves tags'
+notmuch reindex '*'
+notmuch dump > OUTPUT
+test_expect_equal_file initial-dump OUTPUT
+
+test_begin_subtest 'reindex preserves tags with special prefixes'
+notmuch tag +attachment2 +encrypted2 +signed2 '*'
+notmuch dump > EXPECTED
+notmuch reindex '*'
+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
+sed -i 's/1258471718-6781-1-git-send-email-dottedmag@dottedmag.net/87iqd9rn3l.fsf@vertex.dottedmag/' $MAIL_DIR/02:2,*
+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
+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
+#= 1258471718-6781-1-git-send-email-dottedmag@dottedmag.net userprop=userval
+#= 1258471718-6781-2-git-send-email-dottedmag@dottedmag.net userprop=userval
+#= 1258491078-29658-1-git-send-email-dottedmag@dottedmag.net userprop=userval1
+#= 20091117190054.GU3165@dottiness.seas.harvard.edu userprop=userval
+#= 20091117203301.GV3165@dottiness.seas.harvard.edu userprop=userval3
+#= 87fx8can9z.fsf@vertex.dottedmag userprop=userval2
+#= 87iqd9rn3l.fsf@vertex.dottedmag userprop=userval
+#= 87lji4lx9v.fsf@yoom.home.cworth.org userprop=userval3
+#= 87lji5cbwo.fsf@yoom.home.cworth.org userprop=userval
+#= cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com userprop=userval
+EOF
+notmuch restore < prop-dump
+notmuch reindex '*'
+notmuch dump | grep '^#=' | sort > OUTPUT
+test_expect_equal_file prop-dump OUTPUT
+
+add_email_corpus lkml
+
+test_begin_subtest "reindex of lkml corpus preserves threads"
+notmuch search '*' | notmuch_search_sanitize > EXPECTED
+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
diff --git a/test/T710-message-id.sh b/test/T710-message-id.sh
new file mode 100755 (executable)
index 0000000..a2d8ec7
--- /dev/null
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+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>
+<1530507300.raoomurnbf.astroid@strange.none>
+<1258787708-21121-2-git-send-email-keithp@keithp.com>
+EOF
+cat <<EOF >EXPECTED
+GOOD: 018b1a8f2d1df62e804ce88b65401304832dfbbf.1346614915.git.jani@nikula.org
+GOOD: 1530507300.raoomurnbf.astroid@strange.none
+GOOD: 1258787708-21121-2-git-send-email-keithp@keithp.com
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "leading and trailing space is OK"
+${TEST_DIRECTORY}/message-id-parse <<EOF >OUTPUT
+   <018b1a8f2d1df62e804ce88b65401304832dfbbf.1346614915.git.jani@nikula.org>
+<1530507300.raoomurnbf.astroid@strange.none>    
+    <1258787708-21121-2-git-send-email-keithp@keithp.com>
+EOF
+cat <<EOF >EXPECTED
+GOOD: 018b1a8f2d1df62e804ce88b65401304832dfbbf.1346614915.git.jani@nikula.org
+GOOD: 1530507300.raoomurnbf.astroid@strange.none
+GOOD: 1258787708-21121-2-git-send-email-keithp@keithp.com
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "<> delimiters are required"
+${TEST_DIRECTORY}/message-id-parse <<EOF >OUTPUT
+018b1a8f2d1df62e804ce88b65401304832dfbbf.1346614915.git.jani@nikula.org>
+<1530507300.raoomurnbf.astroid@strange.none
+1258787708-21121-2-git-send-email-keithp@keithp.com
+EOF
+cat <<EOF >EXPECTED
+BAD: 018b1a8f2d1df62e804ce88b65401304832dfbbf.1346614915.git.jani@nikula.org>
+BAD: <1530507300.raoomurnbf.astroid@strange.none
+BAD: 1258787708-21121-2-git-send-email-keithp@keithp.com
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "embedded whitespace is forbidden"
+${TEST_DIRECTORY}/message-id-parse <<EOF >OUTPUT
+<018b1a8f2d1df62e804ce88b65401304832dfbbf.1346614915 .git.jani@nikula.org>
+<1530507300.raoomurnbf.astroid @strange.none>
+<1258787708-21121-\f2-git-send-email-keithp@keithp.com>
+EOF
+cat <<EOF >EXPECTED
+BAD: <018b1a8f2d1df62e804ce88b65401304832dfbbf.1346614915 .git.jani@nikula.org>
+BAD: <1530507300.raoomurnbf.astroid    @strange.none>
+BAD: <1258787708-21121-\f2-git-send-email-keithp@keithp.com>
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_begin_subtest "folded real life bad In-Reply-To values"
+${TEST_DIRECTORY}/message-id-parse <<EOF >OUTPUT
+<22597.31869.380767.339702@chiark.greenend.org.uk> (Ian Jackson's message of "Mon, 5 Dec 2016 14:41:01 +0000")
+<20170625141242.loaalhis2eodo66n@gaara.hadrons.org>  <149719990964.27883.13021127452105787770.reportbug@seneca.home.org>
+Your message of Tue, 09 Dec 2014 13:21:11 +0100. <1900758.CgLNVPbY9N@liber>
+EOF
+cat <<EOF >EXPECTED
+BAD: <22597.31869.380767.339702@chiark.greenend.org.uk> (Ian Jackson's message of "Mon, 5 Dec 2016 14:41:01 +0000")
+BAD: <20170625141242.loaalhis2eodo66n@gaara.hadrons.org>  <149719990964.27883.13021127452105787770.reportbug@seneca.home.org>
+BAD: Your message of Tue, 09 Dec 2014 13:21:11 +0100. <1900758.CgLNVPbY9N@liber>
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_done
diff --git a/test/T720-emacs-attachment-warnings.sh b/test/T720-emacs-attachment-warnings.sh
new file mode 100755 (executable)
index 0000000..6e03f39
--- /dev/null
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+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)'
+
+test_done
diff --git a/test/T720-lib-lifetime.sh b/test/T720-lib-lifetime.sh
new file mode 100755 (executable)
index 0000000..e5afeaa
--- /dev/null
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2018 rhn
+#
+
+
+test_description="Lifetime constraints for library"
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus threading
+
+test_begin_subtest "building database"
+test_expect_success "NOTMUCH_NEW"
+
+test_begin_subtest "Message outlives parent Messages from replies"
+
+test_C ${MAIL_DIR} <<'EOF'
+#include <stdio.h>
+#include <stdlib.h>
+#include <notmuch.h>
+int main (int argc, char** argv)
+{
+   notmuch_database_t *db;
+   notmuch_status_t stat;
+   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);
+   }
+
+   notmuch_query_t *query = notmuch_query_create (db, "id:B00-root@example.org");
+   notmuch_threads_t *threads;
+
+   stat = notmuch_query_search_threads (query, &threads);
+   if (stat != NOTMUCH_STATUS_SUCCESS) {
+     fprintf (stderr, "error querying threads: %d\n", stat);
+     exit (1);
+   }
+
+   if (!notmuch_threads_valid (threads)) {
+     fprintf (stderr, "invalid threads");
+     exit (1);
+   }
+
+   notmuch_thread_t *thread = notmuch_threads_get (threads);
+   notmuch_messages_t *messages = notmuch_thread_get_messages (thread);
+
+   if (!notmuch_messages_valid (messages)) {
+     fprintf (stderr, "invalid messages");
+     exit (1);
+   }
+
+   notmuch_message_t *message = notmuch_messages_get (messages);
+   notmuch_messages_t *replies = notmuch_message_get_replies (message);
+   if (!notmuch_messages_valid (replies)) {
+     fprintf (stderr, "invalid replies");
+     exit (1);
+   }
+
+   notmuch_message_t *reply = notmuch_messages_get (replies);
+
+   notmuch_messages_destroy (replies); // the reply should not get destroyed here
+   notmuch_message_destroy (message);
+   notmuch_messages_destroy (messages); // nor here
+
+   const char *mid = notmuch_message_get_message_id (reply); // should not crash when accessing
+   fprintf (stdout, "Reply id: %s\n", mid);
+   notmuch_message_destroy (reply);
+   notmuch_thread_destroy (thread); // this destroys the reply
+   notmuch_threads_destroy (threads);
+   notmuch_query_destroy (query);
+   notmuch_database_destroy (db);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+Reply id: B01-child@example.org
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T730-emacs-forwarding.sh b/test/T730-emacs-forwarding.sh
new file mode 100755 (executable)
index 0000000..7b6ebf1
--- /dev/null
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+
+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
+# a References-header pointing to the original (forwarded) message.
+
+message_id='OriginalMessage@notmuchmail.org'
+add_message \
+    [id]="$message_id" \
+    '[from]="user@example.com"' \
+    '[subject]="This is the original message"' \
+    '[body]="Dummy text."'
+
+test_emacs_expect_t "
+  (let ((message-send-mail-function (lambda () t)))
+    (notmuch-show \"id:$message_id\")
+    (notmuch-show-forward-message)
+    (save-restriction
+      (message-narrow-to-headers)
+      (message-remove-header \"Fcc\")
+      (message-remove-header \"To\")
+      (message-add-header \"To: nobody@example.com\"))
+
+    (notmuch-mua-send)
+    (notmuch-test-expect-equal
+        (message-field-value \"References\") \"<$message_id>\"))
+"
+
+test_begin_subtest "Forwarding adding the forwarded tag"
+# Check that sending the forwarding message in the previous
+# subtest did add the forwarded-tag to the message that was forwarded.
+
+test_expect_equal "$(notmuch search --output=tags id:$message_id | sort)" \
+"forwarded
+inbox
+unread"
+
+test_done
diff --git a/test/T740-body.sh b/test/T740-body.sh
new file mode 100755 (executable)
index 0000000..548b30a
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+test_description='search body'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_message "[body]=thebody-1" "[subject]=subject-1"
+add_message "[body]=nothing-to-see-here-1" "[subject]=thebody-1"
+
+test_begin_subtest 'search with body: prefix'
+notmuch search body:thebody | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; subject-1 (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'search without body: prefix'
+notmuch search thebody | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; subject-1 (inbox unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; thebody-1 (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'negated body: prefix'
+notmuch search thebody and not body:thebody | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; thebody-1 (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'search unprefixed for prefixed term'
+notmuch search subject | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; subject-1 (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'search with body: prefix for term only in subject'
+notmuch search body:subject | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/T750-gzip.sh b/test/T750-gzip.sh
new file mode 100755 (executable)
index 0000000..5648896
--- /dev/null
@@ -0,0 +1,177 @@
+#!/usr/bin/env bash
+test_description='support for gzipped messages'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+#######################################################################
+# notmuch new
+test_begin_subtest "Single new gzipped message"
+generate_message
+gzip $gen_msg_filename
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Single new gzipped message (full-scan)"
+generate_message
+gzip $gen_msg_filename
+output=$(NOTMUCH_NEW --debug --full-scan 2>&1)
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "Multiple new messages, one gzipped"
+generate_message
+gzip $gen_msg_filename
+generate_message
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "Added 2 new messages to the database."
+
+test_begin_subtest "Multiple new messages, one gzipped (full-scan)"
+generate_message
+gzip $gen_msg_filename
+generate_message
+output=$(NOTMUCH_NEW --debug --full-scan 2>&1)
+test_expect_equal "$output" "Added 2 new messages to the database."
+
+test_begin_subtest "Renamed (gzipped) message"
+generate_message
+echo $gen_message_filename
+notmuch new > /dev/null
+gzip $gen_msg_filename
+output=$(NOTMUCH_NEW --debug)
+test_expect_equal "$output" "(D) add_files, pass 2: queuing passed file ${gen_msg_filename} for deletion from database
+No new mail. Detected 1 file rename."
+
+######################################################################
+# notmuch search
+
+test_begin_subtest "notmuch search with partially gzipped mail store"
+notmuch search '*' | notmuch_search_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Single new gzipped message (inbox unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Single new gzipped message (full-scan) (inbox unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Multiple new messages, one gzipped (inbox unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Multiple new messages, one gzipped (inbox unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Multiple new messages, one gzipped (full-scan) (inbox unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Multiple new messages, one gzipped (full-scan) (inbox unread)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Renamed (gzipped) message (inbox unread)
+EOF
+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-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
+
+######################################################################
+# notmuch show
+
+test_begin_subtest "show un-gzipped message"
+notmuch show id:msg-006@notmuch-test-suite | notmuch_show_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+\fmessage{ id:msg-006@notmuch-test-suite depth:0 match:1 excluded:0 filename:/XXX/mail/msg-006
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox unread)
+Subject: Multiple new messages, one gzipped (full-scan)
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Fri, 05 Jan 2001 15:43:51 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This is just a test message (#6)
+\fpart}
+\fbody}
+\fmessage}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "show un-gzipped message (format mbox)"
+notmuch show --format=mbox id:msg-006@notmuch-test-suite | notmuch_show_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+From test_suite@notmuchmail.org Fri Jan  5 15:43:51 2001
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Message-Id: <msg-006@notmuch-test-suite>
+Subject: Multiple new messages, one gzipped (full-scan)
+Date: Fri, 05 Jan 2001 15:43:51 +0000
+
+This is just a test message (#6)
+
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "show un-gzipped message (format raw)"
+notmuch show --format=raw id:msg-006@notmuch-test-suite | notmuch_show_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Message-Id: <msg-006@notmuch-test-suite>
+Subject: Multiple new messages, one gzipped (full-scan)
+Date: Fri, 05 Jan 2001 15:43:51 +0000
+
+This is just a test message (#6)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "show gzipped message"
+notmuch show id:msg-007@notmuch-test-suite | notmuch_show_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+\fmessage{ id:msg-007@notmuch-test-suite depth:0 match:1 excluded:0 filename:/XXX/mail/msg-007.gz
+\fheader{
+Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox unread)
+Subject: Renamed (gzipped) message
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Date: Fri, 05 Jan 2001 15:43:50 +0000
+\fheader}
+\fbody{
+\fpart{ ID: 1, Content-type: text/plain
+This is just a test message (#7)
+\fpart}
+\fbody}
+\fmessage}
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "show gzipped message (mbox)"
+notmuch show --format=mbox id:msg-007@notmuch-test-suite | notmuch_show_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+From test_suite@notmuchmail.org Fri Jan  5 15:43:50 2001
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Message-Id: <msg-007@notmuch-test-suite>
+Subject: Renamed (gzipped) message
+Date: Fri, 05 Jan 2001 15:43:50 +0000
+
+This is just a test message (#7)
+
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "show gzipped message (raw)"
+notmuch show --format=raw id:msg-007@notmuch-test-suite | notmuch_show_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Notmuch Test Suite <test_suite@notmuchmail.org>
+Message-Id: <msg-007@notmuch-test-suite>
+Subject: Renamed (gzipped) message
+Date: Fri, 05 Jan 2001 15:43:50 +0000
+
+This is just a test message (#7)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+# there are more than 200 messages in this corpus
+add_email_corpus lkml
+test_begin_subtest "new doesn't run out of file descriptors with many gzipped files"
+ulimit -n 200
+find ${MAIL_DIR} -name .notmuch -prune -o -type f -print0 | xargs -0 gzip --
+test_expect_success "notmuch new"
+
+test_done
diff --git a/test/T750-user-header.sh b/test/T750-user-header.sh
new file mode 100755 (executable)
index 0000000..03c4365
--- /dev/null
@@ -0,0 +1,122 @@
+#!/usr/bin/env bash
+test_description='indexing user specified headers'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+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"
+notmuch config set index.header.$'\xFF' "List-Id" 2>&1 | sed 's/:.*$//' >OUTPUT
+cat <<EOF > EXPECTED
+Invalid utf8
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "adding illegal prefix name, reserved for notmuch"
+notmuch config set index.header.list "List-Id" 2>OUTPUT
+cat <<EOF > EXPECTED
+Prefix names starting with lower case letters are reserved: list
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "adding illegal prefix name, non-word character."
+notmuch config set index.header.l:st "List-Id" 2>OUTPUT
+cat <<EOF > EXPECTED
+Non-word character in prefix name: l:st
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "adding empty prefix name."
+notmuch config set index.header. "List-Id" 2>OUTPUT
+Non-word character in prefix name: l:st
+cat <<EOF > EXPECTED
+Empty prefix name: index.header.
+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 "adding existing user header"
+test_expect_code 0 "notmuch config set index.header.List \"List-Id\""
+
+
+test_begin_subtest "retrieve user header"
+output=$(notmuch config get index.header.List)
+test_expect_equal "List-Id" "$output"
+
+test_begin_subtest 'reindex after adding header preserves threads'
+notmuch reindex '*'
+notmuch search '*' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file initial-threads OUTPUT
+
+test_begin_subtest "List all user headers"
+notmuch config set index.header.Spam "X-Spam"
+notmuch config list | grep ^index.header | notmuch_config_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+index.header.List=List-Id
+index.header.Spam=X-Spam
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "parse user prefix"
+NOTMUCH_DEBUG_QUERY=t notmuch count 'List:"notmuch"' 2>&1 | grep Tmail >OUTPUT
+cat <<EOF > EXPECTED
+Query((Tmail AND XUList:notmuch@1))
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "parse user prefix, stemmed"
+NOTMUCH_DEBUG_QUERY=t notmuch count 'List:notmuch' 2>&1 | grep Tmail >OUTPUT
+cat <<EOF > EXPECTED
+Query((Tmail AND ZXUList:notmuch@1))
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "parse user prefix, phrase"
+NOTMUCH_DEBUG_QUERY=t notmuch count 'List:notmuchmail.org' 2>&1 | grep Tmail >OUTPUT
+cat <<EOF > EXPECTED
+Query((Tmail AND (XUList:notmuchmail@1 PHRASE 2 XUList:org@2)))
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "index user header"
+notmuch config set index.header.List "List-Id"
+notmuch reindex '*'
+notmuch search --output=files List:notmuch | notmuch_search_files_sanitize | sort > OUTPUT
+cat <<EOF > EXPECTED
+MAIL_DIR/bar/baz/05:2,
+MAIL_DIR/bar/baz/23:2,
+MAIL_DIR/bar/baz/24:2,
+MAIL_DIR/bar/cur/20:2,
+MAIL_DIR/bar/new/21:2,
+MAIL_DIR/bar/new/22:2,
+MAIL_DIR/foo/cur/08:2,
+MAIL_DIR/foo/new/03:2,
+MAIL_DIR/new/04:2,
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "index user header, config from file"
+field_name="Test"
+printf "\n[index]\nheader.${field_name} = List-Id\n" >> notmuch-config
+notmuch reindex '*'
+notmuch search --output=files ${field_name}:notmuch | notmuch_search_files_sanitize | sort > OUTPUT
+cat <<EOF > EXPECTED
+MAIL_DIR/bar/baz/05:2,
+MAIL_DIR/bar/baz/23:2,
+MAIL_DIR/bar/baz/24:2,
+MAIL_DIR/bar/cur/20:2,
+MAIL_DIR/bar/new/21:2,
+MAIL_DIR/bar/new/22:2,
+MAIL_DIR/foo/cur/08:2,
+MAIL_DIR/foo/new/03:2,
+MAIL_DIR/new/04:2,
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
diff --git a/test/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..dbd16a9
--- /dev/null
@@ -0,0 +1,5 @@
+# It's unclear how TSan-friendly GLib is:
+# https://gitlab.gnome.org/GNOME/glib/-/issues/1672
+race:g_rw_lock_reader_lock
+# https://gitlab.gnome.org/GNOME/glib/-/issues/1952
+race:g_slice_alloc0
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
diff --git a/test/aggregate-results.sh b/test/aggregate-results.sh
new file mode 100755 (executable)
index 0000000..6845fcf
--- /dev/null
@@ -0,0 +1,99 @@
+#!/usr/bin/env bash
+
+set -eu
+
+fixed=0
+success=0
+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
+               fixed)
+                       fixed=$((fixed + value)) ;;
+               success)
+                       success=$((success + value)) ;;
+               failed)
+                       failed=$((failed + value)) ;;
+               broken)
+                       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 ] && [ "$rep_failed" -eq 0 ]; then
+       pluralize_s "$total"
+       printf "All $total test$s "
+       if [ "$broken" -eq 0 ]; then
+               echo "passed."
+       else
+               pluralize_s "$broken"
+               echo "behaved as expected ($broken expected failure$s)."
+       fi
+else
+       echo "$success/$total tests passed."
+       if [ "$broken" -ne 0 ]; then
+               pluralize_s "$broken"
+               echo "$broken broken test$s failed as expected."
+       fi
+       if [ "$fixed" -ne 0 ]; then
+               pluralize_s "$fixed"
+               echo "$fixed broken test$s now fixed."
+       fi
+       if [ "$failed" -ne 0 ]; then
+               pluralize_s "$failed"
+               echo "$failed test$s failed."
+       fi
+fi
+
+skipped=$((total - fixed - success - failed - broken))
+if [ "$skipped" -ne 0 ]; then
+       pluralize_s "$skipped"
+       echo "$skipped test$s skipped."
+fi
+if [ "$all_skipped" -ne 0 ]; then
+       pluralize_s "$all_skipped"
+       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 ] && [ "$rep_failed" -eq 0 ]
+then
+       exit 0
+else
+       exit 1
+fi
diff --git a/test/arg-test.c b/test/arg-test.c
new file mode 100644 (file)
index 0000000..4447773
--- /dev/null
@@ -0,0 +1,87 @@
+#include <stdio.h>
+#include "command-line-arguments.h"
+
+
+int
+main (int argc, char **argv)
+{
+
+    int opt_index = 1;
+
+    int kw_val = 0;
+    int kwb_val = 0;
+    int fl_val = 0;
+    int int_val = 0;
+    const char *pos_arg1 = NULL;
+    const char *pos_arg2 = NULL;
+    const char *string_val = NULL;
+    bool bool_val = false;
+    bool fl_set = false, int_set = false, bool_set = false, kwb_set = false,
+        kw_set = false, string_set = false, pos1_set = false, pos2_set = false;
+
+    notmuch_opt_desc_t parent_options[] = {
+       { .opt_flags = &fl_val, .name = "flag", .present = &fl_set, .keywords =
+             (notmuch_keyword_t []){ { "one",   1 << 0 },
+                                     { "two",   1 << 1 },
+                                     { "three", 1 << 2 },
+                                     { 0, 0 } } },
+       { .opt_int = &int_val, .name = "int", .present = &int_set },
+       { }
+    };
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_bool = &bool_val, .name = "boolean", .present = &bool_set },
+       { .opt_keyword = &kw_val, .name = "keyword", .present = &kw_set, .keywords =
+             (notmuch_keyword_t []){ { "zero", 0 },
+                                     { "one", 1 },
+                                     { "two", 2 },
+                                     { 0, 0 } } },
+       { .opt_keyword = &kwb_val, .name = "boolkeyword", .present = &kwb_set,
+         .keyword_no_arg_value = "true", .keywords =
+             (notmuch_keyword_t []){ { "false", 0 },
+                                     { "true", 1 },
+                                     { "auto", 2 },
+                                     { 0, 0 } } },
+       { .opt_inherit = parent_options },
+       { .opt_string = &string_val, .name = "string", .present = &string_set },
+       { .opt_position = &pos_arg1, .present = &pos1_set },
+       { .opt_position = &pos_arg2, .present = &pos2_set },
+       { }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+
+    if (opt_index < 0)
+       return 1;
+
+    if (bool_set)
+       printf ("boolean %d\n", bool_val);
+
+    if (kw_set)
+       printf ("keyword %d\n", kw_val);
+
+    if (kwb_set)
+       printf ("boolkeyword %d\n", kwb_val);
+
+    if (fl_set)
+       printf ("flags %d\n", fl_val);
+
+    if (int_set)
+       printf ("int %d\n", int_val);
+
+    if (string_set)
+       printf ("string %s\n", string_val);
+
+    if (pos1_set)
+       printf ("positional arg 1 %s\n", pos_arg1);
+
+    if (pos2_set)
+       printf ("positional arg 2 %s\n", pos_arg2);
+
+
+    for (; opt_index < argc; opt_index++) {
+       printf ("non parsed arg %d = %s\n", opt_index, argv[opt_index]);
+    }
+
+    return 0;
+}
diff --git a/test/atomicity.py b/test/atomicity.py
new file mode 100644 (file)
index 0000000..389517e
--- /dev/null
@@ -0,0 +1,78 @@
+# This gdb Python script runs notmuch new and simulates killing and
+# restarting notmuch new after every Xapian commit.  To simulate this
+# more efficiently, this script runs notmuch new and, immediately
+# after every Xapian commit, it *pauses* the running notmuch new,
+# copies the entire database and maildir to a snapshot directory, and
+# executes a full notmuch new on that snapshot, comparing the final
+# results with the expected output.  It can then resume the paused
+# notmuch new, which is still running on the original maildir, and
+# repeat this process.
+
+import gdb
+import os
+import glob
+import shutil
+import subprocess
+
+gdb.execute('set args new')
+
+# Make Xapian commit after every operation instead of batching
+gdb.execute('set environment XAPIAN_FLUSH_THRESHOLD = 1')
+
+maildir = os.environ['MAIL_DIR']
+
+# Trap calls to rename, which happens just before Xapian commits
+class RenameBreakpoint(gdb.Breakpoint):
+    def __init__(self, *args, **kwargs):
+        super(RenameBreakpoint, self).__init__(*args, **kwargs)
+        self.last_inodes = {}
+        self.n = 0
+
+    def stop(self):
+        xapiandir = '%s/.notmuch/xapian' % maildir
+        if os.path.isfile('%s/iamchert' % xapiandir):
+            # As an optimization, only consider snapshots after a
+            # Xapian has really committed.  The chert backend
+            # overwrites record.base? as the last step in the commit,
+            # so keep an eye on their inumbers.
+            inodes = {}
+            for path in glob.glob('%s/record.base*' % xapiandir):
+                inodes[path] = os.stat(path).st_ino
+            if inodes == self.last_inodes:
+                # Continue
+                return False
+            self.last_inodes = inodes
+
+        # Save a backtrace in case the test does fail
+        backtrace = gdb.execute('backtrace', to_string=True)
+        open('backtrace.%d' % self.n, 'w').write(backtrace)
+
+        # Snapshot the database
+        shutil.rmtree('%s.snap/.notmuch' % maildir)
+        shutil.copytree('%s/.notmuch' % maildir, '%s.snap/.notmuch' % maildir)
+        # Restore the mtime of $MAIL_DIR.snap/
+        shutil.copystat('%s/.notmuch' % maildir, '%s.snap/.notmuch' % maildir)
+
+        # Run notmuch new to completion on the snapshot
+        env = os.environ.copy()
+        env.update(NOTMUCH_CONFIG=os.environ['NOTMUCH_CONFIG'] + '.snap',
+                   XAPIAN_FLUSH_THRESHOLD='1000')
+        subprocess.check_call(
+            ['notmuch', 'new'], env=env, stdout=open('/dev/null', 'w'))
+        subprocess.check_call(
+            ['notmuch', 'search', '*'], env=env,
+            stdout=open('search.%d' % self.n, 'w'))
+
+        # Tell the shell how far we've gotten
+        open('outcount', 'w').write(str(self.n + 1))
+
+        # Continue
+        self.n += 1
+        return False
+RenameBreakpoint('rename')
+
+try:
+    gdb.execute('run')
+except Exception:
+    import traceback
+    raise SystemExit(traceback.format_exc())
diff --git a/test/corpora/README b/test/corpora/README
new file mode 100644 (file)
index 0000000..05ea386
--- /dev/null
@@ -0,0 +1,19 @@
+This directory contains email corpora for testing.
+
+default
+  The default corpus is based on about 50 messages from early in the
+  history of the notmuch mailing list, which allows for reliably
+  testing commands that need to operate on a not-totally-trivial
+  number of messages.
+
+broken
+  The broken corpus contains messages that are broken and/or RFC
+  non-compliant, ensuring we deal with them in a sane way.
+
+html
+  The html corpus contains html parts
+
+crypto
+  The crypto corpus contains encrypted messages for testing.
+  It should probably also contain signed messages in the future.
+  Please add them!
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
+
+--=-=-=--
diff --git a/test/corpora/broken/broken-cc b/test/corpora/broken/broken-cc
new file mode 100644 (file)
index 0000000..57ae9ba
--- /dev/null
@@ -0,0 +1,9 @@
+From: Alice <alice@example.org>
+To: Daniel <daniel@example.org>
+Cc: Bob <bob@example.org>
+Subject: wowsers!
+cc: Charles <charles@example.org>
+Message-Id: <multiple-cc@example.org>
+Date: Thu, 16 Jun 2016 22:14:41 -0400
+
+Note the Cc: and cc: headers.
diff --git a/test/corpora/broken/loop/loop-12 b/test/corpora/broken/loop/loop-12
new file mode 100644 (file)
index 0000000..b5c3af7
--- /dev/null
@@ -0,0 +1,8 @@
+From: Alice <alice@example.org>
+To: Daniel <daniel@example.org>
+Subject: referencing in-reply-to-loop-21
+Message-ID: <mid-loop-12@example.org>
+In-Reply-To: <mid-loop-21@example.org>
+Date: Thu, 16 Jun 2016 22:14:41 -0400
+
+Note Message-ID and In-Reply-To: in file in-reply-to-loop-21
diff --git a/test/corpora/broken/loop/loop-21 b/test/corpora/broken/loop/loop-21
new file mode 100644 (file)
index 0000000..234f032
--- /dev/null
@@ -0,0 +1,8 @@
+From: Alice <alice@example.org>
+To: Daniel <daniel@example.org>
+Subject: referencing in-reply-to-loop-12
+Message-ID: <mid-loop-21@example.org>
+In-Reply-To: <mid-loop-12@example.org>
+Date: Fri, 17 Jun 2016 22:14:41 -0400
+
+Note Message-ID and In-Reply-To: in file in-reply-to-loop-12
diff --git a/test/corpora/crypto/basic-encrypted.eml b/test/corpora/crypto/basic-encrypted.eml
new file mode 100644 (file)
index 0000000..b139a73
--- /dev/null
@@ -0,0 +1,27 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Here is the password
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <basic-encrypted@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+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==--
diff --git a/test/corpora/crypto/encrypted-signed.eml b/test/corpora/crypto/encrypted-signed.eml
new file mode 100644 (file)
index 0000000..f5d5d12
--- /dev/null
@@ -0,0 +1,34 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Lyrics
+Date: Wed 29 May 2019 06:09:22 PM EDT
+Message-ID: <encrypted-signed@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdAwRMXQWEiw2ZldkVtILgy2w/vFrKxAgAxFmyDGvtbiTow
+xLMHH7PgH+S0JIjEOM0jFglpyMdzvNBWII726THKbFnOcR38CsbK55lCYw2uhABS
+0sD1AU/yQZNNDwjuJT6wjtxMdfI4DaFMD5XTpioNtRcphLpN7MHbYr4MYCxszRY7
+ZLIAkUCoY/3D26dcOkB0EMGELsRsPmKaCq/m3FVvxHHku94MzLnjV/eGRE40AiSe
+JzteAKwXzUAYzYw+LCj9WPZvy1Rs37I4VgukEgSYXMidgc9fUQ4yYKesbyQ8/iMW
+Ryo+X2yu7Iv3a7pp2qdArsJwatWpyRuASVVA7nZlNQS0YGh/fuSX3x8TJuSPliFr
+sdTVuE6sJIlqttH9CRgxMjLY5YbpF3lBTqlv2tmz0VERhWKKsh5VOiUvJvZ3p5hn
+FqnD1bcoWoBPZRNOoF6PzGkvWQIqysGLgg24wZr7ZRgZ8mHRgykBN6cmZHQKf16m
+zqqf8sppaRal6d/L72EzmeHEusUn6FlWcEDlXLc5anghYdna7qhbLqsoY7X504SQ
+Mx4tj1P4XVhgoXdLRR2EHOLrzUCrkiQ9cKfoAcUFJTcbGGYYiYwzUCkZWhsHOKsQ
+y7tajvWUzGaJ8aiZ1dfdUraOzrvOOif4TdnFJrTpM0Agy6IH8tbm8EnhNOxkjQPr
+3t7eS3JcuC0=
+=qz8x
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/crypto/simple-encrypted b/test/corpora/crypto/simple-encrypted
new file mode 100644 (file)
index 0000000..6869972
--- /dev/null
@@ -0,0 +1,36 @@
+From: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+To: dkg@fifthhorseman.net
+Subject: encrypted message
+Date: Mon, 22 Dec 2016 08:34:56 -0400
+Message-ID: <simple-encrypted@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hQIMAzt6p/AU5ptaAQ/+IDRx5dZWKjUz9qITP76w8OvtmTV9p871UoGWil1DSd3J
+dHgf56rXDWS73dzJ5EigevxLVMD3Xv8QEJBgMWb6yC+uR8ZdJ8h7hlE2lYyEg3Ch
+smqzcaYp2nKWw9JZQqubsMaeIgVu0exb4YE8g/qlUOL2mD64dXhnkJ68GGMmiEPJ
++d0H7fTMwstbxvKPKDmFJUztH43V2NSfxpeQUTi4iWmQtUGw6THYjjAWNFy7jVEE
+ozloT9W3ER5cRaXyKE4GWMBlUAOB0YwwsVnBU2JGUtTBzNHxQAbeoKrG6myrzmnC
+LhUNag0gMuNEbGR78XfWKpCbccEC1VLf9uUXze6yeuRXDuhfsvilKOH9MpaR9n+G
+JuYVAAobDc5wGOt5VGMka50ToaALrNt3FrWqni/0jqwqshEVKM3Kqzq6cSjh1TPf
+pfxE9aUDdvf+Nn16ZBGgyLox2NV4GkSQYq2ySzAk3XwLU80F0nCmtOV3EH+OMrv6
+sZI/Svwzaqzrs/w17cvpf0czXjE/N9V1MHdNtIkfb707WkO0l9/wtYvlrg/KyrU2
+FH5aecEO9VpMumgzBqP1MrrnzVlSM3kgRLIu06oshQYD+jjFn2YzvkwZ+pWoAxsQ
+ninQxoF8Ck2D7s8uGzx+HSQQpRVBM5AGfVdEkmzW/sZrlz63ZGUFh0FYqLl30rfS
+cQGqSoZz0ugTSxnTlg0nuzFmG7ylC1cx3dlSrnfv+l1azwLAr58ptoYZ0mO+D+Fy
+bePpCGoFAPKi0cZ/4eFlLKL7uYPmGeEo5Ku2wXU/SXtPfl4vRxzvPXTJTvD06vIx
+Dh1P0ChAJtxUjGGY6xkCHMY0
+=frmz
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/default/01:2, b/test/corpora/default/01:2,
new file mode 100644 (file)
index 0000000..7e9e349
--- /dev/null
@@ -0,0 +1,34 @@
+From: "Mikhail Gusarov" <dottedmag@dottedmag.net>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 21:28:37 +0600
+Subject: [notmuch] [PATCH 1/2] Close message file after parsing message
+       headers
+Message-ID: <1258471718-6781-1-git-send-email-dottedmag@dottedmag.net>
+
+Keeping unused files open helps to see "Too many open files" often.
+
+Signed-off-by: Mikhail Gusarov <dottedmag at dottedmag.net>
+---
+ lib/message-file.c |    5 +++++
+ 1 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/lib/message-file.c b/lib/message-file.c
+index 8a3f8ee..197ab01 100644
+--- a/lib/message-file.c
++++ b/lib/message-file.c
+@@ -325,6 +325,11 @@ notmuch_message_file_get_header (notmuch_message_file_t *message,
+           return decoded_value;
+     }
++    if (message->parsing_finished) {
++        fclose (message->file);
++        message->file = NULL;
++    }
++
+     if (message->line)
+       free (message->line);
+     message->line = NULL;
+-- 
+1.6.3.3
+
+
diff --git a/test/corpora/default/02:2, b/test/corpora/default/02:2,
new file mode 100644 (file)
index 0000000..dadcdaa
--- /dev/null
@@ -0,0 +1,32 @@
+From: "Mikhail Gusarov" <dottedmag@dottedmag.net>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 21:28:38 +0600
+Subject: [notmuch] [PATCH 2/2] Include <stdint.h> to get uint32_t in C++
+       file with gcc 4.4
+In-Reply-To: <1258471718-6781-1-git-send-email-dottedmag@dottedmag.net>
+References: <1258471718-6781-1-git-send-email-dottedmag@dottedmag.net>
+Message-ID: <1258471718-6781-2-git-send-email-dottedmag@dottedmag.net>
+
+
+Signed-off-by: Mikhail Gusarov <dottedmag at dottedmag.net>
+---
+ lib/message.cc |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/lib/message.cc b/lib/message.cc
+index 72c350f..a4b090b 100644
+--- a/lib/message.cc
++++ b/lib/message.cc
+@@ -21,6 +21,8 @@
+ #include "notmuch-private.h"
+ #include "database-private.h"
++#include <stdint.h>
++
+ #include <gmime/gmime.h>
+ #include <xapian.h>
+-- 
+1.6.3.3
+
+
diff --git a/test/corpora/default/bar/17:2, b/test/corpora/default/bar/17:2,
new file mode 100644 (file)
index 0000000..d3b7568
--- /dev/null
@@ -0,0 +1,23 @@
+From: "Israel Herraiz" <isra@herraiz.org>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 23:57:18 +0100
+Subject: [notmuch] New to the list
+Message-ID: <1258498485-sup-142@elly>
+
+Hi all,
+
+I have subscribed to the list. As suggested by the welcome message, I
+am introducing myself. My name is Israel Herraiz, and I have done a
+couple of contributions to Sup, the probably well-known here e-mail
+client.
+
+"Not much" sounds interesting, and I wonder whether it could be
+integrated with the views of Sup (inbox, threads, etc). So I have
+subscribed to the list to keep an eye on what's going on here.
+
+I have just heard of "Not much". I have not even tried to download the
+code yet.
+
+Cheers,
+Israel
+
diff --git a/test/corpora/default/bar/18:2, b/test/corpora/default/bar/18:2,
new file mode 100644 (file)
index 0000000..f522f69
--- /dev/null
@@ -0,0 +1,12 @@
+From: "Aron Griffis" <agriffis@n01se.net>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 18:21:38 -0500
+Subject: [notmuch] archive
+Message-ID: <20091117232137.GA7669@griffis1.net>
+
+Just subscribed, I'd like to catch up on the previous postings,
+but the archive link seems to be bogus?
+
+Thanks,
+Aron
+
diff --git a/test/corpora/default/bar/baz/05:2, b/test/corpora/default/bar/baz/05:2,
new file mode 100644 (file)
index 0000000..ce174b5
--- /dev/null
@@ -0,0 +1,104 @@
+MIME-Version: 1.0
+Date: Tue, 17 Nov 2009 11:36:14 -0800
+Message-ID: <cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com>
+From: Alex Botero-Lowry <alex.boterolowry@gmail.com>
+To: notmuch@notmuchmail.org
+Content-Type: multipart/mixed; boundary=0016e687869333b1570478963d35
+Subject: [notmuch] preliminary FreeBSD support
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+Sender: notmuch-bounces@notmuchmail.org
+Errors-To: notmuch-bounces@notmuchmail.org
+
+--0016e687869333b1570478963d35
+Content-Type: multipart/alternative; boundary=0016e687869333b14e0478963d33
+
+--0016e687869333b14e0478963d33
+Content-Type: text/plain; charset=ISO-8859-1
+
+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.
+
+--0016e687869333b14e0478963d33
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: quoted-printable
+
+I saw the announcement this morning, and was very excited, as I had been ho=
+ping sup would be turned into a library,<br>since I like the concept more t=
+han the UI (I&#39;d rather an emacs interface).<br><br>I did a preliminary =
+compile which worked out fine, but sysconf(_SC_SC_GETPW_R_SIZE_MAX) returns=
+ -1 on<br>
+FreeBSD, so notmuch_config_open segfaulted.<br><br>Attached is a patch that=
+ supplies a default buffer size of 64 in cases where -1 is returned.<br><br=
+><a href=3D"http://www.opengroup.org/austin/docs/austin_328.txt">http://www=
+.opengroup.org/austin/docs/austin_328.txt</a> - seems to indicate this is a=
+cceptable behavior,<br>
+and <a href=3D"http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg01680=
+8.html">http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg016808.html<=
+/a> specifically uses 64 as the<br>buffer size.<br><br><br>
+
+--0016e687869333b14e0478963d33--
+--0016e687869333b1570478963d35
+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"
+Content-Transfer-Encoding: base64
+X-Attachment-Id: f_g252e6gs0
+
+RnJvbSBlM2JjNGJiZDdiOWQwZDA4NjgxNmFiNWY4ZjJkNmZmZWExZGQzZWE0IE1vbiBTZXAgMTcg
+MDA6MDA6MDAgMjAwMQpGcm9tOiBBbGV4YW5kZXIgQm90ZXJvLUxvd3J5IDxhbGV4LmJvdGVyb2xv
+d3J5QGdtYWlsLmNvbT4KRGF0ZTogVHVlLCAxNyBOb3YgMjAwOSAxMTozMDozOSAtMDgwMApTdWJq
+ZWN0OiBbUEFUQ0hdIERlYWwgd2l0aCBzaXR1YXRpb24gd2hlcmUgc3lzY29uZihfU0NfR0VUUFdf
+Ul9TSVpFX01BWCkgcmV0dXJucyAtMQoKLS0tCiBub3RtdWNoLWNvbmZpZy5jIHwgICAgMiArKwog
+MSBmaWxlcyBjaGFuZ2VkLCAyIGluc2VydGlvbnMoKyksIDAgZGVsZXRpb25zKC0pCgpkaWZmIC0t
+Z2l0IGEvbm90bXVjaC1jb25maWcuYyBiL25vdG11Y2gtY29uZmlnLmMKaW5kZXggMjQ4MTQ5Yy4u
+ZTcyMjBkOCAxMDA2NDQKLS0tIGEvbm90bXVjaC1jb25maWcuYworKysgYi9ub3RtdWNoLWNvbmZp
+Zy5jCkBAIC03Nyw2ICs3Nyw3IEBAIHN0YXRpYyBjaGFyICoKIGdldF9uYW1lX2Zyb21fcGFzc3dk
+X2ZpbGUgKHZvaWQgKmN0eCkKIHsKICAgICBsb25nIHB3X2J1Zl9zaXplID0gc3lzY29uZihfU0Nf
+R0VUUFdfUl9TSVpFX01BWCk7CisgICAgaWYgKHB3X2J1Zl9zaXplID09IC0xKSBwd19idWZfc2l6
+ZSA9IDY0OwogICAgIGNoYXIgKnB3X2J1ZiA9IHRhbGxvY196ZXJvX3NpemUgKGN0eCwgcHdfYnVm
+X3NpemUpOwogICAgIHN0cnVjdCBwYXNzd2QgcGFzc3dkLCAqaWdub3JlZDsKICAgICBjaGFyICpu
+YW1lOwpAQCAtMTAxLDYgKzEwMiw3IEBAIHN0YXRpYyBjaGFyICoKIGdldF91c2VybmFtZV9mcm9t
+X3Bhc3N3ZF9maWxlICh2b2lkICpjdHgpCiB7CiAgICAgbG9uZyBwd19idWZfc2l6ZSA9IHN5c2Nv
+bmYoX1NDX0dFVFBXX1JfU0laRV9NQVgpOworICAgIGlmIChwd19idWZfc2l6ZSA9PSAtMSkgcHdf
+YnVmX3NpemUgPSA2NDsKICAgICBjaGFyICpwd19idWYgPSB0YWxsb2NfemVyb19zaXplIChjdHgs
+IHB3X2J1Zl9zaXplKTsKICAgICBzdHJ1Y3QgcGFzc3dkIHBhc3N3ZCwgKmlnbm9yZWQ7CiAgICAg
+Y2hhciAqbmFtZTsKLS0gCjEuNi41LjIKCg==
+--0016e687869333b1570478963d35
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+--0016e687869333b1570478963d35--
+
diff --git a/test/corpora/default/bar/baz/23:2, b/test/corpora/default/bar/baz/23:2,
new file mode 100644 (file)
index 0000000..9bb62d7
--- /dev/null
@@ -0,0 +1,145 @@
+Date: Tue, 17 Nov 2009 19:58:29 -0500
+From: Lars Kellogg-Stedman <lars@seas.harvard.edu>
+To: notmuch <notmuch@notmuchmail.org>
+Message-ID: <20091118005829.GB25380@dottiness.seas.harvard.edu>
+MIME-Version: 1.0
+User-Agent: Mutt/1.5.19 (2009-01-05)
+Subject: [notmuch] "notmuch help" outputs to stderr?
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============1359248349=="
+Sender: notmuch-bounces@notmuchmail.org
+Errors-To: notmuch-bounces@notmuchmail.org
+
+
+--===============1359248349==
+Content-Type: multipart/signed; micalg=pgp-sha256;
+       protocol="application/pgp-signature"; boundary="L6iaP+gRLNZHKoI4"
+Content-Disposition: inline
+
+
+--L6iaP+gRLNZHKoI4
+Content-Type: multipart/mixed; boundary="z6Eq5LdranGa6ru8"
+Content-Disposition: inline
+
+
+--z6Eq5LdranGa6ru8
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+I'm just noticing that 'notmuch help ...' outputs to stderr, which
+isn't terribly intuitive.  For example, the obvious invocation:
+
+  notmuch help | less
+
+=2E..isn't terribly helpful.
+
+I've attached a patch that lets usage() take a FILE * argument so that
+you can output to stderr in response to usage errors, and stdout in
+response to an explicit request.
+
+--=20
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+
+
+--z6Eq5LdranGa6ru8
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: attachment; filename="notmuch-help.patch"
+Content-Transfer-Encoding: quoted-printable
+
+diff --git a/notmuch.c b/notmuch.c
+index c47e640..a35cb99 100644
+--- a/notmuch.c
++++ b/notmuch.c
+@@ -157,23 +157,23 @@ command_t commands[] =3D {
+ };
+=20
+ static void
+-usage (void)
++usage (FILE *out)
+ {
+     command_t *command;
+     unsigned int i;
+=20
+-    fprintf (stderr, "Usage: notmuch <command> [args...]\n");
+-    fprintf (stderr, "\n");
+-    fprintf (stderr, "Where <command> and [args...] are as follows:\n");
+-    fprintf (stderr, "\n");
++    fprintf (out, "Usage: notmuch <command> [args...]\n");
++    fprintf (out, "\n");
++    fprintf (out, "Where <command> and [args...] are as follows:\n");
++    fprintf (out, "\n");
+=20
+     for (i =3D 0; i < ARRAY_SIZE (commands); i++) {
+       command =3D &commands[i];
+=20
+-      fprintf (stderr, "\t%s\t%s\n\n", command->name, command->summary);
++      fprintf (out, "\t%s\t%s\n\n", command->name, command->summary);
+     }
+=20
+-    fprintf (stderr, "Use \"notmuch help <command>\" for more details on e=
+ach command.\n\n");
++    fprintf (out, "Use \"notmuch help <command>\" for more details on each=
+ command.\n\n");
+ }
+=20
+ static int
+@@ -183,8 +183,8 @@ notmuch_help_command (unused (void *ctx), int argc, cha=
+r *argv[])
+     unsigned int i;
+=20
+     if (argc =3D=3D 0) {
+-      fprintf (stderr, "The notmuch mail system.\n\n");
+-      usage ();
++      fprintf (stdout, "The notmuch mail system.\n\n");
++      usage (stdout);
+       return 0;
+     }
+=20
+
+--z6Eq5LdranGa6ru8--
+
+--L6iaP+gRLNZHKoI4
+Content-Type: application/pgp-signature
+Content-Disposition: inline
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.9 (GNU/Linux)
+
+iQEcBAEBCAAGBQJLA0a1AAoJENdGlQYxQazYr78IAJtqTWIpBqSdOWqTzt/r4XNn
+KJ5mWAoNfq4H+3kx3xoWOFYS7qAYeJoHQWCDbMdb+zEXvPX6hMFn9+OxRN+N5FdQ
+uxGTugSG9xSsK28oGDCQUtr5uheo+tH0jygPjI+LTD97vjUYS4K2qzhLGFJmpLcj
+1akMJXM0gSdPZT8dJyjxvC15pgboLspE4+b6jexXmd4UoFvXgqvjkYHeV4Wk+s0L
+xu+HkCGXL9WHYc3t171fFAru4Zd1AUxFQl4BZ2Y+OqRZUrD28Mtz3zGQxbJQoifl
+JFrgPAWioLN71SkVq/y+efjvGSl0osPpKU5dftMmyY1zV7k7mMlO08ZSJU+wANA=
+=Iijt
+-----END PGP SIGNATURE-----
+
+--L6iaP+gRLNZHKoI4--
+
+--===============1359248349==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+--===============1359248349==--
+
diff --git a/test/corpora/default/bar/baz/24:2, b/test/corpora/default/bar/baz/24:2,
new file mode 100644 (file)
index 0000000..c800020
--- /dev/null
@@ -0,0 +1,204 @@
+Return-path: <notmuch-bounces@notmuchmail.org>
+Envelope-to: cworth@localhost
+Delivery-date: Wed, 18 Nov 2009 01:43:47 -0800
+Received: from yoom.home.cworth.org ([127.0.0.1])
+       by yoom.home.cworth.org with esmtp (Exim 4.69)
+       (envelope-from <notmuch-bounces@notmuchmail.org>)
+       id 1NAgpH-0005Ab-20
+       for cworth@localhost; Wed, 18 Nov 2009 01:27:47 -0800
+X-Original-To: cworth@cworth.org
+Delivered-To: cworth@cworth.org
+Received: from olra.theworths.org [82.165.184.25]
+       by yoom.home.cworth.org with IMAP (fetchmail-6.3.9-rc2)
+       for <cworth@localhost> (single-drop); Wed, 18 Nov 2009 01:27:47 -0800 (PST)
+Received: from localhost (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id 12248431FC3
+       for <cworth@cworth.org>; Tue, 17 Nov 2009 17:01:22 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
+X-Spam-Flag: NO
+X-Spam-Score: -6.17
+X-Spam-Level: 
+X-Spam-Status: No, score=-6.17 tagged_above=-999 required=2 tests=[AWL=0.429,
+       BAYES_00=-2.599, RCVD_IN_DNSWL_MED=-4] autolearn=unavailable
+Received: from olra.theworths.org ([127.0.0.1])
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id TmBdVd1i-Wjb; Tue, 17 Nov 2009 17:01:20 -0800 (PST)
+Received: from olra.theworths.org (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id AF876431FBC;
+       Tue, 17 Nov 2009 17:01:20 -0800 (PST)
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id 75784431FBC
+       for <notmuch@notmuchmail.org>; Tue, 17 Nov 2009 17:01:19 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
+Received: from olra.theworths.org ([127.0.0.1])
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id IoYHzHoKBskU for <notmuch@notmuchmail.org>;
+       Tue, 17 Nov 2009 17:01:18 -0800 (PST)
+Received: from smtp-outbound.seas.harvard.edu (smtp-outbound.seas.harvard.edu
+       [140.247.51.171])
+       by olra.theworths.org (Postfix) with ESMTP id 7E033431FAE
+       for <notmuch@notmuchmail.org>; Tue, 17 Nov 2009 17:01:18 -0800 (PST)
+Received: from dottiness.seas.harvard.edu (dottiness.seas.harvard.edu
+       [140.247.52.224])
+       by smtp-outbound.seas.harvard.edu (8.13.8/8.13.8) with SMTP id
+       nAI11Gkj008772
+       for <notmuch@notmuchmail.org>; Tue, 17 Nov 2009 20:01:16 -0500
+Received: by dottiness.seas.harvard.edu (sSMTP sendmail emulation);
+       Tue, 17 Nov 2009 20:01:16 -0500
+Date: Tue, 17 Nov 2009 20:01:16 -0500
+From: Lars Kellogg-Stedman <lars@seas.harvard.edu>
+To: notmuch <notmuch@notmuchmail.org>
+Message-ID: <20091118010116.GC25380@dottiness.seas.harvard.edu>
+References: <20091118005829.GB25380@dottiness.seas.harvard.edu>
+MIME-Version: 1.0
+In-Reply-To: <20091118005829.GB25380@dottiness.seas.harvard.edu>
+User-Agent: Mutt/1.5.19 (2009-01-05)
+Subject: Re: [notmuch] "notmuch help" outputs to stderr?
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============0848253760=="
+Sender: notmuch-bounces@notmuchmail.org
+Errors-To: notmuch-bounces@notmuchmail.org
+
+
+--===============0848253760==
+Content-Type: multipart/signed; micalg=pgp-sha256;
+       protocol="application/pgp-signature"; boundary="ZInfyf7laFu/Kiw7"
+Content-Disposition: inline
+
+
+--ZInfyf7laFu/Kiw7
+Content-Type: multipart/mixed; boundary="KdquIMZPjGJQvRdI"
+Content-Disposition: inline
+
+
+--KdquIMZPjGJQvRdI
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+> I've attached a patch that lets usage() take a FILE * argument so that
+> you can output to stderr in response to usage errors, and stdout in
+> response to an explicit request.
+
+Whoops, missed a couple of stderr's in that last patch.  New one
+attached.
+
+--=20
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+
+
+--KdquIMZPjGJQvRdI
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: attachment; filename="notmuch-help.patch"
+Content-Transfer-Encoding: quoted-printable
+
+diff --git a/notmuch.c b/notmuch.c
+index c47e640..446c810 100644
+--- a/notmuch.c
++++ b/notmuch.c
+@@ -157,23 +157,23 @@ command_t commands[] =3D {
+ };
+=20
+ static void
+-usage (void)
++usage (FILE *out)
+ {
+     command_t *command;
+     unsigned int i;
+=20
+-    fprintf (stderr, "Usage: notmuch <command> [args...]\n");
+-    fprintf (stderr, "\n");
+-    fprintf (stderr, "Where <command> and [args...] are as follows:\n");
+-    fprintf (stderr, "\n");
++    fprintf (out, "Usage: notmuch <command> [args...]\n");
++    fprintf (out, "\n");
++    fprintf (out, "Where <command> and [args...] are as follows:\n");
++    fprintf (out, "\n");
+=20
+     for (i =3D 0; i < ARRAY_SIZE (commands); i++) {
+       command =3D &commands[i];
+=20
+-      fprintf (stderr, "\t%s\t%s\n\n", command->name, command->summary);
++      fprintf (out, "\t%s\t%s\n\n", command->name, command->summary);
+     }
+=20
+-    fprintf (stderr, "Use \"notmuch help <command>\" for more details on e=
+ach command.\n\n");
++    fprintf (out, "Use \"notmuch help <command>\" for more details on each=
+ command.\n\n");
+ }
+=20
+ static int
+@@ -183,8 +183,8 @@ notmuch_help_command (unused (void *ctx), int argc, cha=
+r *argv[])
+     unsigned int i;
+=20
+     if (argc =3D=3D 0) {
+-      fprintf (stderr, "The notmuch mail system.\n\n");
+-      usage ();
++      fprintf (stdout, "The notmuch mail system.\n\n");
++      usage (stdout);
+       return 0;
+     }
+=20
+@@ -192,8 +192,8 @@ notmuch_help_command (unused (void *ctx), int argc, cha=
+r *argv[])
+       command =3D &commands[i];
+=20
+       if (strcmp (argv[0], command->name) =3D=3D 0) {
+-          fprintf (stderr, "Help for \"notmuch %s\":\n\n", argv[0]);
+-          fprintf (stderr, "\t%s\t%s\n\n%s\n\n", command->name,
++          fprintf (stdout, "Help for \"notmuch %s\":\n\n", argv[0]);
++          fprintf (stdout, "\t%s\t%s\n\n%s\n\n", command->name,
+                    command->summary, command->documentation);
+           return 0;
+       }
+
+--KdquIMZPjGJQvRdI--
+
+--ZInfyf7laFu/Kiw7
+Content-Type: application/pgp-signature
+Content-Disposition: inline
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.9 (GNU/Linux)
+
+iQEcBAEBCAAGBQJLA0dcAAoJENdGlQYxQazY4nIIAIBCds86/uTmnouvyoPruUUR
+Bg5mXcnjuopz1Nwotl9s9U5sGeZuZngxyEvDz1Z1aTEjwab8ndNTf1xCwIoqBs+l
+i/sc4nPYubLdy1Ab/84DKVtCSbj+v5rtqhegwUWV7S1BY7t8dKNPNv7YBg7P0Azs
+6s3CUxDV5eJCcxCGxxWHH8JDKRf7rDs6vzDwyPWLxlg1Xb1lEM/sRgPCKiShPdO3
+Ak2hECusjskALhSDYX8/FLMd9HwLBC13sfWuSi/pHUAIOI2jru2p5sXrVSlTnFIJ
+fiMbPhKWiEaJj2kmm4pRwAhbTWp/J8ZvXWp0AyosxXQhQUWqujiyxgfiXS70SdQ=
+=t3Yc
+-----END PGP SIGNATURE-----
+
+--ZInfyf7laFu/Kiw7--
+
+--===============0848253760==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+--===============0848253760==--
+
diff --git a/test/corpora/default/bar/baz/cur/25:2, b/test/corpora/default/bar/baz/cur/25:2,
new file mode 100644 (file)
index 0000000..7378f82
--- /dev/null
@@ -0,0 +1,32 @@
+From: "Stewart Smith" <stewart@flamingspork.com>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 12:05:53 +1100
+Subject: [notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++
+       libs.
+Message-ID: <1258506353-20352-1-git-send-email-stewart@flamingspork.com>
+
+Previously, Ubuntu 9.10, gcc 4.4.1 was getting:
+
+ccache gcc `pkg-config --libs glib-2.0 gmime-2.4 talloc` `xapian-config --libs` notmuch.o notmuch-config.o notmuch-dump.o notmuch-new.o notmuch-reply.o notmuch-restore.o notmuch-search.o notmuch-setup.o notmuch-show.o notmuch-tag.o notmuch-time.o gmime-filter-reply.o query-string.o show-message.o lib/notmuch.a -o notmuch
+/usr/bin/ld: lib/notmuch.a(database.o): in function global constructors keyed to BOOLEAN_PREFIX_INTERNAL:database.cc(.text+0x3a): error: undefined reference to 'std::ios_base::Init::Init()'
+---
+ Makefile.local |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/Makefile.local b/Makefile.local
+index f824bed..dbd3e20 100644
+--- a/Makefile.local
++++ b/Makefile.local
+@@ -18,7 +18,7 @@ notmuch_client_srcs =                \
+ notmuch_client_modules = $(notmuch_client_srcs:.c=.o)
+ notmuch: $(notmuch_client_modules) lib/notmuch.a
+-      $(CC) $(LDFLAGS) $^ -o $@
++      $(CXX) $(LDFLAGS) $^ -o $@
+ notmuch.1.gz:
+       gzip --stdout notmuch.1 > notmuch.1.gz
+-- 
+1.6.3.3
+
+
diff --git a/test/corpora/default/bar/baz/cur/26:2, b/test/corpora/default/bar/baz/cur/26:2,
new file mode 100644 (file)
index 0000000..f3c5f53
--- /dev/null
@@ -0,0 +1,121 @@
+From: "Stewart Smith" <stewart@flamingspork.com>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 12:56:40 +1100
+Subject: [notmuch] [PATCH 2/2] Read mail directory in inode number order
+Message-ID: <1258509400-32511-1-git-send-email-stewart@flamingspork.com>
+
+This gives a rather decent reduction in number of seeks required when
+reading a Maildir that isn't in pagecache.
+
+Most filesystems give some locality on disk based on inode numbers.
+In ext[234] this is the inode tables, in XFS groups of sequential inode
+numbers are together on disk and the most significant bits indicate
+allocation group (i.e inode 1,000,000 is always after inode 1,000).
+
+With this patch, we read in the whole directory, sort by inode number
+before stat()ing the contents.
+
+Ideally, directory is sequential and then we make one scan through the
+file system stat()ing.
+
+Since the universe is not ideal, we'll probably seek during reading the
+directory and a fair bit while reading the inodes themselves.
+
+However... with readahead, and stat()ing in inode order, we should be
+in the best place possible to hit the cache.
+
+In a (not very good) benchmark of "how long does it take to find the first
+15,000 messages in my Maildir after 'echo 3 > /proc/sys/vm/drop_caches'",
+this patch consistently cut at least 8 seconds off the scan time.
+
+Without patch: 50 seconds
+With patch: 38-42 seconds.
+
+(I did this in a previous maildir reading project and saw large improvements too)
+---
+ notmuch-new.c |   32 +++++++++++++++-----------------
+ 1 files changed, 15 insertions(+), 17 deletions(-)
+
+diff --git a/notmuch-new.c b/notmuch-new.c
+index 83a05ba..11fad8c 100644
+--- a/notmuch-new.c
++++ b/notmuch-new.c
+@@ -73,6 +73,11 @@ add_files_print_progress (add_files_state_t *state)
+     fflush (stdout);
+ }
++static int ino_cmp(const struct dirent **a, const struct dirent **b)
++{
++  return ((*a)->d_ino < (*b)->d_ino)? -1: 1;
++}
++
+ /* Examine 'path' recursively as follows:
+  *
+  *   o Ask the filesystem for the mtime of 'path' (path_mtime)
+@@ -100,13 +105,12 @@ add_files_recursive (notmuch_database_t *notmuch,
+                    add_files_state_t *state)
+ {
+     DIR *dir = NULL;
+-    struct dirent *e, *entry = NULL;
+-    int entry_length;
+-    int err;
++    struct dirent *entry = NULL;
+     char *next = NULL;
+     time_t path_mtime, path_dbtime;
+     notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
+     notmuch_message_t *message = NULL;
++    struct dirent **namelist = NULL;
+     /* If we're told to, we bail out on encountering a read-only
+      * directory, (with this being a clear clue from the user to
+@@ -122,31 +126,23 @@ add_files_recursive (notmuch_database_t *notmuch,
+     path_mtime = st->st_mtime;
+     path_dbtime = notmuch_database_get_timestamp (notmuch, path);
++    int n_entries= scandir(path, &namelist, 0, ino_cmp);
+-    dir = opendir (path);
+-    if (dir == NULL) {
++    if (n_entries == -1) {
+       fprintf (stderr, "Error opening directory %s: %s\n",
+                path, strerror (errno));
+       ret = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
+     }
+-    entry_length = offsetof (struct dirent, d_name) +
+-      pathconf (path, _PC_NAME_MAX) + 1;
+-    entry = malloc (entry_length);
++    int i=0;
+     while (!interrupted) {
+-      err = readdir_r (dir, entry, &e);
+-      if (err) {
+-          fprintf (stderr, "Error reading directory: %s\n",
+-                   strerror (errno));
+-          ret = NOTMUCH_STATUS_FILE_ERROR;
+-          goto DONE;
+-      }
+-
+-      if (e == NULL)
++      if (i == n_entries)
+           break;
++        entry= namelist[i++];
++
+       /* If this directory hasn't been modified since the last
+        * add_files, then we only need to look further for
+        * sub-directories. */
+@@ -243,6 +239,8 @@ add_files_recursive (notmuch_database_t *notmuch,
+       free (entry);
+     if (dir)
+       closedir (dir);
++    if (namelist)
++      free (namelist);
+     return ret;
+ }
+-- 
+1.6.3.3
+
+
diff --git a/test/corpora/default/bar/baz/new/27:2, b/test/corpora/default/bar/baz/new/27:2,
new file mode 100644 (file)
index 0000000..7f0f045
--- /dev/null
@@ -0,0 +1,21 @@
+From: "Keith Packard" <keithp@keithp.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 17:59:49 -0800
+Subject: [notmuch] New to the list
+In-Reply-To: <1258498485-sup-142@elly>
+References: <1258498485-sup-142@elly>
+Message-ID: <yun3a4cegoa.fsf@aiko.keithp.com>
+
+On Tue, 17 Nov 2009 23:57:18 +0100, Israel Herraiz <isra at herraiz.org> wrote:
+
+> "Not much" sounds interesting, and I wonder whether it could be
+> integrated with the views of Sup (inbox, threads, etc). So I have
+> subscribed to the list to keep an eye on what's going on here.
+
+We've tried to clone much of the sup UI inside emacs, including the
+inbox and threaded message presentation. Of course, we had to "improve"
+it a bit, as much due to the differences between curses and emacs as due
+to personal preferences...
+
+-keith
+
diff --git a/test/corpora/default/bar/baz/new/28:2, b/test/corpora/default/bar/baz/new/28:2,
new file mode 100644 (file)
index 0000000..83ce01b
--- /dev/null
@@ -0,0 +1,38 @@
+From: "Keith Packard" <keithp@keithp.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 18:03:17 -0800
+Subject: [notmuch] Introducing myself
+In-Reply-To: <20091118002059.067214ed@hikari>
+References: <20091118002059.067214ed@hikari>
+Message-ID: <yun1vjwegii.fsf@aiko.keithp.com>
+
+On Wed, 18 Nov 2009 00:20:59 +0100, Adrian Perez de Castro <aperez at igalia.com> wrote:
+
+> Some time ago I thought
+> about doing something like Not Much and in fact I played a bit with the
+> Python+Xapian and the Python+Whoosh combinations, because I find relaxing
+> to code things in Python when I am not working and also it is installed
+> by default on most distribution. I got to have some mailboxes indexed and
+> basic searching working a couple of months ago.
+
+Sup certainly started a lot of people thinking...
+
+> Also, I would like to share one idea I had in mind, that you might find
+> interesting: One thing I have found very annoying is having to re-tag my
+> mail when the indexes get b0rked (it happened a couple of times to me while
+> using Sup), so I was planning to mails as read/unread and adding the tags
+> not just to the index, but to the mail text itself, e.g. by adding a
+> "X-Tags" header field or by reusing the "Keywords" one.
+
+Easier than that, notmuch (and sup too), provide a 'dump' command which
+just lists all of the message IDs and their associated tags. Makes
+saving tags easy and doesn't involve rewriting messages. I do this once
+a day just before my computer is backed up to an external drive.
+
+If the index is destroyed, you can reindex the messages and then reapply
+all of the tags with 'notmuch restore'.
+
+--
+keith.packard at intel.com
+
+
diff --git a/test/corpora/default/bar/cur/19:2, b/test/corpora/default/bar/cur/19:2,
new file mode 100644 (file)
index 0000000..1b7872b
--- /dev/null
@@ -0,0 +1,360 @@
+From: "Ingmar Vanhassel" <ingmar@exherbo.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 00:23:42 +0100
+Subject: [notmuch] [PATCH] Typsos
+Message-ID: <1258500222-32066-1-git-send-email-ingmar@exherbo.org>
+
+---
+ Makefile                |    4 ++--
+ README                  |    6 +++---
+ gmime-filter-reply.h    |    2 +-
+ lib/database.cc         |    2 +-
+ lib/index.cc            |    2 +-
+ lib/message.cc          |    2 +-
+ lib/messages.c          |    2 +-
+ lib/notmuch-private.h   |    2 +-
+ lib/notmuch.h           |   10 +++++-----
+ lib/sha1.c              |    2 +-
+ lib/thread.cc           |    2 +-
+ notmuch-completion.bash |    2 +-
+ notmuch-new.c           |    4 ++--
+ notmuch-search.c        |    2 +-
+ notmuch.1               |    4 ++--
+ notmuch.el              |   10 +++++-----
+ show-message.c          |    2 +-
+ 17 files changed, 30 insertions(+), 30 deletions(-)
+
+diff --git a/Makefile b/Makefile
+index 436dacf..96aaa73 100644
+--- a/Makefile
++++ b/Makefile
+@@ -1,4 +1,4 @@
+-# Default FLAGS, (can be overriden by user such as "make CFLAGS=-O2")
++# Default FLAGS, (can be overridden by user such as "make CFLAGS=-O2")
+ WARN_FLAGS=-Wall -Wextra -Wmissing-declarations -Wwrite-strings -Wswitch-enum
+ CFLAGS=-O2
+@@ -14,7 +14,7 @@ override CXXFLAGS += $(WARN_FLAGS) $(extra_cflags) $(extra_cxxflags)
+ override LDFLAGS += `pkg-config --libs glib-2.0 gmime-2.4 talloc` \
+                       `xapian-config --libs`
+-# Include our local Makfile.local first so that its first target is default
++# Include our local Makefile.local first so that its first target is default
+ include Makefile.local
+ include lib/Makefile.local
+diff --git a/README b/README
+index 40f05ab..27af77f 100644
+--- a/README
++++ b/README
+@@ -3,7 +3,7 @@ Notmuch - thread-based email index, search and tagging.
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages. It uses the Xapian library to
+ provide fast, full-text search of very large collection of email with
+-a very convenient search syntas.
++a very convenient search syntax.
+ Notmuch is free software, released under the GNU General Public
+ License version 3 (or later).
+@@ -45,7 +45,7 @@ obtaining a more sophisticated interface:
+       notmuch.el file in this distribution.
+       If someone were to write a curses-based interface, or similar,
+-      it might also be reasonable to buil on the "notmuch"
++      it might also be reasonable to build on the "notmuch"
+       command-line interface.
+      2. Build on top of the notmuch library interface.
+@@ -67,4 +67,4 @@ still in development. We would appreciate any contributions to these
+ efforts.
+-      
+\ No newline at end of file
++      
+diff --git a/gmime-filter-reply.h b/gmime-filter-reply.h
+index 41cbc13..b7cbc6b 100644
+--- a/gmime-filter-reply.h
++++ b/gmime-filter-reply.h
+@@ -40,7 +40,7 @@ typedef struct _GMimeFilterReplyClass GMimeFilterReplyClass;
+  * @saw_nl: previous char was a \n
+  * @saw_angle: previous char was a >
+  *
+- * A filter to insert/remove reply markers (lines begining with >)
++ * A filter to insert/remove reply markers (lines beginning with >)
+  **/
+ struct _GMimeFilterReply {
+       GMimeFilter parent_object;
+diff --git a/lib/database.cc b/lib/database.cc
+index 3c8d626..27597cf 100644
+--- a/lib/database.cc
++++ b/lib/database.cc
+@@ -180,7 +180,7 @@ notmuch_status_to_string (notmuch_status_t status)
+     case NOTMUCH_STATUS_TAG_TOO_LONG:
+       return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
+     case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
+-      return "Unblanced number of calls to notmuch_message_freeze/thaw";
++      return "Unbalanced number of calls to notmuch_message_freeze/thaw";
+     default:
+     case NOTMUCH_STATUS_LAST_STATUS:
+       return "Unknown error status value";
+diff --git a/lib/index.cc b/lib/index.cc
+index 65b83b3..80df64b 100644
+--- a/lib/index.cc
++++ b/lib/index.cc
+@@ -198,7 +198,7 @@ _index_mime_part (notmuch_message_t *message,
+               if (i == 1)
+                   continue;
+               if (i > 1)
+-                  fprintf (stderr, "Warning: Unexpected extra parts of mutlipart/signed. Indexing anyway.\n");
++                  fprintf (stderr, "Warning: Unexpected extra parts of multipart/signed. Indexing anyway.\n");
+           }
+           _index_mime_part (message,
+                             g_mime_multipart_get_part (multipart, i));
+diff --git a/lib/message.cc b/lib/message.cc
+index a4b090b..1d6623f 100644
+--- a/lib/message.cc
++++ b/lib/message.cc
+@@ -144,7 +144,7 @@ _notmuch_message_create (const void *talloc_owner,
+ }
+ /* Create a new notmuch_message_t object for a specific message ID,
+- * (which may or may not already exist in the databas).
++ * (which may or may not already exist in the database).
+  *
+  * Here, 'talloc owner' is an optional talloc context to which the new
+  * message will belong. This allows for the caller to not bother
+diff --git a/lib/messages.c b/lib/messages.c
+index a588f8f..2f7c283 100644
+--- a/lib/messages.c
++++ b/lib/messages.c
+@@ -47,7 +47,7 @@ _notmuch_message_list_create (const void *ctx)
+     return list;
+ }
+-/* Append 'node' (which can of course point to an aribtrarily long
++/* Append 'node' (which can of course point to an arbitrarily long
+  * list of nodes) to the end of 'list'.
+  */
+ void
+diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h
+index 6036ce4..af82e58 100644
+--- a/lib/notmuch-private.h
++++ b/lib/notmuch-private.h
+@@ -235,7 +235,7 @@ notmuch_message_file_open (const char *filename);
+ notmuch_message_file_t *
+ _notmuch_message_file_open_ctx (void *ctx, const char *filename);
+-/* Close a notmuch message preivously opened with notmuch_message_open. */
++/* Close a notmuch message previously opened with notmuch_message_open. */
+ void
+ notmuch_message_file_close (notmuch_message_file_t *message);
+diff --git a/lib/notmuch.h b/lib/notmuch.h
+index 32b5332..384c177 100644
+--- a/lib/notmuch.h
++++ b/lib/notmuch.h
+@@ -222,7 +222,7 @@ notmuch_database_get_timestamp (notmuch_database_t *database,
+ /* Add a new message to the given notmuch database.
+  *
+- * Here,'filename' should be a path relative to the the path of
++ * Here,'filename' should be a path relative to the path of
+  * 'database' (see notmuch_database_get_path), or else should be an
+  * absolute filename with initial components that match the path of
+  * 'database'.
+@@ -258,7 +258,7 @@ notmuch_database_add_message (notmuch_database_t *database,
+                             const char *filename,
+                             notmuch_message_t **message);
+-/* Find a message with the given messsage_id.
++/* Find a message with the given message_id.
+  *
+  * If the database contains a message with the given message_id, then
+  * a new notmuch_message_t object is returned. The caller should call
+@@ -620,7 +620,7 @@ notmuch_messages_advance (notmuch_messages_t *messages);
+ /* Destroy a notmuch_messages_t object.
+  *
+  * It's not strictly necessary to call this function. All memory from
+- * the notmuch_messages_t object will be reclaimed when the containg
++ * the notmuch_messages_t object will be reclaimed when the containing
+  * query object is destroyed.
+  */
+ void
+@@ -865,7 +865,7 @@ notmuch_tags_has_more (notmuch_tags_t *tags);
+ /* Get the current tag from 'tags' as a string.
+  *
+  * Note: The returned string belongs to 'tags' and has a lifetime
+- * identical to it (and the query to which it utlimately belongs).
++ * identical to it (and the query to which it ultimately belongs).
+  *
+  * See the documentation of notmuch_message_get_tags for example code
+  * showing how to iterate over a notmuch_tags_t object.
+@@ -884,7 +884,7 @@ notmuch_tags_advance (notmuch_tags_t *tags);
+ /* Destroy a notmuch_tags_t object.
+  *
+  * It's not strictly necessary to call this function. All memory from
+- * the notmuch_tags_t object will be reclaimed when the containg
++ * the notmuch_tags_t object will be reclaimed when the containing
+  * message or query objects are destroyed.
+  */
+ void
+diff --git a/lib/sha1.c b/lib/sha1.c
+index ff4dd16..cc48108 100644
+--- a/lib/sha1.c
++++ b/lib/sha1.c
+@@ -43,7 +43,7 @@ _hex_of_sha1_digest (const unsigned char digest[SHA1_DIGEST_SIZE])
+     return result;
+ }
+-/* Create a hexadcimal string version of the SHA-1 digest of 'str'
++/* Create a hexadecimal string version of the SHA-1 digest of 'str'
+  * (including its null terminating character).
+  *
+  * This function returns a newly allocated string which the caller
+diff --git a/lib/thread.cc b/lib/thread.cc
+index 4411d64..da58edc 100644
+--- a/lib/thread.cc
++++ b/lib/thread.cc
+@@ -190,7 +190,7 @@ _resolve_thread_relationships (unused (notmuch_thread_t *thread))
+  * subject line, the total count of messages, and all authors). The
+  * second search is for all messages that are in the thread and that
+  * also match the given query_string. This is to allow for a separate
+- * count of matched messages, and to allow a viewer to diplay these
++ * count of matched messages, and to allow a viewer to display these
+  * messages differently.
+  *
+  * Here, 'ctx' is talloc context for the resulting thread object.
+diff --git a/notmuch-completion.bash b/notmuch-completion.bash
+index ad55f6d..cdad05d 100644
+--- a/notmuch-completion.bash
++++ b/notmuch-completion.bash
+@@ -1,4 +1,4 @@
+-# Bash completion for notmutch
++# Bash completion for notmuch
+ #
+ # Copyright ?? 2009 Carl Worth
+ #
+diff --git a/notmuch-new.c b/notmuch-new.c
+index 83a05ba..5405a9f 100644
+--- a/notmuch-new.c
++++ b/notmuch-new.c
+@@ -303,7 +303,7 @@ add_files (notmuch_database_t *notmuch,
+ /* XXX: This should be merged with the add_files function since it
+  * shares a lot of logic with it. */
+-/* Recursively count all regular files in path and all sub-direcotries
++/* Recursively count all regular files in path and all sub-directories
+  * of path.  The result is added to *count (which should be
+  * initialized to zero by the top-level caller before calling
+  * count_files). */
+@@ -469,7 +469,7 @@ notmuch_new_command (void *ctx,
+     if (elapsed > 1 && ! add_files_state.saw_read_only_directory) {
+       printf ("\nTip: If you have any sub-directories that are archives (that is,\n"
+-              "they will never receive new mail), marking these directores as\n"
++              "they will never receive new mail), marking these directories as\n"
+               "read-only (chmod u-w /path/to/dir) will make \"notmuch new\"\n"
+               "much more efficient (it won't even look in those directories).\n");
+     }
+diff --git a/notmuch-search.c b/notmuch-search.c
+index 8db09c7..ac81372 100644
+--- a/notmuch-search.c
++++ b/notmuch-search.c
+@@ -76,7 +76,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
+     query_str = query_string_from_args (ctx, argc, argv);
+     if (query_str == NULL) {
+-      fprintf (stderr, "Out of moemory.\n");
++      fprintf (stderr, "Out of memory.\n");
+       return 1;
+     }
+diff --git a/notmuch.1 b/notmuch.1
+index 6c3d10f..86d5f59 100644
+--- a/notmuch.1
++++ b/notmuch.1
+@@ -60,7 +60,7 @@ archives, and will then proceed to build a database that indexes the
+ mail to allow for fast search of the archive.
+ This directory can contain any number of sub-directories and should
+-primarily contain only files with indvidual email messages
++primarily contain only files with individual email messages
+ (eg. maildir or mh archives are perfect). If there are other,
+ non-email files (such as indexes maintained by other email programs)
+ then notmuch will do its best to detect those and ignore them.
+@@ -173,7 +173,7 @@ Constructs a reply template for a set of messages.
+ See the documentation of
+ .B search
+-for deatils of the supported syntax of search terms.
++for details of the supported syntax of search terms.
+ To make replying to email easier,
+ .B notmuch reply
+diff --git a/notmuch.el b/notmuch.el
+index 8894a8e..7e01ed6 100644
+--- a/notmuch.el
++++ b/notmuch.el
+@@ -205,7 +205,7 @@ Unlike builtin `next-line' this version accepts no arguments."
+ (defun notmuch-show-mark-read-then-archive-thread ()
+   "Remove \"unread\" tag from each message, then archive and show next thread.
+-Archive each message currrently shown by removing the \"unread\"
++Archive each message currently shown by removing the \"unread\"
+ and \"inbox\" tag from each. Then kill this buffer and show the
+ next thread from the search from which this thread was originally
+ shown.
+@@ -220,7 +220,7 @@ buffer."
+ (defun notmuch-show-archive-thread ()
+   "Archive each message in thread, and show next thread from search.
+-Archive each message currrently shown by removing the \"inbox\"
++Archive each message currently shown by removing the \"inbox\"
+ tag from each. Then kill this buffer and show the next thread
+ from the search from which this thread was originally shown.
+@@ -340,7 +340,7 @@ there are no more unread messages past the current point."
+       (notmuch-show-next-message)))
+ (defun notmuch-show-next-open-message ()
+-  "Advance to the the next message which is not hidden.
++  "Advance to the next message which is not hidden.
+ If read messages are currently hidden, advance to the next unread
+ message. Otherwise, advance to the next message."
+@@ -674,7 +674,7 @@ thread from that buffer can be show when done with this one)."
+       )))
+ (defvar notmuch-search-authors-width 40
+-  "Number of columns to use to diplay authors in a notmuch-search buffer.")
++  "Number of columns to use to display authors in a notmuch-search buffer.")
+ (defvar notmuch-search-mode-map
+   (let ((map (make-sparse-keymap)))
+@@ -910,7 +910,7 @@ the beginning of the buffer).
+ This command toggles the sort order for the current search.
+-Note that any fitlered searches created by
++Note that any filtered searches created by
+ `notmuch-search-filter' retain the search order of the parent
+ search."
+   (interactive)
+diff --git a/show-message.c b/show-message.c
+index 79b02e2..38f5897 100644
+--- a/show-message.c
++++ b/show-message.c
+@@ -38,7 +38,7 @@ show_message_part (GMimeObject *part, int *part_count,
+               if (i == 1)
+                   continue;
+               if (i > 1)
+-                  fprintf (stderr, "Warning: Unexpected extra parts of mutlipart/signed. Continuing.\n");
++                  fprintf (stderr, "Warning: Unexpected extra parts of multipart/signed. Continuing.\n");
+           }
+           show_message_part (g_mime_multipart_get_part (multipart, i),
+                              part_count, show_part);
+-- 
+1.6.5.2.433.g23cdb
+
+
diff --git a/test/corpora/default/bar/cur/20:2, b/test/corpora/default/bar/cur/20:2,
new file mode 100644 (file)
index 0000000..f08a314
--- /dev/null
@@ -0,0 +1,101 @@
+Date: Wed, 18 Nov 2009 00:20:59 +0100
+From: Adrian Perez de Castro <aperez@igalia.com>
+To: notmuch@notmuchmail.org
+Message-ID: <20091118002059.067214ed@hikari>
+Organization: Igalia
+X-Mailer: Claws Mail 3.7.3 (GTK+ 2.18.3; x86_64-redhat-linux-gnu)
+Face: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAAXNSR0IArs4c6QAAADBQTFRFBwcHFhYWKCgoNzc3SEhIV1dXaGhod3d3iIiIlpaWqKiouLi4x8fH2NjY5+fn/v7+rSjDkgAAAjVJREFUOE9l07tvE0EQwOHfrkV9O+eko7g701BBfECJsIigT2IpooIqaSiRUEB0REj00FBQgYSCkhry+gecUPJybJeIxLumTbilsH2PMNXufDOa3ZVW+1JkpbUmD/8+vXR3c7or4Gz93mH309Kz8/C9/RQge7VfhW/LW+PF8IkrQ7Z6OKmQr1tl+LU/yWP9mxJka9O88fZHPwf/7u0kLyCnX3I4fQhgjAgIfi+HHw5A1Y2ggIMcFKAEnRoL0M3BosI4TI2IATjuT8DvSNJoNNJgkIhxlr9TUHeSpDnfohlIrMBlU+BGmsZqfr69FMfGMw4NoG835+J62riWyjQ/uXlTQjNUIoYegMsBM0pCD8oDas7n4HQsBghXFxJTW42KDs+4XLfjsN0wOYgABqARjMKIHIaAQnmHjsI5Cvi9Cf6k03OoWBkpIP3Q7354+dEimFBKHbMP9oKjwfd9gbrxR5KDToczK4uPF8UgNomKU2GaENRi77zyDKICxKBS4xXYbONPMQMdYZTBwMiMWiUg9g6UJ3OBogzjV8E7sBVwyvfAOYdQhsABzuOxI1MGZbs98Q6Md5UOfbbR2R0eWOesrnRw5ajT6f60LrNhWIHZpBnUWv2s14ukArWWTqTes3YQxRXgFkcMu70TPYqqUBs0YwmO967OVIdTG4bY4a7WLaqgLm5vbHdH5np0Dri//fmg7y8scB4u3+zsuNlH0X+g19bby69b+TYH6isvns8VdQWgxj9tHP8AR5/hSdYqkwsAAAAASUVORK5CYII=
+Mime-Version: 1.0
+Subject: [notmuch] Introducing myself
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============1167731900=="
+Sender: notmuch-bounces@notmuchmail.org
+Errors-To: notmuch-bounces@notmuchmail.org
+
+--===============1167731900==
+Content-Type: multipart/signed; micalg=PGP-SHA1;
+ boundary="Sig_/ayZz9m37AOMROJCyUudvXvZ"; protocol="application/pgp-signature"
+
+--Sig_/ayZz9m37AOMROJCyUudvXvZ
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: quoted-printable
+
+
+Hello to all,
+
+I have just heard about Not Much today in some random Linux-related news
+site (LWN?), my name is Adrian Perez and I work as systems administrator
+(although I can do some code as well :P). I have always thought that the
+ideas behind Sup were great, but after some time using it, I got tired of
+the oddities that it has. I also do not like doing things like having to
+install Ruby just for reading and sorting mails. Some time ago I thought
+about doing something like Not Much and in fact I played a bit with the
+Python+Xapian and the Python+Whoosh combinations, because I find relaxing
+to code things in Python when I am not working and also it is installed
+by default on most distribution. I got to have some mailboxes indexed and
+basic searching working a couple of months ago. Lately I have been very
+busy and had no time for coding, and them... boom! Not Much appears -- and
+it is almost exactly what I was trying to do, but faster. I have been
+playing a bit with Not Much today, and I think it has potential.
+
+Also, I would like to share one idea I had in mind, that you might find
+interesting: One thing I have found very annoying is having to re-tag my
+mail when the indexes get b0rked (it happened a couple of times to me while
+using Sup), so I was planning to mails as read/unread and adding the tags
+not just to the index, but to the mail text itself, e.g. by adding a
+"X-Tags" header field or by reusing the "Keywords" one. This way, the index
+could be totally recreated by re-reading the mail directories, and this
+would also allow to a tools like OfflineIMAP [1] to get the mails into a
+local maildir, tagging and indexing the mails with the e-mail reader and
+then syncing back the messages with the "X-Tags" header to the IMAP server.
+This would allow to use the mail reader from a different computer and still
+have everything tagged finely.
+
+Best regards,
+
+
+---
+[1] http://software.complete.org/software/projects/show/offlineimap
+
+--=20
+Adrian Perez de Castro <aperez@igalia.com>
+Igalia - Free Software Engineering
+
+--Sig_/ayZz9m37AOMROJCyUudvXvZ
+Content-Type: application/pgp-signature; name=signature.asc
+Content-Disposition: attachment; filename=signature.asc
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v2.0.13 (GNU/Linux)
+
+iEYEARECAAYFAksDL+AACgkQkcVZ2+TJEjtsuQCfXmilW8WpMQHCnwwJjRE1PWZy
+oFAAn3MmXC5sW7MvCFjs7ks6U16zgMEg
+=eL9p
+-----END PGP SIGNATURE-----
+
+--Sig_/ayZz9m37AOMROJCyUudvXvZ--
+
+--===============1167731900==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+--===============1167731900==--
+
diff --git a/test/corpora/default/bar/new/21:2, b/test/corpora/default/bar/new/21:2,
new file mode 100644 (file)
index 0000000..7ff55cc
--- /dev/null
@@ -0,0 +1,102 @@
+MIME-Version: 1.0
+Date: Tue, 17 Nov 2009 16:23:53 -0800
+Message-ID: <cf0c4d610911171623q3e27a0adx802e47039b57604b@mail.gmail.com>
+From: Alex Botero-Lowry <alex.boterolowry@gmail.com>
+To: notmuch@notmuchmail.org
+Content-Type: multipart/mixed; boundary=0016e64ca4d8f27a4804789a4139
+Subject: [notmuch] [PATCH] Error out if no query is supplied to search
+       instead of going into an infinite loop
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+Sender: notmuch-bounces@notmuchmail.org
+Errors-To: notmuch-bounces@notmuchmail.org
+
+--0016e64ca4d8f27a4804789a4139
+Content-Type: multipart/alternative; boundary=0016e64ca4d8f27a3604789a4137
+
+--0016e64ca4d8f27a3604789a4137
+Content-Type: text/plain; charset=ISO-8859-1
+
+In this case error out when no query is supplied. There seems to be an
+infinite-loop casued by i think notmuch_query_search_threads having
+an exception:
+
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+A Xapian exception occurred: Syntax: <expression> AND <expression>
+
+I'll look into that bug specifically a bit later.
+
+It might be better to do a usage instead of just throwing an error here?
+
+alex
+
+--0016e64ca4d8f27a3604789a4137
+Content-Type: text/html; charset=ISO-8859-1
+
+In this case error out when no query is supplied. There seems to be an infinite-loop casued by i think notmuch_query_search_threads having<br>an exception:<br><br>A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>
+A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>
+A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>
+A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>
+A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>
+A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br>A Xapian exception occurred: Syntax: &lt;expression&gt; AND &lt;expression&gt;<br><br>I&#39;ll look into that bug specifically a bit later.<br>
+<br>It might be better to do a usage instead of just throwing an error here?<br><br>alex<br>
+
+--0016e64ca4d8f27a3604789a4137--
+--0016e64ca4d8f27a4804789a4139
+Content-Type: application/octet-stream; 
+       name="0001-Error-out-if-no-query-is-supplied-to-search-instead-.patch"
+Content-Disposition: attachment; 
+       filename="0001-Error-out-if-no-query-is-supplied-to-search-instead-.patch"
+Content-Transfer-Encoding: base64
+X-Attachment-Id: f_g25cms190
+
+RnJvbSAzZjk0MzFmNzRhNWZmNjZjODRjODY5YTNlMjZjMmJhZDQyYmVkMWIxIE1vbiBTZXAgMTcg
+MDA6MDA6MDAgMjAwMQpGcm9tOiBBbGV4YW5kZXIgQm90ZXJvLUxvd3J5IDxhbGV4LmJvdGVyb2xv
+d3J5QGdtYWlsLmNvbT4KRGF0ZTogVHVlLCAxNyBOb3YgMjAwOSAxNjoyMDoyOCAtMDgwMApTdWJq
+ZWN0OiBbUEFUQ0hdIEVycm9yIG91dCBpZiBubyBxdWVyeSBpcyBzdXBwbGllZCB0byBzZWFyY2gg
+aW5zdGVhZCBvZiBnb2luZyBpbnRvIGFuIGluZmluaXRlIGxvb3AKCi0tLQogbm90bXVjaC1zZWFy
+Y2guYyB8ICAgIDUgKysrKysKIDEgZmlsZXMgY2hhbmdlZCwgNSBpbnNlcnRpb25zKCspLCAwIGRl
+bGV0aW9ucygtKQoKZGlmZiAtLWdpdCBhL25vdG11Y2gtc2VhcmNoLmMgYi9ub3RtdWNoLXNlYXJj
+aC5jCmluZGV4IDhkYjA5YzcuLmQ5NGZjY2QgMTAwNjQ0Ci0tLSBhL25vdG11Y2gtc2VhcmNoLmMK
+KysrIGIvbm90bXVjaC1zZWFyY2guYwpAQCAtNjYsNiArNjYsMTEgQEAgbm90bXVjaF9zZWFyY2hf
+Y29tbWFuZCAodm9pZCAqY3R4LCBpbnQgYXJnYywgY2hhciAqYXJndltdKQogICAgIGFyZ2MgLT0g
+aTsKICAgICBhcmd2ICs9IGk7CiAKKyAgICBpZiAoYXJnYyA9PSAwKSB7CisgICAgICAgIGZwcmlu
+dGYgKHN0ZGVyciwgIk5vIHF1ZXJ5IHByb3ZpZGVkXG4iKTsKKyAgICAgICAgcmV0dXJuIDE7Cisg
+ICAgfQorCiAgICAgY29uZmlnID0gbm90bXVjaF9jb25maWdfb3BlbiAoY3R4LCBOVUxMLCBOVUxM
+KTsKICAgICBpZiAoY29uZmlnID09IE5VTEwpCiAJcmV0dXJuIDE7Ci0tIAoxLjYuNS4yCgo=
+--0016e64ca4d8f27a4804789a4139
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+--0016e64ca4d8f27a4804789a4139--
+
diff --git a/test/corpora/default/bar/new/22:2, b/test/corpora/default/bar/new/22:2,
new file mode 100644 (file)
index 0000000..08adada
--- /dev/null
@@ -0,0 +1,84 @@
+Date: Tue, 17 Nov 2009 19:50:40 -0500
+From: Lars Kellogg-Stedman <lars@seas.harvard.edu>
+To: Keith Packard <keithp@keithp.com>
+Message-ID: <20091118005040.GA25380@dottiness.seas.harvard.edu>
+References: <20091117190054.GU3165@dottiness.seas.harvard.edu>
+       <87iqd9rn3l.fsf@vertex.dottedmag>
+       <20091117203301.GV3165@dottiness.seas.harvard.edu>
+       <yunaayketfm.fsf@aiko.keithp.com>
+MIME-Version: 1.0
+In-Reply-To: <yunaayketfm.fsf@aiko.keithp.com>
+User-Agent: Mutt/1.5.19 (2009-01-05)
+Cc: notmuch@notmuchmail.org
+Subject: Re: [notmuch] Working with Maildir storage?
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============1483126515=="
+Sender: notmuch-bounces@notmuchmail.org
+Errors-To: notmuch-bounces@notmuchmail.org
+
+
+--===============1483126515==
+Content-Type: multipart/signed; micalg=pgp-sha256;
+       protocol="application/pgp-signature"; boundary="9amGYk9869ThD9tj"
+Content-Disposition: inline
+
+
+--9amGYk9869ThD9tj
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+> 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).
+
+--=20
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+
+
+--9amGYk9869ThD9tj
+Content-Type: application/pgp-signature
+Content-Disposition: inline
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.9 (GNU/Linux)
+
+iQEcBAEBCAAGBQJLA0TgAAoJENdGlQYxQazYsG0IAJ1t9h4Q3ma8z8ejeKR22Xh0
+WcuRX2x9yEXy/+aG9W7Mot0mqUQCiLdmHM/2h5N9BFHyJvfOUf8lmssrJ5OS/kp5
+j7FIx3GUELBmEZqFUPjRSQPk1hZURYdRsloKkrbQ2kAivjjb50zAAQ8Av4Cgj6cS
+3HvNNmeVfJt1NS75vm+/wn48M8Vrcdv4gvNlSOhgFOixknvRoxSyNDOHYBKvHnSV
+2HnO0GzhAQzDZAwdHBzJtb8vRmglrH33TVdrE7OW+sojYB3Wyz8r9+HIt8Q8ovzX
+nQ8p0Nf5DlF7tye3JYo0EeNm5EQJ4q0YyVYInhmtpi3A5Cyu50GcB/GZ5Sd6ajo=
+=WULe
+-----END PGP SIGNATURE-----
+
+--9amGYk9869ThD9tj--
+
+--===============1483126515==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+--===============1483126515==--
+
diff --git a/test/corpora/default/cur/29:2, b/test/corpora/default/cur/29:2,
new file mode 100644 (file)
index 0000000..c76eff3
--- /dev/null
@@ -0,0 +1,21 @@
+From: "Keith Packard" <keithp@keithp.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 18:04:31 -0800
+Subject: [notmuch] archive
+In-Reply-To: <20091117232137.GA7669@griffis1.net>
+References: <20091117232137.GA7669@griffis1.net>
+Message-ID: <yunzl6kd1w0.fsf@aiko.keithp.com>
+
+On Tue, 17 Nov 2009 18:21:38 -0500, Aron Griffis <agriffis at n01se.net> wrote:
+
+> Just subscribed, I'd like to catch up on the previous postings,
+> but the archive link seems to be bogus?
+
+Yeah, the archive appears broken and will need to wait until Carl
+arrives in Barcelona to get fixed.
+
+--
+keith.packard at intel.com
+
+
+
diff --git a/test/corpora/default/cur/30:2, b/test/corpora/default/cur/30:2,
new file mode 100644 (file)
index 0000000..a5b94a0
--- /dev/null
@@ -0,0 +1,75 @@
+From: "Stewart Smith" <stewart@flamingspork.com>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 13:22:20 +1100
+Subject: [notmuch] [PATCH] count_files: sort directory in inode order before
+       statting
+Message-ID: <1258510940-7018-1-git-send-email-stewart@flamingspork.com>
+
+---
+ notmuch-new.c |   30 ++++++++++--------------------
+ 1 files changed, 10 insertions(+), 20 deletions(-)
+
+diff --git a/notmuch-new.c b/notmuch-new.c
+index 11fad8c..c5f841a 100644
+--- a/notmuch-new.c
++++ b/notmuch-new.c
+@@ -308,36 +308,26 @@ add_files (notmuch_database_t *notmuch,
+ static void
+ count_files (const char *path, int *count)
+ {
+-    DIR *dir;
+-    struct dirent *e, *entry = NULL;
+-    int entry_length;
+-    int err;
++    struct dirent *entry = NULL;
+     char *next;
+     struct stat st;
++    struct dirent **namelist = NULL;
+-    dir = opendir (path);
++    int n_entries= scandir(path, &namelist, 0, ino_cmp);
+-    if (dir == NULL) {
++    if (n_entries == -1) {
+       fprintf (stderr, "Warning: failed to open directory %s: %s\n",
+                path, strerror (errno));
+       goto DONE;
+     }
+-    entry_length = offsetof (struct dirent, d_name) +
+-      pathconf (path, _PC_NAME_MAX) + 1;
+-    entry = malloc (entry_length);
++    int i=0;
+     while (!interrupted) {
+-      err = readdir_r (dir, entry, &e);
+-      if (err) {
+-          fprintf (stderr, "Error reading directory: %s\n",
+-                   strerror (errno));
+-          free (entry);
+-          goto DONE;
+-      }
++        if (i == n_entries)
++            break;
+-      if (e == NULL)
+-          break;
++        entry= namelist[i++];
+       /* Ignore special directories to avoid infinite recursion.
+        * Also ignore the .notmuch directory.
+@@ -376,8 +366,8 @@ count_files (const char *path, int *count)
+   DONE:
+     if (entry)
+       free (entry);
+-
+-    closedir (dir);
++    if (namelist)
++        free (namelist);
+ }
+ int
+-- 
+1.6.3.3
+
+
diff --git a/test/corpora/default/cur/31:2, b/test/corpora/default/cur/31:2,
new file mode 100644 (file)
index 0000000..88f17ca
--- /dev/null
@@ -0,0 +1,31 @@
+From: "Jjgod Jiang" <gzjjgod@gmail.com>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 11:50:17 +0800
+Subject: [notmuch] Mac OS X/Darwin compatibility issues
+Message-ID: <ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com>
+
+Hi,
+
+When I tried to compile notmuch under Mac OS X 10.6, several issues
+arisen:
+
+1. g++ reports 'warning: command line option "-Wmissing-declarations"
+is valid for C/ObjC but not for C++'
+
+2.
+notmuch-reply.c: In function ?address_is_users?:
+notmuch-reply.c:87: warning: passing argument 2 of
+?notmuch_config_get_user_other_email? from incompatible pointer type
+
+That's due to the size incompatibility of 'unsigned int' and 'size_t'
+(size_t is uint64_t in Mac OS X).
+
+3. Several errors about missing GNU extensions like getline() and strndup():
+
+warning: implicit declaration of function ?getline?
+error: ?strndup? was not declared in this scope
+
+We can implement these with fgets() and strncpy() though.
+
+- Jiang
+
diff --git a/test/corpora/default/cur/32:2, b/test/corpora/default/cur/32:2,
new file mode 100644 (file)
index 0000000..c1633cd
--- /dev/null
@@ -0,0 +1,165 @@
+From: "Jan Janak" <jan@ryngle.com>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 05:57:03 +0100
+Subject: [notmuch] [PATCH] notmuch new: Support for conversion of spool
+       subdirectories into tags
+Message-ID: <1258520223-15328-1-git-send-email-jan@ryngle.com>
+
+This patch makes it possible to convert subdirectory names inside the
+spool directory into message tags. Messages stored in subdirectory
+"foo" will be marked with tag "foo". Message duplicates found in several
+subdirectories will be given one tag per subdirectory.
+
+This feature can be used to synchronize notmuch's tags with with gmail
+tags, for example. Gmail IMAP servers convert tags to IMAP
+subdirectories and those subdirectories can be converted back to tags
+in notmuch.
+
+The patch modifies notmuch_database_add_message function to return a
+pointer to the message even if a message duplicate was found in the
+database. This is needed if we want to add a tag for each subdirectory
+in which a message duplicate was found.
+
+In addition to that, it makes the pointer to notmuch_config_t global
+(previously it was a local variable in notmuch_new_command). The
+configuration data structure is used by the function which converts
+subdirectory names to tags.
+
+Finally, there is a new function called subdir_to_tag. The function
+extracts the name of the subdirectory inside the spool from the full
+path of the message (also removing Maildir's cur,dir,and tmp
+subdirectories) and adds it as a new tag to the message.
+
+Signed-off-by: Jan Janak <jan at ryngle.com>
+---
+ lib/database.cc |    3 +-
+ notmuch-new.c   |   74 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
+ 2 files changed, 74 insertions(+), 3 deletions(-)
+
+diff --git a/lib/database.cc b/lib/database.cc
+index 3c8d626..f7799d2 100644
+--- a/lib/database.cc
++++ b/lib/database.cc
+@@ -949,7 +949,8 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
+   DONE:
+     if (message) {
+-      if (ret == NOTMUCH_STATUS_SUCCESS && message_ret)
++              if ((ret == NOTMUCH_STATUS_SUCCESS ||
++                       ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) && message_ret)
+           *message_ret = message;
+       else
+           notmuch_message_destroy (message);
+diff --git a/notmuch-new.c b/notmuch-new.c
+index 83a05ba..d94ce16 100644
+--- a/notmuch-new.c
++++ b/notmuch-new.c
+@@ -19,6 +19,9 @@
+  */
+ #include "notmuch-client.h"
++#include <libgen.h>
++
++static notmuch_config_t *config = 0;
+ static volatile sig_atomic_t do_add_files_print_progress = 0;
+@@ -45,6 +48,69 @@ tag_inbox_and_unread (notmuch_message_t *message)
+     notmuch_message_add_tag (message, "unread");
+ }
++/*
++ * Extracts the sub-directory from the filename and adds it as a new tag to
++ * the message. The filename must begin with the database directory configured
++ * in the configuration file. This prefix is then removed. If the remaining
++ * sub-directory ends with one of the Maildir special directories (/tmp, /new,
++ * /cur) then they are removed as well. If there is anything left then the
++ * function adds it as a new tag to the message.
++ *
++ * The function does nothing if it cannot extract a sub-directory from
++ * filename.
++ */
++static void
++subdir_to_tag (char* filename, notmuch_message_t *message)
++{
++      const char* db_path;
++      char *msg_dir, *tmp;
++      int db_path_len, msg_dir_len;
++
++      if (config == NULL) return;
++    db_path = notmuch_config_get_database_path (config);
++      if (db_path == NULL) return;
++      db_path_len = strlen(db_path);
++
++      /* Make a copy of the string as dirname may need to modify it. */
++      tmp = talloc_strdup(message, filename);
++      msg_dir = dirname(tmp);
++      msg_dir_len = strlen(msg_dir);
++
++      /* If msg_dir starts with db_path, remove it, including the / which delimits
++       * it from the rest of the directory name. */
++      if (db_path_len < msg_dir_len &&
++              !strncmp(db_path, msg_dir, db_path_len)) {
++              msg_dir += db_path_len + 1;
++              msg_dir_len -= db_path_len + 1;
++      } else {
++              /* If we get here, either the message filename is not inside the
++               * database directory configured in the configuration file, or it is a
++               * file in the root directory of the database. Either way we just skip
++               * it because we do not know how to convert it to a meaningful
++               * subdirectory string that we could add as tag. */
++              goto out;
++      }
++
++      /* Special conditioning for Maildirs. If the remainder of the directory
++       * name ends with /new, /cur, or /tmp then remove it. */
++      if ((msg_dir_len >= 4) &&
++              (!strncmp(msg_dir + msg_dir_len - 4, "/new", 4) ||
++               !strncmp(msg_dir + msg_dir_len - 4, "/cur", 4) ||
++               !strncmp(msg_dir + msg_dir_len - 4, "/tmp", 4))) {
++              msg_dir[msg_dir_len - 4] = '\0';
++      }
++
++      /* If, after all the modifications, we still have a subdirectory, add it
++       * as tag. */
++      if (strlen(msg_dir)) {
++              notmuch_message_add_tag (message, msg_dir);
++      }
++
++out:
++      talloc_free(tmp);
++}
++
++
+ static void
+ add_files_print_progress (add_files_state_t *state)
+ {
+@@ -186,10 +252,15 @@ add_files_recursive (notmuch_database_t *notmuch,
+                   case NOTMUCH_STATUS_SUCCESS:
+                       state->added_messages++;
+                       tag_inbox_and_unread (message);
++                      subdir_to_tag(next, message);
+                       break;
+                   /* Non-fatal issues (go on to next file) */
+                   case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+-                      /* Stay silent on this one. */
++                      /* Stay silent on this one. The message already exists in the
++                               * database, that means we may have found a duplicate in
++                               * another directory. If that's the case then we add another
++                               * tag to the message with the sub-directory. */
++                              subdir_to_tag(next, message);
+                       break;
+                   case NOTMUCH_STATUS_FILE_NOT_EMAIL:
+                       fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
+@@ -386,7 +457,6 @@ int
+ notmuch_new_command (void *ctx,
+                    unused (int argc), unused (char *argv[]))
+ {
+-    notmuch_config_t *config;
+     notmuch_database_t *notmuch;
+     add_files_state_t add_files_state;
+     double elapsed;
+-- 
+1.6.3.3
+
+
diff --git a/test/corpora/default/cur/33:2, b/test/corpora/default/cur/33:2,
new file mode 100644 (file)
index 0000000..a9b3252
--- /dev/null
@@ -0,0 +1,13 @@
+From: "Rolland Santimano" <rollandsantimano@yahoo.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 21:12:23 -0800 (PST)
+Subject: [notmuch] Link to mailing list archives ?
+Message-ID: <736613.51770.qm@web113505.mail.gq1.yahoo.com>
+
+The link[1] provided on the list page[2] is broken:
+[1] http://notmuchmail.org/pipermail/notmuch/
+[2] http://notmuchmail.org/mailman/listinfo/notmuch
+
+
+      
+
diff --git a/test/corpora/default/cur/34:2, b/test/corpora/default/cur/34:2,
new file mode 100644 (file)
index 0000000..b94dd06
--- /dev/null
@@ -0,0 +1,46 @@
+From: "Alexander Botero-Lowry" <alex.boterolowry@gmail.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 21:45:36 -0800
+Subject: [notmuch] Mac OS X/Darwin compatibility issues
+In-Reply-To: <ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com>
+References: <ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com>
+Message-ID: <86einw2xof.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me>
+
+On Wed, 18 Nov 2009 11:50:17 +0800, Jjgod Jiang <gzjjgod at gmail.com> wrote:
+> Hi,
+> 
+> When I tried to compile notmuch under Mac OS X 10.6, several issues
+> arisen:
+> 
+> 1. g++ reports 'warning: command line option "-Wmissing-declarations"
+> is valid for C/ObjC but not for C++'
+> 
+I got that too. I presume it's newly supported in GCC4.4?
+
+> 3. Several errors about missing GNU extensions like getline() and strndup():
+> 
+strndup from V8:
+
+char* strndup(char* str, size_t n) {
+  // Stupid implementation of strndup since macos isn't born with
+  // one.
+  size_t len = strlen(str);
+  if (len <= n)
+    return StrDup(str);
+  char* result = new char[n+1];
+  size_t i;
+  for (i = 0; i <= n; i++)
+    result[i] = str[i];
+  result[i] = '\0';
+  return result;
+}
+
+> warning: implicit declaration of function ?getline?
+> error: ?strndup? was not declared in this scope
+> 
+for getline do you mind trying #define _GNU_SOURCE 1
+before #include <stdio.h> in the offending files? The FreeBSD man pages
+mentions that as a way of enabling the GNU version of getline().
+
+Alex
+
diff --git a/test/corpora/default/cur/35:2, b/test/corpora/default/cur/35:2,
new file mode 100644 (file)
index 0000000..d727670
--- /dev/null
@@ -0,0 +1,24 @@
+From: "Jjgod Jiang" <gzjjgod@gmail.com>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 14:14:27 +0800
+Subject: [notmuch] Mac OS X/Darwin compatibility issues
+In-Reply-To: <86einw2xof.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me>
+References: <ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com>
+       <86einw2xof.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me>
+Message-ID: <ddd65cda0911172214t60d22b63hcfeb5a19ab54a39b@mail.gmail.com>
+
+Hi,
+
+On Wed, Nov 18, 2009 at 1:45 PM, Alexander Botero-Lowry
+<alex.boterolowry at gmail.com> wrote:
+> for getline do you mind trying #define _GNU_SOURCE 1
+> before #include <stdio.h> in the offending files? The FreeBSD man pages
+> mentions that as a way of enabling the GNU version of getline().
+
+It seems even _GNU_SOURCE is defined, getline is still not present.
+the C lib in Mac OS X simply doesn't have it. See also [1].
+
+- Jiang
+
+[1] http://stackoverflow.com/questions/1117108/compiling-c-code-using-gnu-c-getline-on-mac-osx
+
diff --git a/test/corpora/default/cur/36:2, b/test/corpora/default/cur/36:2,
new file mode 100644 (file)
index 0000000..4cd0d20
--- /dev/null
@@ -0,0 +1,25 @@
+From: "Alexander Botero-Lowry" <alex.boterolowry@gmail.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 22:19:29 -0800
+Subject: [notmuch] Mac OS X/Darwin compatibility issues
+In-Reply-To: <ddd65cda0911172214t60d22b63hcfeb5a19ab54a39b@mail.gmail.com>
+References: <ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com>
+       <86einw2xof.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me>
+       <ddd65cda0911172214t60d22b63hcfeb5a19ab54a39b@mail.gmail.com>
+Message-ID: <86d43g2w3y.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me>
+
+On Wed, 18 Nov 2009 14:14:27 +0800, Jjgod Jiang <gzjjgod at gmail.com> wrote:
+> Hi,
+> 
+> On Wed, Nov 18, 2009 at 1:45 PM, Alexander Botero-Lowry
+> <alex.boterolowry at gmail.com> wrote:
+> > for getline do you mind trying #define _GNU_SOURCE 1
+> > before #include <stdio.h> in the offending files? The FreeBSD man pages
+> > mentions that as a way of enabling the GNU version of getline().
+> 
+> It seems even _GNU_SOURCE is defined, getline is still not present.
+> the C lib in Mac OS X simply doesn't have it. See also [1].
+> 
+Alas. Since it's ostensibly based on the FreeBSD one, I figured there
+was a chance that would fix the problem. :/
+
diff --git a/test/corpora/default/cur/37:2, b/test/corpora/default/cur/37:2,
new file mode 100644 (file)
index 0000000..4e17e82
--- /dev/null
@@ -0,0 +1,22 @@
+From: "Alexander Botero-Lowry" <alex.boterolowry@gmail.com>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 00:02:56 -0800
+Subject: [notmuch] request for pull
+Message-ID: <86aayk2rbj.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me>
+
+The following changes since commit e8c9c3e6a534fc6c2919c2c1de63cea7250eb488:
+  Ingmar Vanhassel (1):
+        Makefile: Manual pages shouldn't be executable
+
+are available in the git repository at:
+
+  git://alexbl.net/notmuch.git master
+
+Alexander Botero-Lowry (2):
+      Error out if no query is supplied to search instead of going into an infinite loop
+      set a local truncate-line variable in notmuch-search-mode, so that subjects don't wrap and make the output look weird
+
+ notmuch-search.c |    5 +++++
+ notmuch.el       |    1 +
+ 2 files changed, 6 insertions(+), 0 deletions(-)
+
diff --git a/test/corpora/default/cur/38:2, b/test/corpora/default/cur/38:2,
new file mode 100644 (file)
index 0000000..f5537ff
--- /dev/null
@@ -0,0 +1,40 @@
+From: "Keith Packard" <keithp@keithp.com>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 00:29:59 -0800
+Subject: [notmuch] [PATCH] Create a default notmuch-show-hook that
+       highlights URLs and uses word-wrap
+Message-ID: <1258532999-9316-1-git-send-email-keithp@keithp.com>
+
+I created the notmuch-show-hook precisely so I could add these two
+options, but I suspect most people will want them, so I just made them
+the default. If you don't want them, you can use remove-hook to get
+rid of this.
+
+Signed-off-by: Keith Packard <keithp at keithp.com>
+---
+ notmuch.el |    8 ++++++++
+ 1 files changed, 8 insertions(+), 0 deletions(-)
+
+diff --git a/notmuch.el b/notmuch.el
+index 1bb1294..c95cb43 100644
+--- a/notmuch.el
++++ b/notmuch.el
+@@ -698,6 +698,14 @@ view, (remove the \"inbox\" tag from each), with either
+   :options '(goto-address)
+   :group 'notmuch)
++; Make show mode a bit prettier, highlighting URLs and using word wrap
++
++(defun notmuch-show-pretty-hook ()
++  (goto-address-mode 1)
++  (visual-line-mode))
++
++(add-hook 'notmuch-show-hook 'notmuch-show-pretty-hook)
++
+ (defun notmuch-show (thread-id &optional parent-buffer)
+   "Run \"notmuch show\" with the given thread ID and display results.
+-- 
+1.6.5.2
+
+
diff --git a/test/corpora/default/cur/39:2, b/test/corpora/default/cur/39:2,
new file mode 100644 (file)
index 0000000..637b3c7
--- /dev/null
@@ -0,0 +1,32 @@
+From: "Alexander Botero-Lowry" <alex.boterolowry@gmail.com>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 00:52:34 -0800
+Subject: [notmuch] [PATCH] Create a default notmuch-show-hook that
+ highlights URLs and uses word-wrap
+In-Reply-To: <1258532999-9316-1-git-send-email-keithp@keithp.com>
+References: <1258532999-9316-1-git-send-email-keithp@keithp.com>
+Message-ID: <867hto2p0t.fsf@fortitudo.i-did-not-set--mail-host-address--so-tickle-me>
+
+On Wed, 18 Nov 2009 00:29:59 -0800, Keith Packard <keithp at keithp.com> wrote:
+> I created the notmuch-show-hook precisely so I could add these two
+> options, but I suspect most people will want them, so I just made them
+> the default. If you don't want them, you can use remove-hook to get
+> rid of this.
+> 
+Yes, hooks should be added for search as well. :)
+
+> +; Make show mode a bit prettier, highlighting URLs and using word wrap
+> +
+> +(defun notmuch-show-pretty-hook ()
+> +  (goto-address-mode 1)
+> +  (visual-line-mode))
+> +
+visual-line-mode turns out to make subject look pretty ugly if there is a
+continuation. It doesn't do much good for the citation headers
+either. We probably need to do our own intelligent wrapping rather then
+use visual-line-mode to make this actually look right.
+
+goto-address-mode is important though. :)
+
+alex
+
diff --git a/test/corpora/default/cur/40:2, b/test/corpora/default/cur/40:2,
new file mode 100644 (file)
index 0000000..91a15a8
--- /dev/null
@@ -0,0 +1,31 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 01:42:02 -0800
+Subject: [notmuch] [PATCH 1/2] Close message file after parsing message
+ headers
+In-Reply-To: <yunbpj0etua.fsf@aiko.keithp.com>
+References: <1258471718-6781-1-git-send-email-dottedmag@dottedmag.net>
+       <87lji5cbwo.fsf@yoom.home.cworth.org> <yunbpj0etua.fsf@aiko.keithp.com>
+Message-ID: <87pr7gqidx.fsf@yoom.home.cworth.org>
+
+On Tue, 17 Nov 2009 13:15:25 -0800, Keith Packard <keithp at keithp.com> wrote:
+> Threading the message also involves displaying the from and to contents,
+> which requires opening the message file. The alternative to the fix I
+> provided is to just parse all of the message headers when first opening
+> the message; it could then be immediately closed and the hash referred
+> to for all header data. Given the choice, just having the caller say
+> when it has finished with a message is probably a reasonable option...
+
+Hi Keith,
+
+Once I finally got back on the ground again, I pushed out a revised
+version of your patch, (didn't need the reply-to stuff anymore since I
+had fixed that differently in the meantime).
+
+I'm pretty happy with the state of this portion of the code now.
+
+Thanks Keith and Mikhail for your input on and code to fix this bug.
+
+-Carl
+
+
diff --git a/test/corpora/default/cur/41:2, b/test/corpora/default/cur/41:2,
new file mode 100644 (file)
index 0000000..da22cc0
--- /dev/null
@@ -0,0 +1,37 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 02:08:10 -0800
+Subject: [notmuch] Working with Maildir storage?
+In-Reply-To: <20091117190054.GU3165@dottiness.seas.harvard.edu>
+References: <20091117190054.GU3165@dottiness.seas.harvard.edu>
+Message-ID: <87ocn0qh6d.fsf@yoom.home.cworth.org>
+
+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/corpora/default/cur/42:2, b/test/corpora/default/cur/42:2,
new file mode 100644 (file)
index 0000000..98fa75f
--- /dev/null
@@ -0,0 +1,30 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 02:19:26 -0800
+Subject: [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands
+ remove inbox (and unread) tags
+In-Reply-To: <1258493565-13508-1-git-send-email-keithp@keithp.com>
+References: <1258493565-13508-1-git-send-email-keithp@keithp.com>
+Message-ID: <87k4xoqgnl.fsf@yoom.home.cworth.org>
+
+On Tue, 17 Nov 2009 13:32:45 -0800, Keith Packard <keithp at keithp.com> wrote:
+> When closing a thread view, mark the thread as archived by removing
+> the "inbox" tag, and for the 'x' variant, the "unread" tag as well,
+> then kill the buffer and update the search window view as well.
+> 
+> This makes 'x' much the same as 'a', but instead of taking you to the
+> next message, it takes you back to the search window instead.
+
+I don't like this---but that's because I use 'x' precisely *because* it
+preserves these tags.
+
+Otherwise, you might as well just remove inbox and unread as soon as the
+message is presented to the user. And that's a bug in a lot of other
+email programs that I'm unwilling to replicate.
+
+We may run into a need to define different ways that people like to work
+with their email here. (I know that so far I've just been coding up the
+way I want my mail to work.)
+
+-Carl
+
diff --git a/test/corpora/default/cur/43:2, b/test/corpora/default/cur/43:2,
new file mode 100644 (file)
index 0000000..2f6c8bc
--- /dev/null
@@ -0,0 +1,26 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 02:22:12 -0800
+Subject: [notmuch] archive
+In-Reply-To: <yunzl6kd1w0.fsf@aiko.keithp.com>
+References: <20091117232137.GA7669@griffis1.net>
+       <yunzl6kd1w0.fsf@aiko.keithp.com>
+Message-ID: <87iqd8qgiz.fsf@yoom.home.cworth.org>
+
+On Tue, 17 Nov 2009 18:04:31 -0800, Keith Packard <keithp at keithp.com> wrote:
+> On Tue, 17 Nov 2009 18:21:38 -0500, Aron Griffis <agriffis at n01se.net> wrote:
+> 
+> > Just subscribed, I'd like to catch up on the previous postings,
+> > but the archive link seems to be bogus?
+> 
+> Yeah, the archive appears broken and will need to wait until Carl
+> arrives in Barcelona to get fixed.
+
+Fixed it in transit in Frankfurt---with only moments to spare on my
+battery and no outlets in sight.
+
+Thanks for the report, Aron. And welcome to notmuch!
+
+-Carl (who wants to reply to a lot more mail, but will have to wait
+ until later for that)
+
diff --git a/test/corpora/default/cur/44:2, b/test/corpora/default/cur/44:2,
new file mode 100644 (file)
index 0000000..c896c18
--- /dev/null
@@ -0,0 +1,29 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 02:43:50 -0800
+Subject: [notmuch] [PATCH] Older versions of install do not support -C.
+In-Reply-To: <1258496327-12086-1-git-send-email-jan@ryngle.com>
+References: <1258496327-12086-1-git-send-email-jan@ryngle.com>
+Message-ID: <87hbssqfix.fsf@yoom.home.cworth.org>
+
+On Tue, 17 Nov 2009 23:18:47 +0100, Jan Janak <jan at ryngle.com> wrote:
+> Do not use -C cmdline option of install, older versions, commonly found in
+> distributions like Debian, do not seem to support it. Running make install
+> on such systems (tested on Debian Lenny) fails.
+> 
+> Signed-off-by: Jan Janak <jan at ryngle.com>
+
+Thanks, Jan. This is pushed now.
+
+And did I say welcome to notmuch yet? (It's easy to lose track with all
+the newcomers---which I'm not complaining about---especially since so
+many are sharing code.)
+
+-Carl
+
+PS. I actually really like the behavior of -C (especially when
+installing a low-level library to avoid big waterfalls of needless
+recompiles). But since we're *not* actually installing a library (yet)
+I'm happy with this patch rather than writing code in configure to check
+if "install -C" works or not.
+
diff --git a/test/corpora/default/cur/45:2, b/test/corpora/default/cur/45:2,
new file mode 100644 (file)
index 0000000..806b0e8
--- /dev/null
@@ -0,0 +1,41 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 02:49:52 -0800
+Subject: [notmuch] What a great idea!
+In-Reply-To: <f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com>
+References: <f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com>
+Message-ID: <87fx8cqf8v.fsf@yoom.home.cworth.org>
+
+On Tue, 17 Nov 2009 23:35:30 +0100, Jan Janak <jan at ryngle.com> wrote:
+> First of all, notmuch is a wonderful idea, both the cmdline tool and
+> the emacs interface! Thanks a lot for writing it, I was really excited
+> when I read the announcement today.
+
+Ah, here's where I planned a nice welcome. So welcome (again), Jan! :-)
+
+I've been having a lot of fun with notmuch already, (though there have
+been some days of pain before it was functional enough and my
+email-reply latency went way up). But regardless---I got through that,
+and I'm able to work more efficiently with notmuch now than I could with
+sup before. So I'm happy.
+
+And I'm delighted when other people find this interesting as well.
+
+> Have you considered sending an announcement to the org-mode mailing list?
+> http://orgmode.org
+
+Thanks for the idea. I think I may have looked into org-mode years ago,
+(when I was investigating planner-mode and various emacs "personal wiki"
+systems for keeping random notes and what-not).
+
+> Various ways of searching/referencing emails from emacs were discussed
+> there several times and none of them were as elegant as notmuch (not
+> even close). Maybe notmuch would attract some of the developers
+> there..
+
+Yeah. I'll drop them a mail. Having a real emacs wizard on board would
+be nice. (I'm afraid the elisp I've written so far for this project is
+fairly grim.)
+
+-Carl
+
diff --git a/test/corpora/default/cur/46:2, b/test/corpora/default/cur/46:2,
new file mode 100644 (file)
index 0000000..bbd1b37
--- /dev/null
@@ -0,0 +1,57 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 03:02:43 -0800
+Subject: [notmuch] New to the list
+In-Reply-To: <1258498485-sup-142@elly>
+References: <1258498485-sup-142@elly>
+Message-ID: <87bpj0qeng.fsf@yoom.home.cworth.org>
+
+On Tue, 17 Nov 2009 23:57:18 +0100, Israel Herraiz <isra at herraiz.org> wrote:
+> I have subscribed to the list. As suggested by the welcome message, I
+> am introducing myself. My name is Israel Herraiz, and I have done a
+> couple of contributions to Sup, the probably well-known here e-mail
+> client.
+
+Welcome, Israel!
+
+I'm glad people read that little bit of text in the welcome email and
+are introducing themselves. I like to think of our new notmuch community
+as a very personable place.
+
+> "Not much" sounds interesting, and I wonder whether it could be
+> integrated with the views of Sup (inbox, threads, etc). So I have
+> subscribed to the list to keep an eye on what's going on here.
+> 
+> I have just heard of "Not much". I have not even tried to download the
+> code yet.
+
+Yes, take a look. If you're already an emacs user, then you'll find the
+interface of notmuch very comfortable, (looks a lot like sup, but lives
+inside of emacs). Even outside of emacs, the command line interface of
+notmuch gives view *fairly* similar to those of sup:
+
+    notmuch search tag:inbox           # Very much like sup's inbox
+
+    notmuch show thread:some-thread-id # A lot like sup's thread -view
+
+The command-line output right now isn't nearly as neat as sup's, (it
+doesn't elide comments--it doesn't do the indenting of threads, etc.),
+even though the command-line interface has all the information it needs
+to do that. The reason for that is to let the emacs code own most of the
+formatting, (so that it can be more flexible--such as making hidden
+things visible, changing column widths, etc.).
+
+But one thing I wonder is if there would be situations where it would
+make sense to get the cleaner output directly out of the command-line
+tool.
+
+For example, for someone who isn't an emacs user, the command-line
+interface might be their only introduction to what the "notmuch
+experience" is like. So maybe "notmuch show" should give nice clean
+output by default and then the emacs code could call "notmuch show
+--format=emacs-friendly" or something to get the current output.
+
+That's an idea anyway.
+
+-Carl
+
diff --git a/test/corpora/default/cur/47:2, b/test/corpora/default/cur/47:2,
new file mode 100644 (file)
index 0000000..9de5532
--- /dev/null
@@ -0,0 +1,84 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 03:15:31 -0800
+Subject: [notmuch] Introducing myself
+In-Reply-To: <20091118002059.067214ed@hikari>
+References: <20091118002059.067214ed@hikari>
+Message-ID: <87aaykqe24.fsf@yoom.home.cworth.org>
+
+On Wed, 18 Nov 2009 00:20:59 +0100, Adrian Perez de Castro <aperez at igalia.com> wrote:
+> I have just heard about Not Much today in some random Linux-related news
+> site (LWN?), my name is Adrian Perez and I work as systems administrator
+
+Welcome to notmuch, Adrian! We're glad to have you here.
+
+> by default on most distribution. I got to have some mailboxes indexed and
+> basic searching working a couple of months ago. Lately I have been very
+> busy and had no time for coding, and them... boom! Not Much appears -- and
+> it is almost exactly what I was trying to do, but faster. I have been
+> playing a bit with Not Much today, and I think it has potential.
+
+It's funny, because I had the exact same experience with sup a couple of
+months ago. I had been frustrated for years with email programs, and had
+been thinking about how I'd like things to work n the back of my mind
+for a long time, (but never *quite* getting to the point where I would
+commit to writing an email system myself).
+
+And then... boom! I found sup and was instantly hooked. It had so much
+of what I had imagined, (and much of what I hadn't yet imagined) that I
+was quite delighted.
+
+It was really quite by accident that I ended up inventing a different
+system. I had started out just trying to speedup index creation for sup.
+If I hadn't run into the problem that it was very difficult[*] to create a
+sup-compatible index from C code, I might have stopped there.
+
+So I'd written a bunch of functional code, only to find myself stuck at
+the very last step, (hooking it up to the existing sup interface). Then
+Keith suggested emacs and it all seemed pretty easy since I'd already
+done all the Xapian work. So it's funny, I was only willing to commit to
+this project because I wasn't consciously aware I was working on it.
+Otherwise it would have seemed to overwhelming to start. :-)
+
+Anyway, that's a lot of off-topic rambling off of your introduction. But
+I'm glad that notmuch can now give that same "boom!" to others, and I'm
+glad you see potential in it.
+
+> Also, I would like to share one idea I had in mind, that you might find
+> interesting: One thing I have found very annoying is having to re-tag my
+> mail when the indexes get b0rked (it happened a couple of times to me while
+> using Sup), so I was planning to mails as read/unread and adding the tags
+> not just to the index, but to the mail text itself, e.g. by adding a
+> "X-Tags" header field or by reusing the "Keywords" one. This way, the index
+> could be totally recreated by re-reading the mail directories, and this
+> would also allow to a tools like OfflineIMAP [1] to get the mails into a
+> local maildir, tagging and indexing the mails with the e-mail reader and
+> then syncing back the messages with the "X-Tags" header to the IMAP server.
+> This would allow to use the mail reader from a different computer and still
+> have everything tagged finely.
+
+It is an interesting idea. But there's also something really comforting
+about the email indexed never modifying the mail files. If you're
+reading the notmuch commit logs closely you'll see that I'm not actually
+careful enough to be trusted with your mail (but I try). So I like that
+I don't even have to trust myself---the worst that happens is that I
+have to recreate my index.
+
+And as Keith mentioned, we've got the "notmuch dump; notmuch restore"
+idea working exactly as it did in sup. (Though I am thinking of also
+adding thread IDs to that now---more on that later.)
+
+The big annoyance I had with sup index creation, (I ended up having to
+do it more than once too), was that it takes *forever*. Right now,
+notmuch is a little bit faster, but not a lot faster. And I've got some
+ideas to fix that. It would be really nice if index creation were pain
+free. (And maybe it is for some user with small amounts of mail---oh, to
+have only 40000 messages to have to index!).
+
+-Carl
+
+[*] The problem here is that sup puts serialized ruby data structures
+into the data field of its Xapian documents. So being compatible with
+sup means being able to recreate serialized data structures for a
+particular version of ruby.
+
diff --git a/test/corpora/default/cur/48:2, b/test/corpora/default/cur/48:2,
new file mode 100644 (file)
index 0000000..419e21d
--- /dev/null
@@ -0,0 +1,17 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 03:22:32 -0800
+Subject: [notmuch] [PATCH] Typsos
+In-Reply-To: <1258500222-32066-1-git-send-email-ingmar@exherbo.org>
+References: <1258500222-32066-1-git-send-email-ingmar@exherbo.org>
+Message-ID: <878we4qdqf.fsf@yoom.home.cworth.org>
+
+On Wed, 18 Nov 2009 00:23:42 +0100, Ingmar Vanhassel <ingmar at exherbo.org> wrote:
+>  17 files changed, 30 insertions(+), 30 deletions(-)
+
+Yikes. That's a lot of typos.
+
+Thanks Ingmar, for cleaning up after my sloppy keyboarding. Pushed.
+
+-Carl
+
diff --git a/test/corpora/default/cur/49:2, b/test/corpora/default/cur/49:2,
new file mode 100644 (file)
index 0000000..b244f8c
--- /dev/null
@@ -0,0 +1,33 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 03:31:23 -0800
+Subject: [notmuch] [PATCH] Error out if no query is supplied to search
+ instead of going into an infinite loop
+In-Reply-To: <cf0c4d610911171623q3e27a0adx802e47039b57604b@mail.gmail.com>
+References: <cf0c4d610911171623q3e27a0adx802e47039b57604b@mail.gmail.com>
+Message-ID: <877htoqdbo.fsf@yoom.home.cworth.org>
+
+On Tue, 17 Nov 2009 16:23:53 -0800, Alex Botero-Lowry <alex.boterolowry at gmail.com> wrote:
+> In this case error out when no query is supplied. There seems to be an
+> infinite-loop casued by i think notmuch_query_search_threads having
+> an exception:
+> A Xapian exception occurred: Syntax: <expression> AND <expression>
+> A Xapian exception occurred: Syntax: <expression> AND <expression>
+> A Xapian exception occurred: Syntax: <expression> AND <expression>
+> 
+> I'll look into that bug specifically a bit later.
+> 
+> It might be better to do a usage instead of just throwing an error here?
+
+Definitely.
+
+Priit Laes reported the same thing in IRC and I've just committed a
+patch to give a nice error message:
+
+$ ./notmuch search
+Error: notmuch search requires at least one search term.
+
+Thanks for the report!
+
+-Carl
+
diff --git a/test/corpora/default/cur/50:2, b/test/corpora/default/cur/50:2,
new file mode 100644 (file)
index 0000000..44e8be5
--- /dev/null
@@ -0,0 +1,39 @@
+From: "Chris Wilson" <chris@chris-wilson.co.uk>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 11:34:54 +0000
+Subject: [notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once
+Message-ID: <1258544095-16616-1-git-send-email-chris@chris-wilson.co.uk>
+
+Currently the same `pkg-config ...` is executed for every target, so
+just store the results in a variable.
+---
+ Makefile |    9 +++++----
+ 1 files changed, 5 insertions(+), 4 deletions(-)
+
+diff --git a/Makefile b/Makefile
+index 96aaa73..023b2ec 100644
+--- a/Makefile
++++ b/Makefile
+@@ -4,15 +4,16 @@ CFLAGS=-O2
+ # Additional flags that we will append to whatever the user set.
+ # These aren't intended for the user to manipulate.
+-extra_cflags = `pkg-config --cflags glib-2.0 gmime-2.4 talloc`
+-extra_cxxflags = `xapian-config --cxxflags`
++extra_cflags := $(shell pkg-config --cflags glib-2.0 gmime-2.4 talloc)
++extra_cxxflags := $(shell xapian-config --cxxflags)
+ # Now smash together user's values with our extra values
+ override CFLAGS += $(WARN_FLAGS) $(extra_cflags)
+ override CXXFLAGS += $(WARN_FLAGS) $(extra_cflags) $(extra_cxxflags)
+-override LDFLAGS += `pkg-config --libs glib-2.0 gmime-2.4 talloc` \
+-                      `xapian-config --libs`
++override LDFLAGS += \
++      $(shell pkg-config --libs glib-2.0 gmime-2.4 talloc) \
++      $(shell xapian-config --libs)
+ # Include our local Makefile.local first so that its first target is default
+ include Makefile.local
+-- 
+1.6.5.2
diff --git a/test/corpora/default/cur/51:2, b/test/corpora/default/cur/51:2,
new file mode 100644 (file)
index 0000000..f522f69
--- /dev/null
@@ -0,0 +1,12 @@
+From: "Aron Griffis" <agriffis@n01se.net>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 18:21:38 -0500
+Subject: [notmuch] archive
+Message-ID: <20091117232137.GA7669@griffis1.net>
+
+Just subscribed, I'd like to catch up on the previous postings,
+but the archive link seems to be bogus?
+
+Thanks,
+Aron
+
diff --git a/test/corpora/default/cur/52:2, b/test/corpora/default/cur/52:2,
new file mode 100644 (file)
index 0000000..6028340
--- /dev/null
@@ -0,0 +1,39 @@
+Message-ID: <4EFC743A.3060609@april.org>
+Date: Thu, 29 Dec 2010 15:07:54 +0100
+From: "=?ISO-8859-1?Q?Fran=E7ois_Boulogne?=" <boulogne.f@gmail.com>
+User-Agent: Mozilla/5.0 (X11; Linux i686;
+ rv:9.0) Gecko/20111224 Thunderbird/9.0.1
+MIME-Version: 1.0
+To: Allan McRae <allan@archlinux.org>, 
+ "Discussion about the Arch User Repository (AUR)" <aur-general@archlinux.org>
+References: <4EFC3931.6030007@april.org> <4EFC3D62.4030202@archlinux.org>
+In-Reply-To: <4EFC3D62.4030202@archlinux.org>
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+Subject: Re: [aur-general] Guidelines: cp, mkdir vs install
+
+Le 29/12/2011 11:13, Allan McRae a écrit :
+> On 29/12/11 19:56, François Boulogne wrote:
+>> Hi,
+>>
+>> Looking to improve the quality of my packages, I read again the guidelines.
+>> https://wiki.archlinux.org/index.php/Arch_Packaging_Standards
+>>
+>> However, it don't see anything about the install command like
+>> install -d $pkgdir/usr/{bin,share/man/man1,share/locale}
+>>
+>> Some contributors on AUR use cp or mkdir to install files/dir (when no
+>> makefile is provided) and others use install command.
+>>
+>> What's the opinion of TU on this point?
+>>
+> 
+> Use install with -m specifying the correct permissions
+> 
+
+Thank you Allan
+
+
+-- 
+François Boulogne.
+https://www.sciunto.org
diff --git a/test/corpora/default/cur/53:2, b/test/corpora/default/cur/53:2,
new file mode 100644 (file)
index 0000000..7a1e2e5
--- /dev/null
@@ -0,0 +1,20 @@
+From: Olivier Berger <olivier.berger@it-sudparis.eu>
+To: olivier.berger@it-sudparis.eu
+Subject: Essai =?iso-8859-1?Q?accentu=E9?=
+User-Agent: Notmuch/0.10.1 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu)
+X-Draft-From: ("nnimap+localdovecot:INBOX" 44228)
+Date: Fri, 16 Dec 2010 16:49:59 +0100
+Message-ID: <877h1wv7mg.fsf@inf-8657.int-evry.fr>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: quoted-printable
+
+Du texte accentu=E9 pour =E7a ...
+
+=E0 la bonne heure !
+--=20
+Olivier BERGER=20
+http://www-public.it-sudparis.eu/~berger_o/ - OpenPGP-Id: 2048R/5819D7E8
+Ingenieur Recherche - Dept INF
+Institut TELECOM, SudParis (http://www.it-sudparis.eu/), Evry (France)
+
diff --git a/test/corpora/default/foo/06:2, b/test/corpora/default/foo/06:2,
new file mode 100644 (file)
index 0000000..3baad49
--- /dev/null
@@ -0,0 +1,36 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 12:19:24 -0800
+Subject: [notmuch] preliminary FreeBSD support
+In-Reply-To: <cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com>
+References: <cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com>
+Message-ID: <87lji4lx9v.fsf@yoom.home.cworth.org>
+
+On Tue, 17 Nov 2009 11:36:14 -0800, Alex Botero-Lowry <alex.boterolowry at gmail.com> wrote:
+> 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).
+
+Hi Alex,
+
+That's great! It's good to hear that there are like-minded people out
+there. I hope that Notmuch will be useful for you.
+
+> 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.
+
+Thanks for the patch. As we discussed in IRC[*], we should probably
+do the correct thing and check for ERANGE and loop as necessary (even if
+sysconf returns a positive value). Example code here:
+
+http://www.opengroup.org/austin/docs/austin_328.txt
+
+-Carl
+
+[*] #notmuch on irc.freenode.net for those who didn't just guess that
+already, (and I'll add that to the website soon).
+
diff --git a/test/corpora/default/foo/baz/11:2, b/test/corpora/default/foo/baz/11:2,
new file mode 100644 (file)
index 0000000..c0701de
--- /dev/null
@@ -0,0 +1,27 @@
+From: "Keith Packard" <keithp@keithp.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 13:15:25 -0800
+Subject: [notmuch] [PATCH 1/2] Close message file after parsing message
+ headers
+In-Reply-To: <87lji5cbwo.fsf@yoom.home.cworth.org>
+References: <1258471718-6781-1-git-send-email-dottedmag@dottedmag.net>
+       <87lji5cbwo.fsf@yoom.home.cworth.org>
+Message-ID: <yunbpj0etua.fsf@aiko.keithp.com>
+
+On Tue, 17 Nov 2009 09:13:27 -0800, Carl Worth <cworth at cworth.org> wrote:
+
+> I didn't apply Keith's fix yet, because I think I'd rather just fix the
+> indexer to store the In-Reply-To header in a separate term prefix from
+> the term used for the References header[*]. That will then let us lookup
+> the in-reply-to value later for thread constructions without having to
+> open the original email file at all.
+
+Threading the message also involves displaying the from and to contents,
+which requires opening the message file. The alternative to the fix I
+provided is to just parse all of the message headers when first opening
+the message; it could then be immediately closed and the hash referred
+to for all header data. Given the choice, just having the caller say
+when it has finished with a message is probably a reasonable option...
+
+-keith
+
diff --git a/test/corpora/default/foo/baz/12:2, b/test/corpora/default/foo/baz/12:2,
new file mode 100644 (file)
index 0000000..fbc604c
--- /dev/null
@@ -0,0 +1,27 @@
+From: "Keith Packard" <keithp@keithp.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 13:24:13 -0800
+Subject: [notmuch] Working with Maildir storage?
+In-Reply-To: <20091117203301.GV3165@dottiness.seas.harvard.edu>
+References: <20091117190054.GU3165@dottiness.seas.harvard.edu>
+       <87iqd9rn3l.fsf@vertex.dottedmag>
+       <20091117203301.GV3165@dottiness.seas.harvard.edu>
+Message-ID: <yunaayketfm.fsf@aiko.keithp.com>
+
+On Tue, 17 Nov 2009 15:33:01 -0500, Lars Kellogg-Stedman <lars at seas.harvard.edu> wrote:
+> > See the patch just posted here.
+
+I've also pushed a slightly more complicated (and complete) fix to my
+private notmuch repository
+
+git://keithp.com/git/notmuch
+
+> Is the list archived anywhere?
+
+Oops. Looks like Carl's mail server is broken. He's traveling to
+Barcelona today and so it won't get fixed for a while.
+
+Thanks to everyone for trying out notmuch!
+
+-keith
+
diff --git a/test/corpora/default/foo/baz/cur/13:2, b/test/corpora/default/foo/baz/cur/13:2,
new file mode 100644 (file)
index 0000000..03cb374
--- /dev/null
@@ -0,0 +1,178 @@
+From: "Keith Packard" <keithp@keithp.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 13:32:45 -0800
+Subject: [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove
+       inbox (and unread) tags
+Message-ID: <1258493565-13508-1-git-send-email-keithp@keithp.com>
+
+When closing a thread view, mark the thread as archived by removing
+the "inbox" tag, and for the 'x' variant, the "unread" tag as well,
+then kill the buffer and update the search window view as well.
+
+This makes 'x' much the same as 'a', but instead of taking you to the
+next message, it takes you back to the search window instead.
+
+Signed-off-by: Keith Packard <keithp at keithp.com>
+---
+ notmuch.el |   86 ++++++++++++++++++++++++++++++++++++++++++++++-------------
+ 1 files changed, 67 insertions(+), 19 deletions(-)
+
+diff --git a/notmuch.el b/notmuch.el
+index 638d49d..7b0d72c 100644
+--- a/notmuch.el
++++ b/notmuch.el
+@@ -31,8 +31,8 @@
+     ; Will be much preferable to switch to direct manipulation for
+     ; toggling visibility of these components. Probably using
+     ; overlays-at to query and manipulate the current overlay.
+-    (define-key map "a" 'notmuch-show-archive-thread)
+-    (define-key map "A" 'notmuch-show-mark-read-then-archive-thread)
++    (define-key map "a" 'notmuch-show-mark-read-archive-thread-next-thread)
++    (define-key map "A" 'notmuch-show-archive-thread-next-thread)
+     (define-key map "b" 'notmuch-show-toggle-body-read-visible)
+     (define-key map "c" 'notmuch-show-toggle-citations-visible)
+     (define-key map "h" 'notmuch-show-toggle-headers-visible)
+@@ -47,7 +47,8 @@
+     (define-key map "s" 'notmuch-show-toggle-signatures-visible)
+     (define-key map "v" 'notmuch-show-view-all-mime-parts)
+     (define-key map "w" 'notmuch-show-view-raw-message)
+-    (define-key map "x" 'kill-this-buffer)
++    (define-key map "x" 'notmuch-show-mark-read-archive-thread-kill-buffer)
++    (define-key map "X" 'notmuch-show-archive-thread-kill-buffer)
+     (define-key map "+" 'notmuch-show-add-tag)
+     (define-key map "-" 'notmuch-show-remove-tag)
+     (define-key map (kbd "DEL") 'notmuch-show-rewind)
+@@ -183,7 +184,33 @@ Unlike builtin `next-line' this version accepts no arguments."
+                        (cons (notmuch-show-get-message-id) nil)))
+         (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
+-(defun notmuch-show-archive-thread-maybe-mark-read (markread)
++(defun notmuch-show-next-thread (markread)
++  (let ((parent-buffer notmuch-show-parent-buffer))
++    (kill-this-buffer)
++    (if parent-buffer
++      (progn
++        (switch-to-buffer parent-buffer)
++        (forward-line)
++        (notmuch-search-show-thread)))))
++  
++(defun notmuch-delete-tags (to-remove from)
++  (if to-remove
++      (delete (car to-remove) (notmuch-delete-tags (cdr to-remove) from))
++    from))
++
++(defun notmuch-kill-message-buffer (markread)
++  (let ((parent-buffer notmuch-show-parent-buffer))
++    (kill-this-buffer)
++    (if parent-buffer
++      (progn
++        (switch-to-buffer parent-buffer)
++        (let ((tags (notmuch-search-get-tags)))
++          (setq tags (delete "inbox" tags))
++          (if markread (setq tags (delete "unread" tags)))
++          (notmuch-search-set-tags tags))
++        (forward-line)))))
++
++(defun notmuch-show-archive-thread-maybe-mark-read (markread shownext)
+   (save-excursion
+     (goto-char (point-min))
+     (while (not (eobp))
+@@ -194,15 +221,9 @@ Unlike builtin `next-line' this version accepts no arguments."
+         (forward-char))
+       (if (not (re-search-forward notmuch-show-message-begin-regexp nil t))
+         (goto-char (point-max)))))
+-  (let ((parent-buffer notmuch-show-parent-buffer))
+-    (kill-this-buffer)
+-    (if parent-buffer
+-      (progn
+-        (switch-to-buffer parent-buffer)
+-        (forward-line)
+-        (notmuch-search-show-thread)))))
++  (if shownext (notmuch-show-next-thread markread) (notmuch-kill-message-buffer markread)))
+-(defun notmuch-show-mark-read-then-archive-thread ()
++(defun notmuch-show-mark-read-archive-thread-next-thread ()
+   "Remove \"unread\" tag from each message, then archive and show next thread.
+ Archive each message currrently shown by removing the \"unread\"
+@@ -215,9 +236,22 @@ being delivered to the same thread. It does not archive the
+ entire thread, but only the messages shown in the current
+ buffer."
+   (interactive)
+-  (notmuch-show-archive-thread-maybe-mark-read t))
++  (notmuch-show-archive-thread-maybe-mark-read t t))
++
++(defun notmuch-show-mark-read-archive-thread-kill-buffer ()
++  "Remove \"unread\" tag from each message, then archive and kill the buffer.
++
++Archive each message currrently shown by removing the \"unread\"
++and \"inbox\" tag from each. Then kill this buffer.
++
++Note: This command is safe from any race condition of new messages
++being delivered to the same thread. It does not archive the
++entire thread, but only the messages shown in the current
++buffer."
++  (interactive)
++  (notmuch-show-archive-thread-maybe-mark-read t nil))
+-(defun notmuch-show-archive-thread ()
++(defun notmuch-show-archive-thread-next-thread ()
+   "Archive each message in thread, and show next thread from search.
+ Archive each message currrently shown by removing the \"inbox\"
+@@ -229,7 +263,20 @@ being delivered to the same thread. It does not archive the
+ entire thread, but only the messages shown in the current
+ buffer."
+   (interactive)
+-  (notmuch-show-archive-thread-maybe-mark-read nil))
++  (notmuch-show-archive-thread-maybe-mark-read nil t))
++
++(defun notmuch-show-archive-thread-kill-buffer ()
++  "Archive each message in thread, and kill the thread buffer.
++
++Archive each message currrently shown by removing the \"inbox\"
++tag from each. Then kill this buffer.
++
++Note: This command is safe from any race condition of new messages
++being delivered to the same thread. It does not archive the
++entire thread, but only the messages shown in the current
++buffer."
++  (interactive)
++  (notmuch-show-archive-thread-maybe-mark-read nil t))
+ (defun notmuch-show-view-raw-message ()
+   "View the raw email of the current message."
+@@ -297,7 +344,7 @@ by searching backward)."
+       (not (re-search-forward notmuch-show-message-begin-regexp nil t)))))
+ (defun notmuch-show-message-unread-p ()
+-  "Preficate testing whether current message is unread."
++  "Predicate testing whether current message is unread."
+   (member "unread" (notmuch-show-get-tags)))
+ (defun notmuch-show-next-message ()
+@@ -434,7 +481,7 @@ which this thread was originally shown."
+       (let ((last (notmuch-show-last-message-p)))
+       (notmuch-show-mark-read-then-next-open-message)
+       (if last
+-          (notmuch-show-archive-thread))))))
++          (notmuch-show-archive-thread-next-thread))))))
+ (defun notmuch-show-markup-citations-region (beg end depth)
+   (goto-char beg)
+@@ -618,8 +665,9 @@ messages. Each time you navigate away from a message with
+ You can add or remove tags from the current message with '+' and
+ '-'.  You can also archive all messages in the current
+-view, (remove the \"inbox\" tag from each), with
+-`notmuch-show-archive-thread' (bound to 'a' by default).
++view, (remove the \"inbox\" tag from each), with either
++`notmuch-show-archive-thread-next-thread' (bound to 'a' by default) or
++`notmuch-show-archive-thread-kill-buffer' (bound to 'x' by default).
+ \\{notmuch-show-mode-map}"
+   (interactive)
+-- 
+1.6.5.2
+
+
diff --git a/test/corpora/default/foo/baz/cur/14:2, b/test/corpora/default/foo/baz/cur/14:2,
new file mode 100644 (file)
index 0000000..d3fe78d
--- /dev/null
@@ -0,0 +1,39 @@
+From: "Jan Janak" <jan@ryngle.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 23:18:47 +0100
+Subject: [notmuch] [PATCH] Older versions of install do not support -C.
+Message-ID: <1258496327-12086-1-git-send-email-jan@ryngle.com>
+
+Do not use -C cmdline option of install, older versions, commonly found in
+distributions like Debian, do not seem to support it. Running make install
+on such systems (tested on Debian Lenny) fails.
+
+Signed-off-by: Jan Janak <jan at ryngle.com>
+---
+ Makefile.local |    8 ++++----
+ 1 files changed, 4 insertions(+), 4 deletions(-)
+
+diff --git a/Makefile.local b/Makefile.local
+index f824bed..f51f1d1 100644
+--- a/Makefile.local
++++ b/Makefile.local
+@@ -27,11 +27,11 @@ install: all notmuch.1.gz
+       for d in $(DESTDIR)$(prefix)/bin/ $(DESTDIR)$(prefix)/share/man/man1 \
+               $(DESTDIR)/etc/bash_completion.d/ ; \
+       do \
+-              install -C -d $$d ; \
++              install -d $$d ; \
+       done ;
+-      install -C notmuch $(DESTDIR)$(prefix)/bin/
+-      install -C -m0644 notmuch.1.gz $(DESTDIR)$(prefix)/share/man/man1/
+-      install -C notmuch-completion.bash \
++      install notmuch $(DESTDIR)$(prefix)/bin/
++      install -m0644 notmuch.1.gz $(DESTDIR)$(prefix)/share/man/man1/
++      install notmuch-completion.bash \
+               $(DESTDIR)/etc/bash_completion.d/notmuch
+ SRCS  := $(SRCS) $(notmuch_client_srcs)
+-- 
+1.6.3.3
+
+
diff --git a/test/corpora/default/foo/baz/new/15:2, b/test/corpora/default/foo/baz/new/15:2,
new file mode 100644 (file)
index 0000000..6824d5e
--- /dev/null
@@ -0,0 +1,22 @@
+From: "Jan Janak" <jan@ryngle.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 23:35:30 +0100
+Subject: [notmuch] What a great idea!
+Message-ID: <f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com>
+
+Hello,
+
+First of all, notmuch is a wonderful idea, both the cmdline tool and
+the emacs interface! Thanks a lot for writing it, I was really excited
+when I read the announcement today.
+
+Have you considered sending an announcement to the org-mode mailing list?
+http://org-mode.org
+
+Various ways of searching/referencing emails from emacs were discussed
+there several times and none of them were as elegant as notmuch (not
+even close). Maybe notmuch would attract some of the developers
+there..
+
+   -- Jan
+
diff --git a/test/corpora/default/foo/baz/new/16:2, b/test/corpora/default/foo/baz/new/16:2,
new file mode 100644 (file)
index 0000000..f531eb9
--- /dev/null
@@ -0,0 +1,27 @@
+From: "Jan Janak" <jan@ryngle.com>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 23:38:47 +0100
+Subject: [notmuch] What a great idea!
+In-Reply-To: <f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com>
+References: <f35dbb950911171435ieecd458o853c873e35f4be95@mail.gmail.com>
+Message-ID: <f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com>
+
+On Tue, Nov 17, 2009 at 11:35 PM, Jan Janak <jan at ryngle.com> wrote:
+> Hello,
+>
+> First of all, notmuch is a wonderful idea, both the cmdline tool and
+> the emacs interface! Thanks a lot for writing it, I was really excited
+> when I read the announcement today.
+>
+> Have you considered sending an announcement to the org-mode mailing list?
+> http://org-mode.org
+
+Sorry, wrong URL, the correct one is: http://orgmode.org
+
+> Various ways of searching/referencing emails from emacs were discussed
+> there several times and none of them were as elegant as notmuch (not
+> even close). Maybe notmuch would attract some of the developers
+> there..
+
+  -- Jan
+
diff --git a/test/corpora/default/foo/cur/07:2, b/test/corpora/default/foo/cur/07:2,
new file mode 100644 (file)
index 0000000..7b1e2bb
--- /dev/null
@@ -0,0 +1,57 @@
+From: "Carl Worth" <cworth@cworth.org>
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 09:13:27 -0800
+Subject: [notmuch] [PATCH 1/2] Close message file after parsing message
+ headers
+In-Reply-To: <1258471718-6781-1-git-send-email-dottedmag@dottedmag.net>
+References: <1258471718-6781-1-git-send-email-dottedmag@dottedmag.net>
+Message-ID: <87lji5cbwo.fsf@yoom.home.cworth.org>
+
+On Tue, 17 Nov 2009 21:28:37 +0600, Mikhail Gusarov <dottedmag at dottedmag.net> wrote:
+> Keeping unused files open helps to see "Too many open files" often.
+> 
+> Signed-off-by: Mikhail Gusarov <dottedmag at dottedmag.net>
+...
+On Tue, 17 Nov 2009 21:28:38 +0600, Mikhail Gusarov <dottedmag at dottedmag.net> wrote:
+> 
+> Signed-off-by: Mikhail Gusarov <dottedmag at dottedmag.net>
+> ---
+>  lib/message.cc |    2 ++
+>  1 files changed, 2 insertions(+), 0 deletions(-)
+
+Hi Mikhail,
+
+Welcome to notmuch, and thanks for these patches! I've pushed both of
+them out now.
+
+Keith ran into the same problem of "too many open files" and wrote a
+more complex fix, (which included what you did here). His code can be
+seen at:
+
+       git://keithp.com/git/notmuch
+
+I didn't apply Keith's fix yet, because I think I'd rather just fix the
+indexer to store the In-Reply-To header in a separate term prefix from
+the term used for the References header[*]. That will then let us lookup
+the in-reply-to value later for thread constructions without having to
+open the original email file at all.
+
+-Carl
+
+[*] Yes, this is my first post to our new mailing list and I'm already
+spouting off about "terms" and "prefixes" without any definitions. I
+apologize for that. I hope that people will ask questions freely here on
+the list whenever anything is not clear, and I'll be glad to explain
+things as needed. (Then when can shove answers into a HACKING document.)
+
+PS. This reply is a great example of a feature that notmuch *almost*
+supports already---repling to multiple messages at once. The "notmuch
+reply" command line does everything necessary to make this work, but we
+haven't yet hooked up any keybindings for this in the emacs client yet.
+Obviously, 'r' from the search view could reply to the entire thread.
+But when viewing a thread, anyone have a good keybinding suggestion?
+(There's obviously 'R' as opposed to 'r', but I think we'll probably
+want to distinguish "reply to sender" from "reply to all" before trying
+to distinguish "reply to message" from "reply to thread" (which I
+imagine is more rare of an operation).
+
diff --git a/test/corpora/default/foo/cur/08:2, b/test/corpora/default/foo/cur/08:2,
new file mode 100644 (file)
index 0000000..baf34d1
--- /dev/null
@@ -0,0 +1,87 @@
+Date: Tue, 17 Nov 2009 15:33:01 -0500
+From: Lars Kellogg-Stedman <lars@seas.harvard.edu>
+To: Mikhail Gusarov <dottedmag@dottedmag.net>
+Message-ID: <20091117203301.GV3165@dottiness.seas.harvard.edu>
+References: <20091117190054.GU3165@dottiness.seas.harvard.edu>
+       <87iqd9rn3l.fsf@vertex.dottedmag>
+MIME-Version: 1.0
+In-Reply-To: <87iqd9rn3l.fsf@vertex.dottedmag>
+User-Agent: Mutt/1.5.19 (2009-01-05)
+Cc: notmuch@notmuchmail.org
+Subject: Re: [notmuch] Working with Maildir storage?
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============0063752545=="
+Sender: notmuch-bounces@notmuchmail.org
+Errors-To: notmuch-bounces@notmuchmail.org
+
+
+--===============0063752545==
+Content-Type: multipart/signed; micalg=pgp-sha256;
+       protocol="application/pgp-signature"; boundary="GGxZz/e2pmGePzrA"
+Content-Disposition: inline
+
+
+--GGxZz/e2pmGePzrA
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+> See the patch just posted here.
+
+Is the list archived anywhere?  The obvious archives
+(http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+think I subscribed too late to get the patch (I only just saw the
+discussion about it).
+
+It doesn't look like the patch is in git yet.
+
+-- Lars
+
+--=20
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+
+
+--GGxZz/e2pmGePzrA
+Content-Type: application/pgp-signature
+Content-Disposition: inline
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.9 (GNU/Linux)
+
+iQEcBAEBCAAGBQJLAwh9AAoJENdGlQYxQazYHJMIAI+XTPOyBTZIxEGTdgVKd2fR
+k27ucKs6lXozfMIIGchNUDXQho+KmiuTfX1XFJeBkqOlhrd9zlGjBGoBM0YBq/Gs
+aStPdonREzsHORjmyQCCpjg4AcqCRTXFbDXzAeXlxMPOrZ3P0XNPzTEM1mVksbmg
+mBBDLdHncy4sSCfFgXwRGGgLv9z5Acqm8xGYr68c9PIXY939ozIKV9LVUhxiNz9g
+We2a9rLDhfwxUqDlGdiNwZZimiKvD/fsYSrBZMDb5HgIYkeNZ4SD8Xu+OgB550wN
+OFfwGi3o8WFK2AyDe5QJDh9Ub+euPNlVzePoGpkltZEHuCcLFJqCHv5XYpbxcjA=
+=GO2Q
+-----END PGP SIGNATURE-----
+
+--GGxZz/e2pmGePzrA--
+
+--===============0063752545==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+--===============0063752545==--
+
diff --git a/test/corpora/default/foo/new/03:2, b/test/corpora/default/foo/new/03:2,
new file mode 100644 (file)
index 0000000..c154ac5
--- /dev/null
@@ -0,0 +1,93 @@
+Date: Tue, 17 Nov 2009 14:00:54 -0500
+From: Lars Kellogg-Stedman <lars@seas.harvard.edu>
+To: notmuch@notmuchmail.org
+Message-ID: <20091117190054.GU3165@dottiness.seas.harvard.edu>
+MIME-Version: 1.0
+User-Agent: Mutt/1.5.19 (2009-01-05)
+Subject: [notmuch] Working with Maildir storage?
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============1685355122=="
+Sender: notmuch-bounces@notmuchmail.org
+Errors-To: notmuch-bounces@notmuchmail.org
+
+
+--===============1685355122==
+Content-Type: multipart/signed; micalg=pgp-sha256;
+       protocol="application/pgp-signature"; boundary="5Dr6Wqe9hdyl7LAI"
+Content-Disposition: inline
+
+
+--5Dr6Wqe9hdyl7LAI
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+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=
+=3D3026: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,
+
+--=20
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+
+
+--5Dr6Wqe9hdyl7LAI
+Content-Type: application/pgp-signature
+Content-Disposition: inline
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.9 (GNU/Linux)
+
+iQEcBAEBCAAGBQJLAvLmAAoJENdGlQYxQazYRtcH/0usClQ1Z+EoTsA+URwIK6hD
+FsZUxFxRjMuOQRn2idZ/zhhg5jJj11ZaHjqxSkDvi2ywkTKUf1vX9LLzVy5hSR9M
+E6XQUd5QWAQXo1VsTeKkukIL0YqsPjdgrT8+Yt+OS2NvhEncql23oxnL2/pHkIFq
+r0NdTmVV5Jcar7w9J6X1Mi9m229a/9jV5FImsWISkIhIWznXU5SiU6zIw8xhP4E0
+xhvVSNJnFryjVHtva870aSQduhHfeLPzpYhqbkMPvlq+bcz6Q/Q2SwxJcGLNMPHa
+os9s9FGhCvFKUhVzezHWPgXNCcNT8qK89rcUldb5Oq4jaJb8RCZCYABplfoyaFs=
+=vO4s
+-----END PGP SIGNATURE-----
+
+--5Dr6Wqe9hdyl7LAI--
+
+--===============1685355122==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+--===============1685355122==--
+
diff --git a/test/corpora/default/foo/new/09:2, b/test/corpora/default/foo/new/09:2,
new file mode 100644 (file)
index 0000000..26b51b1
--- /dev/null
@@ -0,0 +1,33 @@
+From: "Mikhail Gusarov" <dottedmag@dottedmag.net>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 02:50:48 +0600
+Subject: [notmuch] Working with Maildir storage?
+In-Reply-To: <20091117203301.GV3165@dottiness.seas.harvard.edu> (Lars
+       Kellogg-Stedman's message of "Tue, 17 Nov 2009 15:33:01 -0500")
+References: <20091117190054.GU3165@dottiness.seas.harvard.edu>
+       <87iqd9rn3l.fsf@vertex.dottedmag>
+       <20091117203301.GV3165@dottiness.seas.harvard.edu>
+Message-ID: <87fx8can9z.fsf@vertex.dottedmag>
+
+
+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
+
+-- 
+  http://fossarchy.blogspot.com/
+-------------- next part --------------
+A non-text attachment was scrubbed...
+Name: not available
+Type: application/pgp-signature
+Size: 834 bytes
+Desc: not available
+URL: <http://notmuchmail.org/pipermail/notmuch/attachments/20091118/0e33d964/attachment.pgp>
+
diff --git a/test/corpora/default/foo/new/10:2, b/test/corpora/default/foo/new/10:2,
new file mode 100644 (file)
index 0000000..4211d73
--- /dev/null
@@ -0,0 +1,54 @@
+From: "Mikhail Gusarov" <dottedmag@dottedmag.net>
+To: notmuch@notmuchmail.org
+Date: Wed, 18 Nov 2009 02:51:18 +0600
+Subject: [notmuch] [PATCH] Handle rename of message file
+Message-ID: <1258491078-29658-1-git-send-email-dottedmag@dottedmag.net>
+
+If message file has been renamed, just update filename in the DB.
+
+Signed-off-by: Mikhail Gusarov <dottedmag at dottedmag.net>
+---
+ lib/database.cc |   21 ++++++++++++---------
+ 1 files changed, 12 insertions(+), 9 deletions(-)
+
+diff --git a/lib/database.cc b/lib/database.cc
+index 3c8d626..c4eb8b6 100644
+--- a/lib/database.cc
++++ b/lib/database.cc
+@@ -925,20 +925,23 @@ notmuch_database_add_message (notmuch_database_t *notmuch,
+       if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+           _notmuch_message_set_filename (message, filename);
+           _notmuch_message_add_term (message, "type", "mail");
++
++          ret = _notmuch_database_link_message (notmuch, message, message_file);
++          if (ret)
++              goto DONE;
++
++          date = notmuch_message_file_get_header (message_file, "date");
++          _notmuch_message_set_date (message, date);
++
++          _notmuch_message_index_file (message, filename);
++      } else if (strcmp(notmuch_message_get_filename(message), filename)) {
++          /* Message file has been moved/renamed */
++          _notmuch_message_set_filename (message, filename);
+       } else {
+           ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+           goto DONE;
+       }
+-      ret = _notmuch_database_link_message (notmuch, message, message_file);
+-      if (ret)
+-          goto DONE;
+-
+-      date = notmuch_message_file_get_header (message_file, "date");
+-      _notmuch_message_set_date (message, date);
+-
+-      _notmuch_message_index_file (message, filename);
+-
+       _notmuch_message_sync (message);
+     } catch (const Xapian::Error &error) {
+       fprintf (stderr, "A Xapian exception occurred: %s.\n",
+-- 
+1.6.3.3
+
+
diff --git a/test/corpora/default/new/04:2, b/test/corpora/default/new/04:2,
new file mode 100644 (file)
index 0000000..0ce678b
--- /dev/null
@@ -0,0 +1,84 @@
+From: Mikhail Gusarov <dottedmag@dottedmag.net>
+To: notmuch@notmuchmail.org
+References: <20091117190054.GU3165@dottiness.seas.harvard.edu>
+Date: Wed, 18 Nov 2009 01:02:38 +0600
+In-Reply-To: <20091117190054.GU3165@dottiness.seas.harvard.edu> (Lars
+       Kellogg-Stedman's message of "Tue, 17 Nov 2009 14:00:54 -0500")
+Message-ID: <87iqd9rn3l.fsf@vertex.dottedmag>
+User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/23.1 (gnu/linux)
+MIME-Version: 1.0
+Subject: Re: [notmuch] Working with Maildir storage?
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============1958295626=="
+Sender: notmuch-bounces@notmuchmail.org
+Errors-To: notmuch-bounces@notmuchmail.org
+
+--===============1958295626==
+Content-Type: multipart/signed; boundary="=-=-=";
+       micalg=pgp-sha1; protocol="application/pgp-signature"
+
+--=-=-=
+Content-Transfer-Encoding: quoted-printable
+
+
+Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did g=
+yre 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.hostna=
+me,U=3D3026:2,S:
+ LK>   Too many open files
+
+See the patch just posted here.
+
+=2D-=20
+  http://fossarchy.blogspot.com/
+
+--=-=-=
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.9 (GNU/Linux)
+
+iQIcBAEBAgAGBQJLAvNOAAoJEJ0g9lA+M4iIjLYQAKp0PXEgl3JMOEBisH52AsIK
+CzzfP4Fzd41K9VH/c1EdQWDYR6FCAA4IUSNICnJhITsYUb0eC5AKJiey3JP0+rmd
+s4qEFBKH2iuphv8Llltcv2Q8DyPuJBkVa3mO9XCCeABZ6v4UvnTSWRVG12csSEih
+ScgienU8sMrM9LwvvVI1ZB2flm2TzsH2hWi30jIgmtBntIKJaTgbFXB50FYFwULa
+gGL/oH3u+YpumedWzPZdCJrw2q7nMvYx8aQ29EDCNLZibAZe+6oDTa6Fv6/0ldpQ
+U+DptR0nJGbJTWa26OTSvmyeIORjAfM+TEI68n7KO9VHYPmVh6awcf0MNKYh2xWk
+eRQNBcKyQNWxeKyCCpT/rrTlpxBWahpvg+V8lkDH2W09wjRp6CUKvifK3Sz3am9m
+5ZUMpvXbwkZD6Ci6l/QytbYK50e8UpvFSu5DBaxBz59ykoypuNg2ayO5Kdi6IF5d
+T+Sw6wo8UKn9a33+vheIc0fkhZXbeSotEmDm7huazm6CgM3dcWXUpTuJvik1cSWp
+4buv98gY6IKWKoUTXODWUr+7VR4gei8du8qOsKem+QDfNX7tmaIRjhrbB24B91Wy
+td3MTJD7GjMNid0INqRY1CRMLo8YlPaq6NBZfcYtYgwa6gpJijz1/MAn8+GMrfhF
+9LI8b9jopNP+pMYBohLA
+=/ksP
+-----END PGP SIGNATURE-----
+--=-=-=--
+
+--===============1958295626==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+--===============1958295626==--
+
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/html/attribute-text b/test/corpora/html/attribute-text
new file mode 100644 (file)
index 0000000..6dae819
--- /dev/null
@@ -0,0 +1,15 @@
+From: David Bremner <david@example.net>
+To: David Bremner <david@example.net>
+Subject: test html attachment
+Date: Tue, 17 Nov 2009 21:28:38 +0600
+Message-ID: <87d1dajhgf.fsf@example.net>
+MIME-Version: 1.0
+Content-Type: text/html
+Content-Disposition: inline; filename=test.html
+
+<html>
+  <body>
+    <input value="a>swordfish">
+  </body>
+  hunter2
+</html>
diff --git a/test/corpora/html/embedded-image b/test/corpora/html/embedded-image
new file mode 100644 (file)
index 0000000..4085153
--- /dev/null
@@ -0,0 +1,69 @@
+From: =?utf-8?b?bWFsbW9ib3Jn?= <daemon@lublin.se>
+To: =?utf-8?b?Ym9lbmRlLm1hbG1vYm9yZw==?= <daemon@lublin.se>
+Date: Tue, 19 Jul 2016 11:54:24 +0200
+X-Feed2Imap-Version: 1.2.5
+Message-Id: <boendemalmoborg-1834@eltanin.uberspace.de>
+Subject: =?utf-8?b?VGFjayBhbGxhIHRyYWZpa2FudGVyIG9jaCBmb3Rnw6RuZ2FyZSE=?=
+Content-Type: multipart/alternative; boundary="=-1468922508-176605-12427-9500-21-="
+MIME-Version: 1.0
+
+
+--=-1468922508-176605-12427-9500-21-=
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 8bit
+
+<http://malmoborg.se/2016/07/tack-alla-trafikanter-och-fotgangare/>
+
+Malmö 2016-07-09
+
+I skrivande stund är vi i färd med att avetablera vår entreprenad på 
+Tigern 3, Regementsgatan 6 i Malmö. Fastigheten har genomgått ett större 
+dräneringsarbete som i sin tur har inneburit vissa 
+trafikbegränsningar på Regementsgatan samt Davidshallsgatan under några 
+veckors tid. Fastighetsägaren är mycket nöjd med vår arbetsinsats och vi 
+kan glatt meddela att båda vägfilerna kommer att öppnas inom kort. Nu 
+kommer den vackra fastigheten att klara sig torrskodd under många år 
+framöver [A]
+
+
+[A] http://malmoborg.se/wp-includes/images/smilies/icon_smile.gif
+-- 
+Feed: Förvaltnings AB Malmöborg
+<http://malmoborg.se>
+Item: Tack alla trafikanter och fotgängare!
+<http://malmoborg.se/2016/07/tack-alla-trafikanter-och-fotgangare/>
+Date: 2016-07-19 11:54:24 +0200
+Author: malmoborg
+Filed under: Nyheter
+
+--=-1468922508-176605-12427-9500-21-=
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+<table border="1" width="100%" cellpadding="0" cellspacing="0" borderspacing="0"><tr><td>
+<table width="100%" bgcolor="#EDEDED" cellpadding="4" cellspacing="2">
+<tr><td align="right"><b>Feed:</b></td>
+<td width="100%"><a href="http://malmoborg.se">
+<b>Förvaltnings AB Malmöborg</b>
+</a>
+</td></tr><tr><td align="right"><b>Item:</b></td>
+<td width="100%"><a href="http://malmoborg.se/2016/07/tack-alla-trafikanter-och-fotgangare/"><b>Tack alla trafikanter och fotgängare!</b>
+</a>
+</td></tr></table></td></tr></table>
+
+<p>Malmö 2016-07-09</p>
+<p>I skrivande stund är vi i färd med att avetablera vår entreprenad på Tigern 3, Regementsgatan 6 i Malmö. Fastigheten har genomgått ett större dräneringsarbete som i sin tur har inneburit vissa trafikbegränsningar på Regementsgatan samt Davidshallsgatan under några veckors tid. Fastighetsägaren är mycket nöjd med vår arbetsinsats och vi kan glatt meddela att båda vägfilerna kommer att öppnas inom kort. Nu kommer den vackra fastigheten att klara sig torrskodd under många år framöver <img src="data:image/gif;base64,R0lGODlhDwAPALMOAP/qAEVFRQAAAP/OAP/JAP+0AP6dAP/+k//9E///////
+xzMzM///6//lAAAAAAAAACH5BAEAAA4ALAAAAAAPAA8AAARb0EkZap3YVabO
+GRcWcAgCnIMRTEEnCCfwpqt2mHEOagoOnz+CKnADxoKFyiHHBBCSAdOiCVg8
+KwPZa7sVrgJZQWI8FhB2msGgwTXTWGqCXP4WBQr4wjDDstQmEQA7
+" alt=":-)" class="wp-smiley" /> </p>
+<p>&nbsp;</p>
+<hr width="100%"/>
+<table width="100%" cellpadding="0" cellspacing="0">
+<tr><td align="right"><font color="#ababab">Date:</font>&nbsp;&nbsp;</td><td><font color="#ababab">2016-07-19 11:54:24 +0200</font></td></tr>
+<tr><td align="right"><font color="#ababab">Author:</font>&nbsp;&nbsp;</td><td><font color="#ababab">malmoborg</font></td></tr>
+<tr><td align="right"><font color="#ababab">Filed under:</font>&nbsp;&nbsp;</td><td><font color="#ababab">Nyheter</font></td></tr>
+</table>
+
+--=-1468922508-176605-12427-9500-21-=--
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
+
+
+--=-=-=--
diff --git a/test/corpora/lkml/cur/1354585346.000260:2, b/test/corpora/lkml/cur/1354585346.000260:2,
new file mode 100644 (file)
index 0000000..04664e5
--- /dev/null
@@ -0,0 +1,99 @@
+Return-Path: <stefan@datenfreihafen.org>
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id 055BC431FBF
+       for <notmuch@notmuchmail.org>; Sat, 21 Nov 2009 16:11:31 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
+Received: from olra.theworths.org ([127.0.0.1])
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id Vz+mNzdau2Gh for <notmuch@notmuchmail.org>;
+       Sat, 21 Nov 2009 16:11:30 -0800 (PST)
+Received: from sirius.lasnet.de (sirius.lasnet.de [78.47.116.19])
+       by olra.theworths.org (Postfix) with ESMTP id CFD61431FAE
+       for <notmuch@notmuchmail.org>; Sat, 21 Nov 2009 16:11:29 -0800 (PST)
+Received: from p5b034af6.dip.t-dialin.net ([91.3.74.246] helo=excalibur)
+       by sirius.lasnet.de with esmtpsa 
+       (Cipher TLS-1.0:RSA_AES_256_CBC_SHA1:32) (Exim 4.63 #1)
+       id 1NC032-0000td-2v by authid <stefan@sostec.de> with cram_md5;
+       Sun, 22 Nov 2009 01:11:28 +0100
+Received: from stefan by excalibur with local (Exim 4.69)
+       (envelope-from <stefan@excalibur.local>)
+       id 1NC031-0001Dm-H7; Sun, 22 Nov 2009 01:11:23 +0100
+From: Stefan Schmidt <stefan@datenfreihafen.org>
+To: notmuch@notmuchmail.org
+Date: Sun, 22 Nov 2009 01:11:01 +0100
+Message-Id: <1258848661-4660-2-git-send-email-stefan@datenfreihafen.org>
+X-Mailer: git-send-email 1.6.5.3
+In-Reply-To: <1258848661-4660-1-git-send-email-stefan@datenfreihafen.org>
+References: <yes> <1258848661-4660-1-git-send-email-stefan@datenfreihafen.org>
+Subject: [notmuch] [PATCH 2/2] notmuch-new: Tag mails not as unread when the
+       seen flag in the maildir is set.
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+X-List-Received-Date: Sun, 22 Nov 2009 00:11:31 -0000
+
+With the new notmuch_message_get_flags() function we can get the information if
+a message was already flagged as seen in maildir by another mailer or tool. This
+gives a more realistic picture instead of flagging all as unread.
+
+Signed-off-by: Stefan Schmidt <stefan@datenfreihafen.org>
+---
+ Makefile      |    2 +-
+ notmuch-new.c |   16 +++++++++++++++-
+ 2 files changed, 16 insertions(+), 2 deletions(-)
+
+diff --git a/Makefile b/Makefile
+index 3fedcf1..dfcfc70 100644
+--- a/Makefile
++++ b/Makefile
+@@ -1,6 +1,6 @@
+ # Default FLAGS, (can be overridden by user such as "make CFLAGS=-O2")
+ WARN_FLAGS=-Wall -Wextra -Wmissing-declarations -Wwrite-strings -Wswitch-enum
+-CFLAGS=-O2
++CFLAGS=-O0 -ggdb3
+ # Additional flags that we will append to whatever the user set.
+ # These aren't intended for the user to manipulate.
+diff --git a/notmuch-new.c b/notmuch-new.c
+index bc35b4e..ef4429d 100644
+--- a/notmuch-new.c
++++ b/notmuch-new.c
+@@ -41,8 +41,22 @@ handle_sigint (unused (int sig))
+ static void
+ tag_inbox_and_unread (notmuch_message_t *message)
+ {
+-    notmuch_message_add_tag (message, "inbox");
++    char *buf;
++    int i;
++
++    buf = notmuch_message_get_flags (message);
++    for (i = 0; i < strlen (buf); i++) {
++        /* If the S flag is set the message can be tagged as read */
++        if (buf[i] == 'S') {
++            notmuch_message_add_tag (message, "read");
++            goto inbox;
++        }
++    }
++
+     notmuch_message_add_tag (message, "unread");
++
++inbox:
++    notmuch_message_add_tag (message, "inbox");
+ }
+ static void
+-- 
+1.6.5.3
+
+
diff --git a/test/corpora/lkml/cur/1354585346.000261:2, b/test/corpora/lkml/cur/1354585346.000261:2,
new file mode 100644 (file)
index 0000000..5ca8132
--- /dev/null
@@ -0,0 +1,131 @@
+Return-Path: <stefan@datenfreihafen.org>
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id E4203431FBF
+       for <notmuch@notmuchmail.org>; Sat, 21 Nov 2009 16:11:31 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
+Received: from olra.theworths.org ([127.0.0.1])
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id k6PahtnYXl0O for <notmuch@notmuchmail.org>;
+       Sat, 21 Nov 2009 16:11:30 -0800 (PST)
+Received: from sirius.lasnet.de (sirius.lasnet.de [78.47.116.19])
+       by olra.theworths.org (Postfix) with ESMTP id 1BAB6431FBC
+       for <notmuch@notmuchmail.org>; Sat, 21 Nov 2009 16:11:30 -0800 (PST)
+Received: from p5b034af6.dip.t-dialin.net ([91.3.74.246] helo=excalibur)
+       by sirius.lasnet.de with esmtpsa 
+       (Cipher TLS-1.0:RSA_AES_256_CBC_SHA1:32) (Exim 4.63 #1)
+       id 1NC02v-0000t5-LF by authid <stefan@sostec.de> with cram_md5;
+       Sun, 22 Nov 2009 01:11:29 +0100
+Received: from stefan by excalibur with local (Exim 4.69)
+       (envelope-from <stefan@excalibur.local>)
+       id 1NC02u-0001Dj-V9; Sun, 22 Nov 2009 01:11:16 +0100
+From: Stefan Schmidt <stefan@datenfreihafen.org>
+To: notmuch@notmuchmail.org
+Date: Sun, 22 Nov 2009 01:11:00 +0100
+Message-Id: <1258848661-4660-1-git-send-email-stefan@datenfreihafen.org>
+X-Mailer: git-send-email 1.6.5.3
+In-Reply-To: <yes>
+References: <yes>
+Subject: [notmuch] [PATCH 1/2] lib/message: Add function to get maildir
+       flags.
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+X-List-Received-Date: Sun, 22 Nov 2009 00:11:32 -0000
+
+With notmuch_message_get_flags() we gain the information if the message was
+flagged as read, draft, trashed, etc. Handy for big mail spooles that were used
+with another mailer.
+
+Signed-off-by: Stefan Schmidt <stefan@datenfreihafen.org>
+---
+ lib/message.cc |   26 ++++++++++++++++++++++++++
+ lib/notmuch.h  |   10 ++++++++++
+ 2 files changed, 36 insertions(+), 0 deletions(-)
+
+diff --git a/lib/message.cc b/lib/message.cc
+index 069cedb..9bec61e 100644
+--- a/lib/message.cc
++++ b/lib/message.cc
+@@ -35,6 +35,7 @@ struct _notmuch_message {
+     char *thread_id;
+     char *in_reply_to;
+     char *filename;
++    char *flags;
+     notmuch_message_file_t *message_file;
+     notmuch_message_list_t *replies;
+@@ -114,6 +115,7 @@ _notmuch_message_create (const void *talloc_owner,
+     message->thread_id = NULL;
+     message->in_reply_to = NULL;
+     message->filename = NULL;
++    message->flags = NULL;
+     message->message_file = NULL;
+     message->replies = _notmuch_message_list_create (message);
+@@ -438,6 +440,30 @@ notmuch_message_get_filename (notmuch_message_t *message)
+     return message->filename;
+ }
++const char *
++notmuch_message_get_flags (notmuch_message_t *message)
++{
++    std::string filename_str, flags;
++    size_t position;
++    const char *db_path;
++
++    if (message->flags)
++      return message->flags;
++
++    filename_str = message->doc.get_data ();
++    db_path = notmuch_database_get_path (message->notmuch);
++
++    if (filename_str[0] != '/')
++      filename_str.insert (0, db_path);
++
++    /* Flags are everything behind ":" */
++    position = filename_str.find (":");
++    flags = filename_str.substr (position + 3); /* We don't want :2, */
++    message->flags = talloc_strdup (message, flags.c_str ());
++
++    return message->flags;
++}
++
+ time_t
+ notmuch_message_get_date (notmuch_message_t *message)
+ {
+diff --git a/lib/notmuch.h b/lib/notmuch.h
+index a61cd02..1da5dfd 100644
+--- a/lib/notmuch.h
++++ b/lib/notmuch.h
+@@ -694,6 +694,16 @@ notmuch_message_get_replies (notmuch_message_t *message);
+ const char *
+ notmuch_message_get_filename (notmuch_message_t *message);
++/* Get the maildir flags for the email corresponding to 'message'.
++ *
++ * The returned flags will be a string of ascii format flags.
++ *
++ * The returned string belongs to the message so should not be
++ * modified or freed by the caller (nor should it be referenced after
++ * the message is destroyed). */
++const char *
++notmuch_message_get_flags (notmuch_message_t *message);
++
+ /* Get the date of 'message' as a time_t value.
+  *
+  * For the original textual representation of the Date header from the
+-- 
+1.6.5.3
+
+
diff --git a/test/corpora/lkml/cur/1354585346.000265:2, b/test/corpora/lkml/cur/1354585346.000265:2,
new file mode 100644 (file)
index 0000000..7f3acd4
--- /dev/null
@@ -0,0 +1,85 @@
+Return-Path: <keithp@keithp.com>
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id 5656D431FBC
+       for <notmuch@notmuchmail.org>; Sat, 21 Nov 2009 16:28:35 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
+Received: from olra.theworths.org ([127.0.0.1])
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id Dam69vzNZiE1 for <notmuch@notmuchmail.org>;
+       Sat, 21 Nov 2009 16:28:34 -0800 (PST)
+Received: from keithp.com (home.keithp.com [63.227.221.253])
+       by olra.theworths.org (Postfix) with ESMTP id AA991431FAE
+       for <notmuch@notmuchmail.org>; Sat, 21 Nov 2009 16:28:34 -0800 (PST)
+Received: from localhost (localhost [127.0.0.1])
+       by keithp.com (Postfix) with ESMTP id 18FC076012A;
+       Sat, 21 Nov 2009 16:28:34 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at keithp.com
+Received: from keithp.com ([127.0.0.1])
+       by localhost (keithp.com [127.0.0.1]) (amavisd-new, port 10024)
+       with LMTP id tw1iDvWYNGRC; Sat, 21 Nov 2009 16:28:31 -0800 (PST)
+Received: by keithp.com (Postfix, from userid 1033)
+       id 60F3176012B; Sat, 21 Nov 2009 16:28:31 -0800 (PST)
+Received: from keithp.com (localhost [127.0.0.1])
+       by keithp.com (Postfix) with ESMTP id 5330D76012A;
+       Sat, 21 Nov 2009 16:28:31 -0800 (PST)
+From: Keith Packard <keithp@keithp.com>
+To: Stefan Schmidt <stefan@datenfreihafen.org>, notmuch@notmuchmail.org
+In-Reply-To: <1258848661-4660-1-git-send-email-stefan@datenfreihafen.org>
+References: <yes> <1258848661-4660-1-git-send-email-stefan@datenfreihafen.org>
+Date: Sat, 21 Nov 2009 16:28:30 -0800
+Message-ID: <yunvdh3pfm9.fsf@aiko.keithp.com>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-=";
+       micalg=pgp-sha1; protocol="application/pgp-signature"
+Subject: Re: [notmuch] [PATCH 1/2] lib/message: Add function to get maildir
+ flags.
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+X-List-Received-Date: Sun, 22 Nov 2009 00:28:35 -0000
+
+--=-=-=
+Content-Transfer-Encoding: quoted-printable
+
+On Sun, 22 Nov 2009 01:11:00 +0100, Stefan Schmidt <stefan@datenfreihafen.o=
+rg> wrote:
+
+> +const char *
+> +notmuch_message_get_flags (notmuch_message_t *message)
+
+This function should interpret the flags that it finds and return a
+suitable set of notmuch tags. I'd suggest that 'unread' messages get
+both 'unread' and 'inbox' tags, as Maildir doesn't distinguish between
+'don't show this to be by default again please' and 'I've read this
+message'. It seems best to hide the maildir-specific details inside the
+library instead of exposing them.
+
+Also, we have only the 'unread' tag; we don't have a 'read' tag, which
+would simply be the inverse of 'unread'.
+
+=2D-=20
+keith.packard@intel.com
+
+--=-=-=
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+
+iD8DBQFLCIWuQp8BWwlsTdMRAvcTAKDXHYc6MTuuZFMzHvZFs1omBbr9CACdEY/b
+jqyT/QmmgoA/GtIcs/DfLMY=
+=LVlh
+-----END PGP SIGNATURE-----
+--=-=-=--
+
diff --git a/test/corpora/lkml/cur/1354585346.000323:2, b/test/corpora/lkml/cur/1354585346.000323:2,
new file mode 100644 (file)
index 0000000..b7fc28f
--- /dev/null
@@ -0,0 +1,105 @@
+Return-Path: <stefan@datenfreihafen.org>
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id 912FF431FBF
+       for <notmuch@notmuchmail.org>; Sun, 22 Nov 2009 10:33:44 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
+Received: from olra.theworths.org ([127.0.0.1])
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id 9T+Abdbhab0i for <notmuch@notmuchmail.org>;
+       Sun, 22 Nov 2009 10:33:43 -0800 (PST)
+Received: from sirius.lasnet.de (sirius.lasnet.de [78.47.116.19])
+       by olra.theworths.org (Postfix) with ESMTP id 39D1C431FAE
+       for <notmuch@notmuchmail.org>; Sun, 22 Nov 2009 10:33:43 -0800 (PST)
+Received: from p5b0353d3.dip.t-dialin.net ([91.3.83.211] helo=excalibur)
+       by sirius.lasnet.de with esmtpsa 
+       (Cipher TLS-1.0:RSA_AES_256_CBC_SHA1:32) (Exim 4.63 #1)
+       id 1NCHFh-0000dR-It by authid <stefan@sostec.de> with cram_md5;
+       Sun, 22 Nov 2009 19:33:40 +0100
+Received: from stefan by excalibur with local (Exim 4.69)
+       (envelope-from <stefan@excalibur.local>)
+       id 1NCHFi-0002ot-2C; Sun, 22 Nov 2009 19:33:38 +0100
+Date: Sun, 22 Nov 2009 19:33:38 +0100
+From: Stefan Schmidt <stefan@datenfreihafen.org>
+To: Keith Packard <keithp@keithp.com>
+Message-ID: <20091122183338.GB5735@excalibur.local>
+References: <yes> <1258848661-4660-1-git-send-email-stefan@datenfreihafen.org>
+       <yunvdh3pfm9.fsf@aiko.keithp.com>
+MIME-Version: 1.0
+Content-Type: multipart/signed; micalg=pgp-sha1;
+       protocol="application/pgp-signature"; boundary="C7zPtVaVf+AK4Oqc"
+Content-Disposition: inline
+In-Reply-To: <yunvdh3pfm9.fsf@aiko.keithp.com>
+X-Mailer: Mutt http://www.mutt.org/
+X-KeyID: 0xDDF51665
+X-Website: http://www.datenfreihafen.org/
+User-Agent: Mutt/1.5.20 (2009-06-14)
+Cc: notmuch@notmuchmail.org
+Subject: Re: [notmuch] [PATCH 1/2] lib/message: Add function to get maildir
+ flags.
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+X-List-Received-Date: Sun, 22 Nov 2009 18:33:44 -0000
+
+
+--C7zPtVaVf+AK4Oqc
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+Content-Transfer-Encoding: quoted-printable
+
+Hello.
+
+On Sat, 2009-11-21 at 16:28, Keith Packard wrote:
+> On Sun, 22 Nov 2009 01:11:00 +0100, Stefan Schmidt <stefan@datenfreihafen=
+=2Eorg> wrote:
+>=20
+> > +const char *
+> > +notmuch_message_get_flags (notmuch_message_t *message)
+>=20
+> This function should interpret the flags that it finds and return a
+> suitable set of notmuch tags. I'd suggest that 'unread' messages get
+> both 'unread' and 'inbox' tags, as Maildir doesn't distinguish between
+> 'don't show this to be by default again please' and 'I've read this
+> message'. It seems best to hide the maildir-specific details inside the
+> library instead of exposing them.
+
+Thanks for the review. On a second thought the interface was really a bit u=
+gly.
+:)
+
+I'm just back to my box and going through the outstanding mails shows me th=
+at
+Michiel Buddingh has a more complete patch on the
+convert-maildir-flags-into-tags issue which Carl has tagged for review. Will
+wait what comes out of it and if anything is left for me to. :)
+
+regards
+Stefan Schmidt
+
+--C7zPtVaVf+AK4Oqc
+Content-Type: application/pgp-signature; name="signature.asc"
+Content-Description: Digital signature
+Content-Disposition: inline
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+Comment: http://www.datenfreihafen.org/contact.html
+
+iEYEARECAAYFAksJhAIACgkQbNSsvd31FmWDDgCgswbE3BE2XeExPzBBJf86efDw
+aFwAoMc3vaBmTjB2kG5ORUmk1E/ICBXK
+=k8v5
+-----END PGP SIGNATURE-----
+
+--C7zPtVaVf+AK4Oqc--
+
diff --git a/test/corpora/lkml/cur/1354585346.000324:2, b/test/corpora/lkml/cur/1354585346.000324:2,
new file mode 100644 (file)
index 0000000..a72ef9a
--- /dev/null
@@ -0,0 +1,71 @@
+Return-Path: <michiel@michielbuddingh.net>
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id B580E431FBC
+       for <notmuch@notmuchmail.org>; Sun, 22 Nov 2009 10:55:27 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
+Received: from olra.theworths.org ([127.0.0.1])
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id JHZeV0d6+Q8a for <notmuch@notmuchmail.org>;
+       Sun, 22 Nov 2009 10:55:26 -0800 (PST)
+Received: from aegir.org.uk (aegir.org.uk [87.238.170.13])
+       by olra.theworths.org (Postfix) with ESMTP id C6AAC431FAE
+       for <notmuch@notmuchmail.org>; Sun, 22 Nov 2009 10:55:26 -0800 (PST)
+Received: from localhost.localdomain (109-9-ftth.onsnetstudenten.nl
+       [145.120.9.109])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by aegir.org.uk (Postfix) with ESMTPSA id 48CE72E02A;
+       Sun, 22 Nov 2009 19:55:26 +0100 (CET)
+Date: Sun, 22 Nov 2009 19:55:26 +0100
+From: Michiel Buddingh' <michiel@michielbuddingh.net>
+To: notmuch@notmuchmail.org, stefan@datenfreihafen.org,
+ keithp@keithp.com
+Message-ID: <4b09891e.YhJ/aJZOBwneOaFr%michiel@michielbuddingh.net>
+References: <yes> <1258848661-4660-1-git-send-email-stefan@datenfreihafen.org>
+       <yunvdh3pfm9.fsf@aiko.keithp.com>
+       <20091122183338.GB5735@excalibur.local>
+In-Reply-To: <20091122183338.GB5735@excalibur.local>
+User-Agent: Heirloom mailx 12.4 7/29/08
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Subject: Re: [notmuch] [PATCH 1/2] lib/message: Add function to get maildir
+ flags.
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+X-List-Received-Date: Sun, 22 Nov 2009 18:55:27 -0000
+
+Stefan Schmidt <stefan@datenfreihafen.org> wrote:
+> > This function should interpret the flags that it finds and return a
+> > suitable set of notmuch tags. I'd suggest that 'unread' messages get
+> > both 'unread' and 'inbox' tags, as Maildir doesn't distinguish between
+> > 'don't show this to be by default again please' and 'I've read this
+> > message'. It seems best to hide the maildir-specific details inside the
+> > library instead of exposing them.
+>
+> Thanks for the review. On a second thought the interface was really a bit ugly.
+> :)
+>
+> I'm just back to my box and going through the outstanding mails shows me that
+> Michiel Buddingh has a more complete patch on the
+> convert-maildir-flags-into-tags issue which Carl has tagged for review. Will
+> wait what comes out of it and if anything is left for me to. :)
+
+Apologies.  In my haste to cover up my appalling and incorrect first patch, I
+neglected to review the archives to see if someone had already done this. Sorry
+for stealing your thunder.
+
+Michiel
+
diff --git a/test/corpora/lkml/cur/1354585346.000325:2, b/test/corpora/lkml/cur/1354585346.000325:2,
new file mode 100644 (file)
index 0000000..fc2da16
--- /dev/null
@@ -0,0 +1,86 @@
+Return-Path: <stefan@datenfreihafen.org>
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id B2DE3431FBC
+       for <notmuch@notmuchmail.org>; Sun, 22 Nov 2009 11:52:54 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
+Received: from olra.theworths.org ([127.0.0.1])
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id TrLH7uZhkvFU for <notmuch@notmuchmail.org>;
+       Sun, 22 Nov 2009 11:52:53 -0800 (PST)
+Received: from sirius.lasnet.de (sirius.lasnet.de [78.47.116.19])
+       by olra.theworths.org (Postfix) with ESMTP id 7024B431FAE
+       for <notmuch@notmuchmail.org>; Sun, 22 Nov 2009 11:52:53 -0800 (PST)
+Received: from p5b0353d3.dip.t-dialin.net ([91.3.83.211] helo=excalibur)
+       by sirius.lasnet.de with esmtpsa 
+       (Cipher TLS-1.0:RSA_AES_256_CBC_SHA1:32) (Exim 4.63 #1)
+       id 1NCIUJ-0002QO-OM by authid <stefan@sostec.de> with cram_md5;
+       Sun, 22 Nov 2009 20:52:50 +0100
+Received: from stefan by excalibur with local (Exim 4.69)
+       (envelope-from <stefan@excalibur.local>)
+       id 1NCIUI-0003ON-Sr; Sun, 22 Nov 2009 20:52:46 +0100
+Date: Sun, 22 Nov 2009 20:52:46 +0100
+From: Stefan Schmidt <stefan@datenfreihafen.org>
+To: Michiel Buddingh' <michiel@michielbuddingh.net>
+Message-ID: <20091122195246.GC5735@excalibur.local>
+References: <yes> <1258848661-4660-1-git-send-email-stefan@datenfreihafen.org>
+       <yunvdh3pfm9.fsf@aiko.keithp.com>
+       <20091122183338.GB5735@excalibur.local>
+       <4b09891e.YhJ/aJZOBwneOaFr%michiel@michielbuddingh.net>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+In-Reply-To: <4b09891e.YhJ/aJZOBwneOaFr%michiel@michielbuddingh.net>
+X-Mailer: Mutt http://www.mutt.org/
+X-KeyID: 0xDDF51665
+X-Website: http://www.datenfreihafen.org/
+User-Agent: Mutt/1.5.20 (2009-06-14)
+Cc: notmuch@notmuchmail.org
+Subject: Re: [notmuch] [PATCH 1/2] lib/message: Add function to get maildir
+ flags.
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+X-List-Received-Date: Sun, 22 Nov 2009 19:52:54 -0000
+
+Hello.
+
+On Sun, 2009-11-22 at 19:55, Michiel Buddingh' wrote:
+> Stefan Schmidt <stefan@datenfreihafen.org> wrote:
+> > > This function should interpret the flags that it finds and return a
+> > > suitable set of notmuch tags. I'd suggest that 'unread' messages get
+> > > both 'unread' and 'inbox' tags, as Maildir doesn't distinguish between
+> > > 'don't show this to be by default again please' and 'I've read this
+> > > message'. It seems best to hide the maildir-specific details inside the
+> > > library instead of exposing them.
+> >
+> > Thanks for the review. On a second thought the interface was really a bit ugly.
+> > :)
+> >
+> > I'm just back to my box and going through the outstanding mails shows me that
+> > Michiel Buddingh has a more complete patch on the
+> > convert-maildir-flags-into-tags issue which Carl has tagged for review. Will
+> > wait what comes out of it and if anything is left for me to. :)
+> 
+> Apologies.  In my haste to cover up my appalling and incorrect first patch, I
+> neglected to review the archives to see if someone had already done this. Sorry
+> for stealing your thunder.
+
+No need to be sorry. I'm interestecd in having this itch scratched, but don't
+care who is doing it. :)
+
+Just go ahead and get it in after Carl's review.
+
+regards
+Stefan Schmidt
+
diff --git a/test/corpora/lkml/cur/1354585346.000539:2, b/test/corpora/lkml/cur/1354585346.000539:2,
new file mode 100644 (file)
index 0000000..daa5673
--- /dev/null
@@ -0,0 +1,81 @@
+Return-Path: <ingmar@exherbo.org>
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id BAA52431FBC
+       for <notmuch@notmuchmail.org>; Thu, 26 Nov 2009 00:43:44 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
+Received: from olra.theworths.org ([127.0.0.1])
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id 0U+OLchDCS5T for <notmuch@notmuchmail.org>;
+       Thu, 26 Nov 2009 00:43:44 -0800 (PST)
+Received: from bach.exherbo.org (bach.exherbo.org [78.47.197.147])
+       by olra.theworths.org (Postfix) with ESMTP id 05223431FAE
+       for <notmuch@notmuchmail.org>; Thu, 26 Nov 2009 00:43:43 -0800 (PST)
+Received: from [83.101.72.69] (helo=localhost)
+       by bach.exherbo.org with esmtpsa (TLSv1:AES256-SHA:256) (Exim 4.69)
+       (envelope-from <ingmar@exherbo.org>) id 1NDZx1-0000VV-3u
+       for notmuch@notmuchmail.org; Thu, 26 Nov 2009 08:43:43 +0000
+Content-Type: text/plain; charset=utf8
+From: Ingmar Vanhassel <ingmar@exherbo.org>
+To: notmuch <notmuch@notmuchmail.org>
+In-reply-to: <1259223435-29656-1-git-send-email-stefan@datenfreihafen.org>
+References: <yes> <1259223435-29656-1-git-send-email-stefan@datenfreihafen.org>
+Date: Thu, 26 Nov 2009 09:43:42 +0100
+Message-Id: <1259224970-sup-5259@cannonball>
+User-Agent: Sup/git
+Content-Transfer-Encoding: 8bit
+Subject: Re: [notmuch] [PATCH] Makefile: Enable backslash escapes for echo.
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+X-List-Received-Date: Thu, 26 Nov 2009 08:43:44 -0000
+
+Excerpts from Stefan Schmidt's message of Thu Nov 26 09:17:15 +0100 2009:
+> This fixes a visual glitch during a silent compile.
+> Before:
+> Use "make V=1" to see the verbose compile lines.\n  CC  debugger.o
+>   CC    gmime-filter-reply.o
+> 
+> After:
+> Use "make V=1" to see the verbose compile lines.
+>   CC    debugger.o
+>   CC    gmime-filter-reply.o
+> 
+> Signed-off-by: Stefan Schmidt <stefan@datenfreihafen.org>
+
+Looks right, works here with bash, dash & zsh, so:
+
+Reviewed-by: Ingmar Vanhassel <ingmar@exherbo.org>
+
+Thanks!
+
+> ---
+>  Makefile |    2 +-
+>  1 files changed, 1 insertions(+), 1 deletions(-)
+> 
+> diff --git a/Makefile b/Makefile
+> index 2cd1b1b..2d19a6e 100644
+> --- a/Makefile
+> +++ b/Makefile
+> @@ -41,7 +41,7 @@ include Makefile.config
+>  # user how to enable verbose compiles.
+>  ifeq ($(V),)
+>  quiet_DOC := "Use \"$(MAKE) V=1\" to see the verbose compile lines.\n"
+> -quiet = @echo $(quiet_DOC)$(eval quiet_DOC:=)"  $1    $@"; $($1)
+> +quiet = @echo -e $(quiet_DOC)$(eval quiet_DOC:=)"  $1    $@"; $($1)
+>  endif
+>  # The user has explicitly enabled quiet compilation.
+>  ifeq ($(V),0)
+-- 
+Exherbo KDE, X.org maintainer
+
diff --git a/test/corpora/lkml/cur/1354585346.000541:2, b/test/corpora/lkml/cur/1354585346.000541:2,
new file mode 100644 (file)
index 0000000..3e70055
--- /dev/null
@@ -0,0 +1,64 @@
+Return-Path: <kha@treskal.com>
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+       by olra.theworths.org (Postfix) with ESMTP id 54307431FBC
+       for <notmuch@notmuchmail.org>; Thu, 26 Nov 2009 03:41:18 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
+Received: from olra.theworths.org ([127.0.0.1])
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
+       with ESMTP id TBdvxm5kBScu for <notmuch@notmuchmail.org>;
+       Thu, 26 Nov 2009 03:41:13 -0800 (PST)
+Received: from mail1.space2u.com (mail1.space2u.com [62.20.1.135])
+       by olra.theworths.org (Postfix) with ESMTP id 80538431FAE
+       for <notmuch@notmuchmail.org>; Thu, 26 Nov 2009 03:41:13 -0800 (PST)
+Received: from mail-bw0-f224.google.com (mail-bw0-f224.google.com
+       [209.85.218.224]) (authenticated bits=0)
+       by mail1.space2u.com (8.14.3/8.14.3) with ESMTP id nAQBf0Ar018995
+       (version=TLSv1/SSLv3 cipher=DES-CBC3-SHA bits=168 verify=NOT)
+       for <notmuch@notmuchmail.org>; Thu, 26 Nov 2009 12:41:01 +0100
+Received: by bwz24 with SMTP id 24so480173bwz.30
+       for <notmuch@notmuchmail.org>; Thu, 26 Nov 2009 03:41:11 -0800 (PST)
+MIME-Version: 1.0
+Received: by 10.204.153.3 with SMTP id i3mr2263267bkw.26.1259235670122; Thu, 
+       26 Nov 2009 03:41:10 -0800 (PST)
+In-Reply-To: <20091126110505.GI25119@ryngle.com>
+References: <1259223435-29656-1-git-send-email-stefan@datenfreihafen.org>
+       <20091126110505.GI25119@ryngle.com>
+Date: Thu, 26 Nov 2009 12:41:10 +0100
+Message-ID: <b8197bcb0911260341o480edc2bof8a30f0b724dd96@mail.gmail.com>
+From: Karl Wiberg <kha@treskal.com>
+To: Jan Janak <jan@ryngle.com>
+Content-Type: text/plain; charset=UTF-8
+Cc: notmuch@notmuchmail.org
+Subject: Re: [notmuch] [PATCH] Makefile: Enable backslash escapes for echo.
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+X-List-Received-Date: Thu, 26 Nov 2009 11:41:18 -0000
+
+On Thu, Nov 26, 2009 at 12:05 PM, Jan Janak <jan@ryngle.com> wrote:
+
+> I sent exactly the same patch a couple of days ago and it was
+> rejected because it does not work everywhere, see:
+>
+> http://notmuchmail.org/pipermail/notmuch/2009/000370.html
+
+And as I said in that thread, I believe you should use printf instead.
+(http://www.in-ulm.de/~mascheck/various/echo+printf/ seems like a good
+reference in this matter.)
+
+-- 
+Karl Wiberg, kha@treskal.com
+   subrabbit.wordpress.com
+   www.treskal.com/kalle
+
diff --git a/test/corpora/lkml/cur/1382298587.001724:2, b/test/corpora/lkml/cur/1382298587.001724:2,
new file mode 100644 (file)
index 0000000..69c794c
--- /dev/null
@@ -0,0 +1,104 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 10/10] cifs: add mount option to enable local caching
+Date: Tue, 22 Jun 2010 20:55:09 +0530
+Lines: 66
+Message-ID: <1277220309-3757-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:25:29 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5Ls-0004PS-BM
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:25:28 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1755015Ab0FVPZ1 (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:25:27 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:48639 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1754070Ab0FVPZ1 (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:25:27 -0400
+X-Greylist: delayed 316 seconds by postgrey-1.27 at vger.kernel.org; Tue, 22 Jun 2010 11:25:26 EDT
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:25:11 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001750>
+
+Add a mount option 'fsc' to enable local caching on CIFS.
+
+As the cifs-utils (userspace) changes are not done yet, this patch enables
+'fsc' by default to assist testing.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+---
+ fs/cifs/cifs_fs_sb.h |    1 +
+ fs/cifs/connect.c    |    8 ++++++++
+ 2 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/fs/cifs/cifs_fs_sb.h b/fs/cifs/cifs_fs_sb.h
+index 246a167..9e77145 100644
+--- a/fs/cifs/cifs_fs_sb.h
++++ b/fs/cifs/cifs_fs_sb.h
+@@ -35,6 +35,7 @@
+ #define CIFS_MOUNT_DYNPERM      0x1000 /* allow in-memory only mode setting   */
+ #define CIFS_MOUNT_NOPOSIXBRL   0x2000 /* mandatory not posix byte range lock */
+ #define CIFS_MOUNT_NOSSYNC      0x4000 /* don't do slow SMBflush on every sync*/
++#define CIFS_MOUNT_FSCACHE    0x8000 /* local caching enabled */
+ struct cifs_sb_info {
+       struct cifsTconInfo *tcon;      /* primary mount */
+diff --git a/fs/cifs/connect.c b/fs/cifs/connect.c
+index 4844dbd..6c6ff3c 100644
+--- a/fs/cifs/connect.c
++++ b/fs/cifs/connect.c
+@@ -98,6 +98,7 @@ struct smb_vol {
+       bool noblocksnd:1;
+       bool noautotune:1;
+       bool nostrictsync:1; /* do not force expensive SMBflush on every sync */
++      bool fsc:1;     /* enable fscache */
+       unsigned int rsize;
+       unsigned int wsize;
+       bool sockopt_tcp_nodelay:1;
+@@ -843,6 +844,9 @@ cifs_parse_mount_options(char *options, const char *devname,
+       /* default to using server inode numbers where available */
+       vol->server_ino = 1;
++      /* XXX: default to fsc for testing until mount.cifs pieces are done */
++      vol->fsc = 1;
++
+       if (!options)
+               return 1;
+@@ -1332,6 +1336,8 @@ cifs_parse_mount_options(char *options, const char *devname,
+                       printk(KERN_WARNING "CIFS: Mount option noac not "
+                               "supported. Instead set "
+                               "/proc/fs/cifs/LookupCacheEnabled to 0\n");
++              } else if (strnicmp(data, "fsc", 3) == 0) {
++                      vol->fsc = true;
+               } else
+                       printk(KERN_WARNING "CIFS: Unknown mount option %s\n",
+                                               data);
+@@ -2405,6 +2411,8 @@ static void setup_cifs_sb(struct smb_vol *pvolume_info,
+               cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_OVERR_GID;
+       if (pvolume_info->dynperm)
+               cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_DYNPERM;
++      if (pvolume_info->fsc)
++              cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_FSCACHE;
+       if (pvolume_info->direct_io) {
+               cFYI(1, "mounting share using direct i/o");
+               cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_DIRECT_IO;
+-- 
+1.6.4.2
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001730:2, b/test/corpora/lkml/cur/1382298587.001730:2,
new file mode 100644 (file)
index 0000000..840be2e
--- /dev/null
@@ -0,0 +1,103 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 00/10] cifs: local caching support using FS-Cache
+Date: Tue, 22 Jun 2010 20:50:05 +0530
+Lines: 66
+Message-ID: <1277220005-3322-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:40:38 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5aY-00055O-BD
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:40:38 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751889Ab0FVPkf (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:40:35 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:50040 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751554Ab0FVPkf (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:40:35 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:20:07 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001756>
+
+This patchset is a first stab at adding persistent, local caching facility for
+CIFS using the FS-Cache interface.
+
+The index hierarchy which is mainly used to locate a file object or discard
+a certain subset of the files cached, currently has three levels:
+       - Server
+       - Share 
+       - File
+
+The server index object is keyed by hostname of the server. The superblock
+index object is keyed by the sharename and the inode object is keyed by the
+UniqueId. The cache coherency is ensured by checking the 'LastWriteTime' and
+size of file.
+
+To use this, apply this patchset in order, mount the share with rsize=4096 and
+try copying a huge file (say few hundred MBs) from mount point to local
+filesystem. During the first time, the cache will be initialized. When you copy
+the second time, it should read from the local cache.
+
+To reduce the impact of page cache and see the local caching in action
+readily, try doing a sync and drop the caches by doing:
+       sync; echo 3 > /proc/sys/vm/drop_caches
+
+Known issues
+-------------
+       - the cache coherency check may not be reliable always as some
+         CIFS servers are known not to update mtime until the filehandle is
+         closed.
+       - not all the Servers under all circumstances provide a unique
+         'UniqueId'.
+
+Todo's
+-------
+       - improvements to avoid potential key collisions
+       - address the above known issues
+
+This set is lightly tested and all the bugs seen during my testing have been
+fixed. However, this can be considered as an RFC for now.
+
+Any Comments or Suggestions are welcome.
+
+Suresh Jayaraman (10)
+  cifs: add kernel config option for CIFS Client caching support
+  cifs: guard cifsglob.h against multiple inclusion
+  cifs: register CIFS for caching
+  cifs: define server-level cache index objects and register them with FS-Cache
+  cifs: define superblock-level cache index objects and register them
+  cifs: define inode-level cache object and register them
+  cifs: FS-Cache page management
+  cifs: store pages into local cache
+  cifs: read pages from FS-Cache
+  cifs: add mount option to enable local caching
+
+ Kconfig      |    9 ++
+ Makefile     |    2 
+ cache.c      |  251 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ cifs_fs_sb.h |    1 
+ cifsfs.c     |   15 +++
+ cifsglob.h   |   14 +++
+ connect.c    |   16 +++
+ file.c       |   51 +++++++++++
+ fscache.c    |  244 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ fscache.h    |  135 +++++++++++++++++++++++++++++++
+ inode.c      |    4 
+ 11 files changed, 742 insertions(+)
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001731:2, b/test/corpora/lkml/cur/1382298587.001731:2,
new file mode 100644 (file)
index 0000000..d8b3168
--- /dev/null
@@ -0,0 +1,67 @@
+From: Suresh Jayaraman <sjayaraman@suse.de>
+Subject: [RFC][PATCH 01/10] cifs: add kernel config option for CIFS Client caching support
+Date: Tue, 22 Jun 2010 20:52:38 +0530
+Lines: 30
+Message-ID: <1277220158-3405-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, David Howells <dhowells@redhat.com>
+To: Steve French <smfrench@gmail.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Jun 22 17:43:27 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OR5dG-0007m9-Ij
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 22 Jun 2010 17:43:26 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1751536Ab0FVPnS (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 22 Jun 2010 11:43:18 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:51303 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1750800Ab0FVPnR (ORCPT
+       <rfc822;groupwise-SJayaraman@novell.com:0:0>);
+       Tue, 22 Jun 2010 11:43:17 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:22:40 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001757>
+
+Add a kernel config option to enable local caching for CIFS.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+---
+ fs/cifs/Kconfig |    9 +++++++++
+ 1 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/fs/cifs/Kconfig b/fs/cifs/Kconfig
+index 80f3525..5739fd7 100644
+--- a/fs/cifs/Kconfig
++++ b/fs/cifs/Kconfig
+@@ -131,6 +131,15 @@ config CIFS_DFS_UPCALL
+           IP addresses) which is needed for implicit mounts of DFS junction
+           points. If unsure, say N.
++config CIFS_FSCACHE
++        bool "Provide CIFS client caching support (EXPERIMENTAL)"
++        depends on EXPERIMENTAL
++        depends on CIFS=m && FSCACHE || CIFS=y && FSCACHE=y
++        help
++          Makes CIFS FS-Cache capable. Say Y here if you want your CIFS data
++          to be cached locally on disk through the general filesystem cache
++          manager. If unsure, say N.
++
+ config CIFS_EXPERIMENTAL
+         bool "CIFS Experimental Features (EXPERIMENTAL)"
+         depends on CIFS && EXPERIMENTAL
+-- 
+1.6.4.2
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001732:2, b/test/corpora/lkml/cur/1382298587.001732:2,
new file mode 100644 (file)
index 0000000..8850953
--- /dev/null
@@ -0,0 +1,73 @@
+From: Suresh Jayaraman <sjayaraman@suse.de>
+Subject: [RFC][PATCH 02/10] cifs: guard cifsglob.h against multiple inclusion
+Date: Tue, 22 Jun 2010 20:52:50 +0530
+Lines: 36
+Message-ID: <1277220170-3442-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, David Howells <dhowells@redhat.com>
+To: Steve French <smfrench@gmail.com>
+X-From: linux-fsdevel-owner@vger.kernel.org Tue Jun 22 17:43:39 2010
+Return-path: <linux-fsdevel-owner@vger.kernel.org>
+Envelope-to: lnx-linux-fsdevel@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-fsdevel-owner@vger.kernel.org>)
+       id 1OR5dT-0007sB-18
+       for lnx-linux-fsdevel@lo.gmane.org; Tue, 22 Jun 2010 17:43:39 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752441Ab0FVPn3 (ORCPT <rfc822;lnx-linux-fsdevel@m.gmane.org>);
+       Tue, 22 Jun 2010 11:43:29 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:41538 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751889Ab0FVPn2 (ORCPT
+       <rfc822;groupwise-SJayaraman@novell.com:0:0>);
+       Tue, 22 Jun 2010 11:43:28 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:22:52 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-fsdevel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-fsdevel.vger.kernel.org>
+X-Mailing-List: linux-fsdevel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001758>
+
+Add conditional compile macros to guard the header file against multiple
+inclusion.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+---
+ fs/cifs/cifsglob.h |    5 +++++
+ 1 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h
+index a88479c..6b2c39d 100644
+--- a/fs/cifs/cifsglob.h
++++ b/fs/cifs/cifsglob.h
+@@ -16,6 +16,9 @@
+  *   the GNU Lesser General Public License for more details.
+  *
+  */
++#ifndef _CIFS_GLOB_H
++#define _CIFS_GLOB_H
++
+ #include <linux/in.h>
+ #include <linux/in6.h>
+ #include <linux/slab.h>
+@@ -733,3 +736,5 @@ GLOBAL_EXTERN unsigned int cifs_min_small;  /* min size of small buf pool */
+ GLOBAL_EXTERN unsigned int cifs_max_pending; /* MAX requests at once to server*/
+ extern const struct slow_work_ops cifs_oplock_break_ops;
++
++#endif        /* _CIFS_GLOB_H */
+-- 
+1.6.4.2
+
+--
+To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001733:2, b/test/corpora/lkml/cur/1382298587.001733:2,
new file mode 100644 (file)
index 0000000..d782f90
--- /dev/null
@@ -0,0 +1,211 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 03/10] cifs: register CIFS for caching
+Date: Tue, 22 Jun 2010 20:53:09 +0530
+Lines: 174
+Message-ID: <1277220189-3485-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:43:52 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5de-0007xC-Ov
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:43:51 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1753125Ab0FVPnt (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:43:49 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:55866 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751261Ab0FVPnt (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:43:49 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:23:11 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001759>
+
+Define CIFS for FS-Cache and register for caching. Upon registration the
+top-level index object cookie will be stuck to the netfs definition by
+FS-Cache.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+---
+ fs/cifs/Makefile  |    2 ++
+ fs/cifs/cache.c   |   53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/cifsfs.c  |    8 ++++++++
+ fs/cifs/fscache.h |   40 ++++++++++++++++++++++++++++++++++++++++
+ 4 files changed, 103 insertions(+), 0 deletions(-)
+ create mode 100644 fs/cifs/cache.c
+ create mode 100644 fs/cifs/fscache.h
+
+diff --git a/fs/cifs/Makefile b/fs/cifs/Makefile
+index 9948c00..e2de709 100644
+--- a/fs/cifs/Makefile
++++ b/fs/cifs/Makefile
+@@ -11,3 +11,5 @@ cifs-y := cifsfs.o cifssmb.o cifs_debug.o connect.o dir.o file.o inode.o \
+ cifs-$(CONFIG_CIFS_UPCALL) += cifs_spnego.o
+ cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o
++
++cifs-$(CONFIG_CIFS_FSCACHE) += cache.o
+diff --git a/fs/cifs/cache.c b/fs/cifs/cache.c
+new file mode 100644
+index 0000000..1080b96
+--- /dev/null
++++ b/fs/cifs/cache.c
+@@ -0,0 +1,53 @@
++/*
++ *   fs/cifs/cache.c - CIFS filesystem cache index structure definitions
++ *
++ *   Copyright (c) 2010 Novell, Inc.
++ *   Authors(s): Suresh Jayaraman (sjayaraman-l3A5Bk7waGM@public.gmane.org>
++ *
++ *   This library is free software; you can redistribute it and/or modify
++ *   it under the terms of the GNU Lesser General Public License as published
++ *   by the Free Software Foundation; either version 2.1 of the License, or
++ *   (at your option) any later version.
++ *
++ *   This library 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 Lesser General Public License for more details.
++ *
++ *   You should have received a copy of the GNU Lesser General Public License
++ *   along with this library; if not, write to the Free Software
++ *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
++ */
++#include <linux/init.h>
++#include <linux/kernel.h>
++#include <linux/sched.h>
++#include <linux/mm.h>
++
++#include "fscache.h"
++#include "cifsglob.h"
++#include "cifs_debug.h"
++
++/*
++ * CIFS filesystem definition for FS-Cache
++ */
++struct fscache_netfs cifs_fscache_netfs = {
++      .name = "cifs",
++      .version = 0,
++};
++
++/*
++ * Register CIFS for caching with FS-Cache
++ */
++int cifs_fscache_register(void)
++{
++      return fscache_register_netfs(&cifs_fscache_netfs);
++}
++
++/*
++ * Unregister CIFS for caching
++ */
++void cifs_fscache_unregister(void)
++{
++      fscache_unregister_netfs(&cifs_fscache_netfs);
++}
++
+diff --git a/fs/cifs/cifsfs.c b/fs/cifs/cifsfs.c
+index 484e52b..c2a7aa9 100644
+--- a/fs/cifs/cifsfs.c
++++ b/fs/cifs/cifsfs.c
+@@ -47,6 +47,7 @@
+ #include <linux/key-type.h>
+ #include "dns_resolve.h"
+ #include "cifs_spnego.h"
++#include "fscache.h"
+ #define CIFS_MAGIC_NUMBER 0xFF534D42  /* the first four bytes of SMB PDUs */
+ int cifsFYI = 0;
+@@ -902,6 +903,10 @@ init_cifs(void)
+               cFYI(1, "cifs_max_pending set to max of 256");
+       }
++      rc = cifs_fscache_register();
++      if (rc)
++              goto out;
++
+       rc = cifs_init_inodecache();
+       if (rc)
+               goto out_clean_proc;
+@@ -949,8 +954,10 @@ init_cifs(void)
+       cifs_destroy_mids();
+  out_destroy_inodecache:
+       cifs_destroy_inodecache();
++      cifs_fscache_unregister();
+  out_clean_proc:
+       cifs_proc_clean();
++ out:
+       return rc;
+ }
+@@ -959,6 +966,7 @@ exit_cifs(void)
+ {
+       cFYI(DBG2, "exit_cifs");
+       cifs_proc_clean();
++      cifs_fscache_unregister();
+ #ifdef CONFIG_CIFS_DFS_UPCALL
+       cifs_dfs_release_automount_timer();
+       unregister_key_type(&key_type_dns_resolver);
+diff --git a/fs/cifs/fscache.h b/fs/cifs/fscache.h
+new file mode 100644
+index 0000000..cec9e2b
+--- /dev/null
++++ b/fs/cifs/fscache.h
+@@ -0,0 +1,40 @@
++/*
++ *   fs/cifs/fscache.h - CIFS filesystem cache interface definitions
++ *
++ *   Copyright (c) 2010 Novell, Inc.
++ *   Authors(s): Suresh Jayaraman (sjayaraman-l3A5Bk7waGM@public.gmane.org>
++ *
++ *   This library is free software; you can redistribute it and/or modify
++ *   it under the terms of the GNU Lesser General Public License as published
++ *   by the Free Software Foundation; either version 2.1 of the License, or
++ *   (at your option) any later version.
++ *
++ *   This library 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 Lesser General Public License for more details.
++ *
++ *   You should have received a copy of the GNU Lesser General Public License
++ *   along with this library; if not, write to the Free Software
++ *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
++ */
++#ifndef _CIFS_FSCACHE_H
++#define _CIFS_FSCACHE_H
++
++#include <linux/fscache.h>
++#include "cifsglob.h"
++
++#ifdef CONFIG_CIFS_FSCACHE
++
++extern struct fscache_netfs cifs_fscache_netfs;
++
++extern int cifs_fscache_register(void);
++extern void cifs_fscache_unregister(void);
++
++#else /* CONFIG_CIFS_FSCACHE */
++static inline int cifs_fscache_register(void) { return 0; }
++static inline void cifs_fscache_unregister(void) {}
++
++#endif /* CONFIG_CIFS_FSCACHE */
++
++#endif /* _CIFS_FSCACHE_H */
+-- 
+1.6.4.2
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001734:2, b/test/corpora/lkml/cur/1382298587.001734:2,
new file mode 100644 (file)
index 0000000..4b64bc3
--- /dev/null
@@ -0,0 +1,223 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 04/10] cifs: define server-level cache index objects and register them with FS-Cache
+Date: Tue, 22 Jun 2010 20:53:18 +0530
+Lines: 186
+Message-ID: <1277220198-3522-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:44:26 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5eD-0008G7-KP
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:44:26 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1753942Ab0FVPoC (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:44:02 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:58783 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751265Ab0FVPoA (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:44:00 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:23:20 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001760>
+
+Define server-level cache index objects (as managed by TCP_ServerInfo structs).
+Each server object is created in the CIFS top-level index object and is itself
+an index into which superblock-level objects are inserted.
+
+Currently, the server objects are keyed by hostname.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+---
+ fs/cifs/Makefile   |    2 +-
+ fs/cifs/cache.c    |   25 +++++++++++++++++++++++++
+ fs/cifs/cifsglob.h |    3 +++
+ fs/cifs/connect.c  |    4 ++++
+ fs/cifs/fscache.c  |   47 +++++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/fscache.h  |   12 ++++++++++++
+ 6 files changed, 92 insertions(+), 1 deletion(-)
+ create mode 100644 fs/cifs/fscache.c
+
+Index: cifs-2.6/fs/cifs/Makefile
+===================================================================
+--- cifs-2.6.orig/fs/cifs/Makefile
++++ cifs-2.6/fs/cifs/Makefile
+@@ -12,4 +12,4 @@ cifs-$(CONFIG_CIFS_UPCALL) += cifs_spneg
+ cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o
+-cifs-$(CONFIG_CIFS_FSCACHE) += cache.o
++cifs-$(CONFIG_CIFS_FSCACHE) += fscache.o cache.o
+Index: cifs-2.6/fs/cifs/cache.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cache.c
++++ cifs-2.6/fs/cifs/cache.c
+@@ -51,3 +51,28 @@ void cifs_fscache_unregister(void)
+       fscache_unregister_netfs(&cifs_fscache_netfs);
+ }
++/*
++ * Server object currently keyed by hostname
++ */
++static uint16_t cifs_server_get_key(const void *cookie_netfs_data,
++                                 void *buffer, uint16_t maxbuf)
++{
++      const struct TCP_Server_Info *server = cookie_netfs_data;
++      uint16_t len = strnlen(server->hostname, sizeof(server->hostname));
++
++      if (len > maxbuf)
++              return 0;
++
++      memcpy(buffer, server->hostname, len);
++
++      return len;
++}
++
++/*
++ * Server object for FS-Cache
++ */
++const struct fscache_cookie_def cifs_fscache_server_index_def = {
++      .name = "CIFS.server",
++      .type = FSCACHE_COOKIE_TYPE_INDEX,
++      .get_key = cifs_server_get_key,
++};
+Index: cifs-2.6/fs/cifs/cifsglob.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cifsglob.h
++++ cifs-2.6/fs/cifs/cifsglob.h
+@@ -193,6 +193,9 @@ struct TCP_Server_Info {
+       bool    sec_mskerberos;         /* supports legacy MS Kerberos */
+       bool    sec_kerberosu2u;        /* supports U2U Kerberos */
+       bool    sec_ntlmssp;            /* supports NTLMSSP */
++#ifdef CONFIG_CIFS_FSCACHE
++      struct fscache_cookie   *fscache; /* client index cache cookie */
++#endif
+ };
+ /*
+Index: cifs-2.6/fs/cifs/connect.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/connect.c
++++ cifs-2.6/fs/cifs/connect.c
+@@ -48,6 +48,7 @@
+ #include "nterr.h"
+ #include "rfc1002pdu.h"
+ #include "cn_cifs.h"
++#include "fscache.h"
+ #define CIFS_PORT 445
+ #define RFC1001_PORT 139
+@@ -1453,6 +1454,8 @@ cifs_put_tcp_session(struct TCP_Server_I
+               return;
+       }
++      cifs_fscache_release_client_cookie(server);
++
+       list_del_init(&server->tcp_ses_list);
+       write_unlock(&cifs_tcp_ses_lock);
+@@ -1572,6 +1575,7 @@ cifs_get_tcp_session(struct smb_vol *vol
+               goto out_err;
+       }
++      cifs_fscache_get_client_cookie(tcp_ses);
+       /* thread spawned, put it on the list */
+       write_lock(&cifs_tcp_ses_lock);
+       list_add(&tcp_ses->tcp_ses_list, &cifs_tcp_ses_list);
+Index: cifs-2.6/fs/cifs/fscache.c
+===================================================================
+--- /dev/null
++++ cifs-2.6/fs/cifs/fscache.c
+@@ -0,0 +1,47 @@
++/*
++ *   fs/cifs/fscache.c - CIFS filesystem cache interface
++ *
++ *   Copyright (c) 2010 Novell, Inc.
++ *   Authors(s): Suresh Jayaraman (sjayaraman-l3A5Bk7waGM@public.gmane.org>
++ *
++ *   This library is free software; you can redistribute it and/or modify
++ *   it under the terms of the GNU Lesser General Public License as published
++ *   by the Free Software Foundation; either version 2.1 of the License, or
++ *   (at your option) any later version.
++ *
++ *   This library 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 Lesser General Public License for more details.
++ *
++ *   You should have received a copy of the GNU Lesser General Public License
++ *   along with this library; if not, write to the Free Software
++ *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
++ */
++#include <linux/init.h>
++#include <linux/kernel.h>
++#include <linux/sched.h>
++#include <linux/mm.h>
++#include <linux/in6.h>
++
++#include "fscache.h"
++#include "cifsglob.h"
++#include "cifs_debug.h"
++
++void cifs_fscache_get_client_cookie(struct TCP_Server_Info *server)
++{
++      server->fscache =
++              fscache_acquire_cookie(cifs_fscache_netfs.primary_index,
++                              &cifs_fscache_server_index_def, server);
++      cFYI(1, "CIFS: get client cookie (0x%p/0x%p)\n",
++                              server, server->fscache);
++}
++
++void cifs_fscache_release_client_cookie(struct TCP_Server_Info *server)
++{
++      cFYI(1, "CIFS: release client cookie (0x%p/0x%p)\n",
++                              server, server->fscache);
++      fscache_relinquish_cookie(server->fscache, 0);
++      server->fscache = NULL;
++}
++
+Index: cifs-2.6/fs/cifs/fscache.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/fscache.h
++++ cifs-2.6/fs/cifs/fscache.h
+@@ -27,14 +27,26 @@
+ #ifdef CONFIG_CIFS_FSCACHE
+ extern struct fscache_netfs cifs_fscache_netfs;
++extern const struct fscache_cookie_def cifs_fscache_server_index_def;
+ extern int cifs_fscache_register(void);
+ extern void cifs_fscache_unregister(void);
++/*
++ * fscache.c
++ */
++extern void cifs_fscache_get_client_cookie(struct TCP_Server_Info *);
++extern void cifs_fscache_release_client_cookie(struct TCP_Server_Info *);
++
+ #else /* CONFIG_CIFS_FSCACHE */
+ static inline int cifs_fscache_register(void) { return 0; }
+ static inline void cifs_fscache_unregister(void) {}
++static inline void
++cifs_fscache_get_client_cookie(struct TCP_Server_Info *server) {}
++static inline void
++cifs_fscache_get_client_cookie(struct TCP_Server_Info *server); {}
++
+ #endif /* CONFIG_CIFS_FSCACHE */
+ #endif /* _CIFS_FSCACHE_H */
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001735:2, b/test/corpora/lkml/cur/1382298587.001735:2,
new file mode 100644 (file)
index 0000000..d76da35
--- /dev/null
@@ -0,0 +1,212 @@
+From: Suresh Jayaraman <sjayaraman@suse.de>
+Subject: [RFC][PATCH 07/10] cifs: FS-Cache page management
+Date: Tue, 22 Jun 2010 20:53:48 +0530
+Lines: 175
+Message-ID: <1277220228-3635-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, David Howells <dhowells@redhat.com>
+To: Steve French <smfrench@gmail.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Jun 22 17:44:27 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OR5eF-0008G7-BK
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 22 Jun 2010 17:44:27 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1754757Ab0FVPoS (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 22 Jun 2010 11:44:18 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:54214 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752542Ab0FVPoB (ORCPT
+       <rfc822;groupwise-SJayaraman@novell.com:0:0>);
+       Tue, 22 Jun 2010 11:44:01 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:23:50 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001761>
+
+Takes care of invalidation and release of FS-Cache marked pages and also
+invalidation of the FsCache page flag when the inode is removed.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+---
+ fs/cifs/cache.c   |   31 +++++++++++++++++++++++++++++++
+ fs/cifs/file.c    |   20 ++++++++++++++++++++
+ fs/cifs/fscache.c |   26 ++++++++++++++++++++++++++
+ fs/cifs/fscache.h |   16 ++++++++++++++++
+ 4 files changed, 93 insertions(+), 0 deletions(-)
+
+diff --git a/fs/cifs/cache.c b/fs/cifs/cache.c
+index b205424..3a733c1 100644
+--- a/fs/cifs/cache.c
++++ b/fs/cifs/cache.c
+@@ -210,6 +210,36 @@ fscache_checkaux cifs_fscache_inode_check_aux(void *cookie_netfs_data,
+       return FSCACHE_CHECKAUX_OKAY;
+ }
++static void cifs_fscache_inode_now_uncached(void *cookie_netfs_data)
++{
++      struct cifsInodeInfo *cifsi = cookie_netfs_data;
++      struct pagevec pvec;
++      pgoff_t first;
++      int loop, nr_pages;
++
++      pagevec_init(&pvec, 0);
++      first = 0;
++
++      cFYI(1, "cifs inode 0x%p now uncached\n", cifsi);
++
++      for (;;) {
++              nr_pages = pagevec_lookup(&pvec,
++                                        cifsi->vfs_inode.i_mapping, first,
++                                        PAGEVEC_SIZE - pagevec_count(&pvec));
++              if (!nr_pages)
++                      break;
++
++              for (loop = 0; loop < nr_pages; loop++)
++                      ClearPageFsCache(pvec.pages[loop]);
++
++              first = pvec.pages[nr_pages - 1]->index + 1;
++
++              pvec.nr = nr_pages;
++              pagevec_release(&pvec);
++              cond_resched();
++      }
++}
++
+ const struct fscache_cookie_def cifs_fscache_inode_object_def = {
+       .name           = "CIFS.uniqueid",
+       .type           = FSCACHE_COOKIE_TYPE_DATAFILE,
+@@ -217,4 +247,5 @@ const struct fscache_cookie_def cifs_fscache_inode_object_def = {
+       .get_attr       = cifs_fscache_inode_get_attr,
+       .get_aux        = cifs_fscache_inode_get_aux,
+       .check_aux      = cifs_fscache_inode_check_aux,
++      .now_uncached   = cifs_fscache_inode_now_uncached,
+ };
+diff --git a/fs/cifs/file.c b/fs/cifs/file.c
+index 55ecb55..786ec04 100644
+--- a/fs/cifs/file.c
++++ b/fs/cifs/file.c
+@@ -2271,6 +2271,22 @@ out:
+       return rc;
+ }
++static int cifs_release_page(struct page *page, gfp_t gfp)
++{
++      if (PagePrivate(page))
++              return 0;
++
++      return cifs_fscache_release_page(page, gfp);
++}
++
++static void cifs_invalidate_page(struct page *page, unsigned long offset)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(page->mapping->host);
++
++      if (offset == 0)
++              cifs_fscache_invalidate_page(page, &cifsi->vfs_inode);
++}
++
+ static void
+ cifs_oplock_break(struct slow_work *work)
+ {
+@@ -2344,6 +2360,8 @@ const struct address_space_operations cifs_addr_ops = {
+       .write_begin = cifs_write_begin,
+       .write_end = cifs_write_end,
+       .set_page_dirty = __set_page_dirty_nobuffers,
++      .releasepage = cifs_release_page,
++      .invalidatepage = cifs_invalidate_page,
+       /* .sync_page = cifs_sync_page, */
+       /* .direct_IO = */
+ };
+@@ -2360,6 +2378,8 @@ const struct address_space_operations cifs_addr_ops_smallbuf = {
+       .write_begin = cifs_write_begin,
+       .write_end = cifs_write_end,
+       .set_page_dirty = __set_page_dirty_nobuffers,
++      .releasepage = cifs_release_page,
++      .invalidatepage = cifs_invalidate_page,
+       /* .sync_page = cifs_sync_page, */
+       /* .direct_IO = */
+ };
+diff --git a/fs/cifs/fscache.c b/fs/cifs/fscache.c
+index ddfd355..c09d3b8 100644
+--- a/fs/cifs/fscache.c
++++ b/fs/cifs/fscache.c
+@@ -130,3 +130,29 @@ void cifs_fscache_reset_inode_cookie(struct inode *inode)
+       }
+ }
++int cifs_fscache_release_page(struct page *page, gfp_t gfp)
++{
++      if (PageFsCache(page)) {
++              struct inode *inode = page->mapping->host;
++              struct cifsInodeInfo *cifsi = CIFS_I(inode);
++
++              cFYI(1, "CIFS: fscache release page (0x%p/0x%p)\n",
++                              cifsi->fscache, page);
++              if (!fscache_maybe_release_page(cifsi->fscache, page, gfp))
++                      return 0;
++      }
++
++      return 1;
++}
++
++void __cifs_fscache_invalidate_page(struct page *page, struct inode *inode)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(inode);
++      struct fscache_cookie *cookie = cifsi->fscache;
++
++      cFYI(1, "CIFS: fscache invalidatepage (0x%p/0x%p/0x%p)\n",
++                      cookie, page, cifsi);
++      fscache_wait_on_page_write(cookie, page);
++      fscache_uncache_page(cookie, page);
++}
++
+diff --git a/fs/cifs/fscache.h b/fs/cifs/fscache.h
+index 836bb02..127cb0a 100644
+--- a/fs/cifs/fscache.h
++++ b/fs/cifs/fscache.h
+@@ -47,6 +47,16 @@ extern void cifs_fscache_release_inode_cookie(struct inode *);
+ extern void cifs_fscache_set_inode_cookie(struct inode *, struct file *);
+ extern void cifs_fscache_reset_inode_cookie(struct inode *);
++extern void __cifs_fscache_invalidate_page(struct page *, struct inode *);
++extern int cifs_fscache_release_page(struct page *page, gfp_t gfp);
++
++static inline void cifs_fscache_invalidate_page(struct page *page,
++                                             struct inode *inode)
++{
++      if (PageFsCache(page))
++              __cifs_fscache_invalidate_page(page, inode);
++}
++
+ #else /* CONFIG_CIFS_FSCACHE */
+ static inline int cifs_fscache_register(void) { return 0; }
+ static inline void cifs_fscache_unregister(void) {}
+@@ -63,7 +73,13 @@ static inline void cifs_fscache_release_inode_cookie(struct inode *inode) {}
+ static inline void cifs_fscache_set_inode_cookie(struct inode *inode,
+                       struct file *filp) {}
+ static inline void cifs_fscache_reset_inode_cookie(struct inode *inode) {}
++static inline void cifs_fscache_release_page(struct page *page, gfp_t gfp)
++{
++      return 1; /* May release page */
++}
++static inline int cifs_fscache_invalidate_page(struct page *page,
++                      struct inode *) {}
+ #endif /* CONFIG_CIFS_FSCACHE */
+-- 
+1.6.4.2
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001736:2, b/test/corpora/lkml/cur/1382298587.001736:2,
new file mode 100644 (file)
index 0000000..f972891
--- /dev/null
@@ -0,0 +1,256 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 09/10] cifs: read pages from FS-Cache
+Date: Tue, 22 Jun 2010 20:54:21 +0530
+Lines: 219
+Message-ID: <1277220261-3717-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:44:46 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5eX-0008O2-Q4
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:44:46 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1752563Ab0FVPom (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:44:42 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:42741 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752542Ab0FVPok (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:44:40 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:24:22 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001762>
+
+Read pages from a FS-Cache data storage object into a CIFS inode.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+---
+ fs/cifs/file.c    |   19 ++++++++++++++
+ fs/cifs/fscache.c |   73 +++++++++++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/fscache.h |   40 ++++++++++++++++++++++++++++-
+ 3 files changed, 131 insertions(+), 1 deletions(-)
+
+diff --git a/fs/cifs/file.c b/fs/cifs/file.c
+index 39c1ce0..42d2f25 100644
+--- a/fs/cifs/file.c
++++ b/fs/cifs/file.c
+@@ -1978,6 +1978,16 @@ static int cifs_readpages(struct file *file, struct address_space *mapping,
+       cifs_sb = CIFS_SB(file->f_path.dentry->d_sb);
+       pTcon = cifs_sb->tcon;
++      /*
++       * Reads as many pages as possible from fscache. Returns -ENOBUFS
++       * immediately if the cookie is negative
++       */
++      rc = cifs_readpages_from_fscache(mapping->host, mapping, page_list,
++                                       &num_pages);
++      cFYI(1, "CIFS: readpages_from_fscache returned %d\n", rc);
++      if (rc == 0)
++              goto read_complete;
++
+       cFYI(DBG2, "rpages: num pages %d", num_pages);
+       for (i = 0; i < num_pages; ) {
+               unsigned contig_pages;
+@@ -2090,6 +2100,7 @@ static int cifs_readpages(struct file *file, struct address_space *mapping,
+               smb_read_data = NULL;
+       }
++read_complete:
+       FreeXid(xid);
+       return rc;
+ }
+@@ -2100,6 +2111,12 @@ static int cifs_readpage_worker(struct file *file, struct page *page,
+       char *read_data;
+       int rc;
++      /* Is the page cached? */
++      rc = cifs_readpage_from_fscache(file->f_path.dentry->d_inode, page);
++      cFYI(1, "CIFS: cifs_readpage_from_fscache returned %d\n", rc);
++      if (rc == 0)
++              goto read_complete;
++
+       page_cache_get(page);
+       read_data = kmap(page);
+       /* for reads over a certain size could initiate async read ahead */
+@@ -2128,6 +2145,8 @@ static int cifs_readpage_worker(struct file *file, struct page *page,
+ io_error:
+       kunmap(page);
+       page_cache_release(page);
++
++read_complete:
+       return rc;
+ }
+diff --git a/fs/cifs/fscache.c b/fs/cifs/fscache.c
+index 13e47d5..6813737 100644
+--- a/fs/cifs/fscache.c
++++ b/fs/cifs/fscache.c
+@@ -145,6 +145,79 @@ int cifs_fscache_release_page(struct page *page, gfp_t gfp)
+       return 1;
+ }
++static void cifs_readpage_from_fscache_complete(struct page *page, void *ctx,
++                                              int error)
++{
++      cFYI(1, "CFS: readpage_from_fscache_complete (0x%p/%d)\n",
++                      page, error);
++      if (!error)
++              SetPageUptodate(page);
++      unlock_page(page);
++}
++
++/*
++ * Retrieve a page from FS-Cache
++ */
++int __cifs_readpage_from_fscache(struct inode *inode, struct page *page)
++{
++      int ret;
++
++      cFYI(1, "CIFS: readpage_from_fscache(fsc:%p, p:%p, i:0x%p\n",
++                      CIFS_I(inode)->fscache, page, inode);
++      ret = fscache_read_or_alloc_page(CIFS_I(inode)->fscache, page,
++                                       cifs_readpage_from_fscache_complete,
++                                       NULL,
++                                       GFP_KERNEL);
++      switch (ret) {
++
++      case 0: /* page found in fscache, read submitted */
++              cFYI(1, "CIFS: readpage_from_fscache: submitted\n");
++              return ret;
++      case -ENOBUFS:  /* page won't be cached */
++      case -ENODATA:  /* page not in cache */
++              cFYI(1, "CIFS: readpage_from_fscache %d\n", ret);
++              return 1;
++
++      default:
++              cFYI(1, "unknown error ret = %d", ret);
++      }
++      return ret;
++}
++
++/*
++ * Retrieve a set of pages from FS-Cache
++ */
++int __cifs_readpages_from_fscache(struct inode *inode,
++                              struct address_space *mapping,
++                              struct list_head *pages,
++                              unsigned *nr_pages)
++{
++      int ret;
++
++      cFYI(1, "CIFS: __cifs_readpages_from_fscache (0x%p/%u/0x%p)\n",
++                      CIFS_I(inode)->fscache, *nr_pages, inode);
++      ret = fscache_read_or_alloc_pages(CIFS_I(inode)->fscache, mapping,
++                                        pages, nr_pages,
++                                        cifs_readpage_from_fscache_complete,
++                                        NULL,
++                                        mapping_gfp_mask(mapping));
++      switch (ret) {
++      case 0: /* read submitted to the cache for all pages */
++              cFYI(1, "CIFS: readpages_from_fscache\n");
++              return ret;
++
++      case -ENOBUFS:  /* some pages are not cached and can't be */
++      case -ENODATA:  /* some pages are not cached */
++              cFYI(1, "CIFS: readpages_from_fscache: no page\n");
++              return 1;
++
++      default:
++              cFYI(1, "unknown error ret = %d", ret);
++      }
++
++      return ret;
++}
++
+ void __cifs_readpage_to_fscache(struct inode *inode, struct page *page)
+ {
+       int ret;
+diff --git a/fs/cifs/fscache.h b/fs/cifs/fscache.h
+index e34d8ab..03bd3fe 100644
+--- a/fs/cifs/fscache.h
++++ b/fs/cifs/fscache.h
+@@ -31,7 +31,6 @@ extern const struct fscache_cookie_def cifs_fscache_server_index_def;
+ extern const struct fscache_cookie_def cifs_fscache_super_index_def;
+ extern const struct fscache_cookie_def cifs_fscache_inode_object_def;
+-
+ extern int cifs_fscache_register(void);
+ extern void cifs_fscache_unregister(void);
+@@ -49,6 +48,11 @@ extern void cifs_fscache_reset_inode_cookie(struct inode *);
+ extern void __cifs_fscache_invalidate_page(struct page *, struct inode *);
+ extern int cifs_fscache_release_page(struct page *page, gfp_t gfp);
++extern int __cifs_readpage_from_fscache(struct inode *, struct page *);
++extern int __cifs_readpages_from_fscache(struct inode *,
++                                       struct address_space *,
++                                       struct list_head *,
++                                       unsigned *);
+ extern void __cifs_readpage_to_fscache(struct inode *, struct page *);
+@@ -59,6 +63,26 @@ static inline void cifs_fscache_invalidate_page(struct page *page,
+               __cifs_fscache_invalidate_page(page, inode);
+ }
++static inline int cifs_readpage_from_fscache(struct inode *inode,
++                                           struct page *page)
++{
++      if (CIFS_I(inode)->fscache)
++              return __cifs_readpage_from_fscache(inode, page);
++
++      return -ENOBUFS;
++}
++
++static inline int cifs_readpages_from_fscache(struct inode *inode,
++                                            struct address_space *mapping,
++                                            struct list_head *pages,
++                                            unsigned *nr_pages)
++{
++      if (CIFS_I(inode)->fscache)
++              return __cifs_readpages_from_fscache(inode, mapping, pages,
++                                                   nr_pages);
++      return -ENOBUFS;
++}
++
+ static inline void cifs_readpage_to_fscache(struct inode *inode,
+                                           struct page *page)
+ {
+@@ -89,6 +113,20 @@ static inline void cifs_fscache_release_page(struct page *page, gfp_t gfp)
+ static inline int cifs_fscache_invalidate_page(struct page *page,
+                       struct inode *) {}
++static inline int
++cifs_readpage_from_fscache(struct inode *inode, struct page *page)
++{
++      return -ENOBUFS;
++}
++
++static inline int cifs_readpages_from_fscache(struct inode *inode,
++                                            struct address_space *mapping,
++                                            struct list_head *pages,
++                                            unsigned *nr_pages)
++{
++      return -ENOBUFS;
++}
++
+ static inline void cifs_readpage_to_fscache(struct inode *inode,
+                       struct page *page) {}
+-- 
+1.6.4.2
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001738:2, b/test/corpora/lkml/cur/1382298587.001738:2,
new file mode 100644 (file)
index 0000000..b1e0edf
--- /dev/null
@@ -0,0 +1,139 @@
+From: Suresh Jayaraman <sjayaraman@suse.de>
+Subject: [RFC][PATCH 08/10] cifs: store pages into local cache
+Date: Tue, 22 Jun 2010 20:54:00 +0530
+Lines: 102
+Message-ID: <1277220240-3674-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, David Howells <dhowells@redhat.com>
+To: Steve French <smfrench@gmail.com>
+X-From: linux-fsdevel-owner@vger.kernel.org Tue Jun 22 17:45:09 2010
+Return-path: <linux-fsdevel-owner@vger.kernel.org>
+Envelope-to: lnx-linux-fsdevel@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-fsdevel-owner@vger.kernel.org>)
+       id 1OR5ev-00007O-6e
+       for lnx-linux-fsdevel@lo.gmane.org; Tue, 22 Jun 2010 17:45:09 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755015Ab0FVPon (ORCPT <rfc822;lnx-linux-fsdevel@m.gmane.org>);
+       Tue, 22 Jun 2010 11:44:43 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:58250 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751265Ab0FVPok (ORCPT
+       <rfc822;groupwise-SJayaraman@novell.com:0:0>);
+       Tue, 22 Jun 2010 11:44:40 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:24:02 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-fsdevel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-fsdevel.vger.kernel.org>
+X-Mailing-List: linux-fsdevel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001764>
+
+Store pages from an CIFS inode into the data storage object associated with
+that inode.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+---
+ fs/cifs/file.c    |    6 ++++++
+ fs/cifs/fscache.c |   13 +++++++++++++
+ fs/cifs/fscache.h |   11 +++++++++++
+ 3 files changed, 30 insertions(+), 0 deletions(-)
+
+diff --git a/fs/cifs/file.c b/fs/cifs/file.c
+index 786ec04..39c1ce0 100644
+--- a/fs/cifs/file.c
++++ b/fs/cifs/file.c
+@@ -2060,6 +2060,8 @@ static int cifs_readpages(struct file *file, struct address_space *mapping,
+                                  we will hit it on next read */
+                               /* break; */
++                              /* send this page to FS-Cache */
++                              cifs_readpage_to_fscache(mapping->host, page);
+                       }
+               } else {
+                       cFYI(1, "No bytes read (%d) at offset %lld . "
+@@ -2117,6 +2119,10 @@ static int cifs_readpage_worker(struct file *file, struct page *page,
+       flush_dcache_page(page);
+       SetPageUptodate(page);
++
++      /* send this page to the cache */
++      cifs_readpage_to_fscache(file->f_path.dentry->d_inode, page);
++
+       rc = 0;
+ io_error:
+diff --git a/fs/cifs/fscache.c b/fs/cifs/fscache.c
+index c09d3b8..13e47d5 100644
+--- a/fs/cifs/fscache.c
++++ b/fs/cifs/fscache.c
+@@ -145,6 +145,19 @@ int cifs_fscache_release_page(struct page *page, gfp_t gfp)
+       return 1;
+ }
++void __cifs_readpage_to_fscache(struct inode *inode, struct page *page)
++{
++      int ret;
++
++      cFYI(1, "CIFS: readpage_to_fscache(fsc: %p, p: %p, i: %p\n",
++                      CIFS_I(inode)->fscache, page, inode);
++      ret = fscache_write_page(CIFS_I(inode)->fscache, page, GFP_KERNEL);
++      cFYI(1, "CIFS: fscache_write_page returned %d\n", ret);
++
++      if (ret != 0)
++              fscache_uncache_page(CIFS_I(inode)->fscache, page);
++}
++
+ void __cifs_fscache_invalidate_page(struct page *page, struct inode *inode)
+ {
+       struct cifsInodeInfo *cifsi = CIFS_I(inode);
+diff --git a/fs/cifs/fscache.h b/fs/cifs/fscache.h
+index 127cb0a..e34d8ab 100644
+--- a/fs/cifs/fscache.h
++++ b/fs/cifs/fscache.h
+@@ -50,6 +50,8 @@ extern void cifs_fscache_reset_inode_cookie(struct inode *);
+ extern void __cifs_fscache_invalidate_page(struct page *, struct inode *);
+ extern int cifs_fscache_release_page(struct page *page, gfp_t gfp);
++extern void __cifs_readpage_to_fscache(struct inode *, struct page *);
++
+ static inline void cifs_fscache_invalidate_page(struct page *page,
+                                              struct inode *inode)
+ {
+@@ -57,6 +59,13 @@ static inline void cifs_fscache_invalidate_page(struct page *page,
+               __cifs_fscache_invalidate_page(page, inode);
+ }
++static inline void cifs_readpage_to_fscache(struct inode *inode,
++                                          struct page *page)
++{
++      if (PageFsCache(page))
++              __cifs_readpage_to_fscache(inode, page);
++}
++
+ #else /* CONFIG_CIFS_FSCACHE */
+ static inline int cifs_fscache_register(void) { return 0; }
+ static inline void cifs_fscache_unregister(void) {}
+@@ -80,6 +89,8 @@ static inline void cifs_fscache_release_page(struct page *page, gfp_t gfp)
+ static inline int cifs_fscache_invalidate_page(struct page *page,
+                       struct inode *) {}
++static inline void cifs_readpage_to_fscache(struct inode *inode,
++                      struct page *page) {}
+ #endif /* CONFIG_CIFS_FSCACHE */
+-- 
+1.6.4.2
+
+--
+To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001739:2, b/test/corpora/lkml/cur/1382298587.001739:2,
new file mode 100644 (file)
index 0000000..d0abda0
--- /dev/null
@@ -0,0 +1,355 @@
+From: Suresh Jayaraman <sjayaraman@suse.de>
+Subject: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Tue, 22 Jun 2010 20:53:33 +0530
+Lines: 318
+Message-ID: <1277220214-3597-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, David Howells <dhowells@redhat.com>
+To: Steve French <smfrench@gmail.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Jun 22 17:45:30 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OR5fF-0000Ka-Na
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 22 Jun 2010 17:45:30 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755952Ab0FVPpP (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 22 Jun 2010 11:45:15 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:59441 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751397Ab0FVPoA (ORCPT
+       <rfc822;groupwise-SJayaraman@novell.com:0:0>);
+       Tue, 22 Jun 2010 11:44:00 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:23:35 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001765>
+
+Define inode-level data storage objects (managed by cifsInodeInfo structs).
+Each inode-level object is created in a super-block level object and is itself
+a data storage object in to which pages from the inode are stored.
+
+The inode object is keyed by UniqueId. The coherency data being used is
+LastWriteTime and the file size.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+---
+ fs/cifs/cache.c    |   80 +++++++++++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/cifsfs.c   |    7 ++++
+ fs/cifs/cifsglob.h |    3 +
+ fs/cifs/file.c     |    6 +++
+ fs/cifs/fscache.c  |   68 +++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/fscache.h  |   12 +++++++
+ fs/cifs/inode.c    |    4 ++
+ 7 files changed, 180 insertions(+)
+
+Index: cifs-2.6/fs/cifs/cache.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cache.c
++++ cifs-2.6/fs/cifs/cache.c
+@@ -138,3 +138,83 @@ const struct fscache_cookie_def cifs_fsc
+       .get_key = cifs_super_get_key,
+ };
++/*
++ * Auxiliary data attached to CIFS inode within the cache
++ */
++struct cifs_fscache_inode_auxdata {
++      struct timespec last_write_time;
++      loff_t          size;
++};
++
++static uint16_t cifs_fscache_inode_get_key(const void *cookie_netfs_data,
++                                         void *buffer, uint16_t maxbuf)
++{
++      const struct cifsInodeInfo *cifsi = cookie_netfs_data;
++      uint16_t keylen;
++
++      /* use the UniqueId as the key */
++      keylen = sizeof(cifsi->uniqueid);
++      if (keylen > maxbuf)
++              keylen = 0;
++      else
++              memcpy(buffer, &cifsi->uniqueid, keylen);
++
++      return keylen;
++}
++
++static void
++cifs_fscache_inode_get_attr(const void *cookie_netfs_data, uint64_t *size)
++{
++      const struct cifsInodeInfo *cifsi = cookie_netfs_data;
++
++      *size = cifsi->vfs_inode.i_size;
++}
++
++static uint16_t
++cifs_fscache_inode_get_aux(const void *cookie_netfs_data, void *buffer,
++                         uint16_t maxbuf)
++{
++      struct cifs_fscache_inode_auxdata auxdata;
++      const struct cifsInodeInfo *cifsi = cookie_netfs_data;
++
++      memset(&auxdata, 0, sizeof(auxdata));
++      auxdata.size = cifsi->vfs_inode.i_size;
++      auxdata.last_write_time = cifsi->vfs_inode.i_ctime;
++
++      if (maxbuf > sizeof(auxdata))
++              maxbuf = sizeof(auxdata);
++
++      memcpy(buffer, &auxdata, maxbuf);
++
++      return maxbuf;
++}
++
++static enum
++fscache_checkaux cifs_fscache_inode_check_aux(void *cookie_netfs_data,
++                                            const void *data,
++                                            uint16_t datalen)
++{
++      struct cifs_fscache_inode_auxdata auxdata;
++      struct cifsInodeInfo *cifsi = cookie_netfs_data;
++
++      if (datalen != sizeof(auxdata))
++              return FSCACHE_CHECKAUX_OBSOLETE;
++
++      memset(&auxdata, 0, sizeof(auxdata));
++      auxdata.size = cifsi->vfs_inode.i_size;
++      auxdata.last_write_time = cifsi->vfs_inode.i_ctime;
++
++      if (memcmp(data, &auxdata, datalen) != 0)
++              return FSCACHE_CHECKAUX_OBSOLETE;
++
++      return FSCACHE_CHECKAUX_OKAY;
++}
++
++const struct fscache_cookie_def cifs_fscache_inode_object_def = {
++      .name           = "CIFS.uniqueid",
++      .type           = FSCACHE_COOKIE_TYPE_DATAFILE,
++      .get_key        = cifs_fscache_inode_get_key,
++      .get_attr       = cifs_fscache_inode_get_attr,
++      .get_aux        = cifs_fscache_inode_get_aux,
++      .check_aux      = cifs_fscache_inode_check_aux,
++};
+Index: cifs-2.6/fs/cifs/cifsfs.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cifsfs.c
++++ cifs-2.6/fs/cifs/cifsfs.c
+@@ -330,6 +330,12 @@ cifs_destroy_inode(struct inode *inode)
+ }
+ static void
++cifs_clear_inode(struct inode *inode)
++{
++      cifs_fscache_release_inode_cookie(inode);
++}
++
++static void
+ cifs_show_address(struct seq_file *s, struct TCP_Server_Info *server)
+ {
+       seq_printf(s, ",addr=");
+@@ -490,6 +496,7 @@ static const struct super_operations cif
+       .alloc_inode = cifs_alloc_inode,
+       .destroy_inode = cifs_destroy_inode,
+       .drop_inode     = cifs_drop_inode,
++      .clear_inode    = cifs_clear_inode,
+ /*    .delete_inode   = cifs_delete_inode,  */  /* Do not need above
+       function unless later we add lazy close of inodes or unless the
+       kernel forgets to call us with the same number of releases (closes)
+Index: cifs-2.6/fs/cifs/cifsglob.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cifsglob.h
++++ cifs-2.6/fs/cifs/cifsglob.h
+@@ -407,6 +407,9 @@ struct cifsInodeInfo {
+       bool invalid_mapping:1;         /* pagecache is invalid */
+       u64  server_eof;                /* current file size on server */
+       u64  uniqueid;                  /* server inode number */
++#ifdef CONFIG_CIFS_FSCACHE
++      struct fscache_cookie *fscache;
++#endif
+       struct inode vfs_inode;
+ };
+Index: cifs-2.6/fs/cifs/file.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/file.c
++++ cifs-2.6/fs/cifs/file.c
+@@ -40,6 +40,7 @@
+ #include "cifs_unicode.h"
+ #include "cifs_debug.h"
+ #include "cifs_fs_sb.h"
++#include "fscache.h"
+ static inline int cifs_convert_flags(unsigned int flags)
+ {
+@@ -282,6 +283,9 @@ int cifs_open(struct inode *inode, struc
+                               CIFSSMBClose(xid, tcon, netfid);
+                               rc = -ENOMEM;
+                       }
++
++                      cifs_fscache_set_inode_cookie(inode, file);
++
+                       goto out;
+               } else if ((rc == -EINVAL) || (rc == -EOPNOTSUPP)) {
+                       if (tcon->ses->serverNOS)
+@@ -373,6 +377,8 @@ int cifs_open(struct inode *inode, struc
+               goto out;
+       }
++      cifs_fscache_set_inode_cookie(inode, file);
++
+       if (oplock & CIFS_CREATE_ACTION) {
+               /* time to set mode which we can not set earlier due to
+                  problems creating new read-only files */
+Index: cifs-2.6/fs/cifs/fscache.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/fscache.c
++++ cifs-2.6/fs/cifs/fscache.c
+@@ -62,3 +62,71 @@ void cifs_fscache_release_super_cookie(s
+       tcon->fscache = NULL;
+ }
++static void cifs_fscache_enable_inode_cookie(struct inode *inode)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(inode);
++      struct cifs_sb_info *cifs_sb = CIFS_SB(inode->i_sb);
++
++      if (cifsi->fscache)
++              return;
++
++      cifsi->fscache = fscache_acquire_cookie(cifs_sb->tcon->fscache,
++                              &cifs_fscache_inode_object_def,
++                              cifsi);
++      cFYI(1, "CIFS: got FH cookie (0x%p/0x%p/0x%p)\n",
++                      cifs_sb->tcon, cifsi, cifsi->fscache);
++}
++
++void cifs_fscache_release_inode_cookie(struct inode *inode)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(inode);
++
++      if (cifsi->fscache) {
++              cFYI(1, "CIFS releasing inode cookie (0x%p/0x%p)\n",
++                              cifsi, cifsi->fscache);
++              fscache_relinquish_cookie(cifsi->fscache, 0);
++              cifsi->fscache = NULL;
++      }
++}
++
++static void cifs_fscache_disable_inode_cookie(struct inode *inode)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(inode);
++
++      if (cifsi->fscache) {
++              cFYI(1, "CIFS disabling inode cookie (0x%p/0x%p)\n",
++                              cifsi, cifsi->fscache);
++              fscache_relinquish_cookie(cifsi->fscache, 1);
++              cifsi->fscache = NULL;
++      }
++}
++
++void cifs_fscache_set_inode_cookie(struct inode *inode, struct file *filp)
++{
++      /* BB: parallel opens - need locking? */
++      if ((filp->f_flags & O_ACCMODE) != O_RDONLY)
++              cifs_fscache_disable_inode_cookie(inode);
++      else {
++              cifs_fscache_enable_inode_cookie(inode);
++              cFYI(1, "CIFS: fscache inode cookie set\n");
++      }
++}
++
++void cifs_fscache_reset_inode_cookie(struct inode *inode)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(inode);
++      struct cifs_sb_info *cifs_sb = CIFS_SB(inode->i_sb);
++      struct fscache_cookie *old = cifsi->fscache;
++
++      if (cifsi->fscache) {
++              /* retire the current fscache cache and get a new one */
++              fscache_relinquish_cookie(cifsi->fscache, 1);
++
++              cifsi->fscache = fscache_acquire_cookie(cifs_sb->tcon->fscache,
++                                      &cifs_fscache_inode_object_def,
++                                      cifsi);
++              cFYI(1, "CIFS: new cookie (0x%p/0x%p) oldcookie 0x%p\n",
++                              cifsi, cifsi->fscache, old);
++      }
++}
++
+Index: cifs-2.6/fs/cifs/fscache.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/fscache.h
++++ cifs-2.6/fs/cifs/fscache.h
+@@ -29,6 +29,8 @@
+ extern struct fscache_netfs cifs_fscache_netfs;
+ extern const struct fscache_cookie_def cifs_fscache_server_index_def;
+ extern const struct fscache_cookie_def cifs_fscache_super_index_def;
++extern const struct fscache_cookie_def cifs_fscache_inode_object_def;
++
+ extern int cifs_fscache_register(void);
+ extern void cifs_fscache_unregister(void);
+@@ -41,6 +43,10 @@ extern void cifs_fscache_release_client_
+ extern void cifs_fscache_get_super_cookie(struct cifsTconInfo *);
+ extern void cifs_fscache_release_super_cookie(struct cifsTconInfo *);
++extern void cifs_fscache_release_inode_cookie(struct inode *);
++extern void cifs_fscache_set_inode_cookie(struct inode *, struct file *);
++extern void cifs_fscache_reset_inode_cookie(struct inode *);
++
+ #else /* CONFIG_CIFS_FSCACHE */
+ static inline int cifs_fscache_register(void) { return 0; }
+ static inline void cifs_fscache_unregister(void) {}
+@@ -53,6 +59,12 @@ static inline void cifs_fscache_get_supe
+ static inline void
+ cifs_fscache_release_super_cookie(struct cifsTconInfo *tcon) {}
++static inline void cifs_fscache_release_inode_cookie(struct inode *inode) {}
++static inline void cifs_fscache_set_inode_cookie(struct inode *inode,
++                      struct file *filp) {}
++static inline void cifs_fscache_reset_inode_cookie(struct inode *inode) {}
++
++
+ #endif /* CONFIG_CIFS_FSCACHE */
+ #endif /* _CIFS_FSCACHE_H */
+Index: cifs-2.6/fs/cifs/inode.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/inode.c
++++ cifs-2.6/fs/cifs/inode.c
+@@ -29,6 +29,7 @@
+ #include "cifsproto.h"
+ #include "cifs_debug.h"
+ #include "cifs_fs_sb.h"
++#include "fscache.h"
+ static void cifs_set_ops(struct inode *inode, const bool is_dfs_referral)
+@@ -776,6 +777,8 @@ retry_iget5_locked:
+                       inode->i_flags |= S_NOATIME | S_NOCMTIME;
+               if (inode->i_state & I_NEW) {
+                       inode->i_ino = hash;
++                      /* initialize per-inode cache cookie pointer */
++                      CIFS_I(inode)->fscache = NULL;
+                       unlock_new_inode(inode);
+               }
+       }
+@@ -1568,6 +1571,7 @@ cifs_invalidate_mapping(struct inode *in
+                       cifs_i->write_behind_rc = rc;
+       }
+       invalidate_remote_inode(inode);
++      cifs_fscache_reset_inode_cookie(inode);
+ }
+ int cifs_revalidate_file(struct file *filp)
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001740:2, b/test/corpora/lkml/cur/1382298587.001740:2,
new file mode 100644 (file)
index 0000000..ef0f657
--- /dev/null
@@ -0,0 +1,214 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 05/10] cifs: define superblock-level cache index objects and register them
+Date: Tue, 22 Jun 2010 20:53:26 +0530
+Lines: 177
+Message-ID: <1277220206-3559-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:45:50 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5fZ-0000Vj-Mj
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:45:50 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1752511Ab0FVPpJ (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:45:09 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:56189 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752441Ab0FVPoA (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:44:00 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:23:29 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001766>
+
+Define superblock-level cache index objects (managed by cifsTconInfo structs).
+Each superblock object is created in a server-level index object and in itself
+an index into which inode-level objects are inserted.
+
+Currently, the superblock objects are keyed by sharename.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+---
+ fs/cifs/cache.c    |   62 +++++++++++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/cifsglob.h |    3 ++
+ fs/cifs/connect.c  |    4 +++
+ fs/cifs/fscache.c  |   17 ++++++++++++++
+ fs/cifs/fscache.h  |    6 +++++
+ 5 files changed, 92 insertions(+)
+
+Index: cifs-2.6/fs/cifs/cache.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cache.c
++++ cifs-2.6/fs/cifs/cache.c
+@@ -76,3 +76,65 @@ const struct fscache_cookie_def cifs_fsc
+       .type = FSCACHE_COOKIE_TYPE_INDEX,
+       .get_key = cifs_server_get_key,
+ };
++
++static char *extract_sharename(const char *treename)
++{
++      const char *src;
++      char *delim, *dst;
++      int len;
++
++      /* skip double chars at the beginning */
++      src = treename + 2;
++
++      /* share name is always preceded by '\\' now */
++      delim = strchr(src, '\\');
++      if (!delim)
++              return ERR_PTR(-EINVAL);
++      delim++;
++      len = strlen(delim);
++
++      /* caller has to free the memory */
++      dst = kstrndup(delim, len, GFP_KERNEL);
++      if (!dst)
++              return ERR_PTR(-ENOMEM);
++
++      return dst;
++}
++
++/*
++ * Superblock object currently keyed by share name
++ */
++static uint16_t cifs_super_get_key(const void *cookie_netfs_data, void *buffer,
++                                 uint16_t maxbuf)
++{
++      const struct cifsTconInfo *tcon = cookie_netfs_data;
++      char *sharename;
++      uint16_t len;
++
++      sharename = extract_sharename(tcon->treeName);
++      if (IS_ERR(sharename)) {
++              cFYI(1, "CIFS: couldn't extract sharename\n");
++              sharename = NULL;
++              return 0;
++      }
++
++      len = strlen(sharename);
++      if (len > maxbuf)
++              return 0;
++
++      memcpy(buffer, sharename, len);
++
++      kfree(sharename);
++
++      return len;
++}
++
++/*
++ * Superblock object for FS-Cache
++ */
++const struct fscache_cookie_def cifs_fscache_super_index_def = {
++      .name = "CIFS.super",
++      .type = FSCACHE_COOKIE_TYPE_INDEX,
++      .get_key = cifs_super_get_key,
++};
++
+Index: cifs-2.6/fs/cifs/cifsglob.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cifsglob.h
++++ cifs-2.6/fs/cifs/cifsglob.h
+@@ -317,6 +317,9 @@ struct cifsTconInfo {
+       bool local_lease:1; /* check leases (only) on local system not remote */
+       bool broken_posix_open; /* e.g. Samba server versions < 3.3.2, 3.2.9 */
+       bool need_reconnect:1; /* connection reset, tid now invalid */
++#ifdef CONFIG_CIFS_FSCACHE
++      struct fscache_cookie *fscache; /* cookie for share */
++#endif
+       /* BB add field for back pointer to sb struct(s)? */
+ };
+Index: cifs-2.6/fs/cifs/connect.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/connect.c
++++ cifs-2.6/fs/cifs/connect.c
+@@ -1773,6 +1773,8 @@ cifs_put_tcon(struct cifsTconInfo *tcon)
+       list_del_init(&tcon->tcon_list);
+       write_unlock(&cifs_tcp_ses_lock);
++      cifs_fscache_release_super_cookie(tcon);
++
+       xid = GetXid();
+       CIFSSMBTDis(xid, tcon);
+       _FreeXid(xid);
+@@ -1843,6 +1845,8 @@ cifs_get_tcon(struct cifsSesInfo *ses, s
+       tcon->nocase = volume_info->nocase;
+       tcon->local_lease = volume_info->local_lease;
++      cifs_fscache_get_super_cookie(tcon);
++
+       write_lock(&cifs_tcp_ses_lock);
+       list_add(&tcon->tcon_list, &ses->tcon_list);
+       write_unlock(&cifs_tcp_ses_lock);
+Index: cifs-2.6/fs/cifs/fscache.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/fscache.c
++++ cifs-2.6/fs/cifs/fscache.c
+@@ -45,3 +45,20 @@ void cifs_fscache_release_client_cookie(
+       server->fscache = NULL;
+ }
++void cifs_fscache_get_super_cookie(struct cifsTconInfo *tcon)
++{
++      tcon->fscache =
++              fscache_acquire_cookie(tcon->ses->server->fscache,
++                              &cifs_fscache_super_index_def, tcon);
++      cFYI(1, "CIFS: get superblock cookie (0x%p/0x%p)\n",
++                              tcon, tcon->fscache);
++}
++
++void cifs_fscache_release_super_cookie(struct cifsTconInfo *tcon)
++{
++      cFYI(1, "CIFS: releasing superblock cookie (0x%p/0x%p)\n",
++                      tcon, tcon->fscache);
++      fscache_relinquish_cookie(tcon->fscache, 0);
++      tcon->fscache = NULL;
++}
++
+Index: cifs-2.6/fs/cifs/fscache.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/fscache.h
++++ cifs-2.6/fs/cifs/fscache.h
+@@ -28,6 +28,7 @@
+ extern struct fscache_netfs cifs_fscache_netfs;
+ extern const struct fscache_cookie_def cifs_fscache_server_index_def;
++extern const struct fscache_cookie_def cifs_fscache_super_index_def;
+ extern int cifs_fscache_register(void);
+ extern void cifs_fscache_unregister(void);
+@@ -37,6 +38,8 @@ extern void cifs_fscache_unregister(void
+  */
+ extern void cifs_fscache_get_client_cookie(struct TCP_Server_Info *);
+ extern void cifs_fscache_release_client_cookie(struct TCP_Server_Info *);
++extern void cifs_fscache_get_super_cookie(struct cifsTconInfo *);
++extern void cifs_fscache_release_super_cookie(struct cifsTconInfo *);
+ #else /* CONFIG_CIFS_FSCACHE */
+ static inline int cifs_fscache_register(void) { return 0; }
+@@ -46,6 +49,9 @@ static inline void
+ cifs_fscache_get_client_cookie(struct TCP_Server_Info *server) {}
+ static inline void
+ cifs_fscache_get_client_cookie(struct TCP_Server_Info *server); {}
++static inline void cifs_fscache_get_super_cookie(struct cifsTconInfo *tcon) {}
++static inline void
++cifs_fscache_release_super_cookie(struct cifsTconInfo *tcon) {}
+ #endif /* CONFIG_CIFS_FSCACHE */
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001887:2, b/test/corpora/lkml/cur/1382298587.001887:2,
new file mode 100644 (file)
index 0000000..8129048
--- /dev/null
@@ -0,0 +1,85 @@
+From: Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>
+Subject: Re: [RFC][PATCH 02/10] cifs: guard cifsglob.h against multiple
+ inclusion
+Date: Tue, 22 Jun 2010 17:37:42 -0400
+Lines: 35
+Message-ID: <20100622173742.448e1e94@corrin.poochiereds.net>
+References: <yes>
+       <1277220170-3442-1-git-send-email-sjayaraman@suse.de>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 23:36:08 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORB8Z-00027v-R8
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 23:36:08 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751663Ab0FVVfq (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 17:35:46 -0400
+Received: from cdptpa-omtalb.mail.rr.com ([75.180.132.121]:46190 "EHLO
+       cdptpa-omtalb.mail.rr.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751933Ab0FVVfo (ORCPT
+       <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>); Tue, 22 Jun 2010 17:35:44 -0400
+X-Authority-Analysis: v=1.0 c=1 a=Y4kVDsoNLLAA:10 a=yQWWgrYGNuUA:10 a=kj9zAlcOel0A:10 a=hGzw-44bAAAA:8 a=6UT2YofcClCzWf3PPoQA:9 a=Ipo6nwFRv7ENfF13HvmH_iG48b8A:4 a=CjuIK1q_8ugA:10 a=0kPLrQdw3YYA:10 a=dowx1zmaLagA:10
+X-Cloudmark-Score: 0
+X-Originating-IP: 71.70.153.3
+Received: from [71.70.153.3] ([71.70.153.3:49036] helo=mail.poochiereds.net)
+       by cdptpa-oedge01.mail.rr.com (envelope-from <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>)
+       (ecelerity 2.2.2.39 r()) with ESMTP
+       id 29/22-24471-DAC212C4; Tue, 22 Jun 2010 21:35:42 +0000
+Received: from corrin.poochiereds.net (unknown [65.88.2.5])
+       by mail.poochiereds.net (Postfix) with ESMTPSA id 1C5A1580F4;
+       Tue, 22 Jun 2010 17:35:41 -0400 (EDT)
+In-Reply-To: <1277220170-3442-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Mailer: Claws Mail 3.7.6 (GTK+ 2.20.1; x86_64-redhat-linux-gnu)
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001913>
+
+On Tue, 22 Jun 2010 20:52:50 +0530
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Add conditional compile macros to guard the header file against multiple
+> inclusion.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+> ---
+>  fs/cifs/cifsglob.h |    5 +++++
+>  1 files changed, 5 insertions(+), 0 deletions(-)
+> 
+> diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h
+> index a88479c..6b2c39d 100644
+> --- a/fs/cifs/cifsglob.h
+> +++ b/fs/cifs/cifsglob.h
+> @@ -16,6 +16,9 @@
+>   *   the GNU Lesser General Public License for more details.
+>   *
+>   */
+> +#ifndef _CIFS_GLOB_H
+> +#define _CIFS_GLOB_H
+> +
+>  #include <linux/in.h>
+>  #include <linux/in6.h>
+>  #include <linux/slab.h>
+> @@ -733,3 +736,5 @@ GLOBAL_EXTERN unsigned int cifs_min_small;  /* min size of small buf pool */
+>  GLOBAL_EXTERN unsigned int cifs_max_pending; /* MAX requests at once to server*/
+>  
+>  extern const struct slow_work_ops cifs_oplock_break_ops;
+> +
+> +#endif      /* _CIFS_GLOB_H */
+
+Strong ACK
+
+Acked-by: Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001892:2, b/test/corpora/lkml/cur/1382298587.001892:2,
new file mode 100644 (file)
index 0000000..82603bf
--- /dev/null
@@ -0,0 +1,254 @@
+From: Jeff Layton <jlayton-vpEMnDpepFuMZCB2o+C8xQ@public.gmane.org>
+Subject: Re: [RFC][PATCH 04/10] cifs: define server-level cache index
+ objects and register them with FS-Cache
+Date: Tue, 22 Jun 2010 17:52:14 -0400
+Lines: 204
+Message-ID: <20100622175214.4c56234f@corrin.poochiereds.net>
+References: <yes>
+       <1277220198-3522-1-git-send-email-sjayaraman@suse.de>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 23:50:23 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORBMJ-0005WJ-Lj
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 23:50:20 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1750777Ab0FVVuS (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 17:50:18 -0400
+Received: from cdptpa-omtalb.mail.rr.com ([75.180.132.120]:55670 "EHLO
+       cdptpa-omtalb.mail.rr.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1750749Ab0FVVuR (ORCPT
+       <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>); Tue, 22 Jun 2010 17:50:17 -0400
+X-Authority-Analysis: v=1.1 cv=8MuG1bpxLlSbaYWWtODGdBCK7StbFcRsMXhWm1NVx/I= c=1 sm=0 a=wpY4Lvx3kJcA:10 a=UBIxAjGgU1YA:10 a=kj9zAlcOel0A:10 a=ld/erqUjW76FpBUqCqkKeA==:17 a=VwQbUJbxAAAA:8 a=qYub2k57AAAA:8 a=uYIlwBZcjrF9BUCsR4kA:9 a=OO1ZLbZb6q4TPdC5pcAA:7 a=jFshslHAf8hJVDYUYRlYN4n-w5YA:4 a=CjuIK1q_8ugA:10 a=x8gzFH9gYPwA:10 a=0kPLrQdw3YYA:10 a=jBoGP612-tUA:10 a=t5DF_bUGhurCx8LQ:21 a=W6P_Gh1y2IibdbqZ:21 a=ld/erqUjW76FpBUqCqkKeA==:117
+X-Cloudmark-Score: 0
+X-Originating-IP: 71.70.153.3
+Received: from [71.70.153.3] ([71.70.153.3:59154] helo=mail.poochiereds.net)
+       by cdptpa-oedge03.mail.rr.com (envelope-from <jlayton-vpEMnDpepFuMZCB2o+C8xQ@public.gmane.org>)
+       (ecelerity 2.2.2.39 r()) with ESMTP
+       id AC/10-00502-710312C4; Tue, 22 Jun 2010 21:50:16 +0000
+Received: from corrin.poochiereds.net (unknown [65.88.2.5])
+       by mail.poochiereds.net (Postfix) with ESMTPSA id 03B11580F4;
+       Tue, 22 Jun 2010 17:50:14 -0400 (EDT)
+In-Reply-To: <1277220198-3522-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Mailer: Claws Mail 3.7.6 (GTK+ 2.20.1; x86_64-redhat-linux-gnu)
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001918>
+
+On Tue, 22 Jun 2010 20:53:18 +0530
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Define server-level cache index objects (as managed by TCP_ServerInfo structs).
+> Each server object is created in the CIFS top-level index object and is itself
+> an index into which superblock-level objects are inserted.
+> 
+> Currently, the server objects are keyed by hostname.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+> ---
+>  fs/cifs/Makefile   |    2 +-
+>  fs/cifs/cache.c    |   25 +++++++++++++++++++++++++
+>  fs/cifs/cifsglob.h |    3 +++
+>  fs/cifs/connect.c  |    4 ++++
+>  fs/cifs/fscache.c  |   47 +++++++++++++++++++++++++++++++++++++++++++++++
+>  fs/cifs/fscache.h  |   12 ++++++++++++
+>  6 files changed, 92 insertions(+), 1 deletion(-)
+>  create mode 100644 fs/cifs/fscache.c
+> 
+> Index: cifs-2.6/fs/cifs/Makefile
+> ===================================================================
+> --- cifs-2.6.orig/fs/cifs/Makefile
+> +++ cifs-2.6/fs/cifs/Makefile
+> @@ -12,4 +12,4 @@ cifs-$(CONFIG_CIFS_UPCALL) += cifs_spneg
+>  
+>  cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o
+>  
+> -cifs-$(CONFIG_CIFS_FSCACHE) += cache.o
+> +cifs-$(CONFIG_CIFS_FSCACHE) += fscache.o cache.o
+> Index: cifs-2.6/fs/cifs/cache.c
+> ===================================================================
+> --- cifs-2.6.orig/fs/cifs/cache.c
+> +++ cifs-2.6/fs/cifs/cache.c
+> @@ -51,3 +51,28 @@ void cifs_fscache_unregister(void)
+>      fscache_unregister_netfs(&cifs_fscache_netfs);
+>  }
+>  
+> +/*
+> + * Server object currently keyed by hostname
+> + */
+> +static uint16_t cifs_server_get_key(const void *cookie_netfs_data,
+> +                               void *buffer, uint16_t maxbuf)
+> +{
+> +    const struct TCP_Server_Info *server = cookie_netfs_data;
+> +    uint16_t len = strnlen(server->hostname, sizeof(server->hostname));
+> +
+
+Would a tuple of address/family/port be a better choice here? Imagine I
+mount "foo" and then later mount "foor.bar.baz". If they are the same
+address and only the UNC differs, then you won't get the benefit of
+the cache, right?
+
+> +    if (len > maxbuf)
+> +            return 0;
+> +
+> +    memcpy(buffer, server->hostname, len);
+> +
+> +    return len;
+> +}
+> +
+> +/*
+> + * Server object for FS-Cache
+> + */
+> +const struct fscache_cookie_def cifs_fscache_server_index_def = {
+> +    .name = "CIFS.server",
+> +    .type = FSCACHE_COOKIE_TYPE_INDEX,
+> +    .get_key = cifs_server_get_key,
+> +};
+> Index: cifs-2.6/fs/cifs/cifsglob.h
+> ===================================================================
+> --- cifs-2.6.orig/fs/cifs/cifsglob.h
+> +++ cifs-2.6/fs/cifs/cifsglob.h
+> @@ -193,6 +193,9 @@ struct TCP_Server_Info {
+>      bool    sec_mskerberos;         /* supports legacy MS Kerberos */
+>      bool    sec_kerberosu2u;        /* supports U2U Kerberos */
+>      bool    sec_ntlmssp;            /* supports NTLMSSP */
+> +#ifdef CONFIG_CIFS_FSCACHE
+> +    struct fscache_cookie   *fscache; /* client index cache cookie */
+> +#endif
+>  };
+>  
+>  /*
+> Index: cifs-2.6/fs/cifs/connect.c
+> ===================================================================
+> --- cifs-2.6.orig/fs/cifs/connect.c
+> +++ cifs-2.6/fs/cifs/connect.c
+> @@ -48,6 +48,7 @@
+>  #include "nterr.h"
+>  #include "rfc1002pdu.h"
+>  #include "cn_cifs.h"
+> +#include "fscache.h"
+>  
+>  #define CIFS_PORT 445
+>  #define RFC1001_PORT 139
+> @@ -1453,6 +1454,8 @@ cifs_put_tcp_session(struct TCP_Server_I
+>              return;
+>      }
+>  
+> +    cifs_fscache_release_client_cookie(server);
+> +
+>      list_del_init(&server->tcp_ses_list);
+>      write_unlock(&cifs_tcp_ses_lock);
+>  
+> @@ -1572,6 +1575,7 @@ cifs_get_tcp_session(struct smb_vol *vol
+>              goto out_err;
+>      }
+>  
+> +    cifs_fscache_get_client_cookie(tcp_ses);
+>      /* thread spawned, put it on the list */
+>      write_lock(&cifs_tcp_ses_lock);
+>      list_add(&tcp_ses->tcp_ses_list, &cifs_tcp_ses_list);
+> Index: cifs-2.6/fs/cifs/fscache.c
+> ===================================================================
+> --- /dev/null
+> +++ cifs-2.6/fs/cifs/fscache.c
+> @@ -0,0 +1,47 @@
+> +/*
+> + *   fs/cifs/fscache.c - CIFS filesystem cache interface
+> + *
+> + *   Copyright (c) 2010 Novell, Inc.
+> + *   Authors(s): Suresh Jayaraman (sjayaraman-l3A5Bk7waGM@public.gmane.org>
+> + *
+> + *   This library is free software; you can redistribute it and/or modify
+> + *   it under the terms of the GNU Lesser General Public License as published
+> + *   by the Free Software Foundation; either version 2.1 of the License, or
+> + *   (at your option) any later version.
+> + *
+> + *   This library 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 Lesser General Public License for more details.
+> + *
+> + *   You should have received a copy of the GNU Lesser General Public License
+> + *   along with this library; if not, write to the Free Software
+> + *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+> + */
+> +#include <linux/init.h>
+> +#include <linux/kernel.h>
+> +#include <linux/sched.h>
+> +#include <linux/mm.h>
+> +#include <linux/in6.h>
+> +
+> +#include "fscache.h"
+> +#include "cifsglob.h"
+> +#include "cifs_debug.h"
+> +
+> +void cifs_fscache_get_client_cookie(struct TCP_Server_Info *server)
+> +{
+> +    server->fscache =
+> +            fscache_acquire_cookie(cifs_fscache_netfs.primary_index,
+> +                            &cifs_fscache_server_index_def, server);
+> +    cFYI(1, "CIFS: get client cookie (0x%p/0x%p)\n",
+> +                            server, server->fscache);
+> +}
+> +
+> +void cifs_fscache_release_client_cookie(struct TCP_Server_Info *server)
+> +{
+> +    cFYI(1, "CIFS: release client cookie (0x%p/0x%p)\n",
+> +                            server, server->fscache);
+> +    fscache_relinquish_cookie(server->fscache, 0);
+> +    server->fscache = NULL;
+> +}
+> +
+> Index: cifs-2.6/fs/cifs/fscache.h
+> ===================================================================
+> --- cifs-2.6.orig/fs/cifs/fscache.h
+> +++ cifs-2.6/fs/cifs/fscache.h
+> @@ -27,14 +27,26 @@
+>  #ifdef CONFIG_CIFS_FSCACHE
+>  
+>  extern struct fscache_netfs cifs_fscache_netfs;
+> +extern const struct fscache_cookie_def cifs_fscache_server_index_def;
+>  
+>  extern int cifs_fscache_register(void);
+>  extern void cifs_fscache_unregister(void);
+>  
+> +/*
+> + * fscache.c
+> + */
+> +extern void cifs_fscache_get_client_cookie(struct TCP_Server_Info *);
+> +extern void cifs_fscache_release_client_cookie(struct TCP_Server_Info *);
+> +
+>  #else /* CONFIG_CIFS_FSCACHE */
+>  static inline int cifs_fscache_register(void) { return 0; }
+>  static inline void cifs_fscache_unregister(void) {}
+>  
+> +static inline void
+> +cifs_fscache_get_client_cookie(struct TCP_Server_Info *server) {}
+> +static inline void
+> +cifs_fscache_get_client_cookie(struct TCP_Server_Info *server); {}
+> +
+>  #endif /* CONFIG_CIFS_FSCACHE */
+>  
+>  #endif /* _CIFS_FSCACHE_H */
+> --
+> To unsubscribe from this list: send the line "unsubscribe linux-cifs" in
+> the body of a message to majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+> More majordomo info at  http://vger.kernel.org/majordomo-info.html
+> 
+
+
+-- 
+Jeff Layton <jlayton-vpEMnDpepFuMZCB2o+C8xQ@public.gmane.org>
+
+
diff --git a/test/corpora/lkml/cur/1382298587.001970:2, b/test/corpora/lkml/cur/1382298587.001970:2,
new file mode 100644 (file)
index 0000000..707d0ad
--- /dev/null
@@ -0,0 +1,103 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: Re: [RFC][PATCH 04/10] cifs: define server-level cache index objects
+ and register them with FS-Cache
+Date: Wed, 23 Jun 2010 11:04:39 +0530
+Lines: 61
+Message-ID: <4C219CEF.5000003@suse.de>
+References: <yes>      <1277220198-3522-1-git-send-email-sjayaraman@suse.de> <20100622175214.4c56234f@corrin.poochiereds.net>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Jeff Layton <jlayton-vpEMnDpepFuMZCB2o+C8xQ@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 07:34:50 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORIbp-0002v4-3W
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 07:34:49 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1750954Ab0FWFes (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 01:34:48 -0400
+Received: from cantor2.suse.de ([195.135.220.15]:58263 "EHLO mx2.suse.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1750809Ab0FWFes (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 01:34:48 -0400
+Received: from relay1.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       by mx2.suse.de (Postfix) with ESMTP id 8C18386A2E;
+       Wed, 23 Jun 2010 07:34:46 +0200 (CEST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091130 SUSE/3.0.0-1.1.1 Thunderbird/3.0
+In-Reply-To: <20100622175214.4c56234f-4QP7MXygkU+dMjc06nkz3ljfA9RmPOcC@public.gmane.org>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001998>
+
+On 06/23/2010 03:22 AM, Jeff Layton wrote:
+> On Tue, 22 Jun 2010 20:53:18 +0530
+> Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+> 
+>> Define server-level cache index objects (as managed by TCP_ServerInfo structs).
+>> Each server object is created in the CIFS top-level index object and is itself
+>> an index into which superblock-level objects are inserted.
+>>
+>> Currently, the server objects are keyed by hostname.
+>>
+>> Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+>> ---
+>>  fs/cifs/Makefile   |    2 +-
+>>  fs/cifs/cache.c    |   25 +++++++++++++++++++++++++
+>>  fs/cifs/cifsglob.h |    3 +++
+>>  fs/cifs/connect.c  |    4 ++++
+>>  fs/cifs/fscache.c  |   47 +++++++++++++++++++++++++++++++++++++++++++++++
+>>  fs/cifs/fscache.h  |   12 ++++++++++++
+>>  6 files changed, 92 insertions(+), 1 deletion(-)
+>>  create mode 100644 fs/cifs/fscache.c
+>>
+>> Index: cifs-2.6/fs/cifs/Makefile
+>> ===================================================================
+>> --- cifs-2.6.orig/fs/cifs/Makefile
+>> +++ cifs-2.6/fs/cifs/Makefile
+>> @@ -12,4 +12,4 @@ cifs-$(CONFIG_CIFS_UPCALL) += cifs_spneg
+>>  
+>>  cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o
+>>  
+>> -cifs-$(CONFIG_CIFS_FSCACHE) += cache.o
+>> +cifs-$(CONFIG_CIFS_FSCACHE) += fscache.o cache.o
+>> Index: cifs-2.6/fs/cifs/cache.c
+>> ===================================================================
+>> --- cifs-2.6.orig/fs/cifs/cache.c
+>> +++ cifs-2.6/fs/cifs/cache.c
+>> @@ -51,3 +51,28 @@ void cifs_fscache_unregister(void)
+>>     fscache_unregister_netfs(&cifs_fscache_netfs);
+>>  }
+>>  
+>> +/*
+>> + * Server object currently keyed by hostname
+>> + */
+>> +static uint16_t cifs_server_get_key(const void *cookie_netfs_data,
+>> +                              void *buffer, uint16_t maxbuf)
+>> +{
+>> +   const struct TCP_Server_Info *server = cookie_netfs_data;
+>> +   uint16_t len = strnlen(server->hostname, sizeof(server->hostname));
+>> +
+> 
+> Would a tuple of address/family/port be a better choice here? Imagine I
+> mount "foo" and then later mount "foor.bar.baz". If they are the same
+> address and only the UNC differs, then you won't get the benefit of
+> the cache, right?
+> 
+
+Good point. I'll fix it up when I do a respin.
+
+Thanks,
+
+-- 
+Suresh Jayaraman
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002189:2, b/test/corpora/lkml/cur/1382298587.002189:2,
new file mode 100644 (file)
index 0000000..3cfc62e
--- /dev/null
@@ -0,0 +1,66 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 03/10] cifs: register CIFS for caching
+Date: Wed, 23 Jun 2010 17:51:17 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 20
+Message-ID: <9603.1277311877@redhat.com>
+References: <1277220189-3485-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 18:51:32 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORTAg-0008Bt-CT
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 18:51:30 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751915Ab0FWQv3 (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 12:51:29 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:50923 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751520Ab0FWQv3 (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 12:51:29 -0400
+Received: from int-mx05.intmail.prod.int.phx2.redhat.com (int-mx05.intmail.prod.int.phx2.redhat.com [10.5.11.18])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NGpLFc028550
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 12:51:21 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx05.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NGpHIG010890;
+       Wed, 23 Jun 2010 12:51:18 -0400
+In-Reply-To: <1277220189-3485-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.18
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002219>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> +    rc = cifs_fscache_register();
+> +    if (rc)
+> +            goto out;
+> +
+>      rc = cifs_init_inodecache();
+>      if (rc)
+>              goto out_clean_proc;
+> @@ -949,8 +954,10 @@ init_cifs(void)
+>      cifs_destroy_mids();
+>   out_destroy_inodecache:
+>      cifs_destroy_inodecache();
+> +    cifs_fscache_unregister();
+>   out_clean_proc:
+
+This is incorrect.  You need to call cifs_fscache_unregister() if
+cifs_init_inodecache() fails.
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002193:2, b/test/corpora/lkml/cur/1382298587.002193:2,
new file mode 100644 (file)
index 0000000..e2ea626
--- /dev/null
@@ -0,0 +1,59 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 05/10] cifs: define superblock-level cache index objects and register them
+Date: Wed, 23 Jun 2010 17:58:10 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 13
+Message-ID: <9720.1277312290@redhat.com>
+References: <1277220206-3559-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 18:58:19 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORTHG-0003Az-Ge
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 18:58:18 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751520Ab0FWQ6R (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 12:58:17 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:62343 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751372Ab0FWQ6R (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 12:58:17 -0400
+Received: from int-mx01.intmail.prod.int.phx2.redhat.com (int-mx01.intmail.prod.int.phx2.redhat.com [10.5.11.11])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NGwDC2031683
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 12:58:13 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx01.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NGwAfq021298;
+       Wed, 23 Jun 2010 12:58:11 -0400
+In-Reply-To: <1277220206-3559-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.11
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002223>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Define superblock-level cache index objects (managed by cifsTconInfo
+> structs).  Each superblock object is created in a server-level index object
+> and in itself an index into which inode-level objects are inserted.
+> 
+> Currently, the superblock objects are keyed by sharename.
+
+Seems reasonable.  Is there any way you can check that the share you are
+looking at on a server is the same as the last time you looked?  Can you
+validate the root directory of the share in some way?
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002194:2, b/test/corpora/lkml/cur/1382298587.002194:2,
new file mode 100644 (file)
index 0000000..d2d1efd
--- /dev/null
@@ -0,0 +1,61 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Wed, 23 Jun 2010 18:02:53 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 15
+Message-ID: <9822.1277312573@redhat.com>
+References: <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 19:03:04 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORTLr-0007Bh-Cs
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 19:03:03 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1752063Ab0FWRDB (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 13:03:01 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:30823 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751804Ab0FWRDA (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 13:03:00 -0400
+Received: from int-mx03.intmail.prod.int.phx2.redhat.com (int-mx03.intmail.prod.int.phx2.redhat.com [10.5.11.16])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH2v0J030982
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 13:02:57 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx03.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH2r9N014323;
+       Wed, 23 Jun 2010 13:02:54 -0400
+In-Reply-To: <1277220214-3597-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.16
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002224>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Define inode-level data storage objects (managed by cifsInodeInfo structs).
+> Each inode-level object is created in a super-block level object and is
+> itself a data storage object in to which pages from the inode are stored.
+> 
+> The inode object is keyed by UniqueId. The coherency data being used is
+> LastWriteTime and the file size.
+
+Isn't there a file creation time too?
+
+I take it you don't support caching on files that are open for writing at this
+time?
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002195:2, b/test/corpora/lkml/cur/1382298587.002195:2,
new file mode 100644 (file)
index 0000000..ec54a81
--- /dev/null
@@ -0,0 +1,59 @@
+From: David Howells <dhowells@redhat.com>
+Subject: Re: [RFC][PATCH 07/10] cifs: FS-Cache page management
+Date: Wed, 23 Jun 2010 18:05:01 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 13
+Message-ID: <9866.1277312701@redhat.com>
+References: <1277220228-3635-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells@redhat.com, Steve French <smfrench@gmail.com>,
+       linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Suresh Jayaraman <sjayaraman@suse.de>
+X-From: linux-fsdevel-owner@vger.kernel.org Wed Jun 23 19:05:19 2010
+Return-path: <linux-fsdevel-owner@vger.kernel.org>
+Envelope-to: lnx-linux-fsdevel@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-fsdevel-owner@vger.kernel.org>)
+       id 1ORTNz-0008Oj-Ho
+       for lnx-linux-fsdevel@lo.gmane.org; Wed, 23 Jun 2010 19:05:15 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752145Ab0FWRFO (ORCPT <rfc822;lnx-linux-fsdevel@m.gmane.org>);
+       Wed, 23 Jun 2010 13:05:14 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:1689 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751804Ab0FWRFN (ORCPT <rfc822;linux-fsdevel@vger.kernel.org>);
+       Wed, 23 Jun 2010 13:05:13 -0400
+Received: from int-mx02.intmail.prod.int.phx2.redhat.com (int-mx02.intmail.prod.int.phx2.redhat.com [10.5.11.12])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH59sl011966
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 13:05:09 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx02.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH52Jl022163;
+       Wed, 23 Jun 2010 13:05:03 -0400
+In-Reply-To: <1277220228-3635-1-git-send-email-sjayaraman@suse.de>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.12
+Sender: linux-fsdevel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-fsdevel.vger.kernel.org>
+X-Mailing-List: linux-fsdevel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002225>
+
+Suresh Jayaraman <sjayaraman@suse.de> wrote:
+
+> Takes care of invalidation and release of FS-Cache marked pages and also
+> invalidation of the FsCache page flag when the inode is removed.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+
+Acked-by: David Howells <dhowells@redhat.com>
+--
+To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002196:2, b/test/corpora/lkml/cur/1382298587.002196:2,
new file mode 100644 (file)
index 0000000..63838dc
--- /dev/null
@@ -0,0 +1,54 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 08/10] cifs: store pages into local cache
+Date: Wed, 23 Jun 2010 18:06:12 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 8
+Message-ID: <9890.1277312772@redhat.com>
+References: <1277220240-3674-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 19:06:21 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORTP3-0000fp-01
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 19:06:21 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1752403Ab0FWRGU (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 13:06:20 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:63621 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751804Ab0FWRGT (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 13:06:19 -0400
+Received: from int-mx08.intmail.prod.int.phx2.redhat.com (int-mx08.intmail.prod.int.phx2.redhat.com [10.5.11.21])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH6FCB012081
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 13:06:15 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx08.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH6CKG013414;
+       Wed, 23 Jun 2010 13:06:13 -0400
+In-Reply-To: <1277220240-3674-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.21
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002226>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Store pages from an CIFS inode into the data storage object associated with
+> that inode.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+
+Acked-by: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002197:2, b/test/corpora/lkml/cur/1382298587.002197:2,
new file mode 100644 (file)
index 0000000..765c399
--- /dev/null
@@ -0,0 +1,53 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 09/10] cifs: read pages from FS-Cache
+Date: Wed, 23 Jun 2010 18:07:40 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 7
+Message-ID: <9918.1277312860@redhat.com>
+References: <1277220261-3717-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 19:07:51 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORTQR-0000nv-JF
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 19:07:47 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751708Ab0FWRHr (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 13:07:47 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:34413 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1750954Ab0FWRHq (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 13:07:46 -0400
+Received: from int-mx04.intmail.prod.int.phx2.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.17])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH7h3Y005904
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 13:07:43 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx04.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH7efR020683;
+       Wed, 23 Jun 2010 13:07:41 -0400
+In-Reply-To: <1277220261-3717-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.17
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002227>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Read pages from a FS-Cache data storage object into a CIFS inode.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+
+Acked-by: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002201:2, b/test/corpora/lkml/cur/1382298587.002201:2,
new file mode 100644 (file)
index 0000000..bae1eef
--- /dev/null
@@ -0,0 +1,58 @@
+From: David Howells <dhowells@redhat.com>
+Subject: Re: [RFC][PATCH 10/10] cifs: add mount option to enable local caching
+Date: Wed, 23 Jun 2010 18:08:34 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 12
+Message-ID: <9942.1277312914@redhat.com>
+References: <1277220309-3757-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells@redhat.com, Steve French <smfrench@gmail.com>,
+       linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Suresh Jayaraman <sjayaraman@suse.de>
+X-From: linux-kernel-owner@vger.kernel.org Wed Jun 23 19:09:22 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1ORTRv-0002J8-2s
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 23 Jun 2010 19:09:19 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753275Ab0FWRIt (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Wed, 23 Jun 2010 13:08:49 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:6156 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1753203Ab0FWRIr (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Wed, 23 Jun 2010 13:08:47 -0400
+Received: from int-mx04.intmail.prod.int.phx2.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.17])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH8dax006028
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 13:08:39 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx04.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH8YmA020846;
+       Wed, 23 Jun 2010 13:08:36 -0400
+In-Reply-To: <1277220309-3757-1-git-send-email-sjayaraman@suse.de>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.17
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002231>
+
+Suresh Jayaraman <sjayaraman@suse.de> wrote:
+
+> Add a mount option 'fsc' to enable local caching on CIFS.
+> 
+> As the cifs-utils (userspace) changes are not done yet, this patch enables
+> 'fsc' by default to assist testing.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+
+Acked-by: David Howells <dhowells@redhat.com>
+
+(Give or take the debugging bit)
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002228:2, b/test/corpora/lkml/cur/1382298587.002228:2,
new file mode 100644 (file)
index 0000000..b401ae3
--- /dev/null
@@ -0,0 +1,100 @@
+From: Scott Lovenberg <scott.lovenberg@gmail.com>
+Subject: Re: [RFC][PATCH 10/10] cifs: add mount option to enable local caching
+Date: Wed, 23 Jun 2010 14:32:24 -0400
+Lines: 37
+Message-ID: <4C225338.9010807@gmail.com>
+References: <yes> <1277220309-3757-1-git-send-email-sjayaraman@suse.de>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench@gmail.com>, linux-cifs@vger.kernel.org,
+       linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
+       David Howells <dhowells@redhat.com>
+To: Suresh Jayaraman <sjayaraman@suse.de>
+X-From: linux-kernel-owner@vger.kernel.org Wed Jun 23 20:32:44 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1ORUke-00020X-7B
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 23 Jun 2010 20:32:44 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753205Ab0FWScd (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Wed, 23 Jun 2010 14:32:33 -0400
+Received: from mail-gx0-f174.google.com ([209.85.161.174]:50118 "EHLO
+       mail-gx0-f174.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752380Ab0FWScb (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Wed, 23 Jun 2010 14:32:31 -0400
+Received: by gxk28 with SMTP id 28so317656gxk.19
+        for <multiple recipients>; Wed, 23 Jun 2010 11:32:31 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:received:received:message-id:date:from
+         :user-agent:mime-version:to:cc:subject:references:in-reply-to
+         :content-type:content-transfer-encoding;
+        bh=iTBSrajJefJVTimpUcvJQmptYefXJDrz9ZyZgxnMvzA=;
+        b=DOZLux9YGwNIWknqofz5rMltvopOT+kRgPsHIYw8Z7Uhh9gR5YAD4V6kKmv1SIaWoo
+         uXjNwY+IPIiD4f4OwwlpwJTd4B7PkBCDIlOkwVcvvS3F6qr6WbXBd0nRuRiFGMwONU3E
+         MqTAWDDwIXLVURr1t+n3MFrKwKj5b7pZT5fHw=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=message-id:date:from:user-agent:mime-version:to:cc:subject
+         :references:in-reply-to:content-type:content-transfer-encoding;
+        b=pTsfPMlTDpE3Oi2w9V3eE2ohOeBEloXhgElmCwGEenBegF7ZhIyoga6tyRJqQ922ws
+         oyxLXSORpOuPJRoIBRXfzae3KXkgKT0eLDjxQNTdS7Jbe+vcJ604sANFcnxBsJ51fThT
+         R/wXt7LiG/T6H4DUpcN7aUjtzlq9JgC2JQ/ws=
+Received: by 10.224.43.197 with SMTP id x5mr5243425qae.127.1277317950764;
+        Wed, 23 Jun 2010 11:32:30 -0700 (PDT)
+Received: from [192.168.0.2] ([64.9.41.61])
+        by mx.google.com with ESMTPS id 15sm3010007qcg.2.2010.06.23.11.32.25
+        (version=TLSv1/SSLv3 cipher=RC4-MD5);
+        Wed, 23 Jun 2010 11:32:26 -0700 (PDT)
+User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.10) Gecko/20100512 Lightning/1.0b1 Thunderbird/3.0.5 ThunderBrowse/3.2.8.1
+In-Reply-To: <1277220309-3757-1-git-send-email-sjayaraman@suse.de>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002258>
+
+On 6/22/2010 11:25 AM, Suresh Jayaraman wrote:
+> Add a mount option 'fsc' to enable local caching on CIFS.
+>
+> As the cifs-utils (userspace) changes are not done yet, this patch enables
+> 'fsc' by default to assist testing.
+>    
+[...]
+> @@ -1332,6 +1336,8 @@ cifs_parse_mount_options(char *options, const char *devname,
+>                      printk(KERN_WARNING "CIFS: Mount option noac not "
+>                              "supported. Instead set "
+>                              "/proc/fs/cifs/LookupCacheEnabled to 0\n");
+> +            } else if (strnicmp(data, "fsc", 3) == 0) {
+> +                    vol->fsc = true;
+>              } else
+>                      printk(KERN_WARNING "CIFS: Unknown mount option %s\n",
+>                                              data);
+> @@ -2405,6 +2411,8 @@ static void setup_cifs_sb(struct smb_vol *pvolume_info,
+>              cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_OVERR_GID;
+>      if (pvolume_info->dynperm)
+>              cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_DYNPERM;
+> +    if (pvolume_info->fsc)
+> +            cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_FSCACHE;
+>      if (pvolume_info->direct_io) {
+>              cFYI(1, "mounting share using direct i/o");
+>              cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_DIRECT_IO;
+>    
+I reworked the CIFS mount option parsing a while back; I'm not sure 
+whether that patch was going to be in the 2.6.35 tree or not (the window 
+just opened, didn't it?).
+
+Jeff, Steve, can you confirm if that patch is going to be in 2.6.35?
+
+Patch refs: http://patchwork.ozlabs.org/patch/53059/  and 
+http://patchwork.ozlabs.org/patch/53674/
+
+
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002878:2, b/test/corpora/lkml/cur/1382298587.002878:2,
new file mode 100644 (file)
index 0000000..66a3e22
--- /dev/null
@@ -0,0 +1,90 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: Re: [RFC][PATCH 10/10] cifs: add mount option to enable local caching
+Date: Fri, 25 Jun 2010 16:18:12 +0530
+Lines: 47
+Message-ID: <4C24896C.4000903@suse.de>
+References: <yes> <1277220309-3757-1-git-send-email-sjayaraman@suse.de> <4C225338.9010807@gmail.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Scott Lovenberg <scott.lovenberg-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 12:48:27 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OS6SO-0003QF-NW
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 12:48:25 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1753965Ab0FYKsX (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 06:48:23 -0400
+Received: from cantor.suse.de ([195.135.220.2]:46395 "EHLO mx1.suse.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1752612Ab0FYKsW (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 06:48:22 -0400
+Received: from relay2.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by mx1.suse.de (Postfix) with ESMTP id 60CED6CB00;
+       Fri, 25 Jun 2010 12:48:21 +0200 (CEST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091130 SUSE/3.0.0-1.1.1 Thunderbird/3.0
+In-Reply-To: <4C225338.9010807-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002912>
+
+On 06/24/2010 12:02 AM, Scott Lovenberg wrote:
+> On 6/22/2010 11:25 AM, Suresh Jayaraman wrote:
+>> Add a mount option 'fsc' to enable local caching on CIFS.
+>>
+>> As the cifs-utils (userspace) changes are not done yet, this patch
+>> enables
+>> 'fsc' by default to assist testing.
+>>    
+> [...]
+>> @@ -1332,6 +1336,8 @@ cifs_parse_mount_options(char *options, const
+>> char *devname,
+>>               printk(KERN_WARNING "CIFS: Mount option noac not "
+>>                   "supported. Instead set "
+>>                   "/proc/fs/cifs/LookupCacheEnabled to 0\n");
+>> +        } else if (strnicmp(data, "fsc", 3) == 0) {
+>> +            vol->fsc = true;
+>>           } else
+>>               printk(KERN_WARNING "CIFS: Unknown mount option %s\n",
+>>                           data);
+>> @@ -2405,6 +2411,8 @@ static void setup_cifs_sb(struct smb_vol
+>> *pvolume_info,
+>>           cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_OVERR_GID;
+>>       if (pvolume_info->dynperm)
+>>           cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_DYNPERM;
+>> +    if (pvolume_info->fsc)
+>> +        cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_FSCACHE;
+>>       if (pvolume_info->direct_io) {
+>>           cFYI(1, "mounting share using direct i/o");
+>>           cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_DIRECT_IO;
+>>    
+> I reworked the CIFS mount option parsing a while back; I'm not sure
+> whether that patch was going to be in the 2.6.35 tree or not (the window
+> just opened, didn't it?).
+
+Not a problem, I could redo this patch alone when the reworked option
+parsing patches get in.
+
+> Jeff, Steve, can you confirm if that patch is going to be in 2.6.35?
+> 
+> Patch refs: http://patchwork.ozlabs.org/patch/53059/  and
+> http://patchwork.ozlabs.org/patch/53674/
+> 
+
+Thanks,
+
+-- 
+Suresh Jayaraman
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002912:2, b/test/corpora/lkml/cur/1382298587.002912:2,
new file mode 100644 (file)
index 0000000..d9c761d
--- /dev/null
@@ -0,0 +1,65 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+ register them
+Date: Fri, 25 Jun 2010 18:20:14 +0530
+Lines: 24
+Message-ID: <4C24A606.5040001@suse.de>
+References: <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes> <9822.1277312573@redhat.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 14:50:26 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OS8MR-0007EU-OS
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 14:50:24 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1754607Ab0FYMuX (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 08:50:23 -0400
+Received: from cantor2.suse.de ([195.135.220.15]:38716 "EHLO mx2.suse.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1753675Ab0FYMuW (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 08:50:22 -0400
+Received: from relay2.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       by mx2.suse.de (Postfix) with ESMTP id B05E686A2E;
+       Fri, 25 Jun 2010 14:50:21 +0200 (CEST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091130 SUSE/3.0.0-1.1.1 Thunderbird/3.0
+In-Reply-To: <9822.1277312573-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002946>
+
+On 06/23/2010 10:32 PM, David Howells wrote:
+> Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+> 
+>> Define inode-level data storage objects (managed by cifsInodeInfo structs).
+>> Each inode-level object is created in a super-block level object and is
+>> itself a data storage object in to which pages from the inode are stored.
+>>
+>> The inode object is keyed by UniqueId. The coherency data being used is
+>> LastWriteTime and the file size.
+> 
+> Isn't there a file creation time too?
+
+I think the creation time is currently being ignored as we won't be able
+to accomodate in POSIX stat struct.
+
+> I take it you don't support caching on files that are open for writing at this
+> time?
+> 
+
+Yes.
+
+
+-- 
+Suresh Jayaraman
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002915:2, b/test/corpora/lkml/cur/1382298587.002915:2,
new file mode 100644 (file)
index 0000000..e43c909
--- /dev/null
@@ -0,0 +1,58 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Fri, 25 Jun 2010 13:55:49 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 12
+Message-ID: <22697.1277470549@redhat.com>
+References: <4C24A606.5040001@suse.de> <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes> <9822.1277312573@redhat.com>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 14:56:04 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OS8Rw-0002tq-3k
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 14:56:04 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1753622Ab0FYM4B (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 08:56:01 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:50162 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1752535Ab0FYM4B (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 08:56:01 -0400
+Received: from int-mx01.intmail.prod.int.phx2.redhat.com (int-mx01.intmail.prod.int.phx2.redhat.com [10.5.11.11])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5PCtqOd018091
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Fri, 25 Jun 2010 08:55:52 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx01.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5PCtn4G016466;
+       Fri, 25 Jun 2010 08:55:51 -0400
+In-Reply-To: <4C24A606.5040001-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.11
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002949>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> I think the creation time is currently being ignored as we won't be able
+> to accomodate in POSIX stat struct.
+
+The FS-Cache interface doesn't use the POSIX stat struct, but it could be
+really useful to save it and use it for cache coherency inside the kernel.
+
+Out of interest, what does Samba do when it comes to generating a creation time
+for UNIX where one does not exist?
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002917:2, b/test/corpora/lkml/cur/1382298587.002917:2,
new file mode 100644 (file)
index 0000000..f7047f8
--- /dev/null
@@ -0,0 +1,67 @@
+From: David Howells <dhowells@redhat.com>
+Subject: Re: [RFC][PATCH 05/10] cifs: define superblock-level cache index objects and register them
+Date: Fri, 25 Jun 2010 13:58:33 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 21
+Message-ID: <22746.1277470713@redhat.com>
+References: <4C24A4A0.90408@suse.de> <1277220206-3559-1-git-send-email-sjayaraman@suse.de> <yes> <9720.1277312290@redhat.com>
+Cc: dhowells@redhat.com, Steve French <smfrench@gmail.com>,
+       linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Suresh Jayaraman <sjayaraman@suse.de>
+X-From: linux-fsdevel-owner@vger.kernel.org Fri Jun 25 15:02:20 2010
+Return-path: <linux-fsdevel-owner@vger.kernel.org>
+Envelope-to: lnx-linux-fsdevel@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with smtp (Exim 4.69)
+       (envelope-from <linux-fsdevel-owner@vger.kernel.org>)
+       id 1OS8Xz-000628-FG
+       for lnx-linux-fsdevel@lo.gmane.org; Fri, 25 Jun 2010 15:02:19 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755357Ab0FYM6k (ORCPT <rfc822;lnx-linux-fsdevel@m.gmane.org>);
+       Fri, 25 Jun 2010 08:58:40 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:50417 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1754086Ab0FYM6j (ORCPT <rfc822;linux-fsdevel@vger.kernel.org>);
+       Fri, 25 Jun 2010 08:58:39 -0400
+Received: from int-mx04.intmail.prod.int.phx2.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.17])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5PCwa7Z005113
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Fri, 25 Jun 2010 08:58:36 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx04.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5PCwXVB011094;
+       Fri, 25 Jun 2010 08:58:34 -0400
+In-Reply-To: <4C24A4A0.90408@suse.de>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.17
+Sender: linux-fsdevel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-fsdevel.vger.kernel.org>
+X-Mailing-List: linux-fsdevel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002951>
+
+Suresh Jayaraman <sjayaraman@suse.de> wrote:
+
+> Also, considering the UNC name of the resource (//server/share) may not
+> be a good idea too as the cache will not be used when for e.g. IPaddress
+> is used to mount.
+
+You could convert the UNC name to an IP address, and just use that as your
+key.
+
+> > validate the root directory of the share in some way?
+>
+> I don't know if there is a way to do this.
+
+Is there an inode number or something?  Even the creation time might do.
+
+David
+--
+To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.002997:2, b/test/corpora/lkml/cur/1382298587.002997:2,
new file mode 100644 (file)
index 0000000..b78073c
--- /dev/null
@@ -0,0 +1,90 @@
+From: Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+ register them
+Date: Fri, 25 Jun 2010 12:53:06 -0400
+Lines: 36
+Message-ID: <20100625125306.7f9b1966@tlielax.poochiereds.net>
+References: <4C24A606.5040001@suse.de>
+       <1277220214-3597-1-git-send-email-sjayaraman@suse.de>
+       <yes>
+       <9822.1277312573@redhat.com>
+       <22697.1277470549@redhat.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>,
+       Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       samba-technical-w/Ol4Ecudpl8XjKLYN78aQ@public.gmane.org
+To: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 18:53:12 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OSC9P-0005Eb-SU
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 18:53:12 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S932199Ab0FYQxK (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 12:53:10 -0400
+Received: from cdptpa-omtalb.mail.rr.com ([75.180.132.122]:53512 "EHLO
+       cdptpa-omtalb.mail.rr.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S932187Ab0FYQxJ (ORCPT
+       <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>); Fri, 25 Jun 2010 12:53:09 -0400
+X-Authority-Analysis: v=1.0 c=1 a=iVNVO0OCT3kA:10 a=yQWWgrYGNuUA:10 a=kj9zAlcOel0A:10 a=20KFwNOVAAAA:8 a=hGzw-44bAAAA:8 a=f0L6POiToRdS6aViIA4A:9 a=tdNtT7bw1iHNm6ggrCkIte35EhAA:4 a=CjuIK1q_8ugA:10 a=jEp0ucaQiEUA:10 a=0kPLrQdw3YYA:10 a=dowx1zmaLagA:10 a=00U40p1LBqVLw4jT:21 a=gh7LVOPznGai4vo_:21
+X-Cloudmark-Score: 0
+X-Originating-IP: 71.70.153.3
+Received: from [71.70.153.3] ([71.70.153.3:42266] helo=mail.poochiereds.net)
+       by cdptpa-oedge01.mail.rr.com (envelope-from <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>)
+       (ecelerity 2.2.2.39 r()) with ESMTP
+       id 2D/E0-24471-3FED42C4; Fri, 25 Jun 2010 16:53:08 +0000
+Received: from tlielax.poochiereds.net (tlielax.poochiereds.net [192.168.1.3])
+       by mail.poochiereds.net (Postfix) with ESMTPS id E9B19580FA;
+       Fri, 25 Jun 2010 12:53:06 -0400 (EDT)
+In-Reply-To: <22697.1277470549-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-Mailer: Claws Mail 3.7.6 (GTK+ 2.20.1; x86_64-redhat-linux-gnu)
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003033>
+
+On Fri, 25 Jun 2010 13:55:49 +0100
+David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org> wrote:
+
+> Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+> 
+> > I think the creation time is currently being ignored as we won't be able
+> > to accomodate in POSIX stat struct.
+> 
+> The FS-Cache interface doesn't use the POSIX stat struct, but it could be
+> really useful to save it and use it for cache coherency inside the kernel.
+> 
+> Out of interest, what does Samba do when it comes to generating a creation time
+> for UNIX where one does not exist?
+> 
+
+(cc'ing samba-technical since we're talking about the create time)
+
+Looks like it mostly uses the ctime. IMO, the mtime would be a better
+choice since it changes less frequently, but I don't guess that it
+matters very much.
+
+I have a few patches that make the cifs_iget code do more stringent
+checks. One of those makes it use the create time like an i_generation
+field to guard against matching inodes that have the same number but
+that have undergone a delete/create cycle. They need a bit more testing
+but I'm planning to post them in time for 2.6.36.
+
+Because of how samba generates this number, it could be somewhat
+problematic to do this. What may save us though is that Linux<->Samba
+mostly uses unix extensions unless someone has specifically disabled
+them on either end. The unix extension calls don't generally send any
+sort of create time field, so we can't rely on it in those codepaths
+anyway.
+
+-- 
+Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>
+
+
diff --git a/test/corpora/lkml/cur/1382298587.003106:2, b/test/corpora/lkml/cur/1382298587.003106:2,
new file mode 100644 (file)
index 0000000..19ea381
--- /dev/null
@@ -0,0 +1,60 @@
+From: David Howells <dhowells@redhat.com>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Fri, 25 Jun 2010 22:46:38 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 13
+Message-ID: <18628.1277502398@redhat.com>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net> <4C24A606.5040001@suse.de> <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes> <9822.1277312573@redhat.com> <22697.1277470549@redhat.com>
+Cc: dhowells@redhat.com, Suresh Jayaraman <sjayaraman@suse.de>,
+       Steve French <smfrench@gmail.com>, linux-cifs@vger.kernel.org,
+       linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
+       samba-technical@lists.samba.org
+To: Jeff Layton <jlayton@samba.org>
+X-From: linux-kernel-owner@vger.kernel.org Fri Jun 25 23:47:07 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OSGjo-0006q8-ME
+       for glk-linux-kernel-3@lo.gmane.org; Fri, 25 Jun 2010 23:47:05 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932250Ab0FYVqv (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Fri, 25 Jun 2010 17:46:51 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:55406 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932088Ab0FYVqs (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Fri, 25 Jun 2010 17:46:48 -0400
+Received: from int-mx02.intmail.prod.int.phx2.redhat.com (int-mx02.intmail.prod.int.phx2.redhat.com [10.5.11.12])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5PLkhIG005974
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Fri, 25 Jun 2010 17:46:43 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx02.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5PLkd77017768;
+       Fri, 25 Jun 2010 17:46:40 -0400
+In-Reply-To: <20100625125306.7f9b1966@tlielax.poochiereds.net>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.12
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003142>
+
+Jeff Layton <jlayton@samba.org> wrote:
+
+> Looks like it mostly uses the ctime. IMO, the mtime would be a better
+> choice since it changes less frequently, but I don't guess that it
+> matters very much.
+
+I'd've thought mtime changes more frequently since that's altered when data is
+written.  ctime is changed when attributes are changed.
+
+Note that Ext4 appears to have a file creation time field in its inode
+(struct ext4_inode::i_crtime[_extra]).  Can Samba be made to use that?
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298587.003112:2, b/test/corpora/lkml/cur/1382298587.003112:2,
new file mode 100644 (file)
index 0000000..e74a864
--- /dev/null
@@ -0,0 +1,105 @@
+From: Jeff Layton <jlayton@samba.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+       register them
+Date: Fri, 25 Jun 2010 18:26:51 -0400
+Lines: 30
+Message-ID: <20100625182651.36800d06@tlielax.poochiereds.net>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net>
+       <4C24A606.5040001@suse.de>
+       <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes>
+       <9822.1277312573@redhat.com> <22697.1277470549@redhat.com>
+       <18628.1277502398@redhat.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: linux-cifs@vger.kernel.org, samba-technical@lists.samba.org,
+       linux-kernel@vger.kernel.org, Steve French <smfrench@gmail.com>,
+       linux-fsdevel@vger.kernel.org
+To: David Howells <dhowells@redhat.com>
+X-From: samba-technical-bounces@lists.samba.org Sat Jun 26 00:27:01 2010
+Return-path: <samba-technical-bounces@lists.samba.org>
+Envelope-to: gnsi-samba-technical@m.gmane.org
+Received: from fn.samba.org ([216.83.154.106] helo=lists.samba.org)
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <samba-technical-bounces@lists.samba.org>)
+       id 1OSHMS-0003Yl-G8
+       for gnsi-samba-technical@m.gmane.org; Sat, 26 Jun 2010 00:27:01 +0200
+Received: from fn.samba.org (localhost [127.0.0.1])
+       by lists.samba.org (Postfix) with ESMTP id 8919DAD2B8;
+       Fri, 25 Jun 2010 16:26:57 -0600 (MDT)
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on fn.samba.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-1.7 required=3.8 tests=AWL,BAYES_00,SPF_NEUTRAL
+       autolearn=no version=3.2.5
+X-Original-To: samba-technical@lists.samba.org
+Delivered-To: samba-technical@lists.samba.org
+Received: from cdptpa-omtalb.mail.rr.com (cdptpa-omtalb.mail.rr.com
+       [75.180.132.122])
+       by lists.samba.org (Postfix) with ESMTP id ECB66AD220
+       for <samba-technical@lists.samba.org>;
+       Fri, 25 Jun 2010 16:26:51 -0600 (MDT)
+X-Authority-Analysis: v=1.0 c=1 a=iVNVO0OCT3kA:10 a=yQWWgrYGNuUA:10
+       a=kj9zAlcOel0A:10 a=20KFwNOVAAAA:8 a=hGzw-44bAAAA:8
+       a=AraS79FXNJ3kHilSTm4A:9 a=3STw0N-n4mJG0pydffwA:7
+       a=0uwppTlTaQ5HiYOalIavAxwTlvEA:4 a=CjuIK1q_8ugA:10
+       a=jEp0ucaQiEUA:10 a=dowx1zmaLagA:10
+X-Cloudmark-Score: 0
+X-Originating-IP: 71.70.153.3
+Received: from [71.70.153.3] ([71.70.153.3:55553] helo=mail.poochiereds.net)
+       by cdptpa-oedge01.mail.rr.com (envelope-from <jlayton@samba.org>)
+       (ecelerity 2.2.2.39 r()) with ESMTP
+       id 78/FA-24471-C2D252C4; Fri, 25 Jun 2010 22:26:53 +0000
+Received: from tlielax.poochiereds.net (tlielax.poochiereds.net [192.168.1.3])
+       by mail.poochiereds.net (Postfix) with ESMTPS id 68F07580FA;
+       Fri, 25 Jun 2010 18:26:52 -0400 (EDT)
+In-Reply-To: <18628.1277502398@redhat.com>
+X-Mailer: Claws Mail 3.7.6 (GTK+ 2.20.1; x86_64-redhat-linux-gnu)
+X-BeenThere: samba-technical@lists.samba.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Discussions on Samba internals. For general questions please
+       subscribe to the list samba@samba.org"
+       <samba-technical.lists.samba.org>
+List-Unsubscribe: <https://lists.samba.org/mailman/options/samba-technical>,
+       <mailto:samba-technical-request@lists.samba.org?subject=unsubscribe>
+List-Archive: <http://lists.samba.org/pipermail/samba-technical>
+List-Post: <mailto:samba-technical@lists.samba.org>
+List-Help: <mailto:samba-technical-request@lists.samba.org?subject=help>
+List-Subscribe: <https://lists.samba.org/mailman/listinfo/samba-technical>,
+       <mailto:samba-technical-request@lists.samba.org?subject=subscribe>
+Sender: samba-technical-bounces@lists.samba.org
+Errors-To: samba-technical-bounces@lists.samba.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003148>
+
+On Fri, 25 Jun 2010 22:46:38 +0100
+David Howells <dhowells@redhat.com> wrote:
+
+> Jeff Layton <jlayton@samba.org> wrote:
+> 
+> > Looks like it mostly uses the ctime. IMO, the mtime would be a better
+> > choice since it changes less frequently, but I don't guess that it
+> > matters very much.
+> 
+> I'd've thought mtime changes more frequently since that's altered when data is
+> written.  ctime is changed when attributes are changed.
+> 
+
+IIUC, updating mtime for a write is also an attribute change, and that
+affects ctime. According to the stat(2) manpage:
+
+       The field st_ctime is changed by writing or by setting  inode  informa-
+       tion (i.e., owner, group, link count, mode, etc.).
+
+> Note that Ext4 appears to have a file creation time field in its inode
+> (struct ext4_inode::i_crtime[_extra]).  Can Samba be made to use that?
+> 
+
+Is it exposed to userspace in any (standard) way? It would be handy to
+have that. While we're wishing...it might also be nice to have a
+standard way to get at the i_generation from userspace too.
+
+-- 
+Jeff Layton <jlayton@samba.org>
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.003117:2, b/test/corpora/lkml/cur/1382298587.003117:2,
new file mode 100644 (file)
index 0000000..7f53e34
--- /dev/null
@@ -0,0 +1,65 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Sat, 26 Jun 2010 00:04:28 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 18
+Message-ID: <20123.1277507068@redhat.com>
+References: <20100625182651.36800d06@tlielax.poochiereds.net> <20100625125306.7f9b1966@tlielax.poochiereds.net> <4C24A606.5040001@suse.de> <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes> <9822.1277312573@redhat.com> <22697.1277470549@redhat.com> <18628.1277502398@redhat.com>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>,
+       Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       samba-technical-w/Ol4Ecudpl8XjKLYN78aQ@public.gmane.org
+To: Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Sat Jun 26 01:04:45 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OSHww-0006Jk-NV
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Sat, 26 Jun 2010 01:04:43 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751807Ab0FYXEl (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 19:04:41 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:62977 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1752149Ab0FYXEl (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 19:04:41 -0400
+Received: from int-mx04.intmail.prod.int.phx2.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.17])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5PN4X40004498
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Fri, 25 Jun 2010 19:04:34 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx04.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5PN4Sld008220;
+       Fri, 25 Jun 2010 19:04:30 -0400
+In-Reply-To: <20100625182651.36800d06-9yPaYZwiELC+kQycOl6kW4xkIHaj4LzF@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.17
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003153>
+
+Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org> wrote:
+
+> IIUC, updating mtime for a write is also an attribute change, and that
+> affects ctime. According to the stat(2) manpage:
+
+You're right.  Okay, ctime is the more frequently changed.
+
+> > Note that Ext4 appears to have a file creation time field in its inode
+> > (struct ext4_inode::i_crtime[_extra]).  Can Samba be made to use that?
+> 
+> Is it exposed to userspace in any (standard) way? It would be handy to
+> have that. While we're wishing...it might also be nice to have a
+> standard way to get at the i_generation from userspace too.
+
+Not at present, but it's something that could be exported by ioctl() or
+getxattr().
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298587.003118:2, b/test/corpora/lkml/cur/1382298587.003118:2,
new file mode 100644 (file)
index 0000000..a1ec438
--- /dev/null
@@ -0,0 +1,122 @@
+From: Steve French <smfrench@gmail.com>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and 
+       register them
+Date: Fri, 25 Jun 2010 18:05:30 -0500
+Lines: 51
+Message-ID: <AANLkTilOTrHLvLv4XWYZO6xCnYZgYT7gO2M-oKZ6VvqM@mail.gmail.com>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net>
+       <4C24A606.5040001@suse.de>
+       <1277220214-3597-1-git-send-email-sjayaraman@suse.de>
+       <9822.1277312573@redhat.com>
+       <22697.1277470549@redhat.com>
+       <18628.1277502398@redhat.com>
+       <20100625182651.36800d06@tlielax.poochiereds.net>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: David Howells <dhowells@redhat.com>,
+       Suresh Jayaraman <sjayaraman@suse.de>,
+       linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, samba-technical@lists.samba.org,
+       Jeff Layton <jlayton@redhat.com>
+To: Jeff Layton <jlayton@samba.org>,
+       "Aneesh Kumar K.V" <aneesh.kumar@linux.vnet.ibm.com>,
+       Mingming Cao <mcao@us.ibm.com>
+X-From: linux-kernel-owner@vger.kernel.org Sat Jun 26 01:05:41 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OSHxs-0006a8-BA
+       for glk-linux-kernel-3@lo.gmane.org; Sat, 26 Jun 2010 01:05:40 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1756188Ab0FYXFd convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Fri, 25 Jun 2010 19:05:33 -0400
+Received: from mail-qw0-f46.google.com ([209.85.216.46]:51369 "EHLO
+       mail-qw0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751575Ab0FYXFb convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Fri, 25 Jun 2010 19:05:31 -0400
+Received: by qwi4 with SMTP id 4so742644qwi.19
+        for <multiple recipients>; Fri, 25 Jun 2010 16:05:30 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:mime-version:received:received:in-reply-to
+         :references:date:message-id:subject:from:to:cc:content-type
+         :content-transfer-encoding;
+        bh=6wKQkGOEeUGN4oPR3Nm4SRxtJr/EBwN8ENmpLnfdCDU=;
+        b=X7L6W0MtpQeW/4iBuj+oDlcP2yCJ3qwUs9lHBq1fRW6WdYblHXjmaN8o++3GDPLAg5
+         0MD07zxbYTGXRSrgCjCrGVm0tT88/6hY2a/rB8g68h/Qso2sIHa7B1iIN8JRR4pPWle0
+         sVjp9Xy/bQn2e0uE481Ii1TLHuWYA/QDXZreU=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=mime-version:in-reply-to:references:date:message-id:subject:from:to
+         :cc:content-type:content-transfer-encoding;
+        b=B+7qQvdOpN5a/KCRrDbssKZX8D3SnP73VMHd9RpkqP9nCHCmSLAgbeH03+/m6CLVAo
+         G+NKWqWtknwPBkYqT/bdP2XEak1yr+0rjOqjUaNvaT7AhzsyHEJBkaNnsbS3qaRy39OP
+         S7OkAyHfmgdeNAHkKnKRF73hfpvgAqR9X4bn8=
+Received: by 10.224.59.223 with SMTP id m31mr1130670qah.63.1277507130411; Fri, 
+       25 Jun 2010 16:05:30 -0700 (PDT)
+Received: by 10.229.46.136 with HTTP; Fri, 25 Jun 2010 16:05:30 -0700 (PDT)
+In-Reply-To: <20100625182651.36800d06@tlielax.poochiereds.net>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003154>
+
+On Fri, Jun 25, 2010 at 5:26 PM, Jeff Layton <jlayton@samba.org> wrote:
+>
+> On Fri, 25 Jun 2010 22:46:38 +0100
+> David Howells <dhowells@redhat.com> wrote:
+>
+> > Jeff Layton <jlayton@samba.org> wrote:
+> >
+> > > Looks like it mostly uses the ctime. IMO, the mtime would be a be=
+tter
+> > > choice since it changes less frequently, but I don't guess that i=
+t
+> > > matters very much.
+> >
+> > I'd've thought mtime changes more frequently since that's altered w=
+hen data is
+> > written. =A0ctime is changed when attributes are changed.
+> >
+>
+> IIUC, updating mtime for a write is also an attribute change, and tha=
+t
+> affects ctime. According to the stat(2) manpage:
+>
+> =A0 =A0 =A0 The field st_ctime is changed by writing or by setting =A0=
+inode =A0informa-
+> =A0 =A0 =A0 tion (i.e., owner, group, link count, mode, etc.).
+>
+> > Note that Ext4 appears to have a file creation time field in its in=
+ode
+> > (struct ext4_inode::i_crtime[_extra]). =A0Can Samba be made to use =
+that?
+> >
+>
+> Is it exposed to userspace in any (standard) way? It would be handy t=
+o
+> have that. While we're wishing...it might also be nice to have a
+> standard way to get at the i_generation from userspace too.
+>
+
+Yes - I have talked with MingMing and Aneesh about those (NFS may
+someday be able to use those too).=A0 An obstacle in the past had been
+that samba server stores its own fake creation time in an ndr encoded
+xattr which complicates things.
+
+MingMing/Annesh -
+Xattr or other way to get at birth time?
+
+
+--
+Thanks,
+
+Steve
+
+
diff --git a/test/corpora/lkml/cur/1382298587.003171:2, b/test/corpora/lkml/cur/1382298587.003171:2,
new file mode 100644 (file)
index 0000000..66e425e
--- /dev/null
@@ -0,0 +1,174 @@
+From: Mingming Cao <mcao@us.ibm.com>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+       register them
+Date: Fri, 25 Jun 2010 17:52:24 -0700
+Lines: 92
+Message-ID: <OFB55E8EC7.E8DD23D5-ON8725774E.0004921E-8825774E.0004CC31@us.ibm.com>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net>  <4C24A606.5040001@suse.de>
+       <1277220214-3597-1-git-send-email-sjayaraman@suse.de>   <9822.1277312573@redhat.com>
+       <22697.1277470549@redhat.com>   <18628.1277502398@redhat.com>   <20100625182651.36800d06@tlielax.poochiereds.net>
+       <AANLkTilOTrHLvLv4XWYZO6xCnYZgYT7gO2M-oKZ6VvqM@mail.gmail.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: quoted-printable
+Cc: linux-cifs@vger.kernel.org, Jeff Layton <jlayton@redhat.com>,
+       samba-technical@lists.samba.org, linux-kernel@vger.kernel.org,
+       David Howells <dhowells@redhat.com>, linux-fsdevel@vger.kernel.org,
+       "Aneesh Kumar K.V" <aneesh.kumar@linux.vnet.ibm.com>
+To: Steve French <smfrench@gmail.com>
+X-From: samba-technical-bounces@lists.samba.org Sat Jun 26 13:36:56 2010
+Return-path: <samba-technical-bounces@lists.samba.org>
+Envelope-to: gnsi-samba-technical@m.gmane.org
+Received: from fn.samba.org ([216.83.154.106] helo=lists.samba.org)
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <samba-technical-bounces@lists.samba.org>)
+       id 1OSTgu-00025d-6P
+       for gnsi-samba-technical@m.gmane.org; Sat, 26 Jun 2010 13:36:56 +0200
+Received: from fn.samba.org (localhost [127.0.0.1])
+       by lists.samba.org (Postfix) with ESMTP id 1ED11AD2C4;
+       Sat, 26 Jun 2010 05:36:45 -0600 (MDT)
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on fn.samba.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-6.6 required=3.8 tests=BAYES_00,HTML_MESSAGE,
+       RCVD_IN_DNSWL_MED,SPF_PASS autolearn=ham version=3.2.5
+X-Original-To: samba-technical@lists.samba.org
+Delivered-To: samba-technical@lists.samba.org
+Received: from e34.co.us.ibm.com (e34.co.us.ibm.com [32.97.110.152])
+       by lists.samba.org (Postfix) with ESMTP id 30F90AD282
+       for <samba-technical@lists.samba.org>;
+       Fri, 25 Jun 2010 18:52:24 -0600 (MDT)
+Received: from d03relay01.boulder.ibm.com (d03relay01.boulder.ibm.com
+       [9.17.195.226])
+       by e34.co.us.ibm.com (8.14.4/8.13.1) with ESMTP id o5Q0iN1h017083
+       for <samba-technical@lists.samba.org>; Fri, 25 Jun 2010 18:44:23 -0600
+Received: from d03av01.boulder.ibm.com (d03av01.boulder.ibm.com [9.17.195.167])
+       by d03relay01.boulder.ibm.com (8.13.8/8.13.8/NCO v10.0) with ESMTP id
+       o5Q0qQTN175324
+       for <samba-technical@lists.samba.org>; Fri, 25 Jun 2010 18:52:26 -0600
+Received: from d03av01.boulder.ibm.com (loopback [127.0.0.1])
+       by d03av01.boulder.ibm.com (8.14.4/8.13.1/NCO v10.0 AVout) with ESMTP
+       id o5Q0qPCF006767
+       for <samba-technical@lists.samba.org>; Fri, 25 Jun 2010 18:52:26 -0600
+Received: from d03nm128.boulder.ibm.com (d03nm128.boulder.ibm.com
+       [9.17.195.32])
+       by d03av01.boulder.ibm.com (8.14.4/8.13.1/NCO v10.0 AVin) with ESMTP id
+       o5Q0qPrh006760; Fri, 25 Jun 2010 18:52:25 -0600
+In-Reply-To: <AANLkTilOTrHLvLv4XWYZO6xCnYZgYT7gO2M-oKZ6VvqM@mail.gmail.com>
+X-KeepSent: B55E8EC7:E8DD23D5-8725774E:0004921E;
+ type=4; name=$KeepSent
+X-Mailer: Lotus Notes Build V852_M2_03302010 March 30, 2010
+X-MIMETrack: Serialize by Router on D03NM128/03/M/IBM(Release 8.0.1|February
+       07, 2008) at 06/25/2010 18:52:25
+X-Mailman-Approved-At: Sat, 26 Jun 2010 05:36:42 -0600
+X-Content-Filtered-By: Mailman/MimeDel 2.1.12
+X-BeenThere: samba-technical@lists.samba.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Discussions on Samba internals. For general questions please
+       subscribe to the list samba@samba.org"
+       <samba-technical.lists.samba.org>
+List-Unsubscribe: <https://lists.samba.org/mailman/options/samba-technical>,
+       <mailto:samba-technical-request@lists.samba.org?subject=unsubscribe>
+List-Archive: <http://lists.samba.org/pipermail/samba-technical>
+List-Post: <mailto:samba-technical@lists.samba.org>
+List-Help: <mailto:samba-technical-request@lists.samba.org?subject=help>
+List-Subscribe: <https://lists.samba.org/mailman/listinfo/samba-technical>,
+       <mailto:samba-technical-request@lists.samba.org?subject=subscribe>
+Sender: samba-technical-bounces@lists.samba.org
+Errors-To: samba-technical-bounces@lists.samba.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003208>
+
+
+
+Steve French <smfrench@gmail.com> wrote on 06/25/2010 04:05:30 PM:
+
+> Steve French <smfrench@gmail.com>
+> 06/25/2010 04:05 PM
+>
+> To
+>
+> Jeff Layton <jlayton@samba.org>, "Aneesh Kumar K.V"
+> <aneesh.kumar@linux.vnet.ibm.com>, Mingming Cao/Beaverton/IBM@IBMUS
+>
+> cc
+>
+> David Howells <dhowells@redhat.com>, Suresh Jayaraman
+> <sjayaraman@suse.de>, linux-cifs@vger.kernel.org, linux-
+> fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, samba-
+> technical@lists.samba.org, Jeff Layton <jlayton@redhat.com>
+>
+> Subject
+>
+> Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+> register them
+>
+> On Fri, Jun 25, 2010 at 5:26 PM, Jeff Layton <jlayton@samba.org> wrot=
+e:
+> >
+> > On Fri, 25 Jun 2010 22:46:38 +0100
+> > David Howells <dhowells@redhat.com> wrote:
+> >
+> > > Jeff Layton <jlayton@samba.org> wrote:
+> > >
+> > > > Looks like it mostly uses the ctime. IMO, the mtime would be a
+better
+> > > > choice since it changes less frequently, but I don't guess that=
+ it
+> > > > matters very much.
+> > >
+> > > I'd've thought mtime changes more frequently since that's
+> altered when data is
+> > > written. =A0ctime is changed when attributes are changed.
+> > >
+> >
+> > IIUC, updating mtime for a write is also an attribute change, and t=
+hat
+> > affects ctime. According to the stat(2) manpage:
+> >
+> > =A0 =A0 =A0 The field st_ctime is changed by writing or by setting
+> =A0inode =A0informa-
+> > =A0 =A0 =A0 tion (i.e., owner, group, link count, mode, etc.).
+> >
+> > > Note that Ext4 appears to have a file creation time field in its
+inode
+> > > (struct ext4_inode::i_crtime[_extra]). =A0Can Samba be made to us=
+e
+that?
+> > >
+> >
+> > Is it exposed to userspace in any (standard) way? It would be handy=
+ to
+> > have that. While we're wishing...it might also be nice to have a
+> > standard way to get at the i_generation from userspace too.
+> >
+>
+> Yes - I have talked with MingMing and Aneesh about those (NFS may
+> someday be able to use those too).=A0 An obstacle in the past had bee=
+n
+> that samba server stores its own fake creation time in an ndr encoded=
+
+> xattr which complicates things.
+>
+> MingMing/Annesh -
+> Xattr or other way to get at birth time?
+>
+>
+
+Not yet,
+ The ext4 file creation time only accesable from the kernel at the mome=
+nt.
+There were discussion
+to make this information avaliable via xattr before, but was rejected,
+since most people
+agree that making this info avalibele via stat() is more standard. Howe=
+ver
+modifying stat() would imply
+big interface change. thus no action has been taken yet.
+
+> --
+> Thanks,
+>
+> Steve=
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.003317:2, b/test/corpora/lkml/cur/1382298587.003317:2,
new file mode 100644 (file)
index 0000000..6fce518
--- /dev/null
@@ -0,0 +1,156 @@
+From: "Aneesh Kumar K. V" <aneesh.kumar@linux.vnet.ibm.com>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Sun, 27 Jun 2010 23:47:21 +0530
+Lines: 100
+Message-ID: <871vbscpce.fsf@linux.vnet.ibm.com>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net> <4C24A606.5040001@suse.de> <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <9822.1277312573@redhat.com> <22697.1277470549@redhat.com> <18628.1277502398@redhat.com> <20100625182651.36800d06@tlielax.poochiereds.net> <AANLkTilOTrHLvLv4XWYZO6xCnYZgYT7gO2M-oKZ6VvqM@mail.gmail.com> <OFB55E8EC7.E8DD23D5-ON8725774E.0004921E-8825774E.0004CC31@us.ibm.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: David Howells <dhowells@redhat.com>,
+       Jeff Layton <jlayton@redhat.com>,
+       Jeff Layton <jlayton@samba.org>, linux-cifs@vger.kernel.org,
+       linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
+       samba-technical@lists.samba.org,
+       Suresh Jayaraman <sjayaraman@suse.de>
+To: Mingming Cao <mcao@us.ibm.com>, Steve French <smfrench@gmail.com>,
+       "DENIEL Philippe" <philippe.deniel@cea.fr>
+X-From: linux-kernel-owner@vger.kernel.org Sun Jun 27 20:18:00 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OSwQZ-0003Kh-Vu
+       for glk-linux-kernel-3@lo.gmane.org; Sun, 27 Jun 2010 20:18:00 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1754631Ab0F0SRq convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 27 Jun 2010 14:17:46 -0400
+Received: from e23smtp07.au.ibm.com ([202.81.31.140]:52430 "EHLO
+       e23smtp07.au.ibm.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1753837Ab0F0SRl convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 27 Jun 2010 14:17:41 -0400
+Received: from d23relay05.au.ibm.com (d23relay05.au.ibm.com [202.81.31.247])
+       by e23smtp07.au.ibm.com (8.14.4/8.13.1) with ESMTP id o5RIHbfJ012483;
+       Mon, 28 Jun 2010 04:17:37 +1000
+Received: from d23av03.au.ibm.com (d23av03.au.ibm.com [9.190.234.97])
+       by d23relay05.au.ibm.com (8.13.8/8.13.8/NCO v10.0) with ESMTP id o5RIHW9f1130634;
+       Mon, 28 Jun 2010 04:17:32 +1000
+Received: from d23av03.au.ibm.com (loopback [127.0.0.1])
+       by d23av03.au.ibm.com (8.14.4/8.13.1/NCO v10.0 AVout) with ESMTP id o5RIHVcR027534;
+       Mon, 28 Jun 2010 04:17:32 +1000
+Received: from skywalker.linux.vnet.ibm.com ([9.77.196.78])
+       by d23av03.au.ibm.com (8.14.4/8.13.1/NCO v10.0 AVin) with ESMTP id o5RIHMFl027485;
+       Mon, 28 Jun 2010 04:17:24 +1000
+In-Reply-To: <OFB55E8EC7.E8DD23D5-ON8725774E.0004921E-8825774E.0004CC31@us.ibm.com>
+User-Agent: Notmuch/ (http://notmuchmail.org) Emacs/24.0.50.1 (i686-pc-linux-gnu)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003357>
+
+On Fri, 25 Jun 2010 17:52:24 -0700, Mingming Cao <mcao@us.ibm.com> wrot=
+e:
+>=20
+>=20
+> Steve French <smfrench@gmail.com> wrote on 06/25/2010 04:05:30 PM:
+>=20
+> > Steve French <smfrench@gmail.com>
+> > 06/25/2010 04:05 PM
+> >
+> > To
+> >
+> > Jeff Layton <jlayton@samba.org>, "Aneesh Kumar K.V"
+> > <aneesh.kumar@linux.vnet.ibm.com>, Mingming Cao/Beaverton/IBM@IBMUS
+> >
+> > cc
+> >
+> > David Howells <dhowells@redhat.com>, Suresh Jayaraman
+> > <sjayaraman@suse.de>, linux-cifs@vger.kernel.org, linux-
+> > fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, samba-
+> > technical@lists.samba.org, Jeff Layton <jlayton@redhat.com>
+> >
+> > Subject
+> >
+> > Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+> > register them
+> >
+> > On Fri, Jun 25, 2010 at 5:26 PM, Jeff Layton <jlayton@samba.org> wr=
+ote:
+> > >
+> > > On Fri, 25 Jun 2010 22:46:38 +0100
+> > > David Howells <dhowells@redhat.com> wrote:
+> > >
+> > > > Jeff Layton <jlayton@samba.org> wrote:
+> > > >
+> > > > > Looks like it mostly uses the ctime. IMO, the mtime would be =
+a
+> better
+> > > > > choice since it changes less frequently, but I don't guess th=
+at it
+> > > > > matters very much.
+> > > >
+> > > > I'd've thought mtime changes more frequently since that's
+> > altered when data is
+> > > > written. =C2=A0ctime is changed when attributes are changed.
+> > > >
+> > >
+> > > IIUC, updating mtime for a write is also an attribute change, and=
+ that
+> > > affects ctime. According to the stat(2) manpage:
+> > >
+> > > =C2=A0 =C2=A0 =C2=A0 The field st_ctime is changed by writing or =
+by setting
+> > =C2=A0inode =C2=A0informa-
+> > > =C2=A0 =C2=A0 =C2=A0 tion (i.e., owner, group, link count, mode, =
+etc.).
+> > >
+> > > > Note that Ext4 appears to have a file creation time field in it=
+s
+> inode
+> > > > (struct ext4_inode::i_crtime[_extra]). =C2=A0Can Samba be made =
+to use
+> that?
+> > > >
+> > >
+> > > Is it exposed to userspace in any (standard) way? It would be han=
+dy to
+> > > have that. While we're wishing...it might also be nice to have a
+> > > standard way to get at the i_generation from userspace too.
+> > >
+> >
+> > Yes - I have talked with MingMing and Aneesh about those (NFS may
+> > someday be able to use those too).=C2=A0 An obstacle in the past ha=
+d been
+> > that samba server stores its own fake creation time in an ndr encod=
+ed
+> > xattr which complicates things.
+> >
+> > MingMing/Annesh -
+> > Xattr or other way to get at birth time?
+> >
+> >
+>=20
+> Not yet,
+>  The ext4 file creation time only accesable from the kernel at the mo=
+ment.
+> There were discussion
+> to make this information avaliable via xattr before, but was rejected=
+,
+> since most people
+> agree that making this info avalibele via stat() is more standard. Ho=
+wever
+> modifying stat() would imply
+> big interface change. thus no action has been taken yet.
+
+NFS ganesha pNFS also had a requirement for getting i_generation and
+inode number in userspace. So may be we should now look at updating
+stat or add a variant syscall that include i_generation and create time
+in the return value
+
+-aneesh
+
+
diff --git a/test/corpora/lkml/cur/1382298587.003318:2, b/test/corpora/lkml/cur/1382298587.003318:2,
new file mode 100644 (file)
index 0000000..058d147
--- /dev/null
@@ -0,0 +1,66 @@
+From: Christoph Hellwig <hch-wEGCiKHe2LqWVfeAwA7xHQ@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+ register them
+Date: Sun, 27 Jun 2010 14:22:29 -0400
+Lines: 9
+Message-ID: <20100627182229.GA492@infradead.org>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net>
+ <4C24A606.5040001@suse.de>
+ <1277220214-3597-1-git-send-email-sjayaraman@suse.de>
+ <9822.1277312573@redhat.com>
+ <22697.1277470549@redhat.com>
+ <18628.1277502398@redhat.com>
+ <20100625182651.36800d06@tlielax.poochiereds.net>
+ <AANLkTilOTrHLvLv4XWYZO6xCnYZgYT7gO2M-oKZ6VvqM@mail.gmail.com>
+ <OFB55E8EC7.E8DD23D5-ON8725774E.0004921E-8825774E.0004CC31@us.ibm.com>
+ <871vbscpce.fsf@linux.vnet.ibm.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Cc: Mingming Cao <mcao-r/Jw6+rmf7HQT0dZR+AlfA@public.gmane.org>, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       DENIEL Philippe <philippe.deniel-KCE40YydGKI@public.gmane.org>,
+       David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>,
+       Jeff Layton <jlayton-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>,
+       Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       samba-technical-w/Ol4Ecudpl8XjKLYN78aQ@public.gmane.org,
+       Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+To: "Aneesh Kumar K. V" <aneesh.kumar-23VcF4HTsmIX0ybBhKVfKdBPR1lH4CV8@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Sun Jun 27 20:22:46 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OSwVB-0005TI-SG
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Sun, 27 Jun 2010 20:22:46 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1752811Ab0F0SWo (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Sun, 27 Jun 2010 14:22:44 -0400
+Received: from bombadil.infradead.org ([18.85.46.34]:55433 "EHLO
+       bombadil.infradead.org" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752728Ab0F0SWn (ORCPT
+       <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>); Sun, 27 Jun 2010 14:22:43 -0400
+Received: from hch by bombadil.infradead.org with local (Exim 4.72 #1 (Red Hat Linux))
+       id 1OSwUv-00009z-9N; Sun, 27 Jun 2010 18:22:29 +0000
+Content-Disposition: inline
+In-Reply-To: <871vbscpce.fsf-23VcF4HTsmIX0ybBhKVfKdBPR1lH4CV8@public.gmane.org>
+User-Agent: Mutt/1.5.20 (2009-08-17)
+X-SRS-Rewrite: SMTP reverse-path rewritten from <hch-wEGCiKHe2LqWVfeAwA7xHQ@public.gmane.org> by bombadil.infradead.org
+       See http://www.infradead.org/rpr.html
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003358>
+
+On Sun, Jun 27, 2010 at 11:47:21PM +0530, Aneesh Kumar K. V wrote:
+> NFS ganesha pNFS also had a requirement for getting i_generation and
+> inode number in userspace. So may be we should now look at updating
+> stat or add a variant syscall that include i_generation and create time
+> in the return value
+
+What's missing in knfsd that you feel the sudden urge to move backwards
+to a userspace nfsd (one with a horribly crappy codebase, too).
+
+
+
diff --git a/test/corpora/lkml/cur/1382298587.003486:2, b/test/corpora/lkml/cur/1382298587.003486:2,
new file mode 100644 (file)
index 0000000..8831b45
--- /dev/null
@@ -0,0 +1,89 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: Re: [RFC][PATCH 05/10] cifs: define superblock-level cache index
+ objects and register them
+Date: Mon, 28 Jun 2010 18:23:13 +0530
+Lines: 48
+Message-ID: <4C289B39.4060901@suse.de>
+References: <22746.1277470713@redhat.com> <4C24A4A0.90408@suse.de> <1277220206-3559-1-git-send-email-sjayaraman@suse.de> <yes> <9720.1277312290@redhat.com> <23204.1277472412@redhat.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Mon Jun 28 14:53:24 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OTDq0-00054Q-At
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Mon, 28 Jun 2010 14:53:24 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1754503Ab0F1MxX (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Mon, 28 Jun 2010 08:53:23 -0400
+Received: from cantor2.suse.de ([195.135.220.15]:48374 "EHLO mx2.suse.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1754456Ab0F1MxW (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Mon, 28 Jun 2010 08:53:22 -0400
+Received: from relay2.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       by mx2.suse.de (Postfix) with ESMTP id 7BDC18672B;
+       Mon, 28 Jun 2010 14:53:21 +0200 (CEST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091130 SUSE/3.0.0-1.1.1 Thunderbird/3.0
+In-Reply-To: <23204.1277472412-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003530>
+
+On 06/25/2010 06:56 PM, David Howells wrote:
+> David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org> wrote:
+> 
+>>>> validate the root directory of the share in some way?
+>>>
+>>> I don't know if there is a way to do this.
+>>
+>> Is there an inode number or something?  Even the creation time might do.
+> 
+> Looking in cifspdu.h, there are a number of things that it might be possible
+> to use.
+> 
+>  (1) FILE_ALL_INFO: CreationTime, IndexNumber, IndexNumber1, FileName
+>      (assuming this isn't flattened to '\' or something for the root of a
+>      share.
+> 
+>  (2) FILE_UNIX_BASIC_INFO: DevMajor, DevMinor, UniqueId.
+> 
+>  (3) FILE_INFO_STANDARD: CreationDate, CreationTime.
+> 
+>  (4) FILE_INFO_BASIC: CreationTime.
+> 
+>  (5) FILE_DIRECTORY_INFO: FileIndex, CreationTime, FileName.
+> 
+>  (6) SEARCH_ID_FULL_DIR_INFO: FileIndex, CreationTime, UniqueId, FileName.
+> 
+>  (7) FILE_BOTH_DIRECTORY_INFO: FileIndex, CreationTime, ShortName, FileName.
+> 
+>  (8) OPEN_RSP_EXT: Fid, CreationTime, VolumeGUID, FileId.
+> 
+> You may have to choose different sets of things, depending on what the server
+> has on offer.  Also, don't forget, if you can't work out whether a share is
+
+Did you mean we need to validate differently for different servers?
+
+I just did some testing and it looks like we could rely on CreationTime,
+IndexNumber for validating with Windows servers (FileName is relative to
+the mapped drive) and UniqueId for validating with Samba servers. I did
+not test all possibilities (there could be more).
+
+> coherent or not from the above, you can always use LastWriteTime, ChangeTime
+> and EndOfFile and just discard the whole subtree if they differ.
+> 
+
+Thanks,
+
+-- 
+Suresh Jayaraman
+
+
diff --git a/test/corpora/lkml/cur/1382298587.004581:2, b/test/corpora/lkml/cur/1382298587.004581:2,
new file mode 100644 (file)
index 0000000..732bfa0
--- /dev/null
@@ -0,0 +1,92 @@
+From: Timur Tabi <timur.tabi@gmail.com>
+Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Wed, 30 Jun 2010 15:55:58 -0500
+Lines: 33
+Message-ID: <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com>
+References: <20100308191005.GE4324@amak.tundra.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: mporter@kernel.crashing.org, linux-kernel@vger.kernel.org,
+       linuxppc-dev@lists.ozlabs.org, thomas.moll@sysgo.com
+To: Alexandre Bounine <abounine@tundra.com>
+X-From: linux-kernel-owner@vger.kernel.org Wed Jun 30 22:56:40 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OU4Kl-0005Kf-V4
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 30 Jun 2010 22:56:40 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1756668Ab0F3U4b convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Wed, 30 Jun 2010 16:56:31 -0400
+Received: from mail-vw0-f46.google.com ([209.85.212.46]:41333 "EHLO
+       mail-vw0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1753416Ab0F3U43 convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Wed, 30 Jun 2010 16:56:29 -0400
+Received: by vws5 with SMTP id 5so1449398vws.19
+        for <linux-kernel@vger.kernel.org>; Wed, 30 Jun 2010 13:56:28 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:received:mime-version:received:in-reply-to
+         :references:from:date:message-id:subject:to:cc:content-type
+         :content-transfer-encoding;
+        bh=FTlit9cHTz/9rLGcvA5/pEZlzxAQ5x20v8HE5XYFwYM=;
+        b=NFbjnxZ4KwcjTy4tFh+BnhWPEGeYTw6z918yIouRaMmbEDph56xq26K9aTBokuYHqe
+         UgFjBn7XWcxvqJPyCetfsDRG+F3M2XwCq/DSCswSPtXSLsy8WKm7cMXVS3hjiO8sMZ97
+         mRMGZkYBJHjWP+ulkBXiq6q7/OQuE8Dkl+rWM=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=mime-version:in-reply-to:references:from:date:message-id:subject:to
+         :cc:content-type:content-transfer-encoding;
+        b=r0N6AOAg+TSvY2kPQPahldj4iRU9oUoSLtHA7JXG2QU4CR9O5GBhxAtr2aY99qUPZd
+         tFS0ZWRAb9cmOgiZhTpNxsBjCJ/e/DQ1ccP5rZ/U40q1SJ1KwN92hqpOoppZ0tkqSB7/
+         UlQtsvPSK7a0bYqufEmscfAi98w1+mfZIbK6U=
+Received: by 10.220.161.203 with SMTP id s11mr5093041vcx.195.1277931388141; 
+       Wed, 30 Jun 2010 13:56:28 -0700 (PDT)
+Received: by 10.220.161.137 with HTTP; Wed, 30 Jun 2010 13:55:58 -0700 (PDT)
+In-Reply-To: <20100308191005.GE4324@amak.tundra.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1004632>
+
+On Mon, Mar 8, 2010 at 2:10 PM, Alexandre Bounine <abounine@tundra.com>=
+ wrote:
+>
+> From: Alexandre Bounine <alexandre.bounine@idt.com>
+>
+> Add Machine Check exception handling into RapidIO port driver
+> for Freescale SoCs (MPC85xx).
+>
+> Signed-off-by: Alexandre Bounine <alexandre.bounine@idt.com>
+> Tested-by: Thomas Moll <thomas.moll@sysgo.com>
+=2E..
+
+> +static int fsl_rio_mcheck_exception(struct pt_regs *regs)
+> +{
+> + =A0 =A0 =A0 const struct exception_table_entry *entry =3D NULL;
+> + =A0 =A0 =A0 unsigned long reason =3D (mfspr(SPRN_MCSR) & MCSR_MASK)=
+;
+
+MCSR_MASK is not defined anywhere, so when I compile this code, I get t=
+his:
+
+  CC      arch/powerpc/sysdev/fsl_rio.o
+arch/powerpc/sysdev/fsl_rio.c: In function 'fsl_rio_mcheck_exception':
+arch/powerpc/sysdev/fsl_rio.c:248: error: 'MCSR_MASK' undeclared
+(first use in this function)
+arch/powerpc/sysdev/fsl_rio.c:248: error: (Each undeclared identifier
+is reported only once
+arch/powerpc/sysdev/fsl_rio.c:248: error: for each function it appears =
+in.)
+
+--=20
+Timur Tabi
+Linux kernel developer at Freescale
+
+
diff --git a/test/corpora/lkml/cur/1382298587.004582:2, b/test/corpora/lkml/cur/1382298587.004582:2,
new file mode 100644 (file)
index 0000000..d149b72
--- /dev/null
@@ -0,0 +1,68 @@
+From: Timur Tabi <timur.tabi@gmail.com>
+Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Wed, 30 Jun 2010 16:00:56 -0500
+Lines: 12
+Message-ID: <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com>
+References: <20100308191005.GE4324@amak.tundra.com> <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1
+Cc: mporter@kernel.crashing.org, linux-kernel@vger.kernel.org,
+       linuxppc-dev@lists.ozlabs.org, thomas.moll@sysgo.com
+To: Alexandre Bounine <abounine@tundra.com>
+X-From: linux-kernel-owner@vger.kernel.org Wed Jun 30 23:01:37 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OU4PZ-0000HS-0T
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 30 Jun 2010 23:01:37 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755703Ab0F3VB2 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Wed, 30 Jun 2010 17:01:28 -0400
+Received: from mail-vw0-f46.google.com ([209.85.212.46]:53141 "EHLO
+       mail-vw0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751784Ab0F3VB1 (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Wed, 30 Jun 2010 17:01:27 -0400
+Received: by vws5 with SMTP id 5so1454517vws.19
+        for <linux-kernel@vger.kernel.org>; Wed, 30 Jun 2010 14:01:26 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:received:mime-version:received:in-reply-to
+         :references:from:date:message-id:subject:to:cc:content-type;
+        bh=+BUKti+Oa03CrnVvRyT591FhcoxqR7S2rzZHtD6WSuY=;
+        b=O/b04HLJrmTE0aIq2mNCRznQrXxAAGHSMarHR5mrgYptmr68froM6UgmDqTZFLhNiH
+         BcT8g+AziiqSV1k/ckXjRyVR0s9Jdv4g2phMNtp8NStbPfOPpLDkUKTQadphOTonCfeK
+         e+ZrLBwh+FCoYNAOjvFioBKj6CxN2Oi5xIhPc=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=mime-version:in-reply-to:references:from:date:message-id:subject:to
+         :cc:content-type;
+        b=UcKGhJIXCTTcSvBWwGwLUefPONGygVPsUnTt4nDSl4udB8JKMyi0EghzzgNXUyq4Dz
+         UCxzZAyxzjvjgsgPS3kzPhSsWG2PRG66pC1OA68RJ5YVOjt55/yOz/yfTqXBVvRSq2fV
+         QNcKACYHSjkIZ7Uq7ZEW9bEGI5tTKdz++N2UA=
+Received: by 10.220.124.73 with SMTP id t9mr5099129vcr.37.1277931686462; Wed, 
+       30 Jun 2010 14:01:26 -0700 (PDT)
+Received: by 10.220.161.137 with HTTP; Wed, 30 Jun 2010 14:00:56 -0700 (PDT)
+In-Reply-To: <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1004633>
+
+On Wed, Jun 30, 2010 at 3:55 PM, Timur Tabi <timur.tabi@gmail.com> wrote:
+
+> MCSR_MASK is not defined anywhere, so when I compile this code, I get this:
+
+Never mind.  I see that it's been fixed already, and that the patch
+that removed MCSR_MASK was posted around the same time that this patch
+was posted.
+
+
+-- 
+Timur Tabi
+Linux kernel developer at Freescale
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001724:2, b/test/corpora/lkml/cur/1382298770.001724:2,
new file mode 100644 (file)
index 0000000..69c794c
--- /dev/null
@@ -0,0 +1,104 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 10/10] cifs: add mount option to enable local caching
+Date: Tue, 22 Jun 2010 20:55:09 +0530
+Lines: 66
+Message-ID: <1277220309-3757-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:25:29 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5Ls-0004PS-BM
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:25:28 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1755015Ab0FVPZ1 (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:25:27 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:48639 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1754070Ab0FVPZ1 (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:25:27 -0400
+X-Greylist: delayed 316 seconds by postgrey-1.27 at vger.kernel.org; Tue, 22 Jun 2010 11:25:26 EDT
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:25:11 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001750>
+
+Add a mount option 'fsc' to enable local caching on CIFS.
+
+As the cifs-utils (userspace) changes are not done yet, this patch enables
+'fsc' by default to assist testing.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+---
+ fs/cifs/cifs_fs_sb.h |    1 +
+ fs/cifs/connect.c    |    8 ++++++++
+ 2 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/fs/cifs/cifs_fs_sb.h b/fs/cifs/cifs_fs_sb.h
+index 246a167..9e77145 100644
+--- a/fs/cifs/cifs_fs_sb.h
++++ b/fs/cifs/cifs_fs_sb.h
+@@ -35,6 +35,7 @@
+ #define CIFS_MOUNT_DYNPERM      0x1000 /* allow in-memory only mode setting   */
+ #define CIFS_MOUNT_NOPOSIXBRL   0x2000 /* mandatory not posix byte range lock */
+ #define CIFS_MOUNT_NOSSYNC      0x4000 /* don't do slow SMBflush on every sync*/
++#define CIFS_MOUNT_FSCACHE    0x8000 /* local caching enabled */
+ struct cifs_sb_info {
+       struct cifsTconInfo *tcon;      /* primary mount */
+diff --git a/fs/cifs/connect.c b/fs/cifs/connect.c
+index 4844dbd..6c6ff3c 100644
+--- a/fs/cifs/connect.c
++++ b/fs/cifs/connect.c
+@@ -98,6 +98,7 @@ struct smb_vol {
+       bool noblocksnd:1;
+       bool noautotune:1;
+       bool nostrictsync:1; /* do not force expensive SMBflush on every sync */
++      bool fsc:1;     /* enable fscache */
+       unsigned int rsize;
+       unsigned int wsize;
+       bool sockopt_tcp_nodelay:1;
+@@ -843,6 +844,9 @@ cifs_parse_mount_options(char *options, const char *devname,
+       /* default to using server inode numbers where available */
+       vol->server_ino = 1;
++      /* XXX: default to fsc for testing until mount.cifs pieces are done */
++      vol->fsc = 1;
++
+       if (!options)
+               return 1;
+@@ -1332,6 +1336,8 @@ cifs_parse_mount_options(char *options, const char *devname,
+                       printk(KERN_WARNING "CIFS: Mount option noac not "
+                               "supported. Instead set "
+                               "/proc/fs/cifs/LookupCacheEnabled to 0\n");
++              } else if (strnicmp(data, "fsc", 3) == 0) {
++                      vol->fsc = true;
+               } else
+                       printk(KERN_WARNING "CIFS: Unknown mount option %s\n",
+                                               data);
+@@ -2405,6 +2411,8 @@ static void setup_cifs_sb(struct smb_vol *pvolume_info,
+               cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_OVERR_GID;
+       if (pvolume_info->dynperm)
+               cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_DYNPERM;
++      if (pvolume_info->fsc)
++              cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_FSCACHE;
+       if (pvolume_info->direct_io) {
+               cFYI(1, "mounting share using direct i/o");
+               cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_DIRECT_IO;
+-- 
+1.6.4.2
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001730:2, b/test/corpora/lkml/cur/1382298770.001730:2,
new file mode 100644 (file)
index 0000000..840be2e
--- /dev/null
@@ -0,0 +1,103 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 00/10] cifs: local caching support using FS-Cache
+Date: Tue, 22 Jun 2010 20:50:05 +0530
+Lines: 66
+Message-ID: <1277220005-3322-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:40:38 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5aY-00055O-BD
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:40:38 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751889Ab0FVPkf (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:40:35 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:50040 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751554Ab0FVPkf (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:40:35 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:20:07 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001756>
+
+This patchset is a first stab at adding persistent, local caching facility for
+CIFS using the FS-Cache interface.
+
+The index hierarchy which is mainly used to locate a file object or discard
+a certain subset of the files cached, currently has three levels:
+       - Server
+       - Share 
+       - File
+
+The server index object is keyed by hostname of the server. The superblock
+index object is keyed by the sharename and the inode object is keyed by the
+UniqueId. The cache coherency is ensured by checking the 'LastWriteTime' and
+size of file.
+
+To use this, apply this patchset in order, mount the share with rsize=4096 and
+try copying a huge file (say few hundred MBs) from mount point to local
+filesystem. During the first time, the cache will be initialized. When you copy
+the second time, it should read from the local cache.
+
+To reduce the impact of page cache and see the local caching in action
+readily, try doing a sync and drop the caches by doing:
+       sync; echo 3 > /proc/sys/vm/drop_caches
+
+Known issues
+-------------
+       - the cache coherency check may not be reliable always as some
+         CIFS servers are known not to update mtime until the filehandle is
+         closed.
+       - not all the Servers under all circumstances provide a unique
+         'UniqueId'.
+
+Todo's
+-------
+       - improvements to avoid potential key collisions
+       - address the above known issues
+
+This set is lightly tested and all the bugs seen during my testing have been
+fixed. However, this can be considered as an RFC for now.
+
+Any Comments or Suggestions are welcome.
+
+Suresh Jayaraman (10)
+  cifs: add kernel config option for CIFS Client caching support
+  cifs: guard cifsglob.h against multiple inclusion
+  cifs: register CIFS for caching
+  cifs: define server-level cache index objects and register them with FS-Cache
+  cifs: define superblock-level cache index objects and register them
+  cifs: define inode-level cache object and register them
+  cifs: FS-Cache page management
+  cifs: store pages into local cache
+  cifs: read pages from FS-Cache
+  cifs: add mount option to enable local caching
+
+ Kconfig      |    9 ++
+ Makefile     |    2 
+ cache.c      |  251 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ cifs_fs_sb.h |    1 
+ cifsfs.c     |   15 +++
+ cifsglob.h   |   14 +++
+ connect.c    |   16 +++
+ file.c       |   51 +++++++++++
+ fscache.c    |  244 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ fscache.h    |  135 +++++++++++++++++++++++++++++++
+ inode.c      |    4 
+ 11 files changed, 742 insertions(+)
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001731:2, b/test/corpora/lkml/cur/1382298770.001731:2,
new file mode 100644 (file)
index 0000000..d8b3168
--- /dev/null
@@ -0,0 +1,67 @@
+From: Suresh Jayaraman <sjayaraman@suse.de>
+Subject: [RFC][PATCH 01/10] cifs: add kernel config option for CIFS Client caching support
+Date: Tue, 22 Jun 2010 20:52:38 +0530
+Lines: 30
+Message-ID: <1277220158-3405-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, David Howells <dhowells@redhat.com>
+To: Steve French <smfrench@gmail.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Jun 22 17:43:27 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OR5dG-0007m9-Ij
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 22 Jun 2010 17:43:26 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1751536Ab0FVPnS (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 22 Jun 2010 11:43:18 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:51303 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1750800Ab0FVPnR (ORCPT
+       <rfc822;groupwise-SJayaraman@novell.com:0:0>);
+       Tue, 22 Jun 2010 11:43:17 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:22:40 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001757>
+
+Add a kernel config option to enable local caching for CIFS.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+---
+ fs/cifs/Kconfig |    9 +++++++++
+ 1 files changed, 9 insertions(+), 0 deletions(-)
+
+diff --git a/fs/cifs/Kconfig b/fs/cifs/Kconfig
+index 80f3525..5739fd7 100644
+--- a/fs/cifs/Kconfig
++++ b/fs/cifs/Kconfig
+@@ -131,6 +131,15 @@ config CIFS_DFS_UPCALL
+           IP addresses) which is needed for implicit mounts of DFS junction
+           points. If unsure, say N.
++config CIFS_FSCACHE
++        bool "Provide CIFS client caching support (EXPERIMENTAL)"
++        depends on EXPERIMENTAL
++        depends on CIFS=m && FSCACHE || CIFS=y && FSCACHE=y
++        help
++          Makes CIFS FS-Cache capable. Say Y here if you want your CIFS data
++          to be cached locally on disk through the general filesystem cache
++          manager. If unsure, say N.
++
+ config CIFS_EXPERIMENTAL
+         bool "CIFS Experimental Features (EXPERIMENTAL)"
+         depends on CIFS && EXPERIMENTAL
+-- 
+1.6.4.2
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001732:2, b/test/corpora/lkml/cur/1382298770.001732:2,
new file mode 100644 (file)
index 0000000..8850953
--- /dev/null
@@ -0,0 +1,73 @@
+From: Suresh Jayaraman <sjayaraman@suse.de>
+Subject: [RFC][PATCH 02/10] cifs: guard cifsglob.h against multiple inclusion
+Date: Tue, 22 Jun 2010 20:52:50 +0530
+Lines: 36
+Message-ID: <1277220170-3442-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, David Howells <dhowells@redhat.com>
+To: Steve French <smfrench@gmail.com>
+X-From: linux-fsdevel-owner@vger.kernel.org Tue Jun 22 17:43:39 2010
+Return-path: <linux-fsdevel-owner@vger.kernel.org>
+Envelope-to: lnx-linux-fsdevel@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-fsdevel-owner@vger.kernel.org>)
+       id 1OR5dT-0007sB-18
+       for lnx-linux-fsdevel@lo.gmane.org; Tue, 22 Jun 2010 17:43:39 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752441Ab0FVPn3 (ORCPT <rfc822;lnx-linux-fsdevel@m.gmane.org>);
+       Tue, 22 Jun 2010 11:43:29 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:41538 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751889Ab0FVPn2 (ORCPT
+       <rfc822;groupwise-SJayaraman@novell.com:0:0>);
+       Tue, 22 Jun 2010 11:43:28 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:22:52 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-fsdevel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-fsdevel.vger.kernel.org>
+X-Mailing-List: linux-fsdevel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001758>
+
+Add conditional compile macros to guard the header file against multiple
+inclusion.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+---
+ fs/cifs/cifsglob.h |    5 +++++
+ 1 files changed, 5 insertions(+), 0 deletions(-)
+
+diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h
+index a88479c..6b2c39d 100644
+--- a/fs/cifs/cifsglob.h
++++ b/fs/cifs/cifsglob.h
+@@ -16,6 +16,9 @@
+  *   the GNU Lesser General Public License for more details.
+  *
+  */
++#ifndef _CIFS_GLOB_H
++#define _CIFS_GLOB_H
++
+ #include <linux/in.h>
+ #include <linux/in6.h>
+ #include <linux/slab.h>
+@@ -733,3 +736,5 @@ GLOBAL_EXTERN unsigned int cifs_min_small;  /* min size of small buf pool */
+ GLOBAL_EXTERN unsigned int cifs_max_pending; /* MAX requests at once to server*/
+ extern const struct slow_work_ops cifs_oplock_break_ops;
++
++#endif        /* _CIFS_GLOB_H */
+-- 
+1.6.4.2
+
+--
+To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001733:2, b/test/corpora/lkml/cur/1382298770.001733:2,
new file mode 100644 (file)
index 0000000..d782f90
--- /dev/null
@@ -0,0 +1,211 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 03/10] cifs: register CIFS for caching
+Date: Tue, 22 Jun 2010 20:53:09 +0530
+Lines: 174
+Message-ID: <1277220189-3485-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:43:52 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5de-0007xC-Ov
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:43:51 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1753125Ab0FVPnt (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:43:49 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:55866 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751261Ab0FVPnt (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:43:49 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:23:11 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001759>
+
+Define CIFS for FS-Cache and register for caching. Upon registration the
+top-level index object cookie will be stuck to the netfs definition by
+FS-Cache.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+---
+ fs/cifs/Makefile  |    2 ++
+ fs/cifs/cache.c   |   53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/cifsfs.c  |    8 ++++++++
+ fs/cifs/fscache.h |   40 ++++++++++++++++++++++++++++++++++++++++
+ 4 files changed, 103 insertions(+), 0 deletions(-)
+ create mode 100644 fs/cifs/cache.c
+ create mode 100644 fs/cifs/fscache.h
+
+diff --git a/fs/cifs/Makefile b/fs/cifs/Makefile
+index 9948c00..e2de709 100644
+--- a/fs/cifs/Makefile
++++ b/fs/cifs/Makefile
+@@ -11,3 +11,5 @@ cifs-y := cifsfs.o cifssmb.o cifs_debug.o connect.o dir.o file.o inode.o \
+ cifs-$(CONFIG_CIFS_UPCALL) += cifs_spnego.o
+ cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o
++
++cifs-$(CONFIG_CIFS_FSCACHE) += cache.o
+diff --git a/fs/cifs/cache.c b/fs/cifs/cache.c
+new file mode 100644
+index 0000000..1080b96
+--- /dev/null
++++ b/fs/cifs/cache.c
+@@ -0,0 +1,53 @@
++/*
++ *   fs/cifs/cache.c - CIFS filesystem cache index structure definitions
++ *
++ *   Copyright (c) 2010 Novell, Inc.
++ *   Authors(s): Suresh Jayaraman (sjayaraman-l3A5Bk7waGM@public.gmane.org>
++ *
++ *   This library is free software; you can redistribute it and/or modify
++ *   it under the terms of the GNU Lesser General Public License as published
++ *   by the Free Software Foundation; either version 2.1 of the License, or
++ *   (at your option) any later version.
++ *
++ *   This library 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 Lesser General Public License for more details.
++ *
++ *   You should have received a copy of the GNU Lesser General Public License
++ *   along with this library; if not, write to the Free Software
++ *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
++ */
++#include <linux/init.h>
++#include <linux/kernel.h>
++#include <linux/sched.h>
++#include <linux/mm.h>
++
++#include "fscache.h"
++#include "cifsglob.h"
++#include "cifs_debug.h"
++
++/*
++ * CIFS filesystem definition for FS-Cache
++ */
++struct fscache_netfs cifs_fscache_netfs = {
++      .name = "cifs",
++      .version = 0,
++};
++
++/*
++ * Register CIFS for caching with FS-Cache
++ */
++int cifs_fscache_register(void)
++{
++      return fscache_register_netfs(&cifs_fscache_netfs);
++}
++
++/*
++ * Unregister CIFS for caching
++ */
++void cifs_fscache_unregister(void)
++{
++      fscache_unregister_netfs(&cifs_fscache_netfs);
++}
++
+diff --git a/fs/cifs/cifsfs.c b/fs/cifs/cifsfs.c
+index 484e52b..c2a7aa9 100644
+--- a/fs/cifs/cifsfs.c
++++ b/fs/cifs/cifsfs.c
+@@ -47,6 +47,7 @@
+ #include <linux/key-type.h>
+ #include "dns_resolve.h"
+ #include "cifs_spnego.h"
++#include "fscache.h"
+ #define CIFS_MAGIC_NUMBER 0xFF534D42  /* the first four bytes of SMB PDUs */
+ int cifsFYI = 0;
+@@ -902,6 +903,10 @@ init_cifs(void)
+               cFYI(1, "cifs_max_pending set to max of 256");
+       }
++      rc = cifs_fscache_register();
++      if (rc)
++              goto out;
++
+       rc = cifs_init_inodecache();
+       if (rc)
+               goto out_clean_proc;
+@@ -949,8 +954,10 @@ init_cifs(void)
+       cifs_destroy_mids();
+  out_destroy_inodecache:
+       cifs_destroy_inodecache();
++      cifs_fscache_unregister();
+  out_clean_proc:
+       cifs_proc_clean();
++ out:
+       return rc;
+ }
+@@ -959,6 +966,7 @@ exit_cifs(void)
+ {
+       cFYI(DBG2, "exit_cifs");
+       cifs_proc_clean();
++      cifs_fscache_unregister();
+ #ifdef CONFIG_CIFS_DFS_UPCALL
+       cifs_dfs_release_automount_timer();
+       unregister_key_type(&key_type_dns_resolver);
+diff --git a/fs/cifs/fscache.h b/fs/cifs/fscache.h
+new file mode 100644
+index 0000000..cec9e2b
+--- /dev/null
++++ b/fs/cifs/fscache.h
+@@ -0,0 +1,40 @@
++/*
++ *   fs/cifs/fscache.h - CIFS filesystem cache interface definitions
++ *
++ *   Copyright (c) 2010 Novell, Inc.
++ *   Authors(s): Suresh Jayaraman (sjayaraman-l3A5Bk7waGM@public.gmane.org>
++ *
++ *   This library is free software; you can redistribute it and/or modify
++ *   it under the terms of the GNU Lesser General Public License as published
++ *   by the Free Software Foundation; either version 2.1 of the License, or
++ *   (at your option) any later version.
++ *
++ *   This library 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 Lesser General Public License for more details.
++ *
++ *   You should have received a copy of the GNU Lesser General Public License
++ *   along with this library; if not, write to the Free Software
++ *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
++ */
++#ifndef _CIFS_FSCACHE_H
++#define _CIFS_FSCACHE_H
++
++#include <linux/fscache.h>
++#include "cifsglob.h"
++
++#ifdef CONFIG_CIFS_FSCACHE
++
++extern struct fscache_netfs cifs_fscache_netfs;
++
++extern int cifs_fscache_register(void);
++extern void cifs_fscache_unregister(void);
++
++#else /* CONFIG_CIFS_FSCACHE */
++static inline int cifs_fscache_register(void) { return 0; }
++static inline void cifs_fscache_unregister(void) {}
++
++#endif /* CONFIG_CIFS_FSCACHE */
++
++#endif /* _CIFS_FSCACHE_H */
+-- 
+1.6.4.2
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001734:2, b/test/corpora/lkml/cur/1382298770.001734:2,
new file mode 100644 (file)
index 0000000..4b64bc3
--- /dev/null
@@ -0,0 +1,223 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 04/10] cifs: define server-level cache index objects and register them with FS-Cache
+Date: Tue, 22 Jun 2010 20:53:18 +0530
+Lines: 186
+Message-ID: <1277220198-3522-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:44:26 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5eD-0008G7-KP
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:44:26 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1753942Ab0FVPoC (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:44:02 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:58783 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751265Ab0FVPoA (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:44:00 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:23:20 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001760>
+
+Define server-level cache index objects (as managed by TCP_ServerInfo structs).
+Each server object is created in the CIFS top-level index object and is itself
+an index into which superblock-level objects are inserted.
+
+Currently, the server objects are keyed by hostname.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+---
+ fs/cifs/Makefile   |    2 +-
+ fs/cifs/cache.c    |   25 +++++++++++++++++++++++++
+ fs/cifs/cifsglob.h |    3 +++
+ fs/cifs/connect.c  |    4 ++++
+ fs/cifs/fscache.c  |   47 +++++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/fscache.h  |   12 ++++++++++++
+ 6 files changed, 92 insertions(+), 1 deletion(-)
+ create mode 100644 fs/cifs/fscache.c
+
+Index: cifs-2.6/fs/cifs/Makefile
+===================================================================
+--- cifs-2.6.orig/fs/cifs/Makefile
++++ cifs-2.6/fs/cifs/Makefile
+@@ -12,4 +12,4 @@ cifs-$(CONFIG_CIFS_UPCALL) += cifs_spneg
+ cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o
+-cifs-$(CONFIG_CIFS_FSCACHE) += cache.o
++cifs-$(CONFIG_CIFS_FSCACHE) += fscache.o cache.o
+Index: cifs-2.6/fs/cifs/cache.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cache.c
++++ cifs-2.6/fs/cifs/cache.c
+@@ -51,3 +51,28 @@ void cifs_fscache_unregister(void)
+       fscache_unregister_netfs(&cifs_fscache_netfs);
+ }
++/*
++ * Server object currently keyed by hostname
++ */
++static uint16_t cifs_server_get_key(const void *cookie_netfs_data,
++                                 void *buffer, uint16_t maxbuf)
++{
++      const struct TCP_Server_Info *server = cookie_netfs_data;
++      uint16_t len = strnlen(server->hostname, sizeof(server->hostname));
++
++      if (len > maxbuf)
++              return 0;
++
++      memcpy(buffer, server->hostname, len);
++
++      return len;
++}
++
++/*
++ * Server object for FS-Cache
++ */
++const struct fscache_cookie_def cifs_fscache_server_index_def = {
++      .name = "CIFS.server",
++      .type = FSCACHE_COOKIE_TYPE_INDEX,
++      .get_key = cifs_server_get_key,
++};
+Index: cifs-2.6/fs/cifs/cifsglob.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cifsglob.h
++++ cifs-2.6/fs/cifs/cifsglob.h
+@@ -193,6 +193,9 @@ struct TCP_Server_Info {
+       bool    sec_mskerberos;         /* supports legacy MS Kerberos */
+       bool    sec_kerberosu2u;        /* supports U2U Kerberos */
+       bool    sec_ntlmssp;            /* supports NTLMSSP */
++#ifdef CONFIG_CIFS_FSCACHE
++      struct fscache_cookie   *fscache; /* client index cache cookie */
++#endif
+ };
+ /*
+Index: cifs-2.6/fs/cifs/connect.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/connect.c
++++ cifs-2.6/fs/cifs/connect.c
+@@ -48,6 +48,7 @@
+ #include "nterr.h"
+ #include "rfc1002pdu.h"
+ #include "cn_cifs.h"
++#include "fscache.h"
+ #define CIFS_PORT 445
+ #define RFC1001_PORT 139
+@@ -1453,6 +1454,8 @@ cifs_put_tcp_session(struct TCP_Server_I
+               return;
+       }
++      cifs_fscache_release_client_cookie(server);
++
+       list_del_init(&server->tcp_ses_list);
+       write_unlock(&cifs_tcp_ses_lock);
+@@ -1572,6 +1575,7 @@ cifs_get_tcp_session(struct smb_vol *vol
+               goto out_err;
+       }
++      cifs_fscache_get_client_cookie(tcp_ses);
+       /* thread spawned, put it on the list */
+       write_lock(&cifs_tcp_ses_lock);
+       list_add(&tcp_ses->tcp_ses_list, &cifs_tcp_ses_list);
+Index: cifs-2.6/fs/cifs/fscache.c
+===================================================================
+--- /dev/null
++++ cifs-2.6/fs/cifs/fscache.c
+@@ -0,0 +1,47 @@
++/*
++ *   fs/cifs/fscache.c - CIFS filesystem cache interface
++ *
++ *   Copyright (c) 2010 Novell, Inc.
++ *   Authors(s): Suresh Jayaraman (sjayaraman-l3A5Bk7waGM@public.gmane.org>
++ *
++ *   This library is free software; you can redistribute it and/or modify
++ *   it under the terms of the GNU Lesser General Public License as published
++ *   by the Free Software Foundation; either version 2.1 of the License, or
++ *   (at your option) any later version.
++ *
++ *   This library 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 Lesser General Public License for more details.
++ *
++ *   You should have received a copy of the GNU Lesser General Public License
++ *   along with this library; if not, write to the Free Software
++ *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
++ */
++#include <linux/init.h>
++#include <linux/kernel.h>
++#include <linux/sched.h>
++#include <linux/mm.h>
++#include <linux/in6.h>
++
++#include "fscache.h"
++#include "cifsglob.h"
++#include "cifs_debug.h"
++
++void cifs_fscache_get_client_cookie(struct TCP_Server_Info *server)
++{
++      server->fscache =
++              fscache_acquire_cookie(cifs_fscache_netfs.primary_index,
++                              &cifs_fscache_server_index_def, server);
++      cFYI(1, "CIFS: get client cookie (0x%p/0x%p)\n",
++                              server, server->fscache);
++}
++
++void cifs_fscache_release_client_cookie(struct TCP_Server_Info *server)
++{
++      cFYI(1, "CIFS: release client cookie (0x%p/0x%p)\n",
++                              server, server->fscache);
++      fscache_relinquish_cookie(server->fscache, 0);
++      server->fscache = NULL;
++}
++
+Index: cifs-2.6/fs/cifs/fscache.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/fscache.h
++++ cifs-2.6/fs/cifs/fscache.h
+@@ -27,14 +27,26 @@
+ #ifdef CONFIG_CIFS_FSCACHE
+ extern struct fscache_netfs cifs_fscache_netfs;
++extern const struct fscache_cookie_def cifs_fscache_server_index_def;
+ extern int cifs_fscache_register(void);
+ extern void cifs_fscache_unregister(void);
++/*
++ * fscache.c
++ */
++extern void cifs_fscache_get_client_cookie(struct TCP_Server_Info *);
++extern void cifs_fscache_release_client_cookie(struct TCP_Server_Info *);
++
+ #else /* CONFIG_CIFS_FSCACHE */
+ static inline int cifs_fscache_register(void) { return 0; }
+ static inline void cifs_fscache_unregister(void) {}
++static inline void
++cifs_fscache_get_client_cookie(struct TCP_Server_Info *server) {}
++static inline void
++cifs_fscache_get_client_cookie(struct TCP_Server_Info *server); {}
++
+ #endif /* CONFIG_CIFS_FSCACHE */
+ #endif /* _CIFS_FSCACHE_H */
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001735:2, b/test/corpora/lkml/cur/1382298770.001735:2,
new file mode 100644 (file)
index 0000000..d76da35
--- /dev/null
@@ -0,0 +1,212 @@
+From: Suresh Jayaraman <sjayaraman@suse.de>
+Subject: [RFC][PATCH 07/10] cifs: FS-Cache page management
+Date: Tue, 22 Jun 2010 20:53:48 +0530
+Lines: 175
+Message-ID: <1277220228-3635-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, David Howells <dhowells@redhat.com>
+To: Steve French <smfrench@gmail.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Jun 22 17:44:27 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OR5eF-0008G7-BK
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 22 Jun 2010 17:44:27 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1754757Ab0FVPoS (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 22 Jun 2010 11:44:18 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:54214 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752542Ab0FVPoB (ORCPT
+       <rfc822;groupwise-SJayaraman@novell.com:0:0>);
+       Tue, 22 Jun 2010 11:44:01 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:23:50 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001761>
+
+Takes care of invalidation and release of FS-Cache marked pages and also
+invalidation of the FsCache page flag when the inode is removed.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+---
+ fs/cifs/cache.c   |   31 +++++++++++++++++++++++++++++++
+ fs/cifs/file.c    |   20 ++++++++++++++++++++
+ fs/cifs/fscache.c |   26 ++++++++++++++++++++++++++
+ fs/cifs/fscache.h |   16 ++++++++++++++++
+ 4 files changed, 93 insertions(+), 0 deletions(-)
+
+diff --git a/fs/cifs/cache.c b/fs/cifs/cache.c
+index b205424..3a733c1 100644
+--- a/fs/cifs/cache.c
++++ b/fs/cifs/cache.c
+@@ -210,6 +210,36 @@ fscache_checkaux cifs_fscache_inode_check_aux(void *cookie_netfs_data,
+       return FSCACHE_CHECKAUX_OKAY;
+ }
++static void cifs_fscache_inode_now_uncached(void *cookie_netfs_data)
++{
++      struct cifsInodeInfo *cifsi = cookie_netfs_data;
++      struct pagevec pvec;
++      pgoff_t first;
++      int loop, nr_pages;
++
++      pagevec_init(&pvec, 0);
++      first = 0;
++
++      cFYI(1, "cifs inode 0x%p now uncached\n", cifsi);
++
++      for (;;) {
++              nr_pages = pagevec_lookup(&pvec,
++                                        cifsi->vfs_inode.i_mapping, first,
++                                        PAGEVEC_SIZE - pagevec_count(&pvec));
++              if (!nr_pages)
++                      break;
++
++              for (loop = 0; loop < nr_pages; loop++)
++                      ClearPageFsCache(pvec.pages[loop]);
++
++              first = pvec.pages[nr_pages - 1]->index + 1;
++
++              pvec.nr = nr_pages;
++              pagevec_release(&pvec);
++              cond_resched();
++      }
++}
++
+ const struct fscache_cookie_def cifs_fscache_inode_object_def = {
+       .name           = "CIFS.uniqueid",
+       .type           = FSCACHE_COOKIE_TYPE_DATAFILE,
+@@ -217,4 +247,5 @@ const struct fscache_cookie_def cifs_fscache_inode_object_def = {
+       .get_attr       = cifs_fscache_inode_get_attr,
+       .get_aux        = cifs_fscache_inode_get_aux,
+       .check_aux      = cifs_fscache_inode_check_aux,
++      .now_uncached   = cifs_fscache_inode_now_uncached,
+ };
+diff --git a/fs/cifs/file.c b/fs/cifs/file.c
+index 55ecb55..786ec04 100644
+--- a/fs/cifs/file.c
++++ b/fs/cifs/file.c
+@@ -2271,6 +2271,22 @@ out:
+       return rc;
+ }
++static int cifs_release_page(struct page *page, gfp_t gfp)
++{
++      if (PagePrivate(page))
++              return 0;
++
++      return cifs_fscache_release_page(page, gfp);
++}
++
++static void cifs_invalidate_page(struct page *page, unsigned long offset)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(page->mapping->host);
++
++      if (offset == 0)
++              cifs_fscache_invalidate_page(page, &cifsi->vfs_inode);
++}
++
+ static void
+ cifs_oplock_break(struct slow_work *work)
+ {
+@@ -2344,6 +2360,8 @@ const struct address_space_operations cifs_addr_ops = {
+       .write_begin = cifs_write_begin,
+       .write_end = cifs_write_end,
+       .set_page_dirty = __set_page_dirty_nobuffers,
++      .releasepage = cifs_release_page,
++      .invalidatepage = cifs_invalidate_page,
+       /* .sync_page = cifs_sync_page, */
+       /* .direct_IO = */
+ };
+@@ -2360,6 +2378,8 @@ const struct address_space_operations cifs_addr_ops_smallbuf = {
+       .write_begin = cifs_write_begin,
+       .write_end = cifs_write_end,
+       .set_page_dirty = __set_page_dirty_nobuffers,
++      .releasepage = cifs_release_page,
++      .invalidatepage = cifs_invalidate_page,
+       /* .sync_page = cifs_sync_page, */
+       /* .direct_IO = */
+ };
+diff --git a/fs/cifs/fscache.c b/fs/cifs/fscache.c
+index ddfd355..c09d3b8 100644
+--- a/fs/cifs/fscache.c
++++ b/fs/cifs/fscache.c
+@@ -130,3 +130,29 @@ void cifs_fscache_reset_inode_cookie(struct inode *inode)
+       }
+ }
++int cifs_fscache_release_page(struct page *page, gfp_t gfp)
++{
++      if (PageFsCache(page)) {
++              struct inode *inode = page->mapping->host;
++              struct cifsInodeInfo *cifsi = CIFS_I(inode);
++
++              cFYI(1, "CIFS: fscache release page (0x%p/0x%p)\n",
++                              cifsi->fscache, page);
++              if (!fscache_maybe_release_page(cifsi->fscache, page, gfp))
++                      return 0;
++      }
++
++      return 1;
++}
++
++void __cifs_fscache_invalidate_page(struct page *page, struct inode *inode)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(inode);
++      struct fscache_cookie *cookie = cifsi->fscache;
++
++      cFYI(1, "CIFS: fscache invalidatepage (0x%p/0x%p/0x%p)\n",
++                      cookie, page, cifsi);
++      fscache_wait_on_page_write(cookie, page);
++      fscache_uncache_page(cookie, page);
++}
++
+diff --git a/fs/cifs/fscache.h b/fs/cifs/fscache.h
+index 836bb02..127cb0a 100644
+--- a/fs/cifs/fscache.h
++++ b/fs/cifs/fscache.h
+@@ -47,6 +47,16 @@ extern void cifs_fscache_release_inode_cookie(struct inode *);
+ extern void cifs_fscache_set_inode_cookie(struct inode *, struct file *);
+ extern void cifs_fscache_reset_inode_cookie(struct inode *);
++extern void __cifs_fscache_invalidate_page(struct page *, struct inode *);
++extern int cifs_fscache_release_page(struct page *page, gfp_t gfp);
++
++static inline void cifs_fscache_invalidate_page(struct page *page,
++                                             struct inode *inode)
++{
++      if (PageFsCache(page))
++              __cifs_fscache_invalidate_page(page, inode);
++}
++
+ #else /* CONFIG_CIFS_FSCACHE */
+ static inline int cifs_fscache_register(void) { return 0; }
+ static inline void cifs_fscache_unregister(void) {}
+@@ -63,7 +73,13 @@ static inline void cifs_fscache_release_inode_cookie(struct inode *inode) {}
+ static inline void cifs_fscache_set_inode_cookie(struct inode *inode,
+                       struct file *filp) {}
+ static inline void cifs_fscache_reset_inode_cookie(struct inode *inode) {}
++static inline void cifs_fscache_release_page(struct page *page, gfp_t gfp)
++{
++      return 1; /* May release page */
++}
++static inline int cifs_fscache_invalidate_page(struct page *page,
++                      struct inode *) {}
+ #endif /* CONFIG_CIFS_FSCACHE */
+-- 
+1.6.4.2
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001736:2, b/test/corpora/lkml/cur/1382298770.001736:2,
new file mode 100644 (file)
index 0000000..f972891
--- /dev/null
@@ -0,0 +1,256 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 09/10] cifs: read pages from FS-Cache
+Date: Tue, 22 Jun 2010 20:54:21 +0530
+Lines: 219
+Message-ID: <1277220261-3717-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:44:46 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5eX-0008O2-Q4
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:44:46 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1752563Ab0FVPom (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:44:42 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:42741 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752542Ab0FVPok (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:44:40 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:24:22 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001762>
+
+Read pages from a FS-Cache data storage object into a CIFS inode.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+---
+ fs/cifs/file.c    |   19 ++++++++++++++
+ fs/cifs/fscache.c |   73 +++++++++++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/fscache.h |   40 ++++++++++++++++++++++++++++-
+ 3 files changed, 131 insertions(+), 1 deletions(-)
+
+diff --git a/fs/cifs/file.c b/fs/cifs/file.c
+index 39c1ce0..42d2f25 100644
+--- a/fs/cifs/file.c
++++ b/fs/cifs/file.c
+@@ -1978,6 +1978,16 @@ static int cifs_readpages(struct file *file, struct address_space *mapping,
+       cifs_sb = CIFS_SB(file->f_path.dentry->d_sb);
+       pTcon = cifs_sb->tcon;
++      /*
++       * Reads as many pages as possible from fscache. Returns -ENOBUFS
++       * immediately if the cookie is negative
++       */
++      rc = cifs_readpages_from_fscache(mapping->host, mapping, page_list,
++                                       &num_pages);
++      cFYI(1, "CIFS: readpages_from_fscache returned %d\n", rc);
++      if (rc == 0)
++              goto read_complete;
++
+       cFYI(DBG2, "rpages: num pages %d", num_pages);
+       for (i = 0; i < num_pages; ) {
+               unsigned contig_pages;
+@@ -2090,6 +2100,7 @@ static int cifs_readpages(struct file *file, struct address_space *mapping,
+               smb_read_data = NULL;
+       }
++read_complete:
+       FreeXid(xid);
+       return rc;
+ }
+@@ -2100,6 +2111,12 @@ static int cifs_readpage_worker(struct file *file, struct page *page,
+       char *read_data;
+       int rc;
++      /* Is the page cached? */
++      rc = cifs_readpage_from_fscache(file->f_path.dentry->d_inode, page);
++      cFYI(1, "CIFS: cifs_readpage_from_fscache returned %d\n", rc);
++      if (rc == 0)
++              goto read_complete;
++
+       page_cache_get(page);
+       read_data = kmap(page);
+       /* for reads over a certain size could initiate async read ahead */
+@@ -2128,6 +2145,8 @@ static int cifs_readpage_worker(struct file *file, struct page *page,
+ io_error:
+       kunmap(page);
+       page_cache_release(page);
++
++read_complete:
+       return rc;
+ }
+diff --git a/fs/cifs/fscache.c b/fs/cifs/fscache.c
+index 13e47d5..6813737 100644
+--- a/fs/cifs/fscache.c
++++ b/fs/cifs/fscache.c
+@@ -145,6 +145,79 @@ int cifs_fscache_release_page(struct page *page, gfp_t gfp)
+       return 1;
+ }
++static void cifs_readpage_from_fscache_complete(struct page *page, void *ctx,
++                                              int error)
++{
++      cFYI(1, "CFS: readpage_from_fscache_complete (0x%p/%d)\n",
++                      page, error);
++      if (!error)
++              SetPageUptodate(page);
++      unlock_page(page);
++}
++
++/*
++ * Retrieve a page from FS-Cache
++ */
++int __cifs_readpage_from_fscache(struct inode *inode, struct page *page)
++{
++      int ret;
++
++      cFYI(1, "CIFS: readpage_from_fscache(fsc:%p, p:%p, i:0x%p\n",
++                      CIFS_I(inode)->fscache, page, inode);
++      ret = fscache_read_or_alloc_page(CIFS_I(inode)->fscache, page,
++                                       cifs_readpage_from_fscache_complete,
++                                       NULL,
++                                       GFP_KERNEL);
++      switch (ret) {
++
++      case 0: /* page found in fscache, read submitted */
++              cFYI(1, "CIFS: readpage_from_fscache: submitted\n");
++              return ret;
++      case -ENOBUFS:  /* page won't be cached */
++      case -ENODATA:  /* page not in cache */
++              cFYI(1, "CIFS: readpage_from_fscache %d\n", ret);
++              return 1;
++
++      default:
++              cFYI(1, "unknown error ret = %d", ret);
++      }
++      return ret;
++}
++
++/*
++ * Retrieve a set of pages from FS-Cache
++ */
++int __cifs_readpages_from_fscache(struct inode *inode,
++                              struct address_space *mapping,
++                              struct list_head *pages,
++                              unsigned *nr_pages)
++{
++      int ret;
++
++      cFYI(1, "CIFS: __cifs_readpages_from_fscache (0x%p/%u/0x%p)\n",
++                      CIFS_I(inode)->fscache, *nr_pages, inode);
++      ret = fscache_read_or_alloc_pages(CIFS_I(inode)->fscache, mapping,
++                                        pages, nr_pages,
++                                        cifs_readpage_from_fscache_complete,
++                                        NULL,
++                                        mapping_gfp_mask(mapping));
++      switch (ret) {
++      case 0: /* read submitted to the cache for all pages */
++              cFYI(1, "CIFS: readpages_from_fscache\n");
++              return ret;
++
++      case -ENOBUFS:  /* some pages are not cached and can't be */
++      case -ENODATA:  /* some pages are not cached */
++              cFYI(1, "CIFS: readpages_from_fscache: no page\n");
++              return 1;
++
++      default:
++              cFYI(1, "unknown error ret = %d", ret);
++      }
++
++      return ret;
++}
++
+ void __cifs_readpage_to_fscache(struct inode *inode, struct page *page)
+ {
+       int ret;
+diff --git a/fs/cifs/fscache.h b/fs/cifs/fscache.h
+index e34d8ab..03bd3fe 100644
+--- a/fs/cifs/fscache.h
++++ b/fs/cifs/fscache.h
+@@ -31,7 +31,6 @@ extern const struct fscache_cookie_def cifs_fscache_server_index_def;
+ extern const struct fscache_cookie_def cifs_fscache_super_index_def;
+ extern const struct fscache_cookie_def cifs_fscache_inode_object_def;
+-
+ extern int cifs_fscache_register(void);
+ extern void cifs_fscache_unregister(void);
+@@ -49,6 +48,11 @@ extern void cifs_fscache_reset_inode_cookie(struct inode *);
+ extern void __cifs_fscache_invalidate_page(struct page *, struct inode *);
+ extern int cifs_fscache_release_page(struct page *page, gfp_t gfp);
++extern int __cifs_readpage_from_fscache(struct inode *, struct page *);
++extern int __cifs_readpages_from_fscache(struct inode *,
++                                       struct address_space *,
++                                       struct list_head *,
++                                       unsigned *);
+ extern void __cifs_readpage_to_fscache(struct inode *, struct page *);
+@@ -59,6 +63,26 @@ static inline void cifs_fscache_invalidate_page(struct page *page,
+               __cifs_fscache_invalidate_page(page, inode);
+ }
++static inline int cifs_readpage_from_fscache(struct inode *inode,
++                                           struct page *page)
++{
++      if (CIFS_I(inode)->fscache)
++              return __cifs_readpage_from_fscache(inode, page);
++
++      return -ENOBUFS;
++}
++
++static inline int cifs_readpages_from_fscache(struct inode *inode,
++                                            struct address_space *mapping,
++                                            struct list_head *pages,
++                                            unsigned *nr_pages)
++{
++      if (CIFS_I(inode)->fscache)
++              return __cifs_readpages_from_fscache(inode, mapping, pages,
++                                                   nr_pages);
++      return -ENOBUFS;
++}
++
+ static inline void cifs_readpage_to_fscache(struct inode *inode,
+                                           struct page *page)
+ {
+@@ -89,6 +113,20 @@ static inline void cifs_fscache_release_page(struct page *page, gfp_t gfp)
+ static inline int cifs_fscache_invalidate_page(struct page *page,
+                       struct inode *) {}
++static inline int
++cifs_readpage_from_fscache(struct inode *inode, struct page *page)
++{
++      return -ENOBUFS;
++}
++
++static inline int cifs_readpages_from_fscache(struct inode *inode,
++                                            struct address_space *mapping,
++                                            struct list_head *pages,
++                                            unsigned *nr_pages)
++{
++      return -ENOBUFS;
++}
++
+ static inline void cifs_readpage_to_fscache(struct inode *inode,
+                       struct page *page) {}
+-- 
+1.6.4.2
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001738:2, b/test/corpora/lkml/cur/1382298770.001738:2,
new file mode 100644 (file)
index 0000000..b1e0edf
--- /dev/null
@@ -0,0 +1,139 @@
+From: Suresh Jayaraman <sjayaraman@suse.de>
+Subject: [RFC][PATCH 08/10] cifs: store pages into local cache
+Date: Tue, 22 Jun 2010 20:54:00 +0530
+Lines: 102
+Message-ID: <1277220240-3674-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, David Howells <dhowells@redhat.com>
+To: Steve French <smfrench@gmail.com>
+X-From: linux-fsdevel-owner@vger.kernel.org Tue Jun 22 17:45:09 2010
+Return-path: <linux-fsdevel-owner@vger.kernel.org>
+Envelope-to: lnx-linux-fsdevel@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-fsdevel-owner@vger.kernel.org>)
+       id 1OR5ev-00007O-6e
+       for lnx-linux-fsdevel@lo.gmane.org; Tue, 22 Jun 2010 17:45:09 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755015Ab0FVPon (ORCPT <rfc822;lnx-linux-fsdevel@m.gmane.org>);
+       Tue, 22 Jun 2010 11:44:43 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:58250 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751265Ab0FVPok (ORCPT
+       <rfc822;groupwise-SJayaraman@novell.com:0:0>);
+       Tue, 22 Jun 2010 11:44:40 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:24:02 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-fsdevel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-fsdevel.vger.kernel.org>
+X-Mailing-List: linux-fsdevel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001764>
+
+Store pages from an CIFS inode into the data storage object associated with
+that inode.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+---
+ fs/cifs/file.c    |    6 ++++++
+ fs/cifs/fscache.c |   13 +++++++++++++
+ fs/cifs/fscache.h |   11 +++++++++++
+ 3 files changed, 30 insertions(+), 0 deletions(-)
+
+diff --git a/fs/cifs/file.c b/fs/cifs/file.c
+index 786ec04..39c1ce0 100644
+--- a/fs/cifs/file.c
++++ b/fs/cifs/file.c
+@@ -2060,6 +2060,8 @@ static int cifs_readpages(struct file *file, struct address_space *mapping,
+                                  we will hit it on next read */
+                               /* break; */
++                              /* send this page to FS-Cache */
++                              cifs_readpage_to_fscache(mapping->host, page);
+                       }
+               } else {
+                       cFYI(1, "No bytes read (%d) at offset %lld . "
+@@ -2117,6 +2119,10 @@ static int cifs_readpage_worker(struct file *file, struct page *page,
+       flush_dcache_page(page);
+       SetPageUptodate(page);
++
++      /* send this page to the cache */
++      cifs_readpage_to_fscache(file->f_path.dentry->d_inode, page);
++
+       rc = 0;
+ io_error:
+diff --git a/fs/cifs/fscache.c b/fs/cifs/fscache.c
+index c09d3b8..13e47d5 100644
+--- a/fs/cifs/fscache.c
++++ b/fs/cifs/fscache.c
+@@ -145,6 +145,19 @@ int cifs_fscache_release_page(struct page *page, gfp_t gfp)
+       return 1;
+ }
++void __cifs_readpage_to_fscache(struct inode *inode, struct page *page)
++{
++      int ret;
++
++      cFYI(1, "CIFS: readpage_to_fscache(fsc: %p, p: %p, i: %p\n",
++                      CIFS_I(inode)->fscache, page, inode);
++      ret = fscache_write_page(CIFS_I(inode)->fscache, page, GFP_KERNEL);
++      cFYI(1, "CIFS: fscache_write_page returned %d\n", ret);
++
++      if (ret != 0)
++              fscache_uncache_page(CIFS_I(inode)->fscache, page);
++}
++
+ void __cifs_fscache_invalidate_page(struct page *page, struct inode *inode)
+ {
+       struct cifsInodeInfo *cifsi = CIFS_I(inode);
+diff --git a/fs/cifs/fscache.h b/fs/cifs/fscache.h
+index 127cb0a..e34d8ab 100644
+--- a/fs/cifs/fscache.h
++++ b/fs/cifs/fscache.h
+@@ -50,6 +50,8 @@ extern void cifs_fscache_reset_inode_cookie(struct inode *);
+ extern void __cifs_fscache_invalidate_page(struct page *, struct inode *);
+ extern int cifs_fscache_release_page(struct page *page, gfp_t gfp);
++extern void __cifs_readpage_to_fscache(struct inode *, struct page *);
++
+ static inline void cifs_fscache_invalidate_page(struct page *page,
+                                              struct inode *inode)
+ {
+@@ -57,6 +59,13 @@ static inline void cifs_fscache_invalidate_page(struct page *page,
+               __cifs_fscache_invalidate_page(page, inode);
+ }
++static inline void cifs_readpage_to_fscache(struct inode *inode,
++                                          struct page *page)
++{
++      if (PageFsCache(page))
++              __cifs_readpage_to_fscache(inode, page);
++}
++
+ #else /* CONFIG_CIFS_FSCACHE */
+ static inline int cifs_fscache_register(void) { return 0; }
+ static inline void cifs_fscache_unregister(void) {}
+@@ -80,6 +89,8 @@ static inline void cifs_fscache_release_page(struct page *page, gfp_t gfp)
+ static inline int cifs_fscache_invalidate_page(struct page *page,
+                       struct inode *) {}
++static inline void cifs_readpage_to_fscache(struct inode *inode,
++                      struct page *page) {}
+ #endif /* CONFIG_CIFS_FSCACHE */
+-- 
+1.6.4.2
+
+--
+To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001739:2, b/test/corpora/lkml/cur/1382298770.001739:2,
new file mode 100644 (file)
index 0000000..d0abda0
--- /dev/null
@@ -0,0 +1,355 @@
+From: Suresh Jayaraman <sjayaraman@suse.de>
+Subject: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Tue, 22 Jun 2010 20:53:33 +0530
+Lines: 318
+Message-ID: <1277220214-3597-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, David Howells <dhowells@redhat.com>
+To: Steve French <smfrench@gmail.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Jun 22 17:45:30 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OR5fF-0000Ka-Na
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 22 Jun 2010 17:45:30 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755952Ab0FVPpP (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 22 Jun 2010 11:45:15 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:59441 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751397Ab0FVPoA (ORCPT
+       <rfc822;groupwise-SJayaraman@novell.com:0:0>);
+       Tue, 22 Jun 2010 11:44:00 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:23:35 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001765>
+
+Define inode-level data storage objects (managed by cifsInodeInfo structs).
+Each inode-level object is created in a super-block level object and is itself
+a data storage object in to which pages from the inode are stored.
+
+The inode object is keyed by UniqueId. The coherency data being used is
+LastWriteTime and the file size.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+---
+ fs/cifs/cache.c    |   80 +++++++++++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/cifsfs.c   |    7 ++++
+ fs/cifs/cifsglob.h |    3 +
+ fs/cifs/file.c     |    6 +++
+ fs/cifs/fscache.c  |   68 +++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/fscache.h  |   12 +++++++
+ fs/cifs/inode.c    |    4 ++
+ 7 files changed, 180 insertions(+)
+
+Index: cifs-2.6/fs/cifs/cache.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cache.c
++++ cifs-2.6/fs/cifs/cache.c
+@@ -138,3 +138,83 @@ const struct fscache_cookie_def cifs_fsc
+       .get_key = cifs_super_get_key,
+ };
++/*
++ * Auxiliary data attached to CIFS inode within the cache
++ */
++struct cifs_fscache_inode_auxdata {
++      struct timespec last_write_time;
++      loff_t          size;
++};
++
++static uint16_t cifs_fscache_inode_get_key(const void *cookie_netfs_data,
++                                         void *buffer, uint16_t maxbuf)
++{
++      const struct cifsInodeInfo *cifsi = cookie_netfs_data;
++      uint16_t keylen;
++
++      /* use the UniqueId as the key */
++      keylen = sizeof(cifsi->uniqueid);
++      if (keylen > maxbuf)
++              keylen = 0;
++      else
++              memcpy(buffer, &cifsi->uniqueid, keylen);
++
++      return keylen;
++}
++
++static void
++cifs_fscache_inode_get_attr(const void *cookie_netfs_data, uint64_t *size)
++{
++      const struct cifsInodeInfo *cifsi = cookie_netfs_data;
++
++      *size = cifsi->vfs_inode.i_size;
++}
++
++static uint16_t
++cifs_fscache_inode_get_aux(const void *cookie_netfs_data, void *buffer,
++                         uint16_t maxbuf)
++{
++      struct cifs_fscache_inode_auxdata auxdata;
++      const struct cifsInodeInfo *cifsi = cookie_netfs_data;
++
++      memset(&auxdata, 0, sizeof(auxdata));
++      auxdata.size = cifsi->vfs_inode.i_size;
++      auxdata.last_write_time = cifsi->vfs_inode.i_ctime;
++
++      if (maxbuf > sizeof(auxdata))
++              maxbuf = sizeof(auxdata);
++
++      memcpy(buffer, &auxdata, maxbuf);
++
++      return maxbuf;
++}
++
++static enum
++fscache_checkaux cifs_fscache_inode_check_aux(void *cookie_netfs_data,
++                                            const void *data,
++                                            uint16_t datalen)
++{
++      struct cifs_fscache_inode_auxdata auxdata;
++      struct cifsInodeInfo *cifsi = cookie_netfs_data;
++
++      if (datalen != sizeof(auxdata))
++              return FSCACHE_CHECKAUX_OBSOLETE;
++
++      memset(&auxdata, 0, sizeof(auxdata));
++      auxdata.size = cifsi->vfs_inode.i_size;
++      auxdata.last_write_time = cifsi->vfs_inode.i_ctime;
++
++      if (memcmp(data, &auxdata, datalen) != 0)
++              return FSCACHE_CHECKAUX_OBSOLETE;
++
++      return FSCACHE_CHECKAUX_OKAY;
++}
++
++const struct fscache_cookie_def cifs_fscache_inode_object_def = {
++      .name           = "CIFS.uniqueid",
++      .type           = FSCACHE_COOKIE_TYPE_DATAFILE,
++      .get_key        = cifs_fscache_inode_get_key,
++      .get_attr       = cifs_fscache_inode_get_attr,
++      .get_aux        = cifs_fscache_inode_get_aux,
++      .check_aux      = cifs_fscache_inode_check_aux,
++};
+Index: cifs-2.6/fs/cifs/cifsfs.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cifsfs.c
++++ cifs-2.6/fs/cifs/cifsfs.c
+@@ -330,6 +330,12 @@ cifs_destroy_inode(struct inode *inode)
+ }
+ static void
++cifs_clear_inode(struct inode *inode)
++{
++      cifs_fscache_release_inode_cookie(inode);
++}
++
++static void
+ cifs_show_address(struct seq_file *s, struct TCP_Server_Info *server)
+ {
+       seq_printf(s, ",addr=");
+@@ -490,6 +496,7 @@ static const struct super_operations cif
+       .alloc_inode = cifs_alloc_inode,
+       .destroy_inode = cifs_destroy_inode,
+       .drop_inode     = cifs_drop_inode,
++      .clear_inode    = cifs_clear_inode,
+ /*    .delete_inode   = cifs_delete_inode,  */  /* Do not need above
+       function unless later we add lazy close of inodes or unless the
+       kernel forgets to call us with the same number of releases (closes)
+Index: cifs-2.6/fs/cifs/cifsglob.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cifsglob.h
++++ cifs-2.6/fs/cifs/cifsglob.h
+@@ -407,6 +407,9 @@ struct cifsInodeInfo {
+       bool invalid_mapping:1;         /* pagecache is invalid */
+       u64  server_eof;                /* current file size on server */
+       u64  uniqueid;                  /* server inode number */
++#ifdef CONFIG_CIFS_FSCACHE
++      struct fscache_cookie *fscache;
++#endif
+       struct inode vfs_inode;
+ };
+Index: cifs-2.6/fs/cifs/file.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/file.c
++++ cifs-2.6/fs/cifs/file.c
+@@ -40,6 +40,7 @@
+ #include "cifs_unicode.h"
+ #include "cifs_debug.h"
+ #include "cifs_fs_sb.h"
++#include "fscache.h"
+ static inline int cifs_convert_flags(unsigned int flags)
+ {
+@@ -282,6 +283,9 @@ int cifs_open(struct inode *inode, struc
+                               CIFSSMBClose(xid, tcon, netfid);
+                               rc = -ENOMEM;
+                       }
++
++                      cifs_fscache_set_inode_cookie(inode, file);
++
+                       goto out;
+               } else if ((rc == -EINVAL) || (rc == -EOPNOTSUPP)) {
+                       if (tcon->ses->serverNOS)
+@@ -373,6 +377,8 @@ int cifs_open(struct inode *inode, struc
+               goto out;
+       }
++      cifs_fscache_set_inode_cookie(inode, file);
++
+       if (oplock & CIFS_CREATE_ACTION) {
+               /* time to set mode which we can not set earlier due to
+                  problems creating new read-only files */
+Index: cifs-2.6/fs/cifs/fscache.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/fscache.c
++++ cifs-2.6/fs/cifs/fscache.c
+@@ -62,3 +62,71 @@ void cifs_fscache_release_super_cookie(s
+       tcon->fscache = NULL;
+ }
++static void cifs_fscache_enable_inode_cookie(struct inode *inode)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(inode);
++      struct cifs_sb_info *cifs_sb = CIFS_SB(inode->i_sb);
++
++      if (cifsi->fscache)
++              return;
++
++      cifsi->fscache = fscache_acquire_cookie(cifs_sb->tcon->fscache,
++                              &cifs_fscache_inode_object_def,
++                              cifsi);
++      cFYI(1, "CIFS: got FH cookie (0x%p/0x%p/0x%p)\n",
++                      cifs_sb->tcon, cifsi, cifsi->fscache);
++}
++
++void cifs_fscache_release_inode_cookie(struct inode *inode)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(inode);
++
++      if (cifsi->fscache) {
++              cFYI(1, "CIFS releasing inode cookie (0x%p/0x%p)\n",
++                              cifsi, cifsi->fscache);
++              fscache_relinquish_cookie(cifsi->fscache, 0);
++              cifsi->fscache = NULL;
++      }
++}
++
++static void cifs_fscache_disable_inode_cookie(struct inode *inode)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(inode);
++
++      if (cifsi->fscache) {
++              cFYI(1, "CIFS disabling inode cookie (0x%p/0x%p)\n",
++                              cifsi, cifsi->fscache);
++              fscache_relinquish_cookie(cifsi->fscache, 1);
++              cifsi->fscache = NULL;
++      }
++}
++
++void cifs_fscache_set_inode_cookie(struct inode *inode, struct file *filp)
++{
++      /* BB: parallel opens - need locking? */
++      if ((filp->f_flags & O_ACCMODE) != O_RDONLY)
++              cifs_fscache_disable_inode_cookie(inode);
++      else {
++              cifs_fscache_enable_inode_cookie(inode);
++              cFYI(1, "CIFS: fscache inode cookie set\n");
++      }
++}
++
++void cifs_fscache_reset_inode_cookie(struct inode *inode)
++{
++      struct cifsInodeInfo *cifsi = CIFS_I(inode);
++      struct cifs_sb_info *cifs_sb = CIFS_SB(inode->i_sb);
++      struct fscache_cookie *old = cifsi->fscache;
++
++      if (cifsi->fscache) {
++              /* retire the current fscache cache and get a new one */
++              fscache_relinquish_cookie(cifsi->fscache, 1);
++
++              cifsi->fscache = fscache_acquire_cookie(cifs_sb->tcon->fscache,
++                                      &cifs_fscache_inode_object_def,
++                                      cifsi);
++              cFYI(1, "CIFS: new cookie (0x%p/0x%p) oldcookie 0x%p\n",
++                              cifsi, cifsi->fscache, old);
++      }
++}
++
+Index: cifs-2.6/fs/cifs/fscache.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/fscache.h
++++ cifs-2.6/fs/cifs/fscache.h
+@@ -29,6 +29,8 @@
+ extern struct fscache_netfs cifs_fscache_netfs;
+ extern const struct fscache_cookie_def cifs_fscache_server_index_def;
+ extern const struct fscache_cookie_def cifs_fscache_super_index_def;
++extern const struct fscache_cookie_def cifs_fscache_inode_object_def;
++
+ extern int cifs_fscache_register(void);
+ extern void cifs_fscache_unregister(void);
+@@ -41,6 +43,10 @@ extern void cifs_fscache_release_client_
+ extern void cifs_fscache_get_super_cookie(struct cifsTconInfo *);
+ extern void cifs_fscache_release_super_cookie(struct cifsTconInfo *);
++extern void cifs_fscache_release_inode_cookie(struct inode *);
++extern void cifs_fscache_set_inode_cookie(struct inode *, struct file *);
++extern void cifs_fscache_reset_inode_cookie(struct inode *);
++
+ #else /* CONFIG_CIFS_FSCACHE */
+ static inline int cifs_fscache_register(void) { return 0; }
+ static inline void cifs_fscache_unregister(void) {}
+@@ -53,6 +59,12 @@ static inline void cifs_fscache_get_supe
+ static inline void
+ cifs_fscache_release_super_cookie(struct cifsTconInfo *tcon) {}
++static inline void cifs_fscache_release_inode_cookie(struct inode *inode) {}
++static inline void cifs_fscache_set_inode_cookie(struct inode *inode,
++                      struct file *filp) {}
++static inline void cifs_fscache_reset_inode_cookie(struct inode *inode) {}
++
++
+ #endif /* CONFIG_CIFS_FSCACHE */
+ #endif /* _CIFS_FSCACHE_H */
+Index: cifs-2.6/fs/cifs/inode.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/inode.c
++++ cifs-2.6/fs/cifs/inode.c
+@@ -29,6 +29,7 @@
+ #include "cifsproto.h"
+ #include "cifs_debug.h"
+ #include "cifs_fs_sb.h"
++#include "fscache.h"
+ static void cifs_set_ops(struct inode *inode, const bool is_dfs_referral)
+@@ -776,6 +777,8 @@ retry_iget5_locked:
+                       inode->i_flags |= S_NOATIME | S_NOCMTIME;
+               if (inode->i_state & I_NEW) {
+                       inode->i_ino = hash;
++                      /* initialize per-inode cache cookie pointer */
++                      CIFS_I(inode)->fscache = NULL;
+                       unlock_new_inode(inode);
+               }
+       }
+@@ -1568,6 +1571,7 @@ cifs_invalidate_mapping(struct inode *in
+                       cifs_i->write_behind_rc = rc;
+       }
+       invalidate_remote_inode(inode);
++      cifs_fscache_reset_inode_cookie(inode);
+ }
+ int cifs_revalidate_file(struct file *filp)
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001740:2, b/test/corpora/lkml/cur/1382298770.001740:2,
new file mode 100644 (file)
index 0000000..ef0f657
--- /dev/null
@@ -0,0 +1,214 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: [RFC][PATCH 05/10] cifs: define superblock-level cache index objects and register them
+Date: Tue, 22 Jun 2010 20:53:26 +0530
+Lines: 177
+Message-ID: <1277220206-3559-1-git-send-email-sjayaraman@suse.de>
+References: <yes>
+Cc: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 17:45:50 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OR5fZ-0000Vj-Mj
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 17:45:50 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1752511Ab0FVPpJ (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 11:45:09 -0400
+Received: from victor.provo.novell.com ([137.65.250.26]:56189 "EHLO
+       victor.provo.novell.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752441Ab0FVPoA (ORCPT
+       <rfc822;groupwise-SJayaraman-Et1tbQHTxzrQT0dZR+AlfA@public.gmane.org:0:0>);
+       Tue, 22 Jun 2010 11:44:00 -0400
+Received: from localhost (prv-ext-foundry1int.gns.novell.com [137.65.251.240])
+       by victor.provo.novell.com with ESMTP; Tue, 22 Jun 2010 09:23:29 -0600
+X-Mailer: git-send-email 1.6.4.2
+In-Reply-To: <yes>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001766>
+
+Define superblock-level cache index objects (managed by cifsTconInfo structs).
+Each superblock object is created in a server-level index object and in itself
+an index into which inode-level objects are inserted.
+
+Currently, the superblock objects are keyed by sharename.
+
+Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+---
+ fs/cifs/cache.c    |   62 +++++++++++++++++++++++++++++++++++++++++++++++++++++
+ fs/cifs/cifsglob.h |    3 ++
+ fs/cifs/connect.c  |    4 +++
+ fs/cifs/fscache.c  |   17 ++++++++++++++
+ fs/cifs/fscache.h  |    6 +++++
+ 5 files changed, 92 insertions(+)
+
+Index: cifs-2.6/fs/cifs/cache.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cache.c
++++ cifs-2.6/fs/cifs/cache.c
+@@ -76,3 +76,65 @@ const struct fscache_cookie_def cifs_fsc
+       .type = FSCACHE_COOKIE_TYPE_INDEX,
+       .get_key = cifs_server_get_key,
+ };
++
++static char *extract_sharename(const char *treename)
++{
++      const char *src;
++      char *delim, *dst;
++      int len;
++
++      /* skip double chars at the beginning */
++      src = treename + 2;
++
++      /* share name is always preceded by '\\' now */
++      delim = strchr(src, '\\');
++      if (!delim)
++              return ERR_PTR(-EINVAL);
++      delim++;
++      len = strlen(delim);
++
++      /* caller has to free the memory */
++      dst = kstrndup(delim, len, GFP_KERNEL);
++      if (!dst)
++              return ERR_PTR(-ENOMEM);
++
++      return dst;
++}
++
++/*
++ * Superblock object currently keyed by share name
++ */
++static uint16_t cifs_super_get_key(const void *cookie_netfs_data, void *buffer,
++                                 uint16_t maxbuf)
++{
++      const struct cifsTconInfo *tcon = cookie_netfs_data;
++      char *sharename;
++      uint16_t len;
++
++      sharename = extract_sharename(tcon->treeName);
++      if (IS_ERR(sharename)) {
++              cFYI(1, "CIFS: couldn't extract sharename\n");
++              sharename = NULL;
++              return 0;
++      }
++
++      len = strlen(sharename);
++      if (len > maxbuf)
++              return 0;
++
++      memcpy(buffer, sharename, len);
++
++      kfree(sharename);
++
++      return len;
++}
++
++/*
++ * Superblock object for FS-Cache
++ */
++const struct fscache_cookie_def cifs_fscache_super_index_def = {
++      .name = "CIFS.super",
++      .type = FSCACHE_COOKIE_TYPE_INDEX,
++      .get_key = cifs_super_get_key,
++};
++
+Index: cifs-2.6/fs/cifs/cifsglob.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/cifsglob.h
++++ cifs-2.6/fs/cifs/cifsglob.h
+@@ -317,6 +317,9 @@ struct cifsTconInfo {
+       bool local_lease:1; /* check leases (only) on local system not remote */
+       bool broken_posix_open; /* e.g. Samba server versions < 3.3.2, 3.2.9 */
+       bool need_reconnect:1; /* connection reset, tid now invalid */
++#ifdef CONFIG_CIFS_FSCACHE
++      struct fscache_cookie *fscache; /* cookie for share */
++#endif
+       /* BB add field for back pointer to sb struct(s)? */
+ };
+Index: cifs-2.6/fs/cifs/connect.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/connect.c
++++ cifs-2.6/fs/cifs/connect.c
+@@ -1773,6 +1773,8 @@ cifs_put_tcon(struct cifsTconInfo *tcon)
+       list_del_init(&tcon->tcon_list);
+       write_unlock(&cifs_tcp_ses_lock);
++      cifs_fscache_release_super_cookie(tcon);
++
+       xid = GetXid();
+       CIFSSMBTDis(xid, tcon);
+       _FreeXid(xid);
+@@ -1843,6 +1845,8 @@ cifs_get_tcon(struct cifsSesInfo *ses, s
+       tcon->nocase = volume_info->nocase;
+       tcon->local_lease = volume_info->local_lease;
++      cifs_fscache_get_super_cookie(tcon);
++
+       write_lock(&cifs_tcp_ses_lock);
+       list_add(&tcon->tcon_list, &ses->tcon_list);
+       write_unlock(&cifs_tcp_ses_lock);
+Index: cifs-2.6/fs/cifs/fscache.c
+===================================================================
+--- cifs-2.6.orig/fs/cifs/fscache.c
++++ cifs-2.6/fs/cifs/fscache.c
+@@ -45,3 +45,20 @@ void cifs_fscache_release_client_cookie(
+       server->fscache = NULL;
+ }
++void cifs_fscache_get_super_cookie(struct cifsTconInfo *tcon)
++{
++      tcon->fscache =
++              fscache_acquire_cookie(tcon->ses->server->fscache,
++                              &cifs_fscache_super_index_def, tcon);
++      cFYI(1, "CIFS: get superblock cookie (0x%p/0x%p)\n",
++                              tcon, tcon->fscache);
++}
++
++void cifs_fscache_release_super_cookie(struct cifsTconInfo *tcon)
++{
++      cFYI(1, "CIFS: releasing superblock cookie (0x%p/0x%p)\n",
++                      tcon, tcon->fscache);
++      fscache_relinquish_cookie(tcon->fscache, 0);
++      tcon->fscache = NULL;
++}
++
+Index: cifs-2.6/fs/cifs/fscache.h
+===================================================================
+--- cifs-2.6.orig/fs/cifs/fscache.h
++++ cifs-2.6/fs/cifs/fscache.h
+@@ -28,6 +28,7 @@
+ extern struct fscache_netfs cifs_fscache_netfs;
+ extern const struct fscache_cookie_def cifs_fscache_server_index_def;
++extern const struct fscache_cookie_def cifs_fscache_super_index_def;
+ extern int cifs_fscache_register(void);
+ extern void cifs_fscache_unregister(void);
+@@ -37,6 +38,8 @@ extern void cifs_fscache_unregister(void
+  */
+ extern void cifs_fscache_get_client_cookie(struct TCP_Server_Info *);
+ extern void cifs_fscache_release_client_cookie(struct TCP_Server_Info *);
++extern void cifs_fscache_get_super_cookie(struct cifsTconInfo *);
++extern void cifs_fscache_release_super_cookie(struct cifsTconInfo *);
+ #else /* CONFIG_CIFS_FSCACHE */
+ static inline int cifs_fscache_register(void) { return 0; }
+@@ -46,6 +49,9 @@ static inline void
+ cifs_fscache_get_client_cookie(struct TCP_Server_Info *server) {}
+ static inline void
+ cifs_fscache_get_client_cookie(struct TCP_Server_Info *server); {}
++static inline void cifs_fscache_get_super_cookie(struct cifsTconInfo *tcon) {}
++static inline void
++cifs_fscache_release_super_cookie(struct cifsTconInfo *tcon) {}
+ #endif /* CONFIG_CIFS_FSCACHE */
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001887:2, b/test/corpora/lkml/cur/1382298770.001887:2,
new file mode 100644 (file)
index 0000000..8129048
--- /dev/null
@@ -0,0 +1,85 @@
+From: Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>
+Subject: Re: [RFC][PATCH 02/10] cifs: guard cifsglob.h against multiple
+ inclusion
+Date: Tue, 22 Jun 2010 17:37:42 -0400
+Lines: 35
+Message-ID: <20100622173742.448e1e94@corrin.poochiereds.net>
+References: <yes>
+       <1277220170-3442-1-git-send-email-sjayaraman@suse.de>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 23:36:08 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORB8Z-00027v-R8
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 23:36:08 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751663Ab0FVVfq (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 17:35:46 -0400
+Received: from cdptpa-omtalb.mail.rr.com ([75.180.132.121]:46190 "EHLO
+       cdptpa-omtalb.mail.rr.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751933Ab0FVVfo (ORCPT
+       <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>); Tue, 22 Jun 2010 17:35:44 -0400
+X-Authority-Analysis: v=1.0 c=1 a=Y4kVDsoNLLAA:10 a=yQWWgrYGNuUA:10 a=kj9zAlcOel0A:10 a=hGzw-44bAAAA:8 a=6UT2YofcClCzWf3PPoQA:9 a=Ipo6nwFRv7ENfF13HvmH_iG48b8A:4 a=CjuIK1q_8ugA:10 a=0kPLrQdw3YYA:10 a=dowx1zmaLagA:10
+X-Cloudmark-Score: 0
+X-Originating-IP: 71.70.153.3
+Received: from [71.70.153.3] ([71.70.153.3:49036] helo=mail.poochiereds.net)
+       by cdptpa-oedge01.mail.rr.com (envelope-from <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>)
+       (ecelerity 2.2.2.39 r()) with ESMTP
+       id 29/22-24471-DAC212C4; Tue, 22 Jun 2010 21:35:42 +0000
+Received: from corrin.poochiereds.net (unknown [65.88.2.5])
+       by mail.poochiereds.net (Postfix) with ESMTPSA id 1C5A1580F4;
+       Tue, 22 Jun 2010 17:35:41 -0400 (EDT)
+In-Reply-To: <1277220170-3442-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Mailer: Claws Mail 3.7.6 (GTK+ 2.20.1; x86_64-redhat-linux-gnu)
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001913>
+
+On Tue, 22 Jun 2010 20:52:50 +0530
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Add conditional compile macros to guard the header file against multiple
+> inclusion.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+> ---
+>  fs/cifs/cifsglob.h |    5 +++++
+>  1 files changed, 5 insertions(+), 0 deletions(-)
+> 
+> diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h
+> index a88479c..6b2c39d 100644
+> --- a/fs/cifs/cifsglob.h
+> +++ b/fs/cifs/cifsglob.h
+> @@ -16,6 +16,9 @@
+>   *   the GNU Lesser General Public License for more details.
+>   *
+>   */
+> +#ifndef _CIFS_GLOB_H
+> +#define _CIFS_GLOB_H
+> +
+>  #include <linux/in.h>
+>  #include <linux/in6.h>
+>  #include <linux/slab.h>
+> @@ -733,3 +736,5 @@ GLOBAL_EXTERN unsigned int cifs_min_small;  /* min size of small buf pool */
+>  GLOBAL_EXTERN unsigned int cifs_max_pending; /* MAX requests at once to server*/
+>  
+>  extern const struct slow_work_ops cifs_oplock_break_ops;
+> +
+> +#endif      /* _CIFS_GLOB_H */
+
+Strong ACK
+
+Acked-by: Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>
+
+
diff --git a/test/corpora/lkml/cur/1382298770.001892:2, b/test/corpora/lkml/cur/1382298770.001892:2,
new file mode 100644 (file)
index 0000000..82603bf
--- /dev/null
@@ -0,0 +1,254 @@
+From: Jeff Layton <jlayton-vpEMnDpepFuMZCB2o+C8xQ@public.gmane.org>
+Subject: Re: [RFC][PATCH 04/10] cifs: define server-level cache index
+ objects and register them with FS-Cache
+Date: Tue, 22 Jun 2010 17:52:14 -0400
+Lines: 204
+Message-ID: <20100622175214.4c56234f@corrin.poochiereds.net>
+References: <yes>
+       <1277220198-3522-1-git-send-email-sjayaraman@suse.de>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Tue Jun 22 23:50:23 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORBMJ-0005WJ-Lj
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Tue, 22 Jun 2010 23:50:20 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1750777Ab0FVVuS (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Tue, 22 Jun 2010 17:50:18 -0400
+Received: from cdptpa-omtalb.mail.rr.com ([75.180.132.120]:55670 "EHLO
+       cdptpa-omtalb.mail.rr.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1750749Ab0FVVuR (ORCPT
+       <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>); Tue, 22 Jun 2010 17:50:17 -0400
+X-Authority-Analysis: v=1.1 cv=8MuG1bpxLlSbaYWWtODGdBCK7StbFcRsMXhWm1NVx/I= c=1 sm=0 a=wpY4Lvx3kJcA:10 a=UBIxAjGgU1YA:10 a=kj9zAlcOel0A:10 a=ld/erqUjW76FpBUqCqkKeA==:17 a=VwQbUJbxAAAA:8 a=qYub2k57AAAA:8 a=uYIlwBZcjrF9BUCsR4kA:9 a=OO1ZLbZb6q4TPdC5pcAA:7 a=jFshslHAf8hJVDYUYRlYN4n-w5YA:4 a=CjuIK1q_8ugA:10 a=x8gzFH9gYPwA:10 a=0kPLrQdw3YYA:10 a=jBoGP612-tUA:10 a=t5DF_bUGhurCx8LQ:21 a=W6P_Gh1y2IibdbqZ:21 a=ld/erqUjW76FpBUqCqkKeA==:117
+X-Cloudmark-Score: 0
+X-Originating-IP: 71.70.153.3
+Received: from [71.70.153.3] ([71.70.153.3:59154] helo=mail.poochiereds.net)
+       by cdptpa-oedge03.mail.rr.com (envelope-from <jlayton-vpEMnDpepFuMZCB2o+C8xQ@public.gmane.org>)
+       (ecelerity 2.2.2.39 r()) with ESMTP
+       id AC/10-00502-710312C4; Tue, 22 Jun 2010 21:50:16 +0000
+Received: from corrin.poochiereds.net (unknown [65.88.2.5])
+       by mail.poochiereds.net (Postfix) with ESMTPSA id 03B11580F4;
+       Tue, 22 Jun 2010 17:50:14 -0400 (EDT)
+In-Reply-To: <1277220198-3522-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Mailer: Claws Mail 3.7.6 (GTK+ 2.20.1; x86_64-redhat-linux-gnu)
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1001918>
+
+On Tue, 22 Jun 2010 20:53:18 +0530
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Define server-level cache index objects (as managed by TCP_ServerInfo structs).
+> Each server object is created in the CIFS top-level index object and is itself
+> an index into which superblock-level objects are inserted.
+> 
+> Currently, the server objects are keyed by hostname.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+> ---
+>  fs/cifs/Makefile   |    2 +-
+>  fs/cifs/cache.c    |   25 +++++++++++++++++++++++++
+>  fs/cifs/cifsglob.h |    3 +++
+>  fs/cifs/connect.c  |    4 ++++
+>  fs/cifs/fscache.c  |   47 +++++++++++++++++++++++++++++++++++++++++++++++
+>  fs/cifs/fscache.h  |   12 ++++++++++++
+>  6 files changed, 92 insertions(+), 1 deletion(-)
+>  create mode 100644 fs/cifs/fscache.c
+> 
+> Index: cifs-2.6/fs/cifs/Makefile
+> ===================================================================
+> --- cifs-2.6.orig/fs/cifs/Makefile
+> +++ cifs-2.6/fs/cifs/Makefile
+> @@ -12,4 +12,4 @@ cifs-$(CONFIG_CIFS_UPCALL) += cifs_spneg
+>  
+>  cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o
+>  
+> -cifs-$(CONFIG_CIFS_FSCACHE) += cache.o
+> +cifs-$(CONFIG_CIFS_FSCACHE) += fscache.o cache.o
+> Index: cifs-2.6/fs/cifs/cache.c
+> ===================================================================
+> --- cifs-2.6.orig/fs/cifs/cache.c
+> +++ cifs-2.6/fs/cifs/cache.c
+> @@ -51,3 +51,28 @@ void cifs_fscache_unregister(void)
+>      fscache_unregister_netfs(&cifs_fscache_netfs);
+>  }
+>  
+> +/*
+> + * Server object currently keyed by hostname
+> + */
+> +static uint16_t cifs_server_get_key(const void *cookie_netfs_data,
+> +                               void *buffer, uint16_t maxbuf)
+> +{
+> +    const struct TCP_Server_Info *server = cookie_netfs_data;
+> +    uint16_t len = strnlen(server->hostname, sizeof(server->hostname));
+> +
+
+Would a tuple of address/family/port be a better choice here? Imagine I
+mount "foo" and then later mount "foor.bar.baz". If they are the same
+address and only the UNC differs, then you won't get the benefit of
+the cache, right?
+
+> +    if (len > maxbuf)
+> +            return 0;
+> +
+> +    memcpy(buffer, server->hostname, len);
+> +
+> +    return len;
+> +}
+> +
+> +/*
+> + * Server object for FS-Cache
+> + */
+> +const struct fscache_cookie_def cifs_fscache_server_index_def = {
+> +    .name = "CIFS.server",
+> +    .type = FSCACHE_COOKIE_TYPE_INDEX,
+> +    .get_key = cifs_server_get_key,
+> +};
+> Index: cifs-2.6/fs/cifs/cifsglob.h
+> ===================================================================
+> --- cifs-2.6.orig/fs/cifs/cifsglob.h
+> +++ cifs-2.6/fs/cifs/cifsglob.h
+> @@ -193,6 +193,9 @@ struct TCP_Server_Info {
+>      bool    sec_mskerberos;         /* supports legacy MS Kerberos */
+>      bool    sec_kerberosu2u;        /* supports U2U Kerberos */
+>      bool    sec_ntlmssp;            /* supports NTLMSSP */
+> +#ifdef CONFIG_CIFS_FSCACHE
+> +    struct fscache_cookie   *fscache; /* client index cache cookie */
+> +#endif
+>  };
+>  
+>  /*
+> Index: cifs-2.6/fs/cifs/connect.c
+> ===================================================================
+> --- cifs-2.6.orig/fs/cifs/connect.c
+> +++ cifs-2.6/fs/cifs/connect.c
+> @@ -48,6 +48,7 @@
+>  #include "nterr.h"
+>  #include "rfc1002pdu.h"
+>  #include "cn_cifs.h"
+> +#include "fscache.h"
+>  
+>  #define CIFS_PORT 445
+>  #define RFC1001_PORT 139
+> @@ -1453,6 +1454,8 @@ cifs_put_tcp_session(struct TCP_Server_I
+>              return;
+>      }
+>  
+> +    cifs_fscache_release_client_cookie(server);
+> +
+>      list_del_init(&server->tcp_ses_list);
+>      write_unlock(&cifs_tcp_ses_lock);
+>  
+> @@ -1572,6 +1575,7 @@ cifs_get_tcp_session(struct smb_vol *vol
+>              goto out_err;
+>      }
+>  
+> +    cifs_fscache_get_client_cookie(tcp_ses);
+>      /* thread spawned, put it on the list */
+>      write_lock(&cifs_tcp_ses_lock);
+>      list_add(&tcp_ses->tcp_ses_list, &cifs_tcp_ses_list);
+> Index: cifs-2.6/fs/cifs/fscache.c
+> ===================================================================
+> --- /dev/null
+> +++ cifs-2.6/fs/cifs/fscache.c
+> @@ -0,0 +1,47 @@
+> +/*
+> + *   fs/cifs/fscache.c - CIFS filesystem cache interface
+> + *
+> + *   Copyright (c) 2010 Novell, Inc.
+> + *   Authors(s): Suresh Jayaraman (sjayaraman-l3A5Bk7waGM@public.gmane.org>
+> + *
+> + *   This library is free software; you can redistribute it and/or modify
+> + *   it under the terms of the GNU Lesser General Public License as published
+> + *   by the Free Software Foundation; either version 2.1 of the License, or
+> + *   (at your option) any later version.
+> + *
+> + *   This library 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 Lesser General Public License for more details.
+> + *
+> + *   You should have received a copy of the GNU Lesser General Public License
+> + *   along with this library; if not, write to the Free Software
+> + *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+> + */
+> +#include <linux/init.h>
+> +#include <linux/kernel.h>
+> +#include <linux/sched.h>
+> +#include <linux/mm.h>
+> +#include <linux/in6.h>
+> +
+> +#include "fscache.h"
+> +#include "cifsglob.h"
+> +#include "cifs_debug.h"
+> +
+> +void cifs_fscache_get_client_cookie(struct TCP_Server_Info *server)
+> +{
+> +    server->fscache =
+> +            fscache_acquire_cookie(cifs_fscache_netfs.primary_index,
+> +                            &cifs_fscache_server_index_def, server);
+> +    cFYI(1, "CIFS: get client cookie (0x%p/0x%p)\n",
+> +                            server, server->fscache);
+> +}
+> +
+> +void cifs_fscache_release_client_cookie(struct TCP_Server_Info *server)
+> +{
+> +    cFYI(1, "CIFS: release client cookie (0x%p/0x%p)\n",
+> +                            server, server->fscache);
+> +    fscache_relinquish_cookie(server->fscache, 0);
+> +    server->fscache = NULL;
+> +}
+> +
+> Index: cifs-2.6/fs/cifs/fscache.h
+> ===================================================================
+> --- cifs-2.6.orig/fs/cifs/fscache.h
+> +++ cifs-2.6/fs/cifs/fscache.h
+> @@ -27,14 +27,26 @@
+>  #ifdef CONFIG_CIFS_FSCACHE
+>  
+>  extern struct fscache_netfs cifs_fscache_netfs;
+> +extern const struct fscache_cookie_def cifs_fscache_server_index_def;
+>  
+>  extern int cifs_fscache_register(void);
+>  extern void cifs_fscache_unregister(void);
+>  
+> +/*
+> + * fscache.c
+> + */
+> +extern void cifs_fscache_get_client_cookie(struct TCP_Server_Info *);
+> +extern void cifs_fscache_release_client_cookie(struct TCP_Server_Info *);
+> +
+>  #else /* CONFIG_CIFS_FSCACHE */
+>  static inline int cifs_fscache_register(void) { return 0; }
+>  static inline void cifs_fscache_unregister(void) {}
+>  
+> +static inline void
+> +cifs_fscache_get_client_cookie(struct TCP_Server_Info *server) {}
+> +static inline void
+> +cifs_fscache_get_client_cookie(struct TCP_Server_Info *server); {}
+> +
+>  #endif /* CONFIG_CIFS_FSCACHE */
+>  
+>  #endif /* _CIFS_FSCACHE_H */
+> --
+> To unsubscribe from this list: send the line "unsubscribe linux-cifs" in
+> the body of a message to majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+> More majordomo info at  http://vger.kernel.org/majordomo-info.html
+> 
+
+
+-- 
+Jeff Layton <jlayton-vpEMnDpepFuMZCB2o+C8xQ@public.gmane.org>
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002189:2, b/test/corpora/lkml/cur/1382298770.002189:2,
new file mode 100644 (file)
index 0000000..3cfc62e
--- /dev/null
@@ -0,0 +1,66 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 03/10] cifs: register CIFS for caching
+Date: Wed, 23 Jun 2010 17:51:17 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 20
+Message-ID: <9603.1277311877@redhat.com>
+References: <1277220189-3485-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 18:51:32 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORTAg-0008Bt-CT
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 18:51:30 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751915Ab0FWQv3 (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 12:51:29 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:50923 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751520Ab0FWQv3 (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 12:51:29 -0400
+Received: from int-mx05.intmail.prod.int.phx2.redhat.com (int-mx05.intmail.prod.int.phx2.redhat.com [10.5.11.18])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NGpLFc028550
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 12:51:21 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx05.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NGpHIG010890;
+       Wed, 23 Jun 2010 12:51:18 -0400
+In-Reply-To: <1277220189-3485-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.18
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002219>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> +    rc = cifs_fscache_register();
+> +    if (rc)
+> +            goto out;
+> +
+>      rc = cifs_init_inodecache();
+>      if (rc)
+>              goto out_clean_proc;
+> @@ -949,8 +954,10 @@ init_cifs(void)
+>      cifs_destroy_mids();
+>   out_destroy_inodecache:
+>      cifs_destroy_inodecache();
+> +    cifs_fscache_unregister();
+>   out_clean_proc:
+
+This is incorrect.  You need to call cifs_fscache_unregister() if
+cifs_init_inodecache() fails.
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002191:2, b/test/corpora/lkml/cur/1382298770.002191:2,
new file mode 100644 (file)
index 0000000..56752a9
--- /dev/null
@@ -0,0 +1,65 @@
+From: David Howells <dhowells@redhat.com>
+Subject: Re: [RFC][PATCH 04/10] cifs: define server-level cache index objects and register them with FS-Cache
+Date: Wed, 23 Jun 2010 17:54:52 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 19
+Message-ID: <9658.1277312092@redhat.com>
+References: <1277220198-3522-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells@redhat.com, Steve French <smfrench@gmail.com>,
+       linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Suresh Jayaraman <sjayaraman@suse.de>
+X-From: linux-fsdevel-owner@vger.kernel.org Wed Jun 23 18:55:07 2010
+Return-path: <linux-fsdevel-owner@vger.kernel.org>
+Envelope-to: lnx-linux-fsdevel@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-fsdevel-owner@vger.kernel.org>)
+       id 1ORTE8-0002ll-VF
+       for lnx-linux-fsdevel@lo.gmane.org; Wed, 23 Jun 2010 18:55:05 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752263Ab0FWQzD (ORCPT <rfc822;lnx-linux-fsdevel@m.gmane.org>);
+       Wed, 23 Jun 2010 12:55:03 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:18394 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751794Ab0FWQzB (ORCPT <rfc822;linux-fsdevel@vger.kernel.org>);
+       Wed, 23 Jun 2010 12:55:01 -0400
+Received: from int-mx04.intmail.prod.int.phx2.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.17])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NGsu1L000993
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 12:54:56 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx04.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NGsrUG016433;
+       Wed, 23 Jun 2010 12:54:54 -0400
+In-Reply-To: <1277220198-3522-1-git-send-email-sjayaraman@suse.de>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.17
+Sender: linux-fsdevel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-fsdevel.vger.kernel.org>
+X-Mailing-List: linux-fsdevel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002221>
+
+Suresh Jayaraman <sjayaraman@suse.de> wrote:
+
+> Define server-level cache index objects (as managed by TCP_ServerInfo
+> structs).  Each server object is created in the CIFS top-level index object
+> and is itself an index into which superblock-level objects are inserted.
+> 
+> Currently, the server objects are keyed by hostname.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+
+Looks reasonable, apart from the index key.  I agree with Jeff that you
+probably want {address,port,family} rather than a hostname.
+
+David
+--
+To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002193:2, b/test/corpora/lkml/cur/1382298770.002193:2,
new file mode 100644 (file)
index 0000000..e2ea626
--- /dev/null
@@ -0,0 +1,59 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 05/10] cifs: define superblock-level cache index objects and register them
+Date: Wed, 23 Jun 2010 17:58:10 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 13
+Message-ID: <9720.1277312290@redhat.com>
+References: <1277220206-3559-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 18:58:19 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORTHG-0003Az-Ge
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 18:58:18 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751520Ab0FWQ6R (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 12:58:17 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:62343 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751372Ab0FWQ6R (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 12:58:17 -0400
+Received: from int-mx01.intmail.prod.int.phx2.redhat.com (int-mx01.intmail.prod.int.phx2.redhat.com [10.5.11.11])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NGwDC2031683
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 12:58:13 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx01.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NGwAfq021298;
+       Wed, 23 Jun 2010 12:58:11 -0400
+In-Reply-To: <1277220206-3559-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.11
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002223>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Define superblock-level cache index objects (managed by cifsTconInfo
+> structs).  Each superblock object is created in a server-level index object
+> and in itself an index into which inode-level objects are inserted.
+> 
+> Currently, the superblock objects are keyed by sharename.
+
+Seems reasonable.  Is there any way you can check that the share you are
+looking at on a server is the same as the last time you looked?  Can you
+validate the root directory of the share in some way?
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002194:2, b/test/corpora/lkml/cur/1382298770.002194:2,
new file mode 100644 (file)
index 0000000..d2d1efd
--- /dev/null
@@ -0,0 +1,61 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Wed, 23 Jun 2010 18:02:53 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 15
+Message-ID: <9822.1277312573@redhat.com>
+References: <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 19:03:04 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORTLr-0007Bh-Cs
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 19:03:03 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1752063Ab0FWRDB (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 13:03:01 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:30823 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751804Ab0FWRDA (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 13:03:00 -0400
+Received: from int-mx03.intmail.prod.int.phx2.redhat.com (int-mx03.intmail.prod.int.phx2.redhat.com [10.5.11.16])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH2v0J030982
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 13:02:57 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx03.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH2r9N014323;
+       Wed, 23 Jun 2010 13:02:54 -0400
+In-Reply-To: <1277220214-3597-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.16
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002224>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Define inode-level data storage objects (managed by cifsInodeInfo structs).
+> Each inode-level object is created in a super-block level object and is
+> itself a data storage object in to which pages from the inode are stored.
+> 
+> The inode object is keyed by UniqueId. The coherency data being used is
+> LastWriteTime and the file size.
+
+Isn't there a file creation time too?
+
+I take it you don't support caching on files that are open for writing at this
+time?
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002195:2, b/test/corpora/lkml/cur/1382298770.002195:2,
new file mode 100644 (file)
index 0000000..ec54a81
--- /dev/null
@@ -0,0 +1,59 @@
+From: David Howells <dhowells@redhat.com>
+Subject: Re: [RFC][PATCH 07/10] cifs: FS-Cache page management
+Date: Wed, 23 Jun 2010 18:05:01 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 13
+Message-ID: <9866.1277312701@redhat.com>
+References: <1277220228-3635-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells@redhat.com, Steve French <smfrench@gmail.com>,
+       linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Suresh Jayaraman <sjayaraman@suse.de>
+X-From: linux-fsdevel-owner@vger.kernel.org Wed Jun 23 19:05:19 2010
+Return-path: <linux-fsdevel-owner@vger.kernel.org>
+Envelope-to: lnx-linux-fsdevel@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-fsdevel-owner@vger.kernel.org>)
+       id 1ORTNz-0008Oj-Ho
+       for lnx-linux-fsdevel@lo.gmane.org; Wed, 23 Jun 2010 19:05:15 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752145Ab0FWRFO (ORCPT <rfc822;lnx-linux-fsdevel@m.gmane.org>);
+       Wed, 23 Jun 2010 13:05:14 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:1689 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751804Ab0FWRFN (ORCPT <rfc822;linux-fsdevel@vger.kernel.org>);
+       Wed, 23 Jun 2010 13:05:13 -0400
+Received: from int-mx02.intmail.prod.int.phx2.redhat.com (int-mx02.intmail.prod.int.phx2.redhat.com [10.5.11.12])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH59sl011966
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 13:05:09 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx02.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH52Jl022163;
+       Wed, 23 Jun 2010 13:05:03 -0400
+In-Reply-To: <1277220228-3635-1-git-send-email-sjayaraman@suse.de>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.12
+Sender: linux-fsdevel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-fsdevel.vger.kernel.org>
+X-Mailing-List: linux-fsdevel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002225>
+
+Suresh Jayaraman <sjayaraman@suse.de> wrote:
+
+> Takes care of invalidation and release of FS-Cache marked pages and also
+> invalidation of the FsCache page flag when the inode is removed.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+
+Acked-by: David Howells <dhowells@redhat.com>
+--
+To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002196:2, b/test/corpora/lkml/cur/1382298770.002196:2,
new file mode 100644 (file)
index 0000000..63838dc
--- /dev/null
@@ -0,0 +1,54 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 08/10] cifs: store pages into local cache
+Date: Wed, 23 Jun 2010 18:06:12 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 8
+Message-ID: <9890.1277312772@redhat.com>
+References: <1277220240-3674-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 19:06:21 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORTP3-0000fp-01
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 19:06:21 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1752403Ab0FWRGU (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 13:06:20 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:63621 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751804Ab0FWRGT (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 13:06:19 -0400
+Received: from int-mx08.intmail.prod.int.phx2.redhat.com (int-mx08.intmail.prod.int.phx2.redhat.com [10.5.11.21])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH6FCB012081
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 13:06:15 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx08.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH6CKG013414;
+       Wed, 23 Jun 2010 13:06:13 -0400
+In-Reply-To: <1277220240-3674-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.21
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002226>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Store pages from an CIFS inode into the data storage object associated with
+> that inode.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+
+Acked-by: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002197:2, b/test/corpora/lkml/cur/1382298770.002197:2,
new file mode 100644 (file)
index 0000000..765c399
--- /dev/null
@@ -0,0 +1,53 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 09/10] cifs: read pages from FS-Cache
+Date: Wed, 23 Jun 2010 18:07:40 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 7
+Message-ID: <9918.1277312860@redhat.com>
+References: <1277220261-3717-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Wed Jun 23 19:07:51 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1ORTQR-0000nv-JF
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Wed, 23 Jun 2010 19:07:47 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751708Ab0FWRHr (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Wed, 23 Jun 2010 13:07:47 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:34413 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1750954Ab0FWRHq (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Wed, 23 Jun 2010 13:07:46 -0400
+Received: from int-mx04.intmail.prod.int.phx2.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.17])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH7h3Y005904
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 13:07:43 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx04.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH7efR020683;
+       Wed, 23 Jun 2010 13:07:41 -0400
+In-Reply-To: <1277220261-3717-1-git-send-email-sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.17
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002227>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> Read pages from a FS-Cache data storage object into a CIFS inode.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+
+Acked-by: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002201:2, b/test/corpora/lkml/cur/1382298770.002201:2,
new file mode 100644 (file)
index 0000000..bae1eef
--- /dev/null
@@ -0,0 +1,58 @@
+From: David Howells <dhowells@redhat.com>
+Subject: Re: [RFC][PATCH 10/10] cifs: add mount option to enable local caching
+Date: Wed, 23 Jun 2010 18:08:34 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 12
+Message-ID: <9942.1277312914@redhat.com>
+References: <1277220309-3757-1-git-send-email-sjayaraman@suse.de> <yes>
+Cc: dhowells@redhat.com, Steve French <smfrench@gmail.com>,
+       linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Suresh Jayaraman <sjayaraman@suse.de>
+X-From: linux-kernel-owner@vger.kernel.org Wed Jun 23 19:09:22 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1ORTRv-0002J8-2s
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 23 Jun 2010 19:09:19 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753275Ab0FWRIt (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Wed, 23 Jun 2010 13:08:49 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:6156 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1753203Ab0FWRIr (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Wed, 23 Jun 2010 13:08:47 -0400
+Received: from int-mx04.intmail.prod.int.phx2.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.17])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH8dax006028
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 23 Jun 2010 13:08:39 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx04.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5NH8YmA020846;
+       Wed, 23 Jun 2010 13:08:36 -0400
+In-Reply-To: <1277220309-3757-1-git-send-email-sjayaraman@suse.de>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.17
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002231>
+
+Suresh Jayaraman <sjayaraman@suse.de> wrote:
+
+> Add a mount option 'fsc' to enable local caching on CIFS.
+> 
+> As the cifs-utils (userspace) changes are not done yet, this patch enables
+> 'fsc' by default to assist testing.
+> 
+> Signed-off-by: Suresh Jayaraman <sjayaraman@suse.de>
+
+Acked-by: David Howells <dhowells@redhat.com>
+
+(Give or take the debugging bit)
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002878:2, b/test/corpora/lkml/cur/1382298770.002878:2,
new file mode 100644 (file)
index 0000000..66a3e22
--- /dev/null
@@ -0,0 +1,90 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: Re: [RFC][PATCH 10/10] cifs: add mount option to enable local caching
+Date: Fri, 25 Jun 2010 16:18:12 +0530
+Lines: 47
+Message-ID: <4C24896C.4000903@suse.de>
+References: <yes> <1277220309-3757-1-git-send-email-sjayaraman@suse.de> <4C225338.9010807@gmail.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+To: Scott Lovenberg <scott.lovenberg-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 12:48:27 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OS6SO-0003QF-NW
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 12:48:25 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1753965Ab0FYKsX (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 06:48:23 -0400
+Received: from cantor.suse.de ([195.135.220.2]:46395 "EHLO mx1.suse.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1752612Ab0FYKsW (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 06:48:22 -0400
+Received: from relay2.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by mx1.suse.de (Postfix) with ESMTP id 60CED6CB00;
+       Fri, 25 Jun 2010 12:48:21 +0200 (CEST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091130 SUSE/3.0.0-1.1.1 Thunderbird/3.0
+In-Reply-To: <4C225338.9010807-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002912>
+
+On 06/24/2010 12:02 AM, Scott Lovenberg wrote:
+> On 6/22/2010 11:25 AM, Suresh Jayaraman wrote:
+>> Add a mount option 'fsc' to enable local caching on CIFS.
+>>
+>> As the cifs-utils (userspace) changes are not done yet, this patch
+>> enables
+>> 'fsc' by default to assist testing.
+>>    
+> [...]
+>> @@ -1332,6 +1336,8 @@ cifs_parse_mount_options(char *options, const
+>> char *devname,
+>>               printk(KERN_WARNING "CIFS: Mount option noac not "
+>>                   "supported. Instead set "
+>>                   "/proc/fs/cifs/LookupCacheEnabled to 0\n");
+>> +        } else if (strnicmp(data, "fsc", 3) == 0) {
+>> +            vol->fsc = true;
+>>           } else
+>>               printk(KERN_WARNING "CIFS: Unknown mount option %s\n",
+>>                           data);
+>> @@ -2405,6 +2411,8 @@ static void setup_cifs_sb(struct smb_vol
+>> *pvolume_info,
+>>           cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_OVERR_GID;
+>>       if (pvolume_info->dynperm)
+>>           cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_DYNPERM;
+>> +    if (pvolume_info->fsc)
+>> +        cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_FSCACHE;
+>>       if (pvolume_info->direct_io) {
+>>           cFYI(1, "mounting share using direct i/o");
+>>           cifs_sb->mnt_cifs_flags |= CIFS_MOUNT_DIRECT_IO;
+>>    
+> I reworked the CIFS mount option parsing a while back; I'm not sure
+> whether that patch was going to be in the 2.6.35 tree or not (the window
+> just opened, didn't it?).
+
+Not a problem, I could redo this patch alone when the reworked option
+parsing patches get in.
+
+> Jeff, Steve, can you confirm if that patch is going to be in 2.6.35?
+> 
+> Patch refs: http://patchwork.ozlabs.org/patch/53059/  and
+> http://patchwork.ozlabs.org/patch/53674/
+> 
+
+Thanks,
+
+-- 
+Suresh Jayaraman
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002879:2, b/test/corpora/lkml/cur/1382298770.002879:2,
new file mode 100644 (file)
index 0000000..5782037
--- /dev/null
@@ -0,0 +1,68 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: Re: [RFC][PATCH 03/10] cifs: register CIFS for caching
+Date: Fri, 25 Jun 2010 16:26:22 +0530
+Lines: 26
+Message-ID: <4C248B56.8030207@suse.de>
+References: <1277220189-3485-1-git-send-email-sjayaraman@suse.de> <yes> <9603.1277311877@redhat.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 12:56:32 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OS6aG-00066f-1L
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 12:56:32 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1754188Ab0FYK4b (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 06:56:31 -0400
+Received: from cantor.suse.de ([195.135.220.2]:46564 "EHLO mx1.suse.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1753651Ab0FYK4a (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 06:56:30 -0400
+Received: from relay2.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by mx1.suse.de (Postfix) with ESMTP id 17F1E6CB00;
+       Fri, 25 Jun 2010 12:56:30 +0200 (CEST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091130 SUSE/3.0.0-1.1.1 Thunderbird/3.0
+In-Reply-To: <9603.1277311877-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002913>
+
+On 06/23/2010 10:21 PM, David Howells wrote:
+> Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+> 
+>> +   rc = cifs_fscache_register();
+>> +   if (rc)
+>> +           goto out;
+>> +
+>>     rc = cifs_init_inodecache();
+>>     if (rc)
+>>             goto out_clean_proc;
+>> @@ -949,8 +954,10 @@ init_cifs(void)
+>>     cifs_destroy_mids();
+>>   out_destroy_inodecache:
+>>     cifs_destroy_inodecache();
+>> +   cifs_fscache_unregister();
+>>   out_clean_proc:
+> 
+> This is incorrect.  You need to call cifs_fscache_unregister() if
+> cifs_init_inodecache() fails.
+> 
+
+Doh! I'll fix it.
+
+
+-- 
+Suresh Jayaraman
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002911:2, b/test/corpora/lkml/cur/1382298770.002911:2,
new file mode 100644 (file)
index 0000000..8e172cb
--- /dev/null
@@ -0,0 +1,84 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: Re: [RFC][PATCH 05/10] cifs: define superblock-level cache index
+ objects and register them
+Date: Fri, 25 Jun 2010 18:14:16 +0530
+Lines: 41
+Message-ID: <4C24A4A0.90408@suse.de>
+References: <1277220206-3559-1-git-send-email-sjayaraman@suse.de> <yes> <9720.1277312290@redhat.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 14:44:28 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OS8Gh-0005Bb-E2
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 14:44:27 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1754703Ab0FYMo0 (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 08:44:26 -0400
+Received: from cantor.suse.de ([195.135.220.2]:51036 "EHLO mx1.suse.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1754222Ab0FYMoZ (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 08:44:25 -0400
+Received: from relay1.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by mx1.suse.de (Postfix) with ESMTP id E07FF8FEA2;
+       Fri, 25 Jun 2010 14:44:24 +0200 (CEST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091130 SUSE/3.0.0-1.1.1 Thunderbird/3.0
+In-Reply-To: <9720.1277312290-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002945>
+
+On 06/23/2010 10:28 PM, David Howells wrote:
+> Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+> 
+>> Define superblock-level cache index objects (managed by cifsTconInfo
+>> structs).  Each superblock object is created in a server-level index object
+>> and in itself an index into which inode-level objects are inserted.
+>>
+>> Currently, the superblock objects are keyed by sharename.
+> 
+> Seems reasonable.  Is there any way you can check that the share you are
+> looking at on a server is the same as the last time you looked?  Can you
+
+Good point.
+
+I thought of using TID (Tree identifier; a unique ID for a resource in
+use by client) along with sharename. But, Server is free to reuse them
+when the tree connection closes and does not guarantee the same Tid for
+a particular resource across tree connections.
+
+Also, considering the UNC name of the resource (//server/share) may not
+be a good idea too as the cache will not be used when for e.g. IPaddress
+is used to mount.
+
+So, if a server does something like this:
+   - export a share 'foo' (original server path: /export/vol1/foo)
+   - client mounts and uses it
+   - server unexports the share 'foo'
+   - server exports 'foo' (original sever path: /export/vol2/foo)
+
+we have a bit of problem..
+
+> validate the root directory of the share in some way?
+> 
+
+I don't know if there is a way to do this.
+
+Thanks,
+
+
+-- 
+Suresh Jayaraman
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002912:2, b/test/corpora/lkml/cur/1382298770.002912:2,
new file mode 100644 (file)
index 0000000..d9c761d
--- /dev/null
@@ -0,0 +1,65 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+ register them
+Date: Fri, 25 Jun 2010 18:20:14 +0530
+Lines: 24
+Message-ID: <4C24A606.5040001@suse.de>
+References: <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes> <9822.1277312573@redhat.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 14:50:26 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OS8MR-0007EU-OS
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 14:50:24 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1754607Ab0FYMuX (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 08:50:23 -0400
+Received: from cantor2.suse.de ([195.135.220.15]:38716 "EHLO mx2.suse.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1753675Ab0FYMuW (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 08:50:22 -0400
+Received: from relay2.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       by mx2.suse.de (Postfix) with ESMTP id B05E686A2E;
+       Fri, 25 Jun 2010 14:50:21 +0200 (CEST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091130 SUSE/3.0.0-1.1.1 Thunderbird/3.0
+In-Reply-To: <9822.1277312573-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002946>
+
+On 06/23/2010 10:32 PM, David Howells wrote:
+> Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+> 
+>> Define inode-level data storage objects (managed by cifsInodeInfo structs).
+>> Each inode-level object is created in a super-block level object and is
+>> itself a data storage object in to which pages from the inode are stored.
+>>
+>> The inode object is keyed by UniqueId. The coherency data being used is
+>> LastWriteTime and the file size.
+> 
+> Isn't there a file creation time too?
+
+I think the creation time is currently being ignored as we won't be able
+to accomodate in POSIX stat struct.
+
+> I take it you don't support caching on files that are open for writing at this
+> time?
+> 
+
+Yes.
+
+
+-- 
+Suresh Jayaraman
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002915:2, b/test/corpora/lkml/cur/1382298770.002915:2,
new file mode 100644 (file)
index 0000000..e43c909
--- /dev/null
@@ -0,0 +1,58 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Fri, 25 Jun 2010 13:55:49 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 12
+Message-ID: <22697.1277470549@redhat.com>
+References: <4C24A606.5040001@suse.de> <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes> <9822.1277312573@redhat.com>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 14:56:04 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OS8Rw-0002tq-3k
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 14:56:04 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1753622Ab0FYM4B (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 08:56:01 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:50162 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1752535Ab0FYM4B (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 08:56:01 -0400
+Received: from int-mx01.intmail.prod.int.phx2.redhat.com (int-mx01.intmail.prod.int.phx2.redhat.com [10.5.11.11])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5PCtqOd018091
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Fri, 25 Jun 2010 08:55:52 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx01.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5PCtn4G016466;
+       Fri, 25 Jun 2010 08:55:51 -0400
+In-Reply-To: <4C24A606.5040001-l3A5Bk7waGM@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.11
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002949>
+
+Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+
+> I think the creation time is currently being ignored as we won't be able
+> to accomodate in POSIX stat struct.
+
+The FS-Cache interface doesn't use the POSIX stat struct, but it could be
+really useful to save it and use it for cache coherency inside the kernel.
+
+Out of interest, what does Samba do when it comes to generating a creation time
+for UNIX where one does not exist?
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002917:2, b/test/corpora/lkml/cur/1382298770.002917:2,
new file mode 100644 (file)
index 0000000..f7047f8
--- /dev/null
@@ -0,0 +1,67 @@
+From: David Howells <dhowells@redhat.com>
+Subject: Re: [RFC][PATCH 05/10] cifs: define superblock-level cache index objects and register them
+Date: Fri, 25 Jun 2010 13:58:33 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 21
+Message-ID: <22746.1277470713@redhat.com>
+References: <4C24A4A0.90408@suse.de> <1277220206-3559-1-git-send-email-sjayaraman@suse.de> <yes> <9720.1277312290@redhat.com>
+Cc: dhowells@redhat.com, Steve French <smfrench@gmail.com>,
+       linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Suresh Jayaraman <sjayaraman@suse.de>
+X-From: linux-fsdevel-owner@vger.kernel.org Fri Jun 25 15:02:20 2010
+Return-path: <linux-fsdevel-owner@vger.kernel.org>
+Envelope-to: lnx-linux-fsdevel@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with smtp (Exim 4.69)
+       (envelope-from <linux-fsdevel-owner@vger.kernel.org>)
+       id 1OS8Xz-000628-FG
+       for lnx-linux-fsdevel@lo.gmane.org; Fri, 25 Jun 2010 15:02:19 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755357Ab0FYM6k (ORCPT <rfc822;lnx-linux-fsdevel@m.gmane.org>);
+       Fri, 25 Jun 2010 08:58:40 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:50417 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1754086Ab0FYM6j (ORCPT <rfc822;linux-fsdevel@vger.kernel.org>);
+       Fri, 25 Jun 2010 08:58:39 -0400
+Received: from int-mx04.intmail.prod.int.phx2.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.17])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5PCwa7Z005113
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Fri, 25 Jun 2010 08:58:36 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx04.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5PCwXVB011094;
+       Fri, 25 Jun 2010 08:58:34 -0400
+In-Reply-To: <4C24A4A0.90408@suse.de>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.17
+Sender: linux-fsdevel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-fsdevel.vger.kernel.org>
+X-Mailing-List: linux-fsdevel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002951>
+
+Suresh Jayaraman <sjayaraman@suse.de> wrote:
+
+> Also, considering the UNC name of the resource (//server/share) may not
+> be a good idea too as the cache will not be used when for e.g. IPaddress
+> is used to mount.
+
+You could convert the UNC name to an IP address, and just use that as your
+key.
+
+> > validate the root directory of the share in some way?
+>
+> I don't know if there is a way to do this.
+
+Is there an inode number or something?  Even the creation time might do.
+
+David
+--
+To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002930:2, b/test/corpora/lkml/cur/1382298770.002930:2,
new file mode 100644 (file)
index 0000000..2041016
--- /dev/null
@@ -0,0 +1,81 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 05/10] cifs: define superblock-level cache index objects and register them
+Date: Fri, 25 Jun 2010 14:26:52 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 35
+Message-ID: <23204.1277472412@redhat.com>
+References: <22746.1277470713@redhat.com> <4C24A4A0.90408@suse.de> <1277220206-3559-1-git-send-email-sjayaraman@suse.de> <yes> <9720.1277312290@redhat.com>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>,
+       Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: unlisted-recipients:; (no To-header on input)
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 15:27:01 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OS8vt-0000Xv-FL
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 15:27:01 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1755944Ab0FYN1A (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 09:27:00 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:15634 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1755398Ab0FYN07 (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 09:26:59 -0400
+Received: from int-mx04.intmail.prod.int.phx2.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.17])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5PDQu1D020638
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Fri, 25 Jun 2010 09:26:56 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx04.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5PDQruU018472;
+       Fri, 25 Jun 2010 09:26:54 -0400
+In-Reply-To: <22746.1277470713-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.17
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1002964>
+
+David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org> wrote:
+
+> > > validate the root directory of the share in some way?
+> >
+> > I don't know if there is a way to do this.
+> 
+> Is there an inode number or something?  Even the creation time might do.
+
+Looking in cifspdu.h, there are a number of things that it might be possible
+to use.
+
+ (1) FILE_ALL_INFO: CreationTime, IndexNumber, IndexNumber1, FileName
+     (assuming this isn't flattened to '\' or something for the root of a
+     share.
+
+ (2) FILE_UNIX_BASIC_INFO: DevMajor, DevMinor, UniqueId.
+
+ (3) FILE_INFO_STANDARD: CreationDate, CreationTime.
+
+ (4) FILE_INFO_BASIC: CreationTime.
+
+ (5) FILE_DIRECTORY_INFO: FileIndex, CreationTime, FileName.
+
+ (6) SEARCH_ID_FULL_DIR_INFO: FileIndex, CreationTime, UniqueId, FileName.
+
+ (7) FILE_BOTH_DIRECTORY_INFO: FileIndex, CreationTime, ShortName, FileName.
+
+ (8) OPEN_RSP_EXT: Fid, CreationTime, VolumeGUID, FileId.
+
+You may have to choose different sets of things, depending on what the server
+has on offer.  Also, don't forget, if you can't work out whether a share is
+coherent or not from the above, you can always use LastWriteTime, ChangeTime
+and EndOfFile and just discard the whole subtree if they differ.
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298770.002997:2, b/test/corpora/lkml/cur/1382298770.002997:2,
new file mode 100644 (file)
index 0000000..b78073c
--- /dev/null
@@ -0,0 +1,90 @@
+From: Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+ register them
+Date: Fri, 25 Jun 2010 12:53:06 -0400
+Lines: 36
+Message-ID: <20100625125306.7f9b1966@tlielax.poochiereds.net>
+References: <4C24A606.5040001@suse.de>
+       <1277220214-3597-1-git-send-email-sjayaraman@suse.de>
+       <yes>
+       <9822.1277312573@redhat.com>
+       <22697.1277470549@redhat.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>,
+       Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       samba-technical-w/Ol4Ecudpl8XjKLYN78aQ@public.gmane.org
+To: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Fri Jun 25 18:53:12 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OSC9P-0005Eb-SU
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Fri, 25 Jun 2010 18:53:12 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S932199Ab0FYQxK (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 12:53:10 -0400
+Received: from cdptpa-omtalb.mail.rr.com ([75.180.132.122]:53512 "EHLO
+       cdptpa-omtalb.mail.rr.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S932187Ab0FYQxJ (ORCPT
+       <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>); Fri, 25 Jun 2010 12:53:09 -0400
+X-Authority-Analysis: v=1.0 c=1 a=iVNVO0OCT3kA:10 a=yQWWgrYGNuUA:10 a=kj9zAlcOel0A:10 a=20KFwNOVAAAA:8 a=hGzw-44bAAAA:8 a=f0L6POiToRdS6aViIA4A:9 a=tdNtT7bw1iHNm6ggrCkIte35EhAA:4 a=CjuIK1q_8ugA:10 a=jEp0ucaQiEUA:10 a=0kPLrQdw3YYA:10 a=dowx1zmaLagA:10 a=00U40p1LBqVLw4jT:21 a=gh7LVOPznGai4vo_:21
+X-Cloudmark-Score: 0
+X-Originating-IP: 71.70.153.3
+Received: from [71.70.153.3] ([71.70.153.3:42266] helo=mail.poochiereds.net)
+       by cdptpa-oedge01.mail.rr.com (envelope-from <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>)
+       (ecelerity 2.2.2.39 r()) with ESMTP
+       id 2D/E0-24471-3FED42C4; Fri, 25 Jun 2010 16:53:08 +0000
+Received: from tlielax.poochiereds.net (tlielax.poochiereds.net [192.168.1.3])
+       by mail.poochiereds.net (Postfix) with ESMTPS id E9B19580FA;
+       Fri, 25 Jun 2010 12:53:06 -0400 (EDT)
+In-Reply-To: <22697.1277470549-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-Mailer: Claws Mail 3.7.6 (GTK+ 2.20.1; x86_64-redhat-linux-gnu)
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003033>
+
+On Fri, 25 Jun 2010 13:55:49 +0100
+David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org> wrote:
+
+> Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org> wrote:
+> 
+> > I think the creation time is currently being ignored as we won't be able
+> > to accomodate in POSIX stat struct.
+> 
+> The FS-Cache interface doesn't use the POSIX stat struct, but it could be
+> really useful to save it and use it for cache coherency inside the kernel.
+> 
+> Out of interest, what does Samba do when it comes to generating a creation time
+> for UNIX where one does not exist?
+> 
+
+(cc'ing samba-technical since we're talking about the create time)
+
+Looks like it mostly uses the ctime. IMO, the mtime would be a better
+choice since it changes less frequently, but I don't guess that it
+matters very much.
+
+I have a few patches that make the cifs_iget code do more stringent
+checks. One of those makes it use the create time like an i_generation
+field to guard against matching inodes that have the same number but
+that have undergone a delete/create cycle. They need a bit more testing
+but I'm planning to post them in time for 2.6.36.
+
+Because of how samba generates this number, it could be somewhat
+problematic to do this. What may save us though is that Linux<->Samba
+mostly uses unix extensions unless someone has specifically disabled
+them on either end. The unix extension calls don't generally send any
+sort of create time field, so we can't rely on it in those codepaths
+anyway.
+
+-- 
+Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>
+
+
diff --git a/test/corpora/lkml/cur/1382298770.003106:2, b/test/corpora/lkml/cur/1382298770.003106:2,
new file mode 100644 (file)
index 0000000..19ea381
--- /dev/null
@@ -0,0 +1,60 @@
+From: David Howells <dhowells@redhat.com>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Fri, 25 Jun 2010 22:46:38 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 13
+Message-ID: <18628.1277502398@redhat.com>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net> <4C24A606.5040001@suse.de> <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes> <9822.1277312573@redhat.com> <22697.1277470549@redhat.com>
+Cc: dhowells@redhat.com, Suresh Jayaraman <sjayaraman@suse.de>,
+       Steve French <smfrench@gmail.com>, linux-cifs@vger.kernel.org,
+       linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
+       samba-technical@lists.samba.org
+To: Jeff Layton <jlayton@samba.org>
+X-From: linux-kernel-owner@vger.kernel.org Fri Jun 25 23:47:07 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OSGjo-0006q8-ME
+       for glk-linux-kernel-3@lo.gmane.org; Fri, 25 Jun 2010 23:47:05 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932250Ab0FYVqv (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Fri, 25 Jun 2010 17:46:51 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:55406 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932088Ab0FYVqs (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Fri, 25 Jun 2010 17:46:48 -0400
+Received: from int-mx02.intmail.prod.int.phx2.redhat.com (int-mx02.intmail.prod.int.phx2.redhat.com [10.5.11.12])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5PLkhIG005974
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Fri, 25 Jun 2010 17:46:43 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx02.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5PLkd77017768;
+       Fri, 25 Jun 2010 17:46:40 -0400
+In-Reply-To: <20100625125306.7f9b1966@tlielax.poochiereds.net>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.12
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003142>
+
+Jeff Layton <jlayton@samba.org> wrote:
+
+> Looks like it mostly uses the ctime. IMO, the mtime would be a better
+> choice since it changes less frequently, but I don't guess that it
+> matters very much.
+
+I'd've thought mtime changes more frequently since that's altered when data is
+written.  ctime is changed when attributes are changed.
+
+Note that Ext4 appears to have a file creation time field in its inode
+(struct ext4_inode::i_crtime[_extra]).  Can Samba be made to use that?
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298770.003117:2, b/test/corpora/lkml/cur/1382298770.003117:2,
new file mode 100644 (file)
index 0000000..7f53e34
--- /dev/null
@@ -0,0 +1,65 @@
+From: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Sat, 26 Jun 2010 00:04:28 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 18
+Message-ID: <20123.1277507068@redhat.com>
+References: <20100625182651.36800d06@tlielax.poochiereds.net> <20100625125306.7f9b1966@tlielax.poochiereds.net> <4C24A606.5040001@suse.de> <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <yes> <9822.1277312573@redhat.com> <22697.1277470549@redhat.com> <18628.1277502398@redhat.com>
+Cc: dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org, Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>,
+       Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       samba-technical-w/Ol4Ecudpl8XjKLYN78aQ@public.gmane.org
+To: Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Sat Jun 26 01:04:45 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OSHww-0006Jk-NV
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Sat, 26 Jun 2010 01:04:43 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1751807Ab0FYXEl (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Fri, 25 Jun 2010 19:04:41 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:62977 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1752149Ab0FYXEl (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Fri, 25 Jun 2010 19:04:41 -0400
+Received: from int-mx04.intmail.prod.int.phx2.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.17])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5PN4X40004498
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Fri, 25 Jun 2010 19:04:34 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx04.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5PN4Sld008220;
+       Fri, 25 Jun 2010 19:04:30 -0400
+In-Reply-To: <20100625182651.36800d06-9yPaYZwiELC+kQycOl6kW4xkIHaj4LzF@public.gmane.org>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.17
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003153>
+
+Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org> wrote:
+
+> IIUC, updating mtime for a write is also an attribute change, and that
+> affects ctime. According to the stat(2) manpage:
+
+You're right.  Okay, ctime is the more frequently changed.
+
+> > Note that Ext4 appears to have a file creation time field in its inode
+> > (struct ext4_inode::i_crtime[_extra]).  Can Samba be made to use that?
+> 
+> Is it exposed to userspace in any (standard) way? It would be handy to
+> have that. While we're wishing...it might also be nice to have a
+> standard way to get at the i_generation from userspace too.
+
+Not at present, but it's something that could be exported by ioctl() or
+getxattr().
+
+David
+
+
diff --git a/test/corpora/lkml/cur/1382298770.003118:2, b/test/corpora/lkml/cur/1382298770.003118:2,
new file mode 100644 (file)
index 0000000..a1ec438
--- /dev/null
@@ -0,0 +1,122 @@
+From: Steve French <smfrench@gmail.com>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and 
+       register them
+Date: Fri, 25 Jun 2010 18:05:30 -0500
+Lines: 51
+Message-ID: <AANLkTilOTrHLvLv4XWYZO6xCnYZgYT7gO2M-oKZ6VvqM@mail.gmail.com>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net>
+       <4C24A606.5040001@suse.de>
+       <1277220214-3597-1-git-send-email-sjayaraman@suse.de>
+       <9822.1277312573@redhat.com>
+       <22697.1277470549@redhat.com>
+       <18628.1277502398@redhat.com>
+       <20100625182651.36800d06@tlielax.poochiereds.net>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: David Howells <dhowells@redhat.com>,
+       Suresh Jayaraman <sjayaraman@suse.de>,
+       linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org, samba-technical@lists.samba.org,
+       Jeff Layton <jlayton@redhat.com>
+To: Jeff Layton <jlayton@samba.org>,
+       "Aneesh Kumar K.V" <aneesh.kumar@linux.vnet.ibm.com>,
+       Mingming Cao <mcao@us.ibm.com>
+X-From: linux-kernel-owner@vger.kernel.org Sat Jun 26 01:05:41 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OSHxs-0006a8-BA
+       for glk-linux-kernel-3@lo.gmane.org; Sat, 26 Jun 2010 01:05:40 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1756188Ab0FYXFd convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Fri, 25 Jun 2010 19:05:33 -0400
+Received: from mail-qw0-f46.google.com ([209.85.216.46]:51369 "EHLO
+       mail-qw0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751575Ab0FYXFb convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Fri, 25 Jun 2010 19:05:31 -0400
+Received: by qwi4 with SMTP id 4so742644qwi.19
+        for <multiple recipients>; Fri, 25 Jun 2010 16:05:30 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:mime-version:received:received:in-reply-to
+         :references:date:message-id:subject:from:to:cc:content-type
+         :content-transfer-encoding;
+        bh=6wKQkGOEeUGN4oPR3Nm4SRxtJr/EBwN8ENmpLnfdCDU=;
+        b=X7L6W0MtpQeW/4iBuj+oDlcP2yCJ3qwUs9lHBq1fRW6WdYblHXjmaN8o++3GDPLAg5
+         0MD07zxbYTGXRSrgCjCrGVm0tT88/6hY2a/rB8g68h/Qso2sIHa7B1iIN8JRR4pPWle0
+         sVjp9Xy/bQn2e0uE481Ii1TLHuWYA/QDXZreU=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=mime-version:in-reply-to:references:date:message-id:subject:from:to
+         :cc:content-type:content-transfer-encoding;
+        b=B+7qQvdOpN5a/KCRrDbssKZX8D3SnP73VMHd9RpkqP9nCHCmSLAgbeH03+/m6CLVAo
+         G+NKWqWtknwPBkYqT/bdP2XEak1yr+0rjOqjUaNvaT7AhzsyHEJBkaNnsbS3qaRy39OP
+         S7OkAyHfmgdeNAHkKnKRF73hfpvgAqR9X4bn8=
+Received: by 10.224.59.223 with SMTP id m31mr1130670qah.63.1277507130411; Fri, 
+       25 Jun 2010 16:05:30 -0700 (PDT)
+Received: by 10.229.46.136 with HTTP; Fri, 25 Jun 2010 16:05:30 -0700 (PDT)
+In-Reply-To: <20100625182651.36800d06@tlielax.poochiereds.net>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003154>
+
+On Fri, Jun 25, 2010 at 5:26 PM, Jeff Layton <jlayton@samba.org> wrote:
+>
+> On Fri, 25 Jun 2010 22:46:38 +0100
+> David Howells <dhowells@redhat.com> wrote:
+>
+> > Jeff Layton <jlayton@samba.org> wrote:
+> >
+> > > Looks like it mostly uses the ctime. IMO, the mtime would be a be=
+tter
+> > > choice since it changes less frequently, but I don't guess that i=
+t
+> > > matters very much.
+> >
+> > I'd've thought mtime changes more frequently since that's altered w=
+hen data is
+> > written. =A0ctime is changed when attributes are changed.
+> >
+>
+> IIUC, updating mtime for a write is also an attribute change, and tha=
+t
+> affects ctime. According to the stat(2) manpage:
+>
+> =A0 =A0 =A0 The field st_ctime is changed by writing or by setting =A0=
+inode =A0informa-
+> =A0 =A0 =A0 tion (i.e., owner, group, link count, mode, etc.).
+>
+> > Note that Ext4 appears to have a file creation time field in its in=
+ode
+> > (struct ext4_inode::i_crtime[_extra]). =A0Can Samba be made to use =
+that?
+> >
+>
+> Is it exposed to userspace in any (standard) way? It would be handy t=
+o
+> have that. While we're wishing...it might also be nice to have a
+> standard way to get at the i_generation from userspace too.
+>
+
+Yes - I have talked with MingMing and Aneesh about those (NFS may
+someday be able to use those too).=A0 An obstacle in the past had been
+that samba server stores its own fake creation time in an ndr encoded
+xattr which complicates things.
+
+MingMing/Annesh -
+Xattr or other way to get at birth time?
+
+
+--
+Thanks,
+
+Steve
+
+
diff --git a/test/corpora/lkml/cur/1382298770.003171:2, b/test/corpora/lkml/cur/1382298770.003171:2,
new file mode 100644 (file)
index 0000000..66e425e
--- /dev/null
@@ -0,0 +1,174 @@
+From: Mingming Cao <mcao@us.ibm.com>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+       register them
+Date: Fri, 25 Jun 2010 17:52:24 -0700
+Lines: 92
+Message-ID: <OFB55E8EC7.E8DD23D5-ON8725774E.0004921E-8825774E.0004CC31@us.ibm.com>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net>  <4C24A606.5040001@suse.de>
+       <1277220214-3597-1-git-send-email-sjayaraman@suse.de>   <9822.1277312573@redhat.com>
+       <22697.1277470549@redhat.com>   <18628.1277502398@redhat.com>   <20100625182651.36800d06@tlielax.poochiereds.net>
+       <AANLkTilOTrHLvLv4XWYZO6xCnYZgYT7gO2M-oKZ6VvqM@mail.gmail.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: quoted-printable
+Cc: linux-cifs@vger.kernel.org, Jeff Layton <jlayton@redhat.com>,
+       samba-technical@lists.samba.org, linux-kernel@vger.kernel.org,
+       David Howells <dhowells@redhat.com>, linux-fsdevel@vger.kernel.org,
+       "Aneesh Kumar K.V" <aneesh.kumar@linux.vnet.ibm.com>
+To: Steve French <smfrench@gmail.com>
+X-From: samba-technical-bounces@lists.samba.org Sat Jun 26 13:36:56 2010
+Return-path: <samba-technical-bounces@lists.samba.org>
+Envelope-to: gnsi-samba-technical@m.gmane.org
+Received: from fn.samba.org ([216.83.154.106] helo=lists.samba.org)
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <samba-technical-bounces@lists.samba.org>)
+       id 1OSTgu-00025d-6P
+       for gnsi-samba-technical@m.gmane.org; Sat, 26 Jun 2010 13:36:56 +0200
+Received: from fn.samba.org (localhost [127.0.0.1])
+       by lists.samba.org (Postfix) with ESMTP id 1ED11AD2C4;
+       Sat, 26 Jun 2010 05:36:45 -0600 (MDT)
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on fn.samba.org
+X-Spam-Level: 
+X-Spam-Status: No, score=-6.6 required=3.8 tests=BAYES_00,HTML_MESSAGE,
+       RCVD_IN_DNSWL_MED,SPF_PASS autolearn=ham version=3.2.5
+X-Original-To: samba-technical@lists.samba.org
+Delivered-To: samba-technical@lists.samba.org
+Received: from e34.co.us.ibm.com (e34.co.us.ibm.com [32.97.110.152])
+       by lists.samba.org (Postfix) with ESMTP id 30F90AD282
+       for <samba-technical@lists.samba.org>;
+       Fri, 25 Jun 2010 18:52:24 -0600 (MDT)
+Received: from d03relay01.boulder.ibm.com (d03relay01.boulder.ibm.com
+       [9.17.195.226])
+       by e34.co.us.ibm.com (8.14.4/8.13.1) with ESMTP id o5Q0iN1h017083
+       for <samba-technical@lists.samba.org>; Fri, 25 Jun 2010 18:44:23 -0600
+Received: from d03av01.boulder.ibm.com (d03av01.boulder.ibm.com [9.17.195.167])
+       by d03relay01.boulder.ibm.com (8.13.8/8.13.8/NCO v10.0) with ESMTP id
+       o5Q0qQTN175324
+       for <samba-technical@lists.samba.org>; Fri, 25 Jun 2010 18:52:26 -0600
+Received: from d03av01.boulder.ibm.com (loopback [127.0.0.1])
+       by d03av01.boulder.ibm.com (8.14.4/8.13.1/NCO v10.0 AVout) with ESMTP
+       id o5Q0qPCF006767
+       for <samba-technical@lists.samba.org>; Fri, 25 Jun 2010 18:52:26 -0600
+Received: from d03nm128.boulder.ibm.com (d03nm128.boulder.ibm.com
+       [9.17.195.32])
+       by d03av01.boulder.ibm.com (8.14.4/8.13.1/NCO v10.0 AVin) with ESMTP id
+       o5Q0qPrh006760; Fri, 25 Jun 2010 18:52:25 -0600
+In-Reply-To: <AANLkTilOTrHLvLv4XWYZO6xCnYZgYT7gO2M-oKZ6VvqM@mail.gmail.com>
+X-KeepSent: B55E8EC7:E8DD23D5-8725774E:0004921E;
+ type=4; name=$KeepSent
+X-Mailer: Lotus Notes Build V852_M2_03302010 March 30, 2010
+X-MIMETrack: Serialize by Router on D03NM128/03/M/IBM(Release 8.0.1|February
+       07, 2008) at 06/25/2010 18:52:25
+X-Mailman-Approved-At: Sat, 26 Jun 2010 05:36:42 -0600
+X-Content-Filtered-By: Mailman/MimeDel 2.1.12
+X-BeenThere: samba-technical@lists.samba.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Discussions on Samba internals. For general questions please
+       subscribe to the list samba@samba.org"
+       <samba-technical.lists.samba.org>
+List-Unsubscribe: <https://lists.samba.org/mailman/options/samba-technical>,
+       <mailto:samba-technical-request@lists.samba.org?subject=unsubscribe>
+List-Archive: <http://lists.samba.org/pipermail/samba-technical>
+List-Post: <mailto:samba-technical@lists.samba.org>
+List-Help: <mailto:samba-technical-request@lists.samba.org?subject=help>
+List-Subscribe: <https://lists.samba.org/mailman/listinfo/samba-technical>,
+       <mailto:samba-technical-request@lists.samba.org?subject=subscribe>
+Sender: samba-technical-bounces@lists.samba.org
+Errors-To: samba-technical-bounces@lists.samba.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003208>
+
+
+
+Steve French <smfrench@gmail.com> wrote on 06/25/2010 04:05:30 PM:
+
+> Steve French <smfrench@gmail.com>
+> 06/25/2010 04:05 PM
+>
+> To
+>
+> Jeff Layton <jlayton@samba.org>, "Aneesh Kumar K.V"
+> <aneesh.kumar@linux.vnet.ibm.com>, Mingming Cao/Beaverton/IBM@IBMUS
+>
+> cc
+>
+> David Howells <dhowells@redhat.com>, Suresh Jayaraman
+> <sjayaraman@suse.de>, linux-cifs@vger.kernel.org, linux-
+> fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, samba-
+> technical@lists.samba.org, Jeff Layton <jlayton@redhat.com>
+>
+> Subject
+>
+> Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+> register them
+>
+> On Fri, Jun 25, 2010 at 5:26 PM, Jeff Layton <jlayton@samba.org> wrot=
+e:
+> >
+> > On Fri, 25 Jun 2010 22:46:38 +0100
+> > David Howells <dhowells@redhat.com> wrote:
+> >
+> > > Jeff Layton <jlayton@samba.org> wrote:
+> > >
+> > > > Looks like it mostly uses the ctime. IMO, the mtime would be a
+better
+> > > > choice since it changes less frequently, but I don't guess that=
+ it
+> > > > matters very much.
+> > >
+> > > I'd've thought mtime changes more frequently since that's
+> altered when data is
+> > > written. =A0ctime is changed when attributes are changed.
+> > >
+> >
+> > IIUC, updating mtime for a write is also an attribute change, and t=
+hat
+> > affects ctime. According to the stat(2) manpage:
+> >
+> > =A0 =A0 =A0 The field st_ctime is changed by writing or by setting
+> =A0inode =A0informa-
+> > =A0 =A0 =A0 tion (i.e., owner, group, link count, mode, etc.).
+> >
+> > > Note that Ext4 appears to have a file creation time field in its
+inode
+> > > (struct ext4_inode::i_crtime[_extra]). =A0Can Samba be made to us=
+e
+that?
+> > >
+> >
+> > Is it exposed to userspace in any (standard) way? It would be handy=
+ to
+> > have that. While we're wishing...it might also be nice to have a
+> > standard way to get at the i_generation from userspace too.
+> >
+>
+> Yes - I have talked with MingMing and Aneesh about those (NFS may
+> someday be able to use those too).=A0 An obstacle in the past had bee=
+n
+> that samba server stores its own fake creation time in an ndr encoded=
+
+> xattr which complicates things.
+>
+> MingMing/Annesh -
+> Xattr or other way to get at birth time?
+>
+>
+
+Not yet,
+ The ext4 file creation time only accesable from the kernel at the mome=
+nt.
+There were discussion
+to make this information avaliable via xattr before, but was rejected,
+since most people
+agree that making this info avalibele via stat() is more standard. Howe=
+ver
+modifying stat() would imply
+big interface change. thus no action has been taken yet.
+
+> --
+> Thanks,
+>
+> Steve=
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.003317:2, b/test/corpora/lkml/cur/1382298770.003317:2,
new file mode 100644 (file)
index 0000000..6fce518
--- /dev/null
@@ -0,0 +1,156 @@
+From: "Aneesh Kumar K. V" <aneesh.kumar@linux.vnet.ibm.com>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and register them
+Date: Sun, 27 Jun 2010 23:47:21 +0530
+Lines: 100
+Message-ID: <871vbscpce.fsf@linux.vnet.ibm.com>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net> <4C24A606.5040001@suse.de> <1277220214-3597-1-git-send-email-sjayaraman@suse.de> <9822.1277312573@redhat.com> <22697.1277470549@redhat.com> <18628.1277502398@redhat.com> <20100625182651.36800d06@tlielax.poochiereds.net> <AANLkTilOTrHLvLv4XWYZO6xCnYZgYT7gO2M-oKZ6VvqM@mail.gmail.com> <OFB55E8EC7.E8DD23D5-ON8725774E.0004921E-8825774E.0004CC31@us.ibm.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: David Howells <dhowells@redhat.com>,
+       Jeff Layton <jlayton@redhat.com>,
+       Jeff Layton <jlayton@samba.org>, linux-cifs@vger.kernel.org,
+       linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
+       samba-technical@lists.samba.org,
+       Suresh Jayaraman <sjayaraman@suse.de>
+To: Mingming Cao <mcao@us.ibm.com>, Steve French <smfrench@gmail.com>,
+       "DENIEL Philippe" <philippe.deniel@cea.fr>
+X-From: linux-kernel-owner@vger.kernel.org Sun Jun 27 20:18:00 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OSwQZ-0003Kh-Vu
+       for glk-linux-kernel-3@lo.gmane.org; Sun, 27 Jun 2010 20:18:00 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1754631Ab0F0SRq convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 27 Jun 2010 14:17:46 -0400
+Received: from e23smtp07.au.ibm.com ([202.81.31.140]:52430 "EHLO
+       e23smtp07.au.ibm.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1753837Ab0F0SRl convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 27 Jun 2010 14:17:41 -0400
+Received: from d23relay05.au.ibm.com (d23relay05.au.ibm.com [202.81.31.247])
+       by e23smtp07.au.ibm.com (8.14.4/8.13.1) with ESMTP id o5RIHbfJ012483;
+       Mon, 28 Jun 2010 04:17:37 +1000
+Received: from d23av03.au.ibm.com (d23av03.au.ibm.com [9.190.234.97])
+       by d23relay05.au.ibm.com (8.13.8/8.13.8/NCO v10.0) with ESMTP id o5RIHW9f1130634;
+       Mon, 28 Jun 2010 04:17:32 +1000
+Received: from d23av03.au.ibm.com (loopback [127.0.0.1])
+       by d23av03.au.ibm.com (8.14.4/8.13.1/NCO v10.0 AVout) with ESMTP id o5RIHVcR027534;
+       Mon, 28 Jun 2010 04:17:32 +1000
+Received: from skywalker.linux.vnet.ibm.com ([9.77.196.78])
+       by d23av03.au.ibm.com (8.14.4/8.13.1/NCO v10.0 AVin) with ESMTP id o5RIHMFl027485;
+       Mon, 28 Jun 2010 04:17:24 +1000
+In-Reply-To: <OFB55E8EC7.E8DD23D5-ON8725774E.0004921E-8825774E.0004CC31@us.ibm.com>
+User-Agent: Notmuch/ (http://notmuchmail.org) Emacs/24.0.50.1 (i686-pc-linux-gnu)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003357>
+
+On Fri, 25 Jun 2010 17:52:24 -0700, Mingming Cao <mcao@us.ibm.com> wrot=
+e:
+>=20
+>=20
+> Steve French <smfrench@gmail.com> wrote on 06/25/2010 04:05:30 PM:
+>=20
+> > Steve French <smfrench@gmail.com>
+> > 06/25/2010 04:05 PM
+> >
+> > To
+> >
+> > Jeff Layton <jlayton@samba.org>, "Aneesh Kumar K.V"
+> > <aneesh.kumar@linux.vnet.ibm.com>, Mingming Cao/Beaverton/IBM@IBMUS
+> >
+> > cc
+> >
+> > David Howells <dhowells@redhat.com>, Suresh Jayaraman
+> > <sjayaraman@suse.de>, linux-cifs@vger.kernel.org, linux-
+> > fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, samba-
+> > technical@lists.samba.org, Jeff Layton <jlayton@redhat.com>
+> >
+> > Subject
+> >
+> > Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+> > register them
+> >
+> > On Fri, Jun 25, 2010 at 5:26 PM, Jeff Layton <jlayton@samba.org> wr=
+ote:
+> > >
+> > > On Fri, 25 Jun 2010 22:46:38 +0100
+> > > David Howells <dhowells@redhat.com> wrote:
+> > >
+> > > > Jeff Layton <jlayton@samba.org> wrote:
+> > > >
+> > > > > Looks like it mostly uses the ctime. IMO, the mtime would be =
+a
+> better
+> > > > > choice since it changes less frequently, but I don't guess th=
+at it
+> > > > > matters very much.
+> > > >
+> > > > I'd've thought mtime changes more frequently since that's
+> > altered when data is
+> > > > written. =C2=A0ctime is changed when attributes are changed.
+> > > >
+> > >
+> > > IIUC, updating mtime for a write is also an attribute change, and=
+ that
+> > > affects ctime. According to the stat(2) manpage:
+> > >
+> > > =C2=A0 =C2=A0 =C2=A0 The field st_ctime is changed by writing or =
+by setting
+> > =C2=A0inode =C2=A0informa-
+> > > =C2=A0 =C2=A0 =C2=A0 tion (i.e., owner, group, link count, mode, =
+etc.).
+> > >
+> > > > Note that Ext4 appears to have a file creation time field in it=
+s
+> inode
+> > > > (struct ext4_inode::i_crtime[_extra]). =C2=A0Can Samba be made =
+to use
+> that?
+> > > >
+> > >
+> > > Is it exposed to userspace in any (standard) way? It would be han=
+dy to
+> > > have that. While we're wishing...it might also be nice to have a
+> > > standard way to get at the i_generation from userspace too.
+> > >
+> >
+> > Yes - I have talked with MingMing and Aneesh about those (NFS may
+> > someday be able to use those too).=C2=A0 An obstacle in the past ha=
+d been
+> > that samba server stores its own fake creation time in an ndr encod=
+ed
+> > xattr which complicates things.
+> >
+> > MingMing/Annesh -
+> > Xattr or other way to get at birth time?
+> >
+> >
+>=20
+> Not yet,
+>  The ext4 file creation time only accesable from the kernel at the mo=
+ment.
+> There were discussion
+> to make this information avaliable via xattr before, but was rejected=
+,
+> since most people
+> agree that making this info avalibele via stat() is more standard. Ho=
+wever
+> modifying stat() would imply
+> big interface change. thus no action has been taken yet.
+
+NFS ganesha pNFS also had a requirement for getting i_generation and
+inode number in userspace. So may be we should now look at updating
+stat or add a variant syscall that include i_generation and create time
+in the return value
+
+-aneesh
+
+
diff --git a/test/corpora/lkml/cur/1382298770.003318:2, b/test/corpora/lkml/cur/1382298770.003318:2,
new file mode 100644 (file)
index 0000000..058d147
--- /dev/null
@@ -0,0 +1,66 @@
+From: Christoph Hellwig <hch-wEGCiKHe2LqWVfeAwA7xHQ@public.gmane.org>
+Subject: Re: [RFC][PATCH 06/10] cifs: define inode-level cache object and
+ register them
+Date: Sun, 27 Jun 2010 14:22:29 -0400
+Lines: 9
+Message-ID: <20100627182229.GA492@infradead.org>
+References: <20100625125306.7f9b1966@tlielax.poochiereds.net>
+ <4C24A606.5040001@suse.de>
+ <1277220214-3597-1-git-send-email-sjayaraman@suse.de>
+ <9822.1277312573@redhat.com>
+ <22697.1277470549@redhat.com>
+ <18628.1277502398@redhat.com>
+ <20100625182651.36800d06@tlielax.poochiereds.net>
+ <AANLkTilOTrHLvLv4XWYZO6xCnYZgYT7gO2M-oKZ6VvqM@mail.gmail.com>
+ <OFB55E8EC7.E8DD23D5-ON8725774E.0004921E-8825774E.0004CC31@us.ibm.com>
+ <871vbscpce.fsf@linux.vnet.ibm.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Cc: Mingming Cao <mcao-r/Jw6+rmf7HQT0dZR+AlfA@public.gmane.org>, Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>,
+       DENIEL Philippe <philippe.deniel-KCE40YydGKI@public.gmane.org>,
+       David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>,
+       Jeff Layton <jlayton-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>,
+       Jeff Layton <jlayton-eUNUBHrolfbYtjvyW6yDsg@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       samba-technical-w/Ol4Ecudpl8XjKLYN78aQ@public.gmane.org,
+       Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+To: "Aneesh Kumar K. V" <aneesh.kumar-23VcF4HTsmIX0ybBhKVfKdBPR1lH4CV8@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Sun Jun 27 20:22:46 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OSwVB-0005TI-SG
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Sun, 27 Jun 2010 20:22:46 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1752811Ab0F0SWo (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Sun, 27 Jun 2010 14:22:44 -0400
+Received: from bombadil.infradead.org ([18.85.46.34]:55433 "EHLO
+       bombadil.infradead.org" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752728Ab0F0SWn (ORCPT
+       <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>); Sun, 27 Jun 2010 14:22:43 -0400
+Received: from hch by bombadil.infradead.org with local (Exim 4.72 #1 (Red Hat Linux))
+       id 1OSwUv-00009z-9N; Sun, 27 Jun 2010 18:22:29 +0000
+Content-Disposition: inline
+In-Reply-To: <871vbscpce.fsf-23VcF4HTsmIX0ybBhKVfKdBPR1lH4CV8@public.gmane.org>
+User-Agent: Mutt/1.5.20 (2009-08-17)
+X-SRS-Rewrite: SMTP reverse-path rewritten from <hch-wEGCiKHe2LqWVfeAwA7xHQ@public.gmane.org> by bombadil.infradead.org
+       See http://www.infradead.org/rpr.html
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003358>
+
+On Sun, Jun 27, 2010 at 11:47:21PM +0530, Aneesh Kumar K. V wrote:
+> NFS ganesha pNFS also had a requirement for getting i_generation and
+> inode number in userspace. So may be we should now look at updating
+> stat or add a variant syscall that include i_generation and create time
+> in the return value
+
+What's missing in knfsd that you feel the sudden urge to move backwards
+to a userspace nfsd (one with a horribly crappy codebase, too).
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.003486:2, b/test/corpora/lkml/cur/1382298770.003486:2,
new file mode 100644 (file)
index 0000000..8831b45
--- /dev/null
@@ -0,0 +1,89 @@
+From: Suresh Jayaraman <sjayaraman-l3A5Bk7waGM@public.gmane.org>
+Subject: Re: [RFC][PATCH 05/10] cifs: define superblock-level cache index
+ objects and register them
+Date: Mon, 28 Jun 2010 18:23:13 +0530
+Lines: 48
+Message-ID: <4C289B39.4060901@suse.de>
+References: <22746.1277470713@redhat.com> <4C24A4A0.90408@suse.de> <1277220206-3559-1-git-send-email-sjayaraman@suse.de> <yes> <9720.1277312290@redhat.com> <23204.1277472412@redhat.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Cc: Steve French <smfrench-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>, linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       linux-fsdevel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+X-From: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Mon Jun 28 14:53:24 2010
+Return-path: <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1OTDq0-00054Q-At
+       for glkc-linux-cifs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Mon, 28 Jun 2010 14:53:24 +0200
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1754503Ab0F1MxX (ORCPT <rfc822;glkc-linux-cifs@m.gmane.org>);
+       Mon, 28 Jun 2010 08:53:23 -0400
+Received: from cantor2.suse.de ([195.135.220.15]:48374 "EHLO mx2.suse.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1754456Ab0F1MxW (ORCPT <rfc822;linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Mon, 28 Jun 2010 08:53:22 -0400
+Received: from relay2.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       by mx2.suse.de (Postfix) with ESMTP id 7BDC18672B;
+       Mon, 28 Jun 2010 14:53:21 +0200 (CEST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091130 SUSE/3.0.0-1.1.1 Thunderbird/3.0
+In-Reply-To: <23204.1277472412-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org>
+Sender: linux-cifs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-cifs.vger.kernel.org>
+X-Mailing-List: linux-cifs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003530>
+
+On 06/25/2010 06:56 PM, David Howells wrote:
+> David Howells <dhowells-H+wXaHxf7aLQT0dZR+AlfA@public.gmane.org> wrote:
+> 
+>>>> validate the root directory of the share in some way?
+>>>
+>>> I don't know if there is a way to do this.
+>>
+>> Is there an inode number or something?  Even the creation time might do.
+> 
+> Looking in cifspdu.h, there are a number of things that it might be possible
+> to use.
+> 
+>  (1) FILE_ALL_INFO: CreationTime, IndexNumber, IndexNumber1, FileName
+>      (assuming this isn't flattened to '\' or something for the root of a
+>      share.
+> 
+>  (2) FILE_UNIX_BASIC_INFO: DevMajor, DevMinor, UniqueId.
+> 
+>  (3) FILE_INFO_STANDARD: CreationDate, CreationTime.
+> 
+>  (4) FILE_INFO_BASIC: CreationTime.
+> 
+>  (5) FILE_DIRECTORY_INFO: FileIndex, CreationTime, FileName.
+> 
+>  (6) SEARCH_ID_FULL_DIR_INFO: FileIndex, CreationTime, UniqueId, FileName.
+> 
+>  (7) FILE_BOTH_DIRECTORY_INFO: FileIndex, CreationTime, ShortName, FileName.
+> 
+>  (8) OPEN_RSP_EXT: Fid, CreationTime, VolumeGUID, FileId.
+> 
+> You may have to choose different sets of things, depending on what the server
+> has on offer.  Also, don't forget, if you can't work out whether a share is
+
+Did you mean we need to validate differently for different servers?
+
+I just did some testing and it looks like we could rely on CreationTime,
+IndexNumber for validating with Windows servers (FileName is relative to
+the mapped drive) and UniqueId for validating with Samba servers. I did
+not test all possibilities (there could be more).
+
+> coherent or not from the above, you can always use LastWriteTime, ChangeTime
+> and EndOfFile and just discard the whole subtree if they differ.
+> 
+
+Thanks,
+
+-- 
+Suresh Jayaraman
+
+
diff --git a/test/corpora/lkml/cur/1382298770.003499:2, b/test/corpora/lkml/cur/1382298770.003499:2,
new file mode 100644 (file)
index 0000000..b10adc4
--- /dev/null
@@ -0,0 +1,63 @@
+From: David Howells <dhowells@redhat.com>
+Subject: Re: [RFC][PATCH 05/10] cifs: define superblock-level cache index objects and register them
+Date: Mon, 28 Jun 2010 14:24:45 +0100
+Organization: Red Hat UK Ltd. Registered Address: Red Hat UK Ltd, Amberley
+       Place, 107-111 Peascod Street, Windsor, Berkshire, SI4 1TE, United
+       Kingdom.
+       Registered in England and Wales under Company Registration No. 3798903
+Lines: 17
+Message-ID: <9513.1277731485@redhat.com>
+References: <4C289B39.4060901@suse.de> <22746.1277470713@redhat.com> <4C24A4A0.90408@suse.de> <1277220206-3559-1-git-send-email-sjayaraman@suse.de> <yes> <9720.1277312290@redhat.com> <23204.1277472412@redhat.com>
+Cc: dhowells@redhat.com, Steve French <smfrench@gmail.com>,
+       linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Suresh Jayaraman <sjayaraman@suse.de>
+X-From: linux-fsdevel-owner@vger.kernel.org Mon Jun 28 15:24:57 2010
+Return-path: <linux-fsdevel-owner@vger.kernel.org>
+Envelope-to: lnx-linux-fsdevel@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-fsdevel-owner@vger.kernel.org>)
+       id 1OTEKW-00048k-S3
+       for lnx-linux-fsdevel@lo.gmane.org; Mon, 28 Jun 2010 15:24:57 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1751608Ab0F1NYz (ORCPT <rfc822;lnx-linux-fsdevel@m.gmane.org>);
+       Mon, 28 Jun 2010 09:24:55 -0400
+Received: from mx1.redhat.com ([209.132.183.28]:26085 "EHLO mx1.redhat.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751395Ab0F1NYy (ORCPT <rfc822;linux-fsdevel@vger.kernel.org>);
+       Mon, 28 Jun 2010 09:24:54 -0400
+Received: from int-mx08.intmail.prod.int.phx2.redhat.com (int-mx08.intmail.prod.int.phx2.redhat.com [10.5.11.21])
+       by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5SDOmfA019811
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Mon, 28 Jun 2010 09:24:49 -0400
+Received: from redhat.com (ovpn01.gateway.prod.ext.phx2.redhat.com [10.5.9.1])
+       by int-mx08.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5SDOjHf030340;
+       Mon, 28 Jun 2010 09:24:47 -0400
+In-Reply-To: <4C289B39.4060901@suse.de>
+X-Scanned-By: MIMEDefang 2.67 on 10.5.11.21
+Sender: linux-fsdevel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-fsdevel.vger.kernel.org>
+X-Mailing-List: linux-fsdevel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1003543>
+
+Suresh Jayaraman <sjayaraman@suse.de> wrote:
+
+> Did you mean we need to validate differently for different servers?
+
+You may need to, yes, as different servers may make different attributes
+available.
+
+This isn't too bad.  Each server index record in the cache has freeform
+auxiliary data, just as does each file data record.  You could, say, stick a
+byte at the front that indicates what you've stored in there.
+
+David
+--
+To unsubscribe from this list: send the line "unsubscribe linux-fsdevel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298770.004581:2, b/test/corpora/lkml/cur/1382298770.004581:2,
new file mode 100644 (file)
index 0000000..732bfa0
--- /dev/null
@@ -0,0 +1,92 @@
+From: Timur Tabi <timur.tabi@gmail.com>
+Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Wed, 30 Jun 2010 15:55:58 -0500
+Lines: 33
+Message-ID: <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com>
+References: <20100308191005.GE4324@amak.tundra.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: mporter@kernel.crashing.org, linux-kernel@vger.kernel.org,
+       linuxppc-dev@lists.ozlabs.org, thomas.moll@sysgo.com
+To: Alexandre Bounine <abounine@tundra.com>
+X-From: linux-kernel-owner@vger.kernel.org Wed Jun 30 22:56:40 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OU4Kl-0005Kf-V4
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 30 Jun 2010 22:56:40 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1756668Ab0F3U4b convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Wed, 30 Jun 2010 16:56:31 -0400
+Received: from mail-vw0-f46.google.com ([209.85.212.46]:41333 "EHLO
+       mail-vw0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1753416Ab0F3U43 convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Wed, 30 Jun 2010 16:56:29 -0400
+Received: by vws5 with SMTP id 5so1449398vws.19
+        for <linux-kernel@vger.kernel.org>; Wed, 30 Jun 2010 13:56:28 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:received:mime-version:received:in-reply-to
+         :references:from:date:message-id:subject:to:cc:content-type
+         :content-transfer-encoding;
+        bh=FTlit9cHTz/9rLGcvA5/pEZlzxAQ5x20v8HE5XYFwYM=;
+        b=NFbjnxZ4KwcjTy4tFh+BnhWPEGeYTw6z918yIouRaMmbEDph56xq26K9aTBokuYHqe
+         UgFjBn7XWcxvqJPyCetfsDRG+F3M2XwCq/DSCswSPtXSLsy8WKm7cMXVS3hjiO8sMZ97
+         mRMGZkYBJHjWP+ulkBXiq6q7/OQuE8Dkl+rWM=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=mime-version:in-reply-to:references:from:date:message-id:subject:to
+         :cc:content-type:content-transfer-encoding;
+        b=r0N6AOAg+TSvY2kPQPahldj4iRU9oUoSLtHA7JXG2QU4CR9O5GBhxAtr2aY99qUPZd
+         tFS0ZWRAb9cmOgiZhTpNxsBjCJ/e/DQ1ccP5rZ/U40q1SJ1KwN92hqpOoppZ0tkqSB7/
+         UlQtsvPSK7a0bYqufEmscfAi98w1+mfZIbK6U=
+Received: by 10.220.161.203 with SMTP id s11mr5093041vcx.195.1277931388141; 
+       Wed, 30 Jun 2010 13:56:28 -0700 (PDT)
+Received: by 10.220.161.137 with HTTP; Wed, 30 Jun 2010 13:55:58 -0700 (PDT)
+In-Reply-To: <20100308191005.GE4324@amak.tundra.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1004632>
+
+On Mon, Mar 8, 2010 at 2:10 PM, Alexandre Bounine <abounine@tundra.com>=
+ wrote:
+>
+> From: Alexandre Bounine <alexandre.bounine@idt.com>
+>
+> Add Machine Check exception handling into RapidIO port driver
+> for Freescale SoCs (MPC85xx).
+>
+> Signed-off-by: Alexandre Bounine <alexandre.bounine@idt.com>
+> Tested-by: Thomas Moll <thomas.moll@sysgo.com>
+=2E..
+
+> +static int fsl_rio_mcheck_exception(struct pt_regs *regs)
+> +{
+> + =A0 =A0 =A0 const struct exception_table_entry *entry =3D NULL;
+> + =A0 =A0 =A0 unsigned long reason =3D (mfspr(SPRN_MCSR) & MCSR_MASK)=
+;
+
+MCSR_MASK is not defined anywhere, so when I compile this code, I get t=
+his:
+
+  CC      arch/powerpc/sysdev/fsl_rio.o
+arch/powerpc/sysdev/fsl_rio.c: In function 'fsl_rio_mcheck_exception':
+arch/powerpc/sysdev/fsl_rio.c:248: error: 'MCSR_MASK' undeclared
+(first use in this function)
+arch/powerpc/sysdev/fsl_rio.c:248: error: (Each undeclared identifier
+is reported only once
+arch/powerpc/sysdev/fsl_rio.c:248: error: for each function it appears =
+in.)
+
+--=20
+Timur Tabi
+Linux kernel developer at Freescale
+
+
diff --git a/test/corpora/lkml/cur/1382298770.004582:2, b/test/corpora/lkml/cur/1382298770.004582:2,
new file mode 100644 (file)
index 0000000..d149b72
--- /dev/null
@@ -0,0 +1,68 @@
+From: Timur Tabi <timur.tabi@gmail.com>
+Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Wed, 30 Jun 2010 16:00:56 -0500
+Lines: 12
+Message-ID: <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com>
+References: <20100308191005.GE4324@amak.tundra.com> <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1
+Cc: mporter@kernel.crashing.org, linux-kernel@vger.kernel.org,
+       linuxppc-dev@lists.ozlabs.org, thomas.moll@sysgo.com
+To: Alexandre Bounine <abounine@tundra.com>
+X-From: linux-kernel-owner@vger.kernel.org Wed Jun 30 23:01:37 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OU4PZ-0000HS-0T
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 30 Jun 2010 23:01:37 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755703Ab0F3VB2 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Wed, 30 Jun 2010 17:01:28 -0400
+Received: from mail-vw0-f46.google.com ([209.85.212.46]:53141 "EHLO
+       mail-vw0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751784Ab0F3VB1 (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Wed, 30 Jun 2010 17:01:27 -0400
+Received: by vws5 with SMTP id 5so1454517vws.19
+        for <linux-kernel@vger.kernel.org>; Wed, 30 Jun 2010 14:01:26 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:received:mime-version:received:in-reply-to
+         :references:from:date:message-id:subject:to:cc:content-type;
+        bh=+BUKti+Oa03CrnVvRyT591FhcoxqR7S2rzZHtD6WSuY=;
+        b=O/b04HLJrmTE0aIq2mNCRznQrXxAAGHSMarHR5mrgYptmr68froM6UgmDqTZFLhNiH
+         BcT8g+AziiqSV1k/ckXjRyVR0s9Jdv4g2phMNtp8NStbPfOPpLDkUKTQadphOTonCfeK
+         e+ZrLBwh+FCoYNAOjvFioBKj6CxN2Oi5xIhPc=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=mime-version:in-reply-to:references:from:date:message-id:subject:to
+         :cc:content-type;
+        b=UcKGhJIXCTTcSvBWwGwLUefPONGygVPsUnTt4nDSl4udB8JKMyi0EghzzgNXUyq4Dz
+         UCxzZAyxzjvjgsgPS3kzPhSsWG2PRG66pC1OA68RJ5YVOjt55/yOz/yfTqXBVvRSq2fV
+         QNcKACYHSjkIZ7Uq7ZEW9bEGI5tTKdz++N2UA=
+Received: by 10.220.124.73 with SMTP id t9mr5099129vcr.37.1277931686462; Wed, 
+       30 Jun 2010 14:01:26 -0700 (PDT)
+Received: by 10.220.161.137 with HTTP; Wed, 30 Jun 2010 14:00:56 -0700 (PDT)
+In-Reply-To: <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1004633>
+
+On Wed, Jun 30, 2010 at 3:55 PM, Timur Tabi <timur.tabi@gmail.com> wrote:
+
+> MCSR_MASK is not defined anywhere, so when I compile this code, I get this:
+
+Never mind.  I see that it's been fixed already, and that the patch
+that removed MCSR_MASK was posted around the same time that this patch
+was posted.
+
+
+-- 
+Timur Tabi
+Linux kernel developer at Freescale
+
+
diff --git a/test/corpora/lkml/cur/1382298775.002830:2, b/test/corpora/lkml/cur/1382298775.002830:2,
new file mode 100644 (file)
index 0000000..1bf40bc
--- /dev/null
@@ -0,0 +1,60 @@
+From: Michael Neuling <mikey@neuling.org>
+Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Tue, 03 Aug 2010 16:06:30 +1000
+Lines: 15
+Message-ID: <4381.1280815590@neuling.org>
+References: <20100308191005.GE4324@amak.tundra.com> <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com> <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com>
+Cc: Alexandre Bounine <abounine@tundra.com>,
+       linuxppc-dev@lists.ozlabs.org, linux-kernel@vger.kernel.org,
+       thomas.moll@sysgo.com
+To: Timur Tabi <timur.tabi@gmail.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Aug 03 08:06:45 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OgAeD-00087x-ED
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 03 Aug 2010 08:06:45 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755287Ab0HCGGf (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 3 Aug 2010 02:06:35 -0400
+Received: from ozlabs.org ([203.10.76.45]:51158 "EHLO ozlabs.org"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1755139Ab0HCGGd (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 3 Aug 2010 02:06:33 -0400
+Received: from localhost.localdomain (localhost [127.0.0.1])
+       by ozlabs.org (Postfix) with ESMTP id B7A371007D1;
+       Tue,  3 Aug 2010 16:06:31 +1000 (EST)
+Received: by localhost.localdomain (Postfix, from userid 1000)
+       id EDBB7C5EB7; Tue,  3 Aug 2010 16:06:30 +1000 (EST)
+Received: from neuling.org (localhost [127.0.0.1])
+       by localhost.localdomain (Postfix) with ESMTP id E8003C51D3;
+       Tue,  3 Aug 2010 16:06:30 +1000 (EST)
+In-reply-to: <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com>
+Comments: In-reply-to Timur Tabi <timur.tabi@gmail.com>
+   message dated "Wed, 30 Jun 2010 16:00:56 -0500."
+X-Mailer: MH-E 8.2; nmh 1.3; GNU Emacs 23.1.1
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1017846>
+
+> > MCSR_MASK is not defined anywhere, so when I compile this code, I get this:
+> 
+> Never mind.  I see that it's been fixed already, and that the patch
+> that removed MCSR_MASK was posted around the same time that this patch
+> was posted.
+
+I don't know what happened here but 2.6.35 is broken because of this
+problem:
+
+arch/powerpc/sysdev/fsl_rio.c:248: error: 'MCSR_MASK' undeclared (first use in this function)
+arch/powerpc/sysdev/fsl_rio.c:248: error: (Each undeclared identifier is reported only once
+arch/powerpc/sysdev/fsl_rio.c:248: error: for each function it appears in.)
+arch/powerpc/sysdev/fsl_rio.c:250: error: 'MCSR_BUS_RBERR' undeclared (first use in this function)
+
+Mikey
+
+
diff --git a/test/corpora/lkml/cur/1382298775.002978:2, b/test/corpora/lkml/cur/1382298775.002978:2,
new file mode 100644 (file)
index 0000000..21e2a10
--- /dev/null
@@ -0,0 +1,91 @@
+From: "Bounine, Alexandre" <Alexandre.Bounine@idt.com>
+Subject: RE: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Tue, 3 Aug 2010 05:17:54 -0700
+Lines: 34
+Message-ID: <0CE8B6BE3C4AD74AB97D9D29BD24E5520114309D@CORPEXCH1.na.ads.idt.com>
+References: <20100308191005.GE4324@amak.tundra.com> <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com> <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com> <4381.1280815590@neuling.org>
+Mime-Version: 1.0
+Content-Type: text/plain;
+       charset="us-ascii"
+Content-Transfer-Encoding: 8BIT
+Cc: "Alexandre Bounine" <abounine@tundra.com>,
+       <linuxppc-dev@lists.ozlabs.org>, <linux-kernel@vger.kernel.org>,
+       <thomas.moll@sysgo.com>
+To: "Michael Neuling" <mikey@neuling.org>,
+       "Timur Tabi" <timur.tabi@gmail.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Aug 03 14:27:12 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OgGaG-0002zE-Fr
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 03 Aug 2010 14:27:04 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1756073Ab0HCM0x (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 3 Aug 2010 08:26:53 -0400
+Received: from mxout1.idt.com ([157.165.5.25]:35046 "EHLO mxout1.idt.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1756010Ab0HCM0w convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 3 Aug 2010 08:26:52 -0400
+X-Greylist: delayed 521 seconds by postgrey-1.27 at vger.kernel.org; Tue, 03 Aug 2010 08:26:52 EDT
+Received: from mail.idt.com (localhost [127.0.0.1])
+       by mxout1.idt.com (8.13.1/8.13.1) with ESMTP id o73CHxil001904;
+       Tue, 3 Aug 2010 05:17:59 -0700
+Received: from corpml3.corp.idt.com (corpml3.corp.idt.com [157.165.140.25])
+       by mail.idt.com (8.13.8/8.13.8) with ESMTP id o73CHvit016488;
+       Tue, 3 Aug 2010 05:17:57 -0700 (PDT)
+Received: from CORPEXCH1.na.ads.idt.com (localhost [127.0.0.1])
+       by corpml3.corp.idt.com (8.11.7p1+Sun/8.11.7) with ESMTP id o73CHtN07516;
+       Tue, 3 Aug 2010 05:17:55 -0700 (PDT)
+X-MimeOLE: Produced By Microsoft Exchange V6.5
+Content-class: urn:content-classes:message
+In-Reply-To: <4381.1280815590@neuling.org>
+X-MS-Has-Attach: 
+X-MS-TNEF-Correlator: 
+Thread-Topic: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Thread-Index: Acsy0pTOmhzzm4GETvS4r2R2pYb40wAMtx8w
+X-Scanned-By: MIMEDefang 2.43
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1017995>
+
+This happened after change to book-e definitions.
+There are patches that address this issue.
+
+> -----Original Message-----
+> From: Michael Neuling [mailto:mikey@neuling.org]
+> Sent: Tuesday, August 03, 2010 2:07 AM
+> To: Timur Tabi
+> Cc: Alexandre Bounine; linuxppc-dev@lists.ozlabs.org;
+linux-kernel@vger.kernel.org;
+> thomas.moll@sysgo.com
+> Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO
+port
+> 
+> > > MCSR_MASK is not defined anywhere, so when I compile this code, I
+get this:
+> >
+> > Never mind.  I see that it's been fixed already, and that the patch
+> > that removed MCSR_MASK was posted around the same time that this
+patch
+> > was posted.
+> 
+> I don't know what happened here but 2.6.35 is broken because of this
+> problem:
+> 
+> arch/powerpc/sysdev/fsl_rio.c:248: error: 'MCSR_MASK' undeclared
+(first use in this function)
+> arch/powerpc/sysdev/fsl_rio.c:248: error: (Each undeclared identifier
+is reported only once
+> arch/powerpc/sysdev/fsl_rio.c:248: error: for each function it appears
+in.)
+> arch/powerpc/sysdev/fsl_rio.c:250: error: 'MCSR_BUS_RBERR' undeclared
+(first use in this function)
+> 
+> Mikey
+
+
diff --git a/test/corpora/lkml/cur/1382298775.002992:2, b/test/corpora/lkml/cur/1382298775.002992:2,
new file mode 100644 (file)
index 0000000..0f11acd
--- /dev/null
@@ -0,0 +1,87 @@
+From: Timur Tabi <timur@freescale.com>
+Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Tue, 3 Aug 2010 08:01:51 -0500
+Lines: 25
+Message-ID: <AANLkTinpwYnyc1oN1VbtBgUF6bk6E5q_Gq1Dj3WXV3wc@mail.gmail.com>
+References: <20100308191005.GE4324@amak.tundra.com> <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com> 
+       <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com> 
+       <4381.1280815590@neuling.org> <0CE8B6BE3C4AD74AB97D9D29BD24E5520114309D@CORPEXCH1.na.ads.idt.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1
+Cc: Michael Neuling <mikey@neuling.org>,
+       Alexandre Bounine <abounine@tundra.com>,
+       linuxppc-dev@lists.ozlabs.org, linux-kernel@vger.kernel.org,
+       thomas.moll@sysgo.com, Kumar Gala <galak@kernel.crashing.org>
+To: "Bounine, Alexandre" <Alexandre.Bounine@idt.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Aug 03 15:02:39 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OgH8b-0003r0-5v
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 03 Aug 2010 15:02:33 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1756383Ab0HCNCY (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 3 Aug 2010 09:02:24 -0400
+Received: from mail-qy0-f181.google.com ([209.85.216.181]:47377 "EHLO
+       mail-qy0-f181.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1754253Ab0HCNCX (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 3 Aug 2010 09:02:23 -0400
+Received: by qyk7 with SMTP id 7so647758qyk.19
+        for <linux-kernel@vger.kernel.org>; Tue, 03 Aug 2010 06:02:22 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:received:mime-version:sender:received
+         :in-reply-to:references:from:date:x-google-sender-auth:message-id
+         :subject:to:cc:content-type;
+        bh=vTJghTE4Rwcgvgu1RS/86u/ljjztFlVQ5ODYWXBRkUM=;
+        b=p7S+ZVc0INWI6uXFwsLVTTEnV8wFAB0u0cDLt5qp0gyuMbF9yqXhukSTbYS8Vf8gCk
+         UFDmrOGjzC1whtvZnRS+Q80vVTR3+1URt/RTCUqirvalLvgluNrzP6sQ3xccFy4LkdLi
+         nGsgcNEqVwPPZgg3uSqew6B5UIoH7S00YzAYU=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=mime-version:sender:in-reply-to:references:from:date
+         :x-google-sender-auth:message-id:subject:to:cc:content-type;
+        b=qNVeIlTzozhY9MXH5PHYIsAL8T7zOBZ+0hWrBlbEy0PiBHW1AAv9nNd6FspugBZVUW
+         q7iPmhg0n6Oa3KFBNjs42dInyCPUqiQs10rGTQCJsSVITmZ/NA9sf8FFbI+Dg7xQiJKj
+         TN/8W0tBK9mUiqVvoO1avTKG1hqyMwTdMqlaM=
+Received: by 10.224.73.18 with SMTP id o18mr2669587qaj.354.1280840541149; Tue, 
+       03 Aug 2010 06:02:21 -0700 (PDT)
+Received: by 10.220.112.69 with HTTP; Tue, 3 Aug 2010 06:01:51 -0700 (PDT)
+In-Reply-To: <0CE8B6BE3C4AD74AB97D9D29BD24E5520114309D@CORPEXCH1.na.ads.idt.com>
+X-Google-Sender-Auth: lBedzmn1VMYh0pQjuCJuDw-lNh8
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1018009>
+
+On Tue, Aug 3, 2010 at 7:17 AM, Bounine, Alexandre
+<Alexandre.Bounine@idt.com> wrote:
+> This happened after change to book-e definitions.
+> There are patches that address this issue.
+
+And those patches should have been applied before 2.6.35 was released.
+ Someone dropped the ball.  2.6.35 is broken for a number of PowerPC
+boards:
+
+$ make mpc85xx_defconfig
+...
+$ make
+...
+  CC      arch/powerpc/sysdev/fsl_rio.o
+arch/powerpc/sysdev/fsl_rio.c: In function 'fsl_rio_mcheck_exception':
+arch/powerpc/sysdev/fsl_rio.c:248: error: 'MCSR_MASK' undeclared
+(first use in this function)
+arch/powerpc/sysdev/fsl_rio.c:248: error: (Each undeclared identifier
+is reported only once
+arch/powerpc/sysdev/fsl_rio.c:248: error: for each function it appears in.)
+make[1]: *** [arch/powerpc/sysdev/fsl_rio.o] Error 1
+
+-- 
+Timur Tabi
+Linux kernel developer at Freescale
+
+
diff --git a/test/corpora/lkml/cur/1382298775.002999:2, b/test/corpora/lkml/cur/1382298775.002999:2,
new file mode 100644 (file)
index 0000000..e6456b6
--- /dev/null
@@ -0,0 +1,109 @@
+From: "Bounine, Alexandre" <Alexandre.Bounine@idt.com>
+Subject: RE: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Tue, 3 Aug 2010 06:24:47 -0700
+Lines: 40
+Message-ID: <0CE8B6BE3C4AD74AB97D9D29BD24E552011430BC@CORPEXCH1.na.ads.idt.com>
+References: <20100308191005.GE4324@amak.tundra.com>
+       <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com>
+       <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com>
+       <4381.1280815590@neuling.org>
+       <0CE8B6BE3C4AD74AB97D9D29BD24E5520114309D@CORPEXCH1.na.ads.idt.com>
+       <AANLkTinpwYnyc1oN1VbtBgUF6bk6E5q_Gq1Dj3WXV3wc@mail.gmail.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Michael Neuling <mikey@neuling.org>, linux-kernel@vger.kernel.org,
+       Alexandre Bounine <abounine@tundra.com>, thomas.moll@sysgo.com,
+       linuxppc-dev@lists.ozlabs.org
+To: "Timur Tabi" <timur@freescale.com>
+X-From: linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org Tue Aug 03 15:25:22 2010
+Return-path: <linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org>
+Envelope-to: glppe-linuxppc-embedded-2@m.gmane.org
+Received: from ozlabs.org ([203.10.76.45])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org>)
+       id 1OgHUd-0006wG-MW
+       for glppe-linuxppc-embedded-2@m.gmane.org; Tue, 03 Aug 2010 15:25:20 +0200
+Received: from bilbo.ozlabs.org (localhost [127.0.0.1])
+       by ozlabs.org (Postfix) with ESMTP id 54FA51007E4
+       for <glppe-linuxppc-embedded-2@m.gmane.org>; Tue,  3 Aug 2010 23:25:09 +1000 (EST)
+Received: from mxout1.idt.com (mxout1.idt.com [157.165.5.25])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (Client CN "mxout1.idt.com", Issuer "idt.com" (not verified))
+       by ozlabs.org (Postfix) with ESMTPS id 5C917B70A6
+       for <linuxppc-dev@lists.ozlabs.org>;
+       Tue,  3 Aug 2010 23:25:00 +1000 (EST)
+Received: from mail.idt.com (localhost [127.0.0.1])
+       by mxout1.idt.com (8.13.1/8.13.1) with ESMTP id o73DOrjO005661;
+       Tue, 3 Aug 2010 06:24:54 -0700
+Received: from corpml1.corp.idt.com (corpml1.corp.idt.com [157.165.140.20])
+       by mail.idt.com (8.13.8/8.13.8) with ESMTP id o73DOndw022603;
+       Tue, 3 Aug 2010 06:24:50 -0700 (PDT)
+Received: from CORPEXCH1.na.ads.idt.com (localhost [127.0.0.1])
+       by corpml1.corp.idt.com (8.11.7p1+Sun/8.11.7) with ESMTP id
+       o73DOml00291; Tue, 3 Aug 2010 06:24:48 -0700 (PDT)
+X-MimeOLE: Produced By Microsoft Exchange V6.5
+Content-class: urn:content-classes:message
+In-Reply-To: <AANLkTinpwYnyc1oN1VbtBgUF6bk6E5q_Gq1Dj3WXV3wc@mail.gmail.com>
+X-MS-Has-Attach: 
+X-MS-TNEF-Correlator: 
+Thread-Topic: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Thread-Index: AcszC8UElBYHaZlHSsmvZEE2KBL+0wAAhg7A
+X-Scanned-By: MIMEDefang 2.43
+X-BeenThere: linuxppc-dev@lists.ozlabs.org
+X-Mailman-Version: 2.1.13
+Precedence: list
+List-Id: Linux on PowerPC Developers Mail List <linuxppc-dev.lists.ozlabs.org>
+List-Unsubscribe: <https://lists.ozlabs.org/options/linuxppc-dev>,
+       <mailto:linuxppc-dev-request@lists.ozlabs.org?subject=unsubscribe>
+List-Archive: <http://lists.ozlabs.org/pipermail/linuxppc-dev>
+List-Post: <mailto:linuxppc-dev@lists.ozlabs.org>
+List-Help: <mailto:linuxppc-dev-request@lists.ozlabs.org?subject=help>
+List-Subscribe: <https://lists.ozlabs.org/listinfo/linuxppc-dev>,
+       <mailto:linuxppc-dev-request@lists.ozlabs.org?subject=subscribe>
+Sender: linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org
+Errors-To: linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1018016>
+
+Yang Li pointed to these patches in his post from July 23, 2010.
+It would be nice to have these patches in mainline code. 
+
+> -----Original Message-----
+> From: timur.tabi@gmail.com [mailto:timur.tabi@gmail.com] On Behalf Of
+Timur Tabi
+> Sent: Tuesday, August 03, 2010 9:02 AM
+> To: Bounine, Alexandre
+> Cc: Michael Neuling; Alexandre Bounine; linuxppc-dev@lists.ozlabs.org;
+linux-kernel@vger.kernel.org;
+> thomas.moll@sysgo.com; Kumar Gala
+> Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO
+port
+> 
+> On Tue, Aug 3, 2010 at 7:17 AM, Bounine, Alexandre
+> <Alexandre.Bounine@idt.com> wrote:
+> > This happened after change to book-e definitions.
+> > There are patches that address this issue.
+> 
+> And those patches should have been applied before 2.6.35 was released.
+>  Someone dropped the ball.  2.6.35 is broken for a number of PowerPC
+> boards:
+> 
+> $ make mpc85xx_defconfig
+> ....
+> $ make
+> ....
+>   CC      arch/powerpc/sysdev/fsl_rio.o
+> arch/powerpc/sysdev/fsl_rio.c: In function 'fsl_rio_mcheck_exception':
+> arch/powerpc/sysdev/fsl_rio.c:248: error: 'MCSR_MASK' undeclared
+> (first use in this function)
+> arch/powerpc/sysdev/fsl_rio.c:248: error: (Each undeclared identifier
+> is reported only once
+> arch/powerpc/sysdev/fsl_rio.c:248: error: for each function it appears
+in.)
+> make[1]: *** [arch/powerpc/sysdev/fsl_rio.o] Error 1
+> 
+> --
+> Timur Tabi
+> Linux kernel developer at Freescale
+
+
diff --git a/test/corpora/lkml/cur/1382298775.003976:2, b/test/corpora/lkml/cur/1382298775.003976:2,
new file mode 100644 (file)
index 0000000..a6ff629
--- /dev/null
@@ -0,0 +1,96 @@
+From: Michael Neuling <mikey@neuling.org>
+Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Thu, 05 Aug 2010 13:34:20 +1000
+Lines: 50
+Message-ID: <26581.1280979260@neuling.org>
+References: <20100308191005.GE4324@amak.tundra.com> <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com> <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com> <4381.1280815590@neuling.org> <0CE8B6BE3C4AD74AB97D9D29BD24E5520114309D@CORPEXCH1.na.ads.idt.com> <AANLkTinpwYnyc1oN1VbtBgUF6bk6E5q_Gq1Dj3WXV3wc@mail.gmail.com> <0CE8B6BE3C4AD74AB97D9D29BD24E552011430BC@CORPEXCH1.na.ads.idt.com>
+Cc: "Timur Tabi" <timur@freescale.com>,
+       "Alexandre Bounine" <abounine@tundra.com>,
+       linuxppc-dev@lists.ozlabs.org, linux-kernel@vger.kernel.org,
+       thomas.moll@sysgo.com, "Kumar Gala" <galak@kernel.crashing.org>
+To: "Bounine, Alexandre" <Alexandre.Bounine@idt.com>
+X-From: linux-kernel-owner@vger.kernel.org Thu Aug 05 05:34:37 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1OgrE1-00024O-Bh
+       for glk-linux-kernel-3@lo.gmane.org; Thu, 05 Aug 2010 05:34:33 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1758775Ab0HEDeX (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Wed, 4 Aug 2010 23:34:23 -0400
+Received: from ozlabs.org ([203.10.76.45]:40810 "EHLO ozlabs.org"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1758704Ab0HEDeV (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Wed, 4 Aug 2010 23:34:21 -0400
+Received: from localhost.localdomain (localhost [127.0.0.1])
+       by ozlabs.org (Postfix) with ESMTP id 97995B70D8;
+       Thu,  5 Aug 2010 13:34:20 +1000 (EST)
+Received: by localhost.localdomain (Postfix, from userid 1000)
+       id 456CDCC199; Thu,  5 Aug 2010 13:34:20 +1000 (EST)
+Received: from neuling.org (localhost [127.0.0.1])
+       by localhost.localdomain (Postfix) with ESMTP id 404C8C6123;
+       Thu,  5 Aug 2010 13:34:20 +1000 (EST)
+In-reply-to: <0CE8B6BE3C4AD74AB97D9D29BD24E552011430BC@CORPEXCH1.na.ads.idt.com>
+Comments: In-reply-to "Bounine, Alexandre" <Alexandre.Bounine@idt.com>
+   message dated "Tue, 03 Aug 2010 06:24:47 -0700."
+X-Mailer: MH-E 8.2; nmh 1.3; GNU Emacs 23.1.1
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1019004>
+
+
+
+In message <0CE8B6BE3C4AD74AB97D9D29BD24E552011430BC@CORPEXCH1.na.ads.idt.com> you wrote:
+> Yang Li pointed to these patches in his post from July 23, 2010.
+> It would be nice to have these patches in mainline code.=20
+
+This is still broken in Kumar's latest tree.  Do you guys wanna repost
+them so Kumar can pick them up easily?
+
+Mikey
+
+> 
+> > -----Original Message-----
+> > From: timur.tabi@gmail.com [mailto:timur.tabi@gmail.com] On Behalf Of
+> Timur Tabi
+> > Sent: Tuesday, August 03, 2010 9:02 AM
+> > To: Bounine, Alexandre
+> > Cc: Michael Neuling; Alexandre Bounine; linuxppc-dev@lists.ozlabs.org;
+> linux-kernel@vger.kernel.org;
+> > thomas.moll@sysgo.com; Kumar Gala
+> > Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO
+> port
+> >=20
+> > On Tue, Aug 3, 2010 at 7:17 AM, Bounine, Alexandre
+> > <Alexandre.Bounine@idt.com> wrote:
+> > > This happened after change to book-e definitions.
+> > > There are patches that address this issue.
+> >=20
+> > And those patches should have been applied before 2.6.35 was released.
+> >  Someone dropped the ball.  2.6.35 is broken for a number of PowerPC
+> > boards:
+> >=20
+> > $ make mpc85xx_defconfig
+> > ....
+> > $ make
+> > ....
+> >   CC      arch/powerpc/sysdev/fsl_rio.o
+> > arch/powerpc/sysdev/fsl_rio.c: In function 'fsl_rio_mcheck_exception':
+> > arch/powerpc/sysdev/fsl_rio.c:248: error: 'MCSR_MASK' undeclared
+> > (first use in this function)
+> > arch/powerpc/sysdev/fsl_rio.c:248: error: (Each undeclared identifier
+> > is reported only once
+> > arch/powerpc/sysdev/fsl_rio.c:248: error: for each function it appears
+> in.)
+> > make[1]: *** [arch/powerpc/sysdev/fsl_rio.o] Error 1
+> >=20
+> > --
+> > Timur Tabi
+> > Linux kernel developer at Freescale
+> 
+
+
diff --git a/test/corpora/lkml/cur/1382298775.004354:2, b/test/corpora/lkml/cur/1382298775.004354:2,
new file mode 100644 (file)
index 0000000..2d69a12
--- /dev/null
@@ -0,0 +1,170 @@
+From: "Bounine, Alexandre" <Alexandre.Bounine@idt.com>
+Subject: RE: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Thu, 5 Aug 2010 10:25:18 -0700
+Lines: 99
+Message-ID: <0CE8B6BE3C4AD74AB97D9D29BD24E552011935BD@CORPEXCH1.na.ads.idt.com>
+References: <20100308191005.GE4324@amak.tundra.com>
+       <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com>
+       <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com>
+       <4381.1280815590@neuling.org>
+       <0CE8B6BE3C4AD74AB97D9D29BD24E5520114309D@CORPEXCH1.na.ads.idt.com>
+       <AANLkTinpwYnyc1oN1VbtBgUF6bk6E5q_Gq1Dj3WXV3wc@mail.gmail.com>
+       <0CE8B6BE3C4AD74AB97D9D29BD24E552011430BC@CORPEXCH1.na.ads.idt.com>
+       <26581.1280979260@neuling.org>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Li Yang-R58472 <r58472@freescale.com>, linux-kernel@vger.kernel.org,
+       Alexandre Bounine <abounine@tundra.com>, thomas.moll@sysgo.com,
+       linuxppc-dev@lists.ozlabs.org, Timur Tabi <timur@freescale.com>
+To: "Michael Neuling" <mikey@neuling.org>
+X-From: linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org Thu Aug 05 19:25:54 2010
+Return-path: <linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org>
+Envelope-to: glppe-linuxppc-embedded-2@m.gmane.org
+Received: from ozlabs.org ([203.10.76.45])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org>)
+       id 1Oh4CX-0002Xd-Hu
+       for glppe-linuxppc-embedded-2@m.gmane.org; Thu, 05 Aug 2010 19:25:54 +0200
+Received: from bilbo.ozlabs.org (localhost [127.0.0.1])
+       by ozlabs.org (Postfix) with ESMTP id ED044100873
+       for <glppe-linuxppc-embedded-2@m.gmane.org>; Fri,  6 Aug 2010 03:25:45 +1000 (EST)
+Received: from mxout1.idt.com (mxout1.idt.com [157.165.5.25])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (Client CN "mxout1.idt.com", Issuer "idt.com" (not verified))
+       by ozlabs.org (Postfix) with ESMTPS id 43F72B6EEA
+       for <linuxppc-dev@lists.ozlabs.org>;
+       Fri,  6 Aug 2010 03:25:34 +1000 (EST)
+Received: from mail.idt.com (localhost [127.0.0.1])
+       by mxout1.idt.com (8.13.1/8.13.1) with ESMTP id o75HPQdX013269;
+       Thu, 5 Aug 2010 10:25:26 -0700
+Received: from corpml1.corp.idt.com (corpml1.corp.idt.com [157.165.140.20])
+       by mail.idt.com (8.13.8/8.13.8) with ESMTP id o75HPMOi016437;
+       Thu, 5 Aug 2010 10:25:23 -0700 (PDT)
+Received: from CORPEXCH1.na.ads.idt.com (localhost [127.0.0.1])
+       by corpml1.corp.idt.com (8.11.7p1+Sun/8.11.7) with ESMTP id
+       o75HPKp19185; Thu, 5 Aug 2010 10:25:21 -0700 (PDT)
+X-MimeOLE: Produced By Microsoft Exchange V6.5
+Content-class: urn:content-classes:message
+In-Reply-To: <26581.1280979260@neuling.org>
+X-MS-Has-Attach: 
+X-MS-TNEF-Correlator: 
+Thread-Topic: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Thread-Index: Acs0TsVj0+lZKwxtSsOT8qDn1XxpJAAdBmhA
+X-Scanned-By: MIMEDefang 2.43
+X-BeenThere: linuxppc-dev@lists.ozlabs.org
+X-Mailman-Version: 2.1.13
+Precedence: list
+List-Id: Linux on PowerPC Developers Mail List <linuxppc-dev.lists.ozlabs.org>
+List-Unsubscribe: <https://lists.ozlabs.org/options/linuxppc-dev>,
+       <mailto:linuxppc-dev-request@lists.ozlabs.org?subject=unsubscribe>
+List-Archive: <http://lists.ozlabs.org/pipermail/linuxppc-dev>
+List-Post: <mailto:linuxppc-dev@lists.ozlabs.org>
+List-Help: <mailto:linuxppc-dev-request@lists.ozlabs.org?subject=help>
+List-Subscribe: <https://lists.ozlabs.org/listinfo/linuxppc-dev>,
+       <mailto:linuxppc-dev-request@lists.ozlabs.org?subject=subscribe>
+Sender: linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org
+Errors-To: linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1019383>
+
+Below is a copy of Leo's message with pointers to the patches.
+
+Alex.
+>Subject: [PATCH] RapidIO,powerpc/85xx: remove MCSR_MASK in fsl_rio
+>
+>Fixes compile problem caused by MCSR_MASK removal from book-E
+definitions.
+
+Hi Alex,
+
+Only with your patch, there will still be problem on SRIO platforms
+other than MPC85xx.
+
+I have posted a patch series to fix this together with several
+compatibility issues a month before.
+
+http://patchwork.ozlabs.org/patch/56135/
+http://patchwork.ozlabs.org/patch/56136/
+http://patchwork.ozlabs.org/patch/56138/
+http://patchwork.ozlabs.org/patch/56137/
+
+
+Can anyone pick the patch series quickly as currently there is a compile
+error when SRIO is enabled.
+
+- Leo
+
+
+> -----Original Message-----
+> From: Michael Neuling [mailto:mikey@neuling.org]
+> Sent: Wednesday, August 04, 2010 11:34 PM
+> To: Bounine, Alexandre
+> Cc: Timur Tabi; Alexandre Bounine; linuxppc-dev@lists.ozlabs.org;
+linux-kernel@vger.kernel.org;
+> thomas.moll@sysgo.com; Kumar Gala
+> Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO
+port
+> 
+> 
+> 
+> In message
+<0CE8B6BE3C4AD74AB97D9D29BD24E552011430BC@CORPEXCH1.na.ads.idt.com> you
+wrote:
+> > Yang Li pointed to these patches in his post from July 23, 2010.
+> > It would be nice to have these patches in mainline code.=20
+> 
+> This is still broken in Kumar's latest tree.  Do you guys wanna repost
+> them so Kumar can pick them up easily?
+> 
+> Mikey
+> 
+> >
+> > > -----Original Message-----
+> > > From: timur.tabi@gmail.com [mailto:timur.tabi@gmail.com] On Behalf
+Of
+> > Timur Tabi
+> > > Sent: Tuesday, August 03, 2010 9:02 AM
+> > > To: Bounine, Alexandre
+> > > Cc: Michael Neuling; Alexandre Bounine;
+linuxppc-dev@lists.ozlabs.org;
+> > linux-kernel@vger.kernel.org;
+> > > thomas.moll@sysgo.com; Kumar Gala
+> > > Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for
+SRIO
+> > port
+> > >=20
+> > > On Tue, Aug 3, 2010 at 7:17 AM, Bounine, Alexandre
+> > > <Alexandre.Bounine@idt.com> wrote:
+> > > > This happened after change to book-e definitions.
+> > > > There are patches that address this issue.
+> > >=20
+> > > And those patches should have been applied before 2.6.35 was
+released.
+> > >  Someone dropped the ball.  2.6.35 is broken for a number of
+PowerPC
+> > > boards:
+> > >=20
+> > > $ make mpc85xx_defconfig
+> > > ....
+> > > $ make
+> > > ....
+> > >   CC      arch/powerpc/sysdev/fsl_rio.o
+> > > arch/powerpc/sysdev/fsl_rio.c: In function
+'fsl_rio_mcheck_exception':
+> > > arch/powerpc/sysdev/fsl_rio.c:248: error: 'MCSR_MASK' undeclared
+> > > (first use in this function)
+> > > arch/powerpc/sysdev/fsl_rio.c:248: error: (Each undeclared
+identifier
+> > > is reported only once
+> > > arch/powerpc/sysdev/fsl_rio.c:248: error: for each function it
+appears
+> > in.)
+> > > make[1]: *** [arch/powerpc/sysdev/fsl_rio.o] Error 1
+> > >=20
+> > > --
+> > > Timur Tabi
+> > > Linux kernel developer at Freescale
+> >
+
+
diff --git a/test/corpora/lkml/cur/1382298775.004363:2, b/test/corpora/lkml/cur/1382298775.004363:2,
new file mode 100644 (file)
index 0000000..f4198fb
--- /dev/null
@@ -0,0 +1,95 @@
+From: Kumar Gala <galak@kernel.crashing.org>
+Subject: Re: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Thu, 5 Aug 2010 12:53:03 -0500
+Lines: 34
+Message-ID: <C9528078-D64C-4944-B960-0E985B3EE0BA@kernel.crashing.org>
+References: <20100308191005.GE4324@amak.tundra.com>
+       <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com>
+       <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com>
+       <4381.1280815590@neuling.org>
+       <0CE8B6BE3C4AD74AB97D9D29BD24E5520114309D@CORPEXCH1.na.ads.idt.com>
+       <AANLkTinpwYnyc1oN1VbtBgUF6bk6E5q_Gq1Dj3WXV3wc@mail.gmail.com>
+       <0CE8B6BE3C4AD74AB97D9D29BD24E552011430BC@CORPEXCH1.na.ads.idt.com>
+       <26581.1280979260@neuling.org>
+       <0CE8B6BE3C4AD74AB97D9D29BD24E552011935BD@CORPEXCH1.na.ads.idt.com>
+Mime-Version: 1.0 (Apple Message framework v1081)
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Michael Neuling <mikey@neuling.org>, Li Yang-R58472 <r58472@freescale.com>,
+       linux-kernel@vger.kernel.org,
+       Alexandre Bounine <abounine@tundra.com>, thomas.moll@sysgo.com,
+       linuxppc-dev@lists.ozlabs.org, Timur Tabi <timur@freescale.com>
+To: "Bounine, Alexandre" <Alexandre.Bounine@IDT.COM>
+X-From: linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org Thu Aug 05 19:53:49 2010
+Return-path: <linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org>
+Envelope-to: glppe-linuxppc-embedded-2@m.gmane.org
+Received: from ozlabs.org ([203.10.76.45])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org>)
+       id 1Oh4dY-0000mU-OI
+       for glppe-linuxppc-embedded-2@m.gmane.org; Thu, 05 Aug 2010 19:53:49 +0200
+Received: from bilbo.ozlabs.org (localhost [127.0.0.1])
+       by ozlabs.org (Postfix) with ESMTP id C0974B71BD
+       for <glppe-linuxppc-embedded-2@m.gmane.org>; Fri,  6 Aug 2010 03:53:41 +1000 (EST)
+Received: from gate.crashing.org (gate.crashing.org [63.228.1.57])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (Client did not present a certificate)
+       by ozlabs.org (Postfix) with ESMTPS id 707ADB6EF1
+       for <linuxppc-dev@lists.ozlabs.org>;
+       Fri,  6 Aug 2010 03:53:31 +1000 (EST)
+Received: from [IPv6:::1] (localhost.localdomain [127.0.0.1])
+       by gate.crashing.org (8.14.1/8.13.8) with ESMTP id o75Hr4pE020296;
+       Thu, 5 Aug 2010 12:53:05 -0500
+In-Reply-To: <0CE8B6BE3C4AD74AB97D9D29BD24E552011935BD@CORPEXCH1.na.ads.idt.com>
+X-Mailer: Apple Mail (2.1081)
+X-BeenThere: linuxppc-dev@lists.ozlabs.org
+X-Mailman-Version: 2.1.13
+Precedence: list
+List-Id: Linux on PowerPC Developers Mail List <linuxppc-dev.lists.ozlabs.org>
+List-Unsubscribe: <https://lists.ozlabs.org/options/linuxppc-dev>,
+       <mailto:linuxppc-dev-request@lists.ozlabs.org?subject=unsubscribe>
+List-Archive: <http://lists.ozlabs.org/pipermail/linuxppc-dev>
+List-Post: <mailto:linuxppc-dev@lists.ozlabs.org>
+List-Help: <mailto:linuxppc-dev-request@lists.ozlabs.org?subject=help>
+List-Subscribe: <https://lists.ozlabs.org/listinfo/linuxppc-dev>,
+       <mailto:linuxppc-dev-request@lists.ozlabs.org?subject=subscribe>
+Sender: linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org
+Errors-To: linuxppc-dev-bounces+glppe-linuxppc-embedded-2=m.gmane.org@lists.ozlabs.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1019393>
+
+
+On Aug 5, 2010, at 12:25 PM, Bounine, Alexandre wrote:
+
+> Below is a copy of Leo's message with pointers to the patches.
+> 
+> Alex.
+> 
+>> Subject: [PATCH] RapidIO,powerpc/85xx: remove MCSR_MASK in fsl_rio
+>> 
+>> Fixes compile problem caused by MCSR_MASK removal from book-E
+> definitions.
+> 
+> Hi Alex,
+> 
+> Only with your patch, there will still be problem on SRIO platforms
+> other than MPC85xx.
+> 
+> I have posted a patch series to fix this together with several
+> compatibility issues a month before.
+> 
+> http://patchwork.ozlabs.org/patch/56135/
+> http://patchwork.ozlabs.org/patch/56136/
+> http://patchwork.ozlabs.org/patch/56138/
+> http://patchwork.ozlabs.org/patch/56137/
+> 
+> 
+> Can anyone pick the patch series quickly as currently there is a compile
+> error when SRIO is enabled.
+> 
+> - Leo
+
+I'm looking at this now and wondering what we added the mcheck handler for in the first place and what its trying to accomplish.
+
+- k
+
+
diff --git a/test/corpora/lkml/cur/1382298775.004374:2, b/test/corpora/lkml/cur/1382298775.004374:2,
new file mode 100644 (file)
index 0000000..48558ad
--- /dev/null
@@ -0,0 +1,75 @@
+From: "Bounine, Alexandre" <Alexandre.Bounine@idt.com>
+Subject: RE: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Date: Thu, 5 Aug 2010 11:17:58 -0700
+Lines: 18
+Message-ID: <0CE8B6BE3C4AD74AB97D9D29BD24E55201193609@CORPEXCH1.na.ads.idt.com>
+References: <20100308191005.GE4324@amak.tundra.com> <AANLkTine3pc2Ai2Woj81Y9fS_KgGs1sIMb2NMR6G74ww@mail.gmail.com> <AANLkTinKbimKyLpvFD7KOvavshu_n8gRcp2BvEJj0XZQ@mail.gmail.com> <4381.1280815590@neuling.org> <0CE8B6BE3C4AD74AB97D9D29BD24E5520114309D@CORPEXCH1.na.ads.idt.com> <AANLkTinpwYnyc1oN1VbtBgUF6bk6E5q_Gq1Dj3WXV3wc@mail.gmail.com> <0CE8B6BE3C4AD74AB97D9D29BD24E552011430BC@CORPEXCH1.na.ads.idt.com> <26581.1280979260@neuling.org> <0CE8B6BE3C4AD74AB97D9D29BD24E552011935BD@CORPEXCH1.na.ads.idt.com> <C9528078-D64C-4944-B960-0E985B3EE0BA@kernel.crashing.org>
+Mime-Version: 1.0
+Content-Type: text/plain;
+       charset="us-ascii"
+Content-Transfer-Encoding: 8BIT
+Cc: "Michael Neuling" <mikey@neuling.org>,
+       "Timur Tabi" <timur@freescale.com>,
+       "Alexandre Bounine" <abounine@tundra.com>,
+       <linuxppc-dev@lists.ozlabs.org>, <linux-kernel@vger.kernel.org>,
+       <thomas.moll@sysgo.com>, "Li Yang-R58472" <r58472@freescale.com>
+To: "Kumar Gala" <galak@kernel.crashing.org>
+X-From: linux-kernel-owner@vger.kernel.org Thu Aug 05 20:18:33 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1Oh51V-00075S-1W
+       for glk-linux-kernel-3@lo.gmane.org; Thu, 05 Aug 2010 20:18:33 +0200
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S934019Ab0HESSU (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Thu, 5 Aug 2010 14:18:20 -0400
+Received: from mxout1.idt.com ([157.165.5.25]:47318 "EHLO mxout1.idt.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S933252Ab0HESSS convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Thu, 5 Aug 2010 14:18:18 -0400
+Received: from mail.idt.com (localhost [127.0.0.1])
+       by mxout1.idt.com (8.13.1/8.13.1) with ESMTP id o75II315017058;
+       Thu, 5 Aug 2010 11:18:03 -0700
+Received: from corpml1.corp.idt.com (corpml1.corp.idt.com [157.165.140.20])
+       by mail.idt.com (8.13.8/8.13.8) with ESMTP id o75II1Ek021771;
+       Thu, 5 Aug 2010 11:18:01 -0700 (PDT)
+Received: from CORPEXCH1.na.ads.idt.com (localhost [127.0.0.1])
+       by corpml1.corp.idt.com (8.11.7p1+Sun/8.11.7) with ESMTP id o75II0M19896;
+       Thu, 5 Aug 2010 11:18:00 -0700 (PDT)
+X-MimeOLE: Produced By Microsoft Exchange V6.5
+Content-class: urn:content-classes:message
+In-Reply-To: <C9528078-D64C-4944-B960-0E985B3EE0BA@kernel.crashing.org>
+X-MS-Has-Attach: 
+X-MS-TNEF-Correlator: 
+Thread-Topic: [PATCH v2 5/7] powerpc/85xx: Add MChk handler for SRIO port
+Thread-Index: Acs0x5rSJ4s7P9ssRjKYVWxFQe3GMgAARQEw
+X-Scanned-By: MIMEDefang 2.43
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1019404>
+
+> I'm looking at this now and wondering what we added the mcheck handler
+for in the first place and what
+> its trying to accomplish.
+> 
+> - k
+
+This protects system from hanging if RIO link fails or enters error
+state. In some situations following maintenance read may initiate link
+recovery from error state.
+
+As it is now, MCheck mostly prevents system from hanging, but it also
+adds sense to return status of maintenance read routine. I am using
+return status in my new set of patches to check if RIO link is valid
+during error recovery.
+
+Alex.
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002253:2, b/test/corpora/lkml/cur/1382298793.002253:2,
new file mode 100644 (file)
index 0000000..cbd67e8
--- /dev/null
@@ -0,0 +1,208 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 00/44] remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:19 -0800
+Lines: 158
+Message-ID: <cover.1289789604.git.joe@perches.com>
+Cc: linux-arm-kernel@lists.infradead.org, linux-kernel@vger.kernel.org,
+       linux-tegra@vger.kernel.org, microblaze-uclinux@itee.uq.edu.au,
+       user-mode-linux-devel@lists.sourceforge.net,
+       user-mode-linux-user@lists.sourceforge.net,
+       cpufreq@vger.kernel.org, linux-i2c@vger.kernel.org,
+       netdev@vger.kernel.org, linux-media@vger.kernel.org,
+       linux-mmc@vger.kernel.org, e1000-devel@lists.sourceforge.net,
+       linux-wireless@vger.kernel.org, ath9k-devel@lists.ath9k.org,
+       platform-driver-x86@vger.kernel.org,
+       ibm-acpi-devel@lists.sourceforge.net, linux-s390@vger.kernel.org,
+       linux-scsi@vger.kernel.org,
+       spi-devel-general@lists.sourceforge.net,
+       devel@driverdev.osuosl.org, linux-usb@vger.kernel.org,
+       xen-devel@lists.xensource.com, virtualization@lists.osdl.org,
+       v9fs-developer@lists.sourceforge.net, ceph-devel@vger.kernel.org,
+       logfs@logfs.org, linux-nfs@vger.kernel.org,
+       ocfs2-devel@oss.oracle.com, linu
+To: Jiri Kosina <trivial@kernel.org>
+X-From: cpufreq-owner@vger.kernel.org Mon Nov 15 04:05:30 2010
+Return-path: <cpufreq-owner@vger.kernel.org>
+Envelope-to: glkc-cpufreq2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <cpufreq-owner@vger.kernel.org>)
+       id 1PHpNp-0000PT-Vh
+       for glkc-cpufreq2@lo.gmane.org; Mon, 15 Nov 2010 04:05:30 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1754566Ab0KODF2 (ORCPT <rfc822;glkc-cpufreq2@m.gmane.org>);
+       Sun, 14 Nov 2010 22:05:28 -0500
+Received: from mail.perches.com ([173.55.12.10]:1118 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751901Ab0KODF1 (ORCPT <rfc822;cpufreq@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:27 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 0D82A24368;
+       Sun, 14 Nov 2010 19:03:52 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+Sender: cpufreq-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <cpufreq.vger.kernel.org>
+X-Mailing-List: cpufreq@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062272>
+
+ya trivial series...
+
+Joe Perches (44):
+  arch/arm: Remove unnecessary semicolons
+  arch/microblaze: Remove unnecessary semicolons
+  arch/um: Remove unnecessary semicolons
+  drivers/cpufreq: Remove unnecessary semicolons
+  drivers/gpio: Remove unnecessary semicolons
+  drivers/i2c: Remove unnecessary semicolons
+  drivers/isdn: Remove unnecessary semicolons
+  drivers/leds: Remove unnecessary semicolons
+  drivers/media/video: Remove unnecessary semicolons
+  drivers/misc: Remove unnecessary semicolons
+  drivers/mmc: Remove unnecessary semicolons
+  drivers/net/bnx2x: Remove unnecessary semicolons
+  drivers/net/e1000e: Remove unnecessary semicolons
+  drivers/net/ixgbe: Remove unnecessary semicolons
+  drivers/net/vxge: Remove unnecessary semicolons
+  drivers/net/wireless/ath: Remove unnecessary semicolons
+  drivers/net/wireless/iwlwifi: Remove unnecessary semicolons
+  drivers/net/cnic.c: Remove unnecessary semicolons
+  drivers/platform/x86: Remove unnecessary semicolons
+  drivers/power: Remove unnecessary semicolons
+  drivers/s390/net: Remove unnecessary semicolons
+  drivers/scsi/be2iscsi: Remove unnecessary semicolons
+  drivers/scsi/bfa: Remove unnecessary semicolons
+  drivers/scsi/lpfc: Remove unnecessary semicolons
+  drivers/scsi/pm8001: Remove unnecessary semicolons
+  drivers/scsi/qla2xxx: Remove unnecessary semicolons
+  drivers/serial: Remove unnecessary semicolons
+  drivers/spi: Remove unnecessary semicolons
+  drivers/staging: Remove unnecessary semicolons
+  drivers/usb/gadget: Remove unnecessary semicolons
+  drivers/xen: Remove unnecessary semicolons
+  fs/9p: Remove unnecessary semicolons
+  fs/ceph: Remove unnecessary semicolons
+  fs/logfs: Remove unnecessary semicolons
+  fs/nfs: Remove unnecessary semicolons
+  fs/ocfs2: Remove unnecessary semicolons
+  fs/ubifs: Remove unnecessary semicolons
+  include/linux/if_macvlan.h: Remove unnecessary semicolons
+  include/net/caif/cfctrl.h: Remove unnecessary semicolons
+  mm/hugetlb.c: Remove unnecessary semicolons
+  net/ipv6/mcast.c: Remove unnecessary semicolons
+  net/sunrpc/addr.c: Remove unnecessary semicolons
+  sound/core/pcm_lib.c: Remove unnecessary semicolons
+  sound/soc/codecs: Remove unnecessary semicolons
+
+ arch/arm/mach-at91/at91cap9_devices.c              |    2 +-
+ arch/arm/mach-at91/at91sam9g45_devices.c           |    2 +-
+ arch/arm/mach-at91/at91sam9rl_devices.c            |    2 +-
+ arch/arm/mach-nuc93x/time.c                        |    2 +-
+ arch/arm/mach-tegra/tegra2_clocks.c                |    2 +-
+ arch/arm/mach-w90x900/cpu.c                        |    2 +-
+ arch/arm/plat-mxc/irq.c                            |    2 +-
+ arch/microblaze/lib/memmove.c                      |    2 +-
+ arch/um/drivers/mmapper_kern.c                     |    2 +-
+ drivers/cpufreq/cpufreq_conservative.c             |    2 +-
+ drivers/gpio/langwell_gpio.c                       |    2 +-
+ drivers/i2c/busses/i2c-designware.c                |    2 +-
+ drivers/isdn/hardware/mISDN/mISDNinfineon.c        |    4 ++--
+ drivers/isdn/hardware/mISDN/mISDNisar.c            |    2 +-
+ drivers/leds/leds-mc13783.c                        |    2 +-
+ drivers/media/video/cx88/cx88-blackbird.c          |    2 +-
+ drivers/media/video/davinci/vpfe_capture.c         |    2 +-
+ drivers/media/video/em28xx/em28xx-cards.c          |    2 +-
+ drivers/misc/bmp085.c                              |    2 +-
+ drivers/misc/isl29020.c                            |    2 +-
+ drivers/mmc/host/davinci_mmc.c                     |    2 +-
+ drivers/net/bnx2x/bnx2x_link.c                     |    4 ++--
+ drivers/net/bnx2x/bnx2x_main.c                     |    2 +-
+ drivers/net/cnic.c                                 |    2 +-
+ drivers/net/e1000e/netdev.c                        |    2 +-
+ drivers/net/ixgbe/ixgbe_sriov.c                    |    2 +-
+ drivers/net/vxge/vxge-main.c                       |    2 +-
+ drivers/net/wireless/ath/ath9k/htc.h               |    2 +-
+ drivers/net/wireless/iwlwifi/iwl-agn.c             |    2 +-
+ drivers/platform/x86/classmate-laptop.c            |    2 +-
+ drivers/platform/x86/thinkpad_acpi.c               |    2 +-
+ drivers/power/intel_mid_battery.c                  |    2 +-
+ drivers/s390/net/qeth_core_sys.c                   |    2 +-
+ drivers/scsi/be2iscsi/be_main.c                    |    4 ++--
+ drivers/scsi/bfa/bfa_fcs_lport.c                   |    2 +-
+ drivers/scsi/lpfc/lpfc_bsg.c                       |    2 +-
+ drivers/scsi/pm8001/pm8001_init.c                  |    2 +-
+ drivers/scsi/qla2xxx/qla_isr.c                     |    4 ++--
+ drivers/scsi/qla2xxx/qla_nx.c                      |    2 +-
+ drivers/serial/mrst_max3110.c                      |    2 +-
+ drivers/spi/amba-pl022.c                           |    2 +-
+ drivers/spi/spi_nuc900.c                           |    2 +-
+ .../staging/ath6kl/hif/sdio/linux_sdio/src/hif.c   |    2 +-
+ drivers/staging/ath6kl/os/linux/ar6000_drv.c       |    2 +-
+ drivers/staging/bcm/InterfaceInit.c                |    2 +-
+ drivers/staging/bcm/InterfaceIsr.c                 |    2 +-
+ drivers/staging/bcm/Misc.c                         |    4 ++--
+ .../comedi/drivers/addi-data/APCI1710_Tor.c        |    2 +-
+ .../comedi/drivers/addi-data/hwdrv_apci1500.c      |    2 +-
+ .../comedi/drivers/addi-data/hwdrv_apci1516.c      |    2 +-
+ .../comedi/drivers/addi-data/hwdrv_apci3501.c      |    2 +-
+ drivers/staging/comedi/drivers/amplc_pci230.c      |    2 +-
+ drivers/staging/comedi/drivers/cb_das16_cs.c       |    2 +-
+ drivers/staging/comedi/drivers/comedi_bond.c       |    2 +-
+ drivers/staging/crystalhd/crystalhd_hw.c           |    2 +-
+ drivers/staging/go7007/go7007-driver.c             |    2 +-
+ drivers/staging/iio/accel/lis3l02dq_ring.c         |    2 +-
+ .../staging/intel_sst/intel_sst_drv_interface.c    |    4 ++--
+ drivers/staging/keucr/smilmain.c                   |    4 ++--
+ drivers/staging/keucr/smilsub.c                    |    4 ++--
+ drivers/staging/msm/lcdc_toshiba_wvga_pt.c         |    2 +-
+ drivers/staging/rt2860/common/cmm_data_pci.c       |    4 ++--
+ drivers/staging/rt2860/rt_linux.c                  |    2 +-
+ drivers/staging/rt2860/rtmp.h                      |    2 +-
+ drivers/staging/rtl8192e/ieee80211/ieee80211_tx.c  |    2 +-
+ drivers/staging/rtl8192e/r819xE_phy.c              |    2 +-
+ drivers/staging/rtl8192u/ieee80211/ieee80211_tx.c  |    2 +-
+ drivers/staging/rtl8192u/r8192U_core.c             |    2 +-
+ drivers/staging/rtl8192u/r819xU_phy.c              |    2 +-
+ drivers/staging/rtl8712/rtl8712_efuse.c            |    2 +-
+ drivers/staging/rtl8712/rtl8712_xmit.c             |    2 +-
+ drivers/staging/rtl8712/rtl871x_xmit.c             |    2 +-
+ drivers/staging/tidspbridge/core/tiomap3430.c      |    4 ++--
+ drivers/staging/tidspbridge/rmgr/nldr.c            |    2 +-
+ drivers/staging/vt6655/card.c                      |    2 +-
+ drivers/staging/vt6655/iwctl.c                     |    2 +-
+ drivers/staging/vt6655/wpa2.c                      |    4 ++--
+ drivers/staging/vt6656/baseband.c                  |    2 +-
+ drivers/staging/vt6656/iwctl.c                     |    2 +-
+ drivers/staging/vt6656/power.c                     |    2 +-
+ drivers/staging/vt6656/wpa2.c                      |    4 ++--
+ drivers/usb/gadget/f_fs.c                          |    2 +-
+ drivers/xen/swiotlb-xen.c                          |    2 +-
+ fs/9p/acl.c                                        |    2 +-
+ fs/9p/xattr.c                                      |    2 +-
+ fs/ceph/mds_client.c                               |    2 +-
+ fs/logfs/readwrite.c                               |    2 +-
+ fs/nfs/getroot.c                                   |    2 +-
+ fs/ocfs2/refcounttree.c                            |    2 +-
+ fs/ubifs/scan.c                                    |    2 +-
+ include/linux/if_macvlan.h                         |    2 +-
+ include/net/caif/cfctrl.h                          |    2 +-
+ mm/hugetlb.c                                       |    2 +-
+ net/ipv6/mcast.c                                   |    2 +-
+ net/sunrpc/addr.c                                  |    2 +-
+ sound/core/pcm_lib.c                               |    2 +-
+ sound/soc/codecs/wm8904.c                          |    2 +-
+ sound/soc/codecs/wm8940.c                          |    1 -
+ sound/soc/codecs/wm8993.c                          |    2 +-
+ sound/soc/codecs/wm_hubs.c                         |    2 +-
+ 100 files changed, 111 insertions(+), 112 deletions(-)
+
+-- 
+1.7.3.1.g432b3.dirty
+
+--
+To unsubscribe from this list: send the line "unsubscribe cpufreq" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002254:2, b/test/corpora/lkml/cur/1382298793.002254:2,
new file mode 100644 (file)
index 0000000..b495912
--- /dev/null
@@ -0,0 +1,100 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 13/44] drivers/net/e1000e: Remove unnecessary
+       semicolons
+Date: Sun, 14 Nov 2010 19:04:32 -0800
+Lines: 34
+Message-ID: <e5cf92d50de7924930d660a5865c3d60d9cd9dc5.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: e1000-devel@lists.sourceforge.net, Bruce Allan <bruce.w.allan@intel.com>,
+       Jesse Brandeburg <jesse.brandeburg@intel.com>,
+       linux-kernel@vger.kernel.org, Greg Rose <gregory.v.rose@intel.com>,
+       John Ronciak <john.ronciak@intel.com>,
+       Jeff Kirsher <jeffrey.t.kirsher@intel.com>, netdev@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: e1000-devel-bounces@lists.sourceforge.net Mon Nov 15 04:05:53 2010
+Return-path: <e1000-devel-bounces@lists.sourceforge.net>
+Envelope-to: glded-e1000-devel@m.gmane.org
+Received: from lists.sourceforge.net ([216.34.181.88])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <e1000-devel-bounces@lists.sourceforge.net>)
+       id 1PHpOD-0000a9-4r
+       for glded-e1000-devel@m.gmane.org; Mon, 15 Nov 2010 04:05:53 +0100
+Received: from localhost ([127.0.0.1] helo=sfs-ml-4.v29.ch3.sourceforge.com)
+       by sfs-ml-4.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <e1000-devel-bounces@lists.sourceforge.net>)
+       id 1PHpO5-0002a8-SR; Mon, 15 Nov 2010 03:05:45 +0000
+Received: from sog-mx-2.v43.ch3.sourceforge.com ([172.29.43.192]
+       helo=mx.sourceforge.net)
+       by sfs-ml-4.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <joe@perches.com>) id 1PHpO5-0002Zz-D2
+       for e1000-devel@lists.sourceforge.net; Mon, 15 Nov 2010 03:05:45 +0000
+X-ACL-Warn: 
+Received: from mail.perches.com ([173.55.12.10])
+       by sog-mx-2.v43.ch3.sourceforge.com with esmtp (Exim 4.69)
+       id 1PHpO1-0002b4-4y
+       for e1000-devel@lists.sourceforge.net; Mon, 15 Nov 2010 03:05:45 +0000
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 462EB24376;
+       Sun, 14 Nov 2010 19:04:03 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+X-Spam-Score: 0.1 (/)
+X-Spam-Report: Spam Filtering performed by mx.sourceforge.net.
+       See http://spamassassin.org/tag/ for more details.
+       -0.0 T_RP_MATCHES_RCVD Envelope sender domain matches handover relay
+       domain 0.1 AWL AWL: From: address is in the auto white-list
+X-Headers-End: 1PHpO1-0002b4-4y
+X-BeenThere: e1000-devel@lists.sourceforge.net
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "e100/e1000\(e\)/ixgb/igb/ixgbe development and discussion"
+       <e1000-devel.lists.sourceforge.net>
+List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/e1000-devel>, 
+       <mailto:e1000-devel-request@lists.sourceforge.net?subject=unsubscribe>
+List-Archive: <http://sourceforge.net/mailarchive/forum.php?forum_name=e1000-devel>
+List-Post: <mailto:e1000-devel@lists.sourceforge.net>
+List-Help: <mailto:e1000-devel-request@lists.sourceforge.net?subject=help>
+List-Subscribe: <https://lists.sourceforge.net/lists/listinfo/e1000-devel>,
+       <mailto:e1000-devel-request@lists.sourceforge.net?subject=subscribe>
+Errors-To: e1000-devel-bounces@lists.sourceforge.net
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062273>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/net/e1000e/netdev.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/net/e1000e/netdev.c b/drivers/net/e1000e/netdev.c
+index c4ca162..a6d54e4 100644
+--- a/drivers/net/e1000e/netdev.c
++++ b/drivers/net/e1000e/netdev.c
+@@ -4595,7 +4595,7 @@ dma_error:
+                       i += tx_ring->count;
+               i--;
+               buffer_info = &tx_ring->buffer_info[i];
+-              e1000_put_txbuf(adapter, buffer_info);;
++              e1000_put_txbuf(adapter, buffer_info);
+       }
+       return 0;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+------------------------------------------------------------------------------
+Centralized Desktop Delivery: Dell and VMware Reference Architecture
+Simplifying enterprise desktop deployment and management using
+Dell EqualLogic storage and VMware View: A highly scalable, end-to-end
+client virtualization framework. Read more!
+http://p.sf.net/sfu/dell-eql-dev2dev
+_______________________________________________
+E1000-devel mailing list
+E1000-devel@lists.sourceforge.net
+https://lists.sourceforge.net/lists/listinfo/e1000-devel
+To learn more about Intel&#174; Ethernet, visit http://communities.intel.com/community/wired
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002255:2, b/test/corpora/lkml/cur/1382298793.002255:2,
new file mode 100644 (file)
index 0000000..1c158be
--- /dev/null
@@ -0,0 +1,100 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 14/44] drivers/net/ixgbe: Remove unnecessary
+       semicolons
+Date: Sun, 14 Nov 2010 19:04:33 -0800
+Lines: 34
+Message-ID: <7d2c334daa75c5221946a17d45c9de1901cf06e7.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: e1000-devel@lists.sourceforge.net, Bruce Allan <bruce.w.allan@intel.com>,
+       Jesse Brandeburg <jesse.brandeburg@intel.com>,
+       linux-kernel@vger.kernel.org, Greg Rose <gregory.v.rose@intel.com>,
+       John Ronciak <john.ronciak@intel.com>,
+       Jeff Kirsher <jeffrey.t.kirsher@intel.com>, netdev@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: e1000-devel-bounces@lists.sourceforge.net Mon Nov 15 04:05:55 2010
+Return-path: <e1000-devel-bounces@lists.sourceforge.net>
+Envelope-to: glded-e1000-devel@m.gmane.org
+Received: from lists.sourceforge.net ([216.34.181.88])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <e1000-devel-bounces@lists.sourceforge.net>)
+       id 1PHpOE-0000bY-KU
+       for glded-e1000-devel@m.gmane.org; Mon, 15 Nov 2010 04:05:54 +0100
+Received: from localhost ([127.0.0.1] helo=sfs-ml-2.v29.ch3.sourceforge.com)
+       by sfs-ml-2.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <e1000-devel-bounces@lists.sourceforge.net>)
+       id 1PHpO6-0004H7-Hr; Mon, 15 Nov 2010 03:05:46 +0000
+Received: from sog-mx-4.v43.ch3.sourceforge.com ([172.29.43.194]
+       helo=mx.sourceforge.net)
+       by sfs-ml-2.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <joe@perches.com>) id 1PHpO6-0004H2-2t
+       for e1000-devel@lists.sourceforge.net; Mon, 15 Nov 2010 03:05:46 +0000
+X-ACL-Warn: 
+Received: from mail.perches.com ([173.55.12.10])
+       by sog-mx-4.v43.ch3.sourceforge.com with esmtp (Exim 4.69)
+       id 1PHpO1-0006jE-SS
+       for e1000-devel@lists.sourceforge.net; Mon, 15 Nov 2010 03:05:46 +0000
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 0D6062436F;
+       Sun, 14 Nov 2010 19:04:04 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+X-Spam-Score: 0.1 (/)
+X-Spam-Report: Spam Filtering performed by mx.sourceforge.net.
+       See http://spamassassin.org/tag/ for more details.
+       -0.0 T_RP_MATCHES_RCVD Envelope sender domain matches handover relay
+       domain 0.1 AWL AWL: From: address is in the auto white-list
+X-Headers-End: 1PHpO1-0006jE-SS
+X-BeenThere: e1000-devel@lists.sourceforge.net
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "e100/e1000\(e\)/ixgb/igb/ixgbe development and discussion"
+       <e1000-devel.lists.sourceforge.net>
+List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/e1000-devel>, 
+       <mailto:e1000-devel-request@lists.sourceforge.net?subject=unsubscribe>
+List-Archive: <http://sourceforge.net/mailarchive/forum.php?forum_name=e1000-devel>
+List-Post: <mailto:e1000-devel@lists.sourceforge.net>
+List-Help: <mailto:e1000-devel-request@lists.sourceforge.net?subject=help>
+List-Subscribe: <https://lists.sourceforge.net/lists/listinfo/e1000-devel>,
+       <mailto:e1000-devel-request@lists.sourceforge.net?subject=subscribe>
+Errors-To: e1000-devel-bounces@lists.sourceforge.net
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062274>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/net/ixgbe/ixgbe_sriov.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/net/ixgbe/ixgbe_sriov.c b/drivers/net/ixgbe/ixgbe_sriov.c
+index 5428153..93f40bc 100644
+--- a/drivers/net/ixgbe/ixgbe_sriov.c
++++ b/drivers/net/ixgbe/ixgbe_sriov.c
+@@ -68,7 +68,7 @@ static int ixgbe_set_vf_multicasts(struct ixgbe_adapter *adapter,
+        * addresses
+        */
+       for (i = 0; i < entries; i++) {
+-              vfinfo->vf_mc_hashes[i] = hash_list[i];;
++              vfinfo->vf_mc_hashes[i] = hash_list[i];
+       }
+       for (i = 0; i < vfinfo->num_vf_mc_hashes; i++) {
+-- 
+1.7.3.1.g432b3.dirty
+
+
+------------------------------------------------------------------------------
+Centralized Desktop Delivery: Dell and VMware Reference Architecture
+Simplifying enterprise desktop deployment and management using
+Dell EqualLogic storage and VMware View: A highly scalable, end-to-end
+client virtualization framework. Read more!
+http://p.sf.net/sfu/dell-eql-dev2dev
+_______________________________________________
+E1000-devel mailing list
+E1000-devel@lists.sourceforge.net
+https://lists.sourceforge.net/lists/listinfo/e1000-devel
+To learn more about Intel&#174; Ethernet, visit http://communities.intel.com/community/wired
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002256:2, b/test/corpora/lkml/cur/1382298793.002256:2,
new file mode 100644 (file)
index 0000000..8626b81
--- /dev/null
@@ -0,0 +1,89 @@
+From: Joe Perches <joe@perches.com>
+Subject: [uml-user] [PATCH 03/44] arch/um: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:22 -0800
+Lines: 28
+Message-ID: <9ab60a1761dde357ebc028c525dae7572e072588.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Jeff Dike <jdike@addtoit.com>, user-mode-linux-user@lists.sourceforge.net,
+       user-mode-linux-devel@lists.sourceforge.net, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: user-mode-linux-user-bounces@lists.sourceforge.net Mon Nov 15 04:06:03 2010
+Return-path: <user-mode-linux-user-bounces@lists.sourceforge.net>
+Envelope-to: gluu-user-mode-linux-user-592@gmane.org
+Received: from lists.sourceforge.net ([216.34.181.88])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <user-mode-linux-user-bounces@lists.sourceforge.net>)
+       id 1PHpOM-0000er-Jl
+       for gluu-user-mode-linux-user-592@gmane.org; Mon, 15 Nov 2010 04:06:02 +0100
+Received: from localhost ([127.0.0.1] helo=sfs-ml-4.v29.ch3.sourceforge.com)
+       by sfs-ml-4.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <user-mode-linux-user-bounces@lists.sourceforge.net>)
+       id 1PHpNz-0002ZS-PM; Mon, 15 Nov 2010 03:05:39 +0000
+Received: from sog-mx-2.v43.ch3.sourceforge.com ([172.29.43.192]
+       helo=mx.sourceforge.net)
+       by sfs-ml-4.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <joe@perches.com>)
+       id 1PHpNy-0002ZA-Q9; Mon, 15 Nov 2010 03:05:38 +0000
+X-ACL-Warn: 
+Received: from mail.perches.com ([173.55.12.10])
+       by sog-mx-2.v43.ch3.sourceforge.com with esmtp (Exim 4.69)
+       id 1PHpNu-0002aj-Ks; Mon, 15 Nov 2010 03:05:38 +0000
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 845BB2436D;
+       Sun, 14 Nov 2010 19:03:56 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+X-Spam-Score: 0.1 (/)
+X-Spam-Report: Spam Filtering performed by mx.sourceforge.net.
+       See http://spamassassin.org/tag/ for more details.
+       -0.0 T_RP_MATCHES_RCVD Envelope sender domain matches handover relay
+       domain 0.1 AWL AWL: From: address is in the auto white-list
+X-Headers-End: 1PHpNu-0002aj-Ks
+X-BeenThere: user-mode-linux-user@lists.sourceforge.net
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: The user-mode Linux user list
+       <user-mode-linux-user.lists.sourceforge.net>
+List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/user-mode-linux-user>,
+       <mailto:user-mode-linux-user-request@lists.sourceforge.net?subject=unsubscribe>
+List-Archive: <http://sourceforge.net/mailarchive/forum.php?forum_name=user-mode-linux-user>
+List-Post: <mailto:user-mode-linux-user@lists.sourceforge.net>
+List-Help: <mailto:user-mode-linux-user-request@lists.sourceforge.net?subject=help>
+List-Subscribe: <https://lists.sourceforge.net/lists/listinfo/user-mode-linux-user>,
+       <mailto:user-mode-linux-user-request@lists.sourceforge.net?subject=subscribe>
+Errors-To: user-mode-linux-user-bounces@lists.sourceforge.net
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062275>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ arch/um/drivers/mmapper_kern.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/arch/um/drivers/mmapper_kern.c b/arch/um/drivers/mmapper_kern.c
+index 8501e7d..6256fa9 100644
+--- a/arch/um/drivers/mmapper_kern.c
++++ b/arch/um/drivers/mmapper_kern.c
+@@ -122,7 +122,7 @@ static int __init mmapper_init(void)
+       if (err) {
+               printk(KERN_ERR "mmapper - misc_register failed, err = %d\n",
+                      err);
+-              return err;;
++              return err;
+       }
+       return 0;
+ }
+-- 
+1.7.3.1.g432b3.dirty
+
+
+------------------------------------------------------------------------------
+Centralized Desktop Delivery: Dell and VMware Reference Architecture
+Simplifying enterprise desktop deployment and management using
+Dell EqualLogic storage and VMware View: A highly scalable, end-to-end
+client virtualization framework. Read more!
+http://p.sf.net/sfu/dell-eql-dev2dev
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002257:2, b/test/corpora/lkml/cur/1382298793.002257:2,
new file mode 100644 (file)
index 0000000..849da33
--- /dev/null
@@ -0,0 +1,81 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 40/44] mm/hugetlb.c: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:59 -0800
+Lines: 28
+Message-ID: <59705f848d35b12ace640f92afcffea02cee0976.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: linux-mm@kvack.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: owner-linux-mm@kvack.org Mon Nov 15 04:06:05 2010
+Return-path: <owner-linux-mm@kvack.org>
+Envelope-to: glkm-linux-mm-2@m.gmane.org
+Received: from kanga.kvack.org ([205.233.56.17])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <owner-linux-mm@kvack.org>)
+       id 1PHpOO-0000gl-5U
+       for glkm-linux-mm-2@m.gmane.org; Mon, 15 Nov 2010 04:06:04 +0100
+Received: by kanga.kvack.org (Postfix)
+       id 6220A8D003C; Sun, 14 Nov 2010 22:06:03 -0500 (EST)
+Delivered-To: linux-mm-outgoing@kvack.org
+Received: by kanga.kvack.org (Postfix, from userid 40)
+       id 5D2638D0017; Sun, 14 Nov 2010 22:06:03 -0500 (EST)
+X-Original-To: int-list-linux-mm@kvack.org
+Delivered-To: int-list-linux-mm@kvack.org
+Received: by kanga.kvack.org (Postfix, from userid 63042)
+       id 427128D003C; Sun, 14 Nov 2010 22:06:03 -0500 (EST)
+X-Original-To: linux-mm@kvack.org
+Delivered-To: linux-mm@kvack.org
+Received: from mail203.messagelabs.com (mail203.messagelabs.com [216.82.254.243])
+       by kanga.kvack.org (Postfix) with SMTP id C08B98D0017
+       for <linux-mm@kvack.org>; Sun, 14 Nov 2010 22:06:02 -0500 (EST)
+X-VirusChecked: Checked
+X-Env-Sender: joe@perches.com
+X-Msg-Ref: server-4.tower-203.messagelabs.com!1289790361!41887937!1
+X-StarScan-Version: 6.2.9; banners=-,-,-
+X-Originating-IP: [173.55.12.10]
+X-SpamReason: No, hits=0.0 required=7.0 tests=
+Received: (qmail 4485 invoked from network); 15 Nov 2010 03:06:01 -0000
+Received: from mail.perches.com (HELO mail.perches.com) (173.55.12.10)
+  by server-4.tower-203.messagelabs.com with SMTP; 15 Nov 2010 03:06:01 -0000
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id EB70E2436B;
+       Sun, 14 Nov 2010 19:04:28 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+X-Bogosity: Ham, tests=bogofilter, spamicity=0.000000, version=1.2.2
+Sender: owner-linux-mm@kvack.org
+Precedence: bulk
+X-Loop: owner-majordomo@kvack.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062276>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ mm/hugetlb.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/mm/hugetlb.c b/mm/hugetlb.c
+index c4a3558..8875242 100644
+--- a/mm/hugetlb.c
++++ b/mm/hugetlb.c
+@@ -540,7 +540,7 @@ static struct page *dequeue_huge_page_vma(struct hstate *h,
+       /* If reserves cannot be used, ensure enough pages are in the pool */
+       if (avoid_reserve && h->free_huge_pages - h->resv_huge_pages == 0)
+-              goto err;;
++              goto err;
+       for_each_zone_zonelist_nodemask(zone, z, zonelist,
+                                               MAX_NR_ZONES - 1, nodemask) {
+-- 
+1.7.3.1.g432b3.dirty
+
+--
+To unsubscribe, send a message with 'unsubscribe linux-mm' in
+the body to majordomo@kvack.org.  For more info on Linux MM,
+see: http://www.linux-mm.org/ .
+Fight unfair telecom policy in Canada: sign http://dissolvethecrtc.ca/
+Don't email: <a href=mailto:"dont@kvack.org"> email@kvack.org </a>
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002258:2, b/test/corpora/lkml/cur/1382298793.002258:2,
new file mode 100644 (file)
index 0000000..271136b
--- /dev/null
@@ -0,0 +1,86 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 31/44] drivers/xen: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:50 -0800
+Lines: 20
+Message-ID: <b3f95cd997859d5d714de322ce17810fe73460cd.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: virtualization@lists.osdl.org,
+       Jeremy Fitzhardinge <jeremy.fitzhardinge@citrix.com>,
+       xen-devel@lists.xensource.com, linux-kernel@vger.kernel.org,
+       Konrad Rzeszutek Wilk <konrad.wilk@oracle.com>
+To: Jiri Kosina <trivial@kernel.org>
+X-From: xen-devel-bounces@lists.xensource.com Mon Nov 15 04:06:05 2010
+Return-path: <xen-devel-bounces@lists.xensource.com>
+Envelope-to: gcvxd-xen-devel@m.gmane.org
+Received: from lists.colo.xensource.com ([70.42.241.110] helo=lists.xensource.com)
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <xen-devel-bounces@lists.xensource.com>)
+       id 1PHpON-0000fV-A9
+       for gcvxd-xen-devel@m.gmane.org; Mon, 15 Nov 2010 04:06:03 +0100
+Received: from localhost ([127.0.0.1] helo=lists.colo.xensource.com)
+       by lists.xensource.com with esmtp (Exim 4.43)
+       id 1PHpOK-0008Sa-VZ; Sun, 14 Nov 2010 19:06:01 -0800
+Received: from spam.xensource.com ([70.42.241.90])
+       by lists.xensource.com with esmtp (Exim 4.43) id 1PHpOG-0008R4-01
+       for xen-devel@lists.xensource.com; Sun, 14 Nov 2010 19:05:56 -0800
+X-ASG-Debug-ID: 1289790355-0739cd1c0001-8pertM
+Received: from mail.perches.com (mail.perches.com [173.55.12.10]) by
+       spam.xensource.com with ESMTP id XhkGr3VGEwXLx5vl for
+       <xen-devel@lists.xensource.com>;
+       Sun, 14 Nov 2010 19:05:55 -0800 (PST)
+X-Barracuda-Envelope-From: joe@perches.com
+X-Barracuda-Apparent-Source-IP: 173.55.12.10
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 044672436E;
+       Sun, 14 Nov 2010 19:04:23 -0800 (PST)
+X-ASG-Orig-Subj: [PATCH 31/44] drivers/xen: Remove unnecessary semicolons
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+X-Barracuda-Connect: mail.perches.com[173.55.12.10]
+X-Barracuda-Start-Time: 1289790355
+X-Barracuda-URL: http://spam.xensource.com:8000/cgi-mod/mark.cgi
+X-Virus-Scanned: by bsmtpd at xensource.com
+X-Barracuda-Spam-Score: 0.00
+X-Barracuda-Spam-Status: No, SCORE=0.00 using per-user scores of TAG_LEVEL=3.5
+       QUARANTINE_LEVEL=6.0 KILL_LEVEL=1000.0 tests=
+X-Barracuda-Spam-Report: Code version 3.2, rules version 3.2.2.46657
+       Rule breakdown below
+       pts rule name              description
+       ---- ----------------------
+       --------------------------------------------------
+X-BeenThere: xen-devel@lists.xensource.com
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Xen developer discussion <xen-devel.lists.xensource.com>
+List-Unsubscribe: <http://lists.xensource.com/mailman/listinfo/xen-devel>,
+       <mailto:xen-devel-request@lists.xensource.com?subject=unsubscribe>
+List-Post: <mailto:xen-devel@lists.xensource.com>
+List-Help: <mailto:xen-devel-request@lists.xensource.com?subject=help>
+List-Subscribe: <http://lists.xensource.com/mailman/listinfo/xen-devel>,
+       <mailto:xen-devel-request@lists.xensource.com?subject=subscribe>
+Sender: xen-devel-bounces@lists.xensource.com
+Errors-To: xen-devel-bounces@lists.xensource.com
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062277>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/xen/swiotlb-xen.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/xen/swiotlb-xen.c b/drivers/xen/swiotlb-xen.c
+index 54469c3..65ea21a 100644
+--- a/drivers/xen/swiotlb-xen.c
++++ b/drivers/xen/swiotlb-xen.c
+@@ -54,7 +54,7 @@ u64 start_dma_addr;
+ static dma_addr_t xen_phys_to_bus(phys_addr_t paddr)
+ {
+-      return phys_to_machine(XPADDR(paddr)).maddr;;
++      return phys_to_machine(XPADDR(paddr)).maddr;
+ }
+ static phys_addr_t xen_bus_to_phys(dma_addr_t baddr)
+-- 
+1.7.3.1.g432b3.dirty
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002259:2, b/test/corpora/lkml/cur/1382298793.002259:2,
new file mode 100644 (file)
index 0000000..de8e427
--- /dev/null
@@ -0,0 +1,58 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 11/44] drivers/mmc: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:30 -0800
+Lines: 21
+Message-ID: <6391af02ba7ec4a76c5c5f462d8013fc1f52f999.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Chris Ball <cjb@laptop.org>, linux-mmc@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:06:20 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpOe-0000ny-CV
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:06:20 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932688Ab0KODFg (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:05:36 -0500
+Received: from mail.perches.com ([173.55.12.10]:1153 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932652Ab0KODFe (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:34 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id DD07924374;
+       Sun, 14 Nov 2010 19:04:01 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062278>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/mmc/host/davinci_mmc.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/mmc/host/davinci_mmc.c b/drivers/mmc/host/davinci_mmc.c
+index e15547c..b643dde 100644
+--- a/drivers/mmc/host/davinci_mmc.c
++++ b/drivers/mmc/host/davinci_mmc.c
+@@ -480,7 +480,7 @@ static void mmc_davinci_send_dma_request(struct mmc_davinci_host *host,
+       struct scatterlist      *sg;
+       unsigned                sg_len;
+       unsigned                bytes_left = host->bytes_left;
+-      const unsigned          shift = ffs(rw_threshold) - 1;;
++      const unsigned          shift = ffs(rw_threshold) - 1;
+       if (host->data_dir == DAVINCI_MMC_DATADIR_WRITE) {
+               template = &host->tx_template;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002260:2, b/test/corpora/lkml/cur/1382298793.002260:2,
new file mode 100644 (file)
index 0000000..2b5145f
--- /dev/null
@@ -0,0 +1,57 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 20/44] drivers/power: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:39 -0800
+Lines: 21
+Message-ID: <2f2ed8aa6745be23063fed55243313839d7ba523.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:06:22 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpOe-0000ny-TB
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:06:21 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932773Ab0KODFm (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:05:42 -0500
+Received: from mail.perches.com ([173.55.12.10]:1185 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932746Ab0KODFk (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:40 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 3664A2436B;
+       Sun, 14 Nov 2010 19:04:08 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062279>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/power/intel_mid_battery.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/power/intel_mid_battery.c b/drivers/power/intel_mid_battery.c
+index 2a10cd3..8397978 100644
+--- a/drivers/power/intel_mid_battery.c
++++ b/drivers/power/intel_mid_battery.c
+@@ -522,7 +522,7 @@ static int pmic_battery_set_charger(struct pmic_power_module_info *pbi,
+       if (retval) {
+               dev_warn(pbi->dev, "%s(): ipc pmic read failed\n",
+                                                               __func__);
+-              return retval;;
++              return retval;
+       }
+       return 0;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002261:2, b/test/corpora/lkml/cur/1382298793.002261:2,
new file mode 100644 (file)
index 0000000..28aac74
--- /dev/null
@@ -0,0 +1,58 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 02/44] arch/microblaze: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:21 -0800
+Lines: 21
+Message-ID: <5d57b90b488b4338bcdc3f0fbf5f6996842bd44d.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Michal Simek <monstr@monstr.eu>, microblaze-uclinux@itee.uq.edu.au,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:06:22 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpOd-0000ny-SE
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:06:20 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932624Ab0KODFc (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:05:32 -0500
+Received: from mail.perches.com ([173.55.12.10]:1127 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1757330Ab0KODF3 (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:29 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id EF7FB2436C;
+       Sun, 14 Nov 2010 19:03:55 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062280>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ arch/microblaze/lib/memmove.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/arch/microblaze/lib/memmove.c b/arch/microblaze/lib/memmove.c
+index 123e361..810fd68 100644
+--- a/arch/microblaze/lib/memmove.c
++++ b/arch/microblaze/lib/memmove.c
+@@ -182,7 +182,7 @@ void *memmove(void *v_dst, const void *v_src, __kernel_size_t c)
+                       for (; c >= 4; c -= 4) {
+                               value = *--i_src;
+                               *--i_dst = buf_hold | ((value & 0xFF000000)>> 24);
+-                              buf_hold = (value & 0xFFFFFF) << 8;;
++                              buf_hold = (value & 0xFFFFFF) << 8;
+                       }
+ #endif
+                       /* Realign the source */
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002262:2, b/test/corpora/lkml/cur/1382298793.002262:2,
new file mode 100644 (file)
index 0000000..b931922
--- /dev/null
@@ -0,0 +1,59 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 25/44] drivers/scsi/pm8001: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:44 -0800
+Lines: 21
+Message-ID: <20b352f91642ca45ad730d8eeec0bbd323d26626.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: jack_wang@usish.com, lindar_liu@usish.com,
+       "James E.J. Bottomley" <James.Bottomley@suse.de>,
+       linux-scsi@vger.kernel.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:06:22 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpOf-0000ny-V3
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:06:22 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932817Ab0KODFp (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:05:45 -0500
+Received: from mail.perches.com ([173.55.12.10]:1198 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932690Ab0KODFn (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:43 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 22C0624374;
+       Sun, 14 Nov 2010 19:04:11 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062281>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/scsi/pm8001/pm8001_init.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/scsi/pm8001/pm8001_init.c b/drivers/scsi/pm8001/pm8001_init.c
+index f8c86b2..be210dd 100644
+--- a/drivers/scsi/pm8001/pm8001_init.c
++++ b/drivers/scsi/pm8001/pm8001_init.c
+@@ -160,7 +160,7 @@ static void pm8001_free(struct pm8001_hba_info *pm8001_ha)
+ static void pm8001_tasklet(unsigned long opaque)
+ {
+       struct pm8001_hba_info *pm8001_ha;
+-      pm8001_ha = (struct pm8001_hba_info *)opaque;;
++      pm8001_ha = (struct pm8001_hba_info *)opaque;
+       if (unlikely(!pm8001_ha))
+               BUG_ON(1);
+       PM8001_CHIP_DISP->isr(pm8001_ha);
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002263:2, b/test/corpora/lkml/cur/1382298793.002263:2,
new file mode 100644 (file)
index 0000000..4014b08
--- /dev/null
@@ -0,0 +1,58 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 43/44] sound/core/pcm_lib.c: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:05:02 -0800
+Lines: 21
+Message-ID: <9fa8e193ce125ef4fd19a952792629c5ee84953f.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Jaroslav Kysela <perex@perex.cz>, Takashi Iwai <tiwai@suse.de>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:06:24 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpOg-0000ny-Vl
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:06:23 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933024Ab0KODGF (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:06:05 -0500
+Received: from mail.perches.com ([173.55.12.10]:1272 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932998Ab0KODGD (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:06:03 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id E309C24378;
+       Sun, 14 Nov 2010 19:04:30 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062282>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ sound/core/pcm_lib.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/sound/core/pcm_lib.c b/sound/core/pcm_lib.c
+index a1707cc..b75db8e 100644
+--- a/sound/core/pcm_lib.c
++++ b/sound/core/pcm_lib.c
+@@ -223,7 +223,7 @@ static void xrun_log(struct snd_pcm_substream *substream,
+       entry->jiffies = jiffies;
+       entry->pos = pos;
+       entry->period_size = runtime->period_size;
+-      entry->buffer_size = runtime->buffer_size;;
++      entry->buffer_size = runtime->buffer_size;
+       entry->old_hw_ptr = runtime->status->hw_ptr;
+       entry->hw_ptr_base = runtime->hw_ptr_base;
+       log->idx = (log->idx + 1) % XRUN_LOG_CNT;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002264:2, b/test/corpora/lkml/cur/1382298793.002264:2,
new file mode 100644 (file)
index 0000000..17d8127
--- /dev/null
@@ -0,0 +1,103 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 44/44] sound/soc/codecs: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:05:03 -0800
+Lines: 62
+Message-ID: <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Mark Brown <broonie@opensource.wolfsonmicro.com>,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>,
+       Jaroslav Kysela <perex@perex.cz>, Takashi Iwai <tiwai@suse.de>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:06:24 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpOh-0000ny-G0
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:06:23 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933041Ab0KODGR (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:06:17 -0500
+Received: from mail.perches.com ([173.55.12.10]:1275 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932999Ab0KODGE (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:06:04 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 2AAD92436C;
+       Sun, 14 Nov 2010 19:04:32 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062283>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ sound/soc/codecs/wm8904.c  |    2 +-
+ sound/soc/codecs/wm8940.c  |    1 -
+ sound/soc/codecs/wm8993.c  |    2 +-
+ sound/soc/codecs/wm_hubs.c |    2 +-
+ 4 files changed, 3 insertions(+), 4 deletions(-)
+
+diff --git a/sound/soc/codecs/wm8904.c b/sound/soc/codecs/wm8904.c
+index 33be84e..99ae66d 100644
+--- a/sound/soc/codecs/wm8904.c
++++ b/sound/soc/codecs/wm8904.c
+@@ -1590,7 +1590,7 @@ static int wm8904_hw_params(struct snd_pcm_substream *substream,
+                      - wm8904->fs);
+       for (i = 1; i < ARRAY_SIZE(clk_sys_rates); i++) {
+               cur_val = abs((wm8904->sysclk_rate /
+-                             clk_sys_rates[i].ratio) - wm8904->fs);;
++                             clk_sys_rates[i].ratio) - wm8904->fs);
+               if (cur_val < best_val) {
+                       best = i;
+                       best_val = cur_val;
+diff --git a/sound/soc/codecs/wm8940.c b/sound/soc/codecs/wm8940.c
+index 2cb16f8..e3f3572 100644
+--- a/sound/soc/codecs/wm8940.c
++++ b/sound/soc/codecs/wm8940.c
+@@ -735,7 +735,6 @@ static int wm8940_probe(struct snd_soc_codec *codec)
+               return ret;
+       return ret;
+-;
+ }
+ static int wm8940_remove(struct snd_soc_codec *codec)
+diff --git a/sound/soc/codecs/wm8993.c b/sound/soc/codecs/wm8993.c
+index 589e3fa..74af9c5 100644
+--- a/sound/soc/codecs/wm8993.c
++++ b/sound/soc/codecs/wm8993.c
+@@ -1225,7 +1225,7 @@ static int wm8993_hw_params(struct snd_pcm_substream *substream,
+                      - wm8993->fs);
+       for (i = 1; i < ARRAY_SIZE(clk_sys_rates); i++) {
+               cur_val = abs((wm8993->sysclk_rate /
+-                             clk_sys_rates[i].ratio) - wm8993->fs);;
++                             clk_sys_rates[i].ratio) - wm8993->fs);
+               if (cur_val < best_val) {
+                       best = i;
+                       best_val = cur_val;
+diff --git a/sound/soc/codecs/wm_hubs.c b/sound/soc/codecs/wm_hubs.c
+index 19ca782..2afbc7a 100644
+--- a/sound/soc/codecs/wm_hubs.c
++++ b/sound/soc/codecs/wm_hubs.c
+@@ -112,7 +112,7 @@ static void calibrate_dc_servo(struct snd_soc_codec *codec)
+               switch (hubs->dcs_readback_mode) {
+               case 0:
+                       reg_l = snd_soc_read(codec, WM8993_DC_SERVO_READBACK_1)
+-                              & WM8993_DCS_INTEG_CHAN_0_MASK;;
++                              & WM8993_DCS_INTEG_CHAN_0_MASK;
+                       reg_r = snd_soc_read(codec, WM8993_DC_SERVO_READBACK_2)
+                               & WM8993_DCS_INTEG_CHAN_1_MASK;
+                       break;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002265:2, b/test/corpora/lkml/cur/1382298793.002265:2,
new file mode 100644 (file)
index 0000000..c4c55c7
--- /dev/null
@@ -0,0 +1,76 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 19/44] drivers/platform/x86: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:38 -0800
+Lines: 35
+Message-ID: <eda82bcfaad265fc5cd3901bc4f41bfcfac2403b.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Thadeu Lima de Souza Cascardo <cascardo@holoscopio.com>,
+       Daniel Oliveira Nascimento <don@syst.com.br>,
+       Matthew Garrett <mjg@redhat.com>,
+       Henrique de Moraes Holschuh <ibm-acpi@hmh.eng.br>,
+       platform-driver-x86@vger.kernel.org, linux-kernel@vger.kernel.org,
+       ibm-acpi-devel@lists.sourceforge.net
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:06:22 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpOf-0000ny-DL
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:06:21 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932793Ab0KODFn (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:05:43 -0500
+Received: from mail.perches.com ([173.55.12.10]:1183 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932690Ab0KODFk (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:40 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 96EF224372;
+       Sun, 14 Nov 2010 19:04:07 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062284>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/platform/x86/classmate-laptop.c |    2 +-
+ drivers/platform/x86/thinkpad_acpi.c    |    2 +-
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/drivers/platform/x86/classmate-laptop.c b/drivers/platform/x86/classmate-laptop.c
+index 341cbfe..d2b7720 100644
+--- a/drivers/platform/x86/classmate-laptop.c
++++ b/drivers/platform/x86/classmate-laptop.c
+@@ -653,7 +653,7 @@ static void cmpc_keys_handler(struct acpi_device *dev, u32 event)
+       if ((event & 0x0F) < ARRAY_SIZE(cmpc_keys_codes))
+               code = cmpc_keys_codes[event & 0x0F];
+-      inputdev = dev_get_drvdata(&dev->dev);;
++      inputdev = dev_get_drvdata(&dev->dev);
+       input_report_key(inputdev, code, !(event & 0x10));
+ }
+diff --git a/drivers/platform/x86/thinkpad_acpi.c b/drivers/platform/x86/thinkpad_acpi.c
+index 2d61186..3cd7814 100644
+--- a/drivers/platform/x86/thinkpad_acpi.c
++++ b/drivers/platform/x86/thinkpad_acpi.c
+@@ -6345,7 +6345,7 @@ static int __init brightness_init(struct ibm_init_struct *iibm)
+                       "as change notification\n");
+       tpacpi_hotkey_driver_mask_set(hotkey_driver_mask
+                               | TP_ACPI_HKEY_BRGHTUP_MASK
+-                              | TP_ACPI_HKEY_BRGHTDWN_MASK);;
++                              | TP_ACPI_HKEY_BRGHTDWN_MASK);
+       return 0;
+ }
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002266:2, b/test/corpora/lkml/cur/1382298793.002266:2,
new file mode 100644 (file)
index 0000000..cc36531
--- /dev/null
@@ -0,0 +1,75 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 28/44] drivers/spi: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:47 -0800
+Lines: 35
+Message-ID: <fe5e5e0efbd97eaa32530eef5ed47efdc3252dad.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: David Brownell <dbrownell@users.sourceforge.net>,
+       Grant Likely <grant.likely@secretlab.ca>,
+       Wan ZongShun <mcuos.com@gmail.com>,
+       spi-devel-general@lists.sourceforge.net,
+       linux-kernel@vger.kernel.org, linux-arm-kernel@lists.infradead.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:06:22 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpOg-0000ny-FJ
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:06:22 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932836Ab0KODFr (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:05:47 -0500
+Received: from mail.perches.com ([173.55.12.10]:1208 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932690Ab0KODFq (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:46 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 454B624375;
+       Sun, 14 Nov 2010 19:04:13 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062285>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/spi/amba-pl022.c |    2 +-
+ drivers/spi/spi_nuc900.c |    2 +-
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/drivers/spi/amba-pl022.c b/drivers/spi/amba-pl022.c
+index fb3d1b3..2e50631 100644
+--- a/drivers/spi/amba-pl022.c
++++ b/drivers/spi/amba-pl022.c
+@@ -956,7 +956,7 @@ static int configure_dma(struct pl022 *pl022)
+               tx_conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES;
+               break;
+       case WRITING_U32:
+-              tx_conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;;
++              tx_conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
+               break;
+       }
+diff --git a/drivers/spi/spi_nuc900.c b/drivers/spi/spi_nuc900.c
+index dff63be..d5be18b 100644
+--- a/drivers/spi/spi_nuc900.c
++++ b/drivers/spi/spi_nuc900.c
+@@ -449,7 +449,7 @@ err_iomap:
+       release_mem_region(hw->res->start, resource_size(hw->res));
+       kfree(hw->ioarea);
+ err_pdata:
+-      spi_master_put(hw->master);;
++      spi_master_put(hw->master);
+ err_nomem:
+       return err;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002267:2, b/test/corpora/lkml/cur/1382298793.002267:2,
new file mode 100644 (file)
index 0000000..d3acae5
--- /dev/null
@@ -0,0 +1,773 @@
+From: Joe Perches <joe@perches.com>
+Subject: =?UTF-8?q?=5BPATCH=2029/44=5D=20drivers/staging=3A=20Remove=20unnecessary=20semicolons?=
+Date: Sun, 14 Nov 2010 19:04:48 -0800
+Lines: 724
+Message-ID: <3246dc176a2c553078e73332f02d802dd8ef7942.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: multipart/mixed; boundary="===============1088501263=="
+Cc: devel@driverdev.osuosl.org, Greg Kroah-Hartman <gregkh@suse.de>,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: devel-bounces@linuxdriverproject.org Mon Nov 15 04:06:51 2010
+Return-path: <devel-bounces@linuxdriverproject.org>
+Envelope-to: glddd-devel@m.gmane.org
+Received: from driverdev.linuxdriverproject.org ([140.211.166.17])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <devel-bounces@linuxdriverproject.org>)
+       id 1PHpP7-000137-TU
+       for glddd-devel@m.gmane.org; Mon, 15 Nov 2010 04:06:50 +0100
+Received: from driverdev.linuxdriverproject.org (localhost [127.0.0.1])
+       by driverdev.linuxdriverproject.org (Postfix) with ESMTP id 01FCE460C4;
+       Sun, 14 Nov 2010 19:05:00 -0800 (PST)
+X-Original-To: devel@driverdev.osuosl.org
+Delivered-To: devel@driverdev.osuosl.org
+Received: from mail.perches.com (mail.perches.com [173.55.12.10])
+       by driverdev.linuxdriverproject.org (Postfix) with ESMTP id 8F70A460C6
+       for <devel@driverdev.osuosl.org>; Sun, 14 Nov 2010 19:04:05 -0800 (PST)
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id C136B2436B;
+       Sun, 14 Nov 2010 19:04:21 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+X-BeenThere: devel@linuxdriverproject.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: Linux Driver Project Developer List <devel.linuxdriverproject.org>
+List-Unsubscribe: <http://driverdev.linuxdriverproject.org/mailman/options/devel>,
+       <mailto:devel-request@linuxdriverproject.org?subject=unsubscribe>
+List-Archive: <http://driverdev.linuxdriverproject.org/pipermail/devel>
+List-Post: <mailto:devel@linuxdriverproject.org>
+List-Help: <mailto:devel-request@linuxdriverproject.org?subject=help>
+List-Subscribe: <http://driverdev.linuxdriverproject.org/mailman/listinfo/devel>,
+       <mailto:devel-request@linuxdriverproject.org?subject=subscribe>
+Sender: devel-bounces@linuxdriverproject.org
+Errors-To: devel-bounces@linuxdriverproject.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062286>
+
+--===============1088501263==
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ .../staging/ath6kl/hif/sdio/linux_sdio/src/hif.c   |    2 +-
+ drivers/staging/ath6kl/os/linux/ar6000_drv.c       |    2 +-
+ drivers/staging/bcm/InterfaceInit.c                |    2 +-
+ drivers/staging/bcm/InterfaceIsr.c                 |    2 +-
+ drivers/staging/bcm/Misc.c                         |    4 ++--
+ .../comedi/drivers/addi-data/APCI1710_Tor.c        |    2 +-
+ .../comedi/drivers/addi-data/hwdrv_apci1500.c      |    2 +-
+ .../comedi/drivers/addi-data/hwdrv_apci1516.c      |    2 +-
+ .../comedi/drivers/addi-data/hwdrv_apci3501.c      |    2 +-
+ drivers/staging/comedi/drivers/amplc_pci230.c      |    2 +-
+ drivers/staging/comedi/drivers/cb_das16_cs.c       |    2 +-
+ drivers/staging/comedi/drivers/comedi_bond.c       |    2 +-
+ drivers/staging/crystalhd/crystalhd_hw.c           |    2 +-
+ drivers/staging/go7007/go7007-driver.c             |    2 +-
+ drivers/staging/iio/accel/lis3l02dq_ring.c         |    2 +-
+ .../staging/intel_sst/intel_sst_drv_interface.c    |    4 ++--
+ drivers/staging/keucr/smilmain.c                   |    4 ++--
+ drivers/staging/keucr/smilsub.c                    |    4 ++--
+ drivers/staging/msm/lcdc_toshiba_wvga_pt.c         |    2 +-
+ drivers/staging/rt2860/common/cmm_data_pci.c       |    4 ++--
+ drivers/staging/rt2860/rt_linux.c                  |    2 +-
+ drivers/staging/rt2860/rtmp.h                      |    2 +-
+ drivers/staging/rtl8192e/ieee80211/ieee80211_tx.c  |    2 +-
+ drivers/staging/rtl8192e/r819xE_phy.c              |    2 +-
+ drivers/staging/rtl8192u/ieee80211/ieee80211_tx.c  |    2 +-
+ drivers/staging/rtl8192u/r8192U_core.c             |    2 +-
+ drivers/staging/rtl8192u/r819xU_phy.c              |    2 +-
+ drivers/staging/rtl8712/rtl8712_efuse.c            |    2 +-
+ drivers/staging/rtl8712/rtl8712_xmit.c             |    2 +-
+ drivers/staging/rtl8712/rtl871x_xmit.c             |    2 +-
+ drivers/staging/tidspbridge/core/tiomap3430.c      |    4 ++--
+ drivers/staging/tidspbridge/rmgr/nldr.c            |    2 +-
+ drivers/staging/vt6655/card.c                      |    2 +-
+ drivers/staging/vt6655/iwctl.c                     |    2 +-
+ drivers/staging/vt6655/wpa2.c                      |    4 ++--
+ drivers/staging/vt6656/baseband.c                  |    2 +-
+ drivers/staging/vt6656/iwctl.c                     |    2 +-
+ drivers/staging/vt6656/power.c                     |    2 +-
+ drivers/staging/vt6656/wpa2.c                      |    4 ++--
+ 39 files changed, 47 insertions(+), 47 deletions(-)
+
+diff --git a/drivers/staging/ath6kl/hif/sdio/linux_sdio/src/hif.c b/drive=
+rs/staging/ath6kl/hif/sdio/linux_sdio/src/hif.c
+index c307a55..3963038 100644
+--- a/drivers/staging/ath6kl/hif/sdio/linux_sdio/src/hif.c
++++ b/drivers/staging/ath6kl/hif/sdio/linux_sdio/src/hif.c
+@@ -876,7 +876,7 @@ HIFAckInterrupt(HIF_DEVICE *device)
+ void
+ HIFUnMaskInterrupt(HIF_DEVICE *device)
+ {
+-    int ret;;
++    int ret;
+=20
+     AR_DEBUG_ASSERT(device !=3D NULL);
+     AR_DEBUG_ASSERT(device->func !=3D NULL);
+diff --git a/drivers/staging/ath6kl/os/linux/ar6000_drv.c b/drivers/stagi=
+ng/ath6kl/os/linux/ar6000_drv.c
+index a659f70..126a36a 100644
+--- a/drivers/staging/ath6kl/os/linux/ar6000_drv.c
++++ b/drivers/staging/ath6kl/os/linux/ar6000_drv.c
+@@ -4439,7 +4439,7 @@ skip_key:
+         for (i =3D assoc_req_ie_pos; i < assoc_req_ie_pos + assocReqLen =
+- 4; i++) {
+             AR_DEBUG_PRINTF(ATH_DEBUG_WLAN_CONNECT,("%2.2x ", assocInfo[=
+i]));
+             sprintf(pos, "%2.2x", assocInfo[i]);
+-            pos +=3D 2;;
++            pos +=3D 2;
+         }
+         AR_DEBUG_PRINTF(ATH_DEBUG_WLAN_CONNECT,("\n"));
+=20
+diff --git a/drivers/staging/bcm/InterfaceInit.c b/drivers/staging/bcm/In=
+terfaceInit.c
+index 824f9a4..a368011 100644
+--- a/drivers/staging/bcm/InterfaceInit.c
++++ b/drivers/staging/bcm/InterfaceInit.c
+@@ -265,7 +265,7 @@ usbbcm_device_probe(struct usb_interface *intf, const=
+ struct usb_device_id *id)
+               uint32_t uiNackZeroLengthInt=3D4;
+               if(wrmalt(psAdapter, DISABLE_USB_ZERO_LEN_INT, &uiNackZeroLengthInt, s=
+izeof(uiNackZeroLengthInt)))
+               {
+-                      return -EIO;;
++                      return -EIO;
+               }
+       }
+=20
+diff --git a/drivers/staging/bcm/InterfaceIsr.c b/drivers/staging/bcm/Int=
+erfaceIsr.c
+index f928fe4..604d07f 100644
+--- a/drivers/staging/bcm/InterfaceIsr.c
++++ b/drivers/staging/bcm/InterfaceIsr.c
+@@ -87,7 +87,7 @@ static void read_int_callback(struct urb *urb/*, struct=
+ pt_regs *regs*/)
+                               BCM_DEBUG_PRINT(Adapter,DBG_TYPE_OTHERS, INTF_INIT, DBG_LVL_ALL,"Int=
+errupt IN endPoint  has got halted/stalled...need to clear this");
+                               Adapter->bEndPointHalted =3D TRUE ;
+                               wake_up(&Adapter->tx_packet_wait_queue);
+-                              urb->status =3D STATUS_SUCCESS ;;
++                              urb->status =3D STATUS_SUCCESS ;
+                               return;
+               }
+           /* software-driven interface shutdown */
+diff --git a/drivers/staging/bcm/Misc.c b/drivers/staging/bcm/Misc.c
+index 22550f7..cd14fec 100644
+--- a/drivers/staging/bcm/Misc.c
++++ b/drivers/staging/bcm/Misc.c
+@@ -764,7 +764,7 @@ void SendIdleModeResponse(PMINI_ADAPTER Adapter)
+=20
+                       /* Wake the LED Thread with IDLEMODE_ENTER State */
+                       Adapter->DriverState =3D LOWPOWER_MODE_ENTER;
+-                      BCM_DEBUG_PRINT(Adapter,DBG_TYPE_RX, RX_DPC, DBG_LVL_ALL,"LED Thread =
+is Running..Hence Setting LED Event as IDLEMODE_ENTER jiffies:%ld",jiffie=
+s);;
++                      BCM_DEBUG_PRINT(Adapter,DBG_TYPE_RX, RX_DPC, DBG_LVL_ALL,"LED Thread =
+is Running..Hence Setting LED Event as IDLEMODE_ENTER jiffies:%ld",jiffie=
+s);
+                       wake_up(&Adapter->LEDInfo.notify_led_event);
+=20
+                       /* Wait for 1 SEC for LED to OFF */
+@@ -1410,7 +1410,7 @@ int bcm_parse_target_params(PMINI_ADAPTER Adapter)
+=20
+ void beceem_parse_target_struct(PMINI_ADAPTER Adapter)
+ {
+-      UINT uiHostDrvrCfg6 =3D0, uiEEPROMFlag =3D 0;;
++      UINT uiHostDrvrCfg6 =3D0, uiEEPROMFlag =3D 0;
+=20
+       if(ntohl(Adapter->pstargetparams->m_u32PhyParameter2) & AUTO_SYNC_DISAB=
+LE)
+       {
+diff --git a/drivers/staging/comedi/drivers/addi-data/APCI1710_Tor.c b/dr=
+ivers/staging/comedi/drivers/addi-data/APCI1710_Tor.c
+index 7361d50..0e6affd 100644
+--- a/drivers/staging/comedi/drivers/addi-data/APCI1710_Tor.c
++++ b/drivers/staging/comedi/drivers/addi-data/APCI1710_Tor.c
+@@ -1008,7 +1008,7 @@ int i_APCI1710_InsnWriteEnableDisableTorCounter(str=
+uct comedi_device *dev,
+       b_ExternGate =3D (unsigned char) data[3];
+       b_CycleMode =3D (unsigned char) data[4];
+       b_InterruptEnable =3D (unsigned char) data[5];
+-      i_ReturnValue =3D insn->n;;
++      i_ReturnValue =3D insn->n;
+       devpriv->tsk_Current =3D current;       /*  Save the current process task str=
+ucture */
+       /**************************/
+       /* Test the module number */
+diff --git a/drivers/staging/comedi/drivers/addi-data/hwdrv_apci1500.c b/=
+drivers/staging/comedi/drivers/addi-data/hwdrv_apci1500.c
+index 2a8a6c7..62f421a 100644
+--- a/drivers/staging/comedi/drivers/addi-data/hwdrv_apci1500.c
++++ b/drivers/staging/comedi/drivers/addi-data/hwdrv_apci1500.c
+@@ -2850,7 +2850,7 @@ static int i_APCI1500_Reset(struct comedi_device *d=
+ev)
+       i_Logic =3D 0;
+       i_CounterLogic =3D 0;
+       i_InterruptMask =3D 0;
+-      i_InputChannel =3D 0;;
++      i_InputChannel =3D 0;
+       i_TimerCounter1Enabled =3D 0;
+       i_TimerCounter2Enabled =3D 0;
+       i_WatchdogCounter3Enabled =3D 0;
+diff --git a/drivers/staging/comedi/drivers/addi-data/hwdrv_apci1516.c b/=
+drivers/staging/comedi/drivers/addi-data/hwdrv_apci1516.c
+index 12fcc35..8a584a0 100644
+--- a/drivers/staging/comedi/drivers/addi-data/hwdrv_apci1516.c
++++ b/drivers/staging/comedi/drivers/addi-data/hwdrv_apci1516.c
+@@ -335,7 +335,7 @@ int i_APCI1516_WriteDigitalOutput(struct comedi_devic=
+e *dev, struct comedi_subde
+                       return -EINVAL;
+               }               /* if else data[3]=3D=3D1) */
+       }                       /* if else data[3]=3D=3D0) */
+-      return (insn->n);;
++      return (insn->n);
+ }
+=20
+ /*
+diff --git a/drivers/staging/comedi/drivers/addi-data/hwdrv_apci3501.c b/=
+drivers/staging/comedi/drivers/addi-data/hwdrv_apci3501.c
+index 356a189..acaceb0 100644
+--- a/drivers/staging/comedi/drivers/addi-data/hwdrv_apci3501.c
++++ b/drivers/staging/comedi/drivers/addi-data/hwdrv_apci3501.c
+@@ -339,7 +339,7 @@ int i_APCI3501_ConfigAnalogOutput(struct comedi_devic=
+e *dev, struct comedi_subde
+ int i_APCI3501_WriteAnalogOutput(struct comedi_device *dev, struct comed=
+i_subdevice *s,
+       struct comedi_insn *insn, unsigned int *data)
+ {
+-      unsigned int ul_Command1 =3D 0, ul_Channel_no, ul_Polarity, ul_DAC_Read=
+y =3D 0;;
++      unsigned int ul_Command1 =3D 0, ul_Channel_no, ul_Polarity, ul_DAC_Read=
+y =3D 0;
+=20
+       ul_Channel_no =3D CR_CHAN(insn->chanspec);
+=20
+diff --git a/drivers/staging/comedi/drivers/amplc_pci230.c b/drivers/stag=
+ing/comedi/drivers/amplc_pci230.c
+index 5d06457..7edeb11 100644
+--- a/drivers/staging/comedi/drivers/amplc_pci230.c
++++ b/drivers/staging/comedi/drivers/amplc_pci230.c
+@@ -971,7 +971,7 @@ static int pci230_attach(struct comedi_device *dev, s=
+truct comedi_devconfig *it)
+       if (thisboard->ao_chans > 0) {
+               s->type =3D COMEDI_SUBD_AO;
+               s->subdev_flags =3D SDF_WRITABLE | SDF_GROUND;
+-              s->n_chan =3D thisboard->ao_chans;;
++              s->n_chan =3D thisboard->ao_chans;
+               s->maxdata =3D (1 << thisboard->ao_bits) - 1;
+               s->range_table =3D &pci230_ao_range;
+               s->insn_write =3D &pci230_ao_winsn;
+diff --git a/drivers/staging/comedi/drivers/cb_das16_cs.c b/drivers/stagi=
+ng/comedi/drivers/cb_das16_cs.c
+index 0345b4c..bb93685 100644
+--- a/drivers/staging/comedi/drivers/cb_das16_cs.c
++++ b/drivers/staging/comedi/drivers/cb_das16_cs.c
+@@ -169,7 +169,7 @@ static int das16cs_attach(struct comedi_device *dev,
+       if (!link)
+               return -EIO;
+=20
+-      dev->iobase =3D link->resource[0]->start;;
++      dev->iobase =3D link->resource[0]->start;
+       printk("I/O base=3D0x%04lx ", dev->iobase);
+=20
+       printk("fingerprint:\n");
+diff --git a/drivers/staging/comedi/drivers/comedi_bond.c b/drivers/stagi=
+ng/comedi/drivers/comedi_bond.c
+index cfcbd9b..d8aefb2 100644
+--- a/drivers/staging/comedi/drivers/comedi_bond.c
++++ b/drivers/staging/comedi/drivers/comedi_bond.c
+@@ -370,7 +370,7 @@ static int doDevConfig(struct comedi_device *dev, str=
+uct comedi_devconfig *it)
+       struct comedi_device *devs_opened[COMEDI_NUM_BOARD_MINORS];
+=20
+       memset(devs_opened, 0, sizeof(devs_opened));
+-      devpriv->name[0] =3D 0;;
++      devpriv->name[0] =3D 0;
+       /* Loop through all comedi devices specified on the command-line,
+          building our device list */
+       for (i =3D 0; i < COMEDI_NDEVCONFOPTS && (!i || it->options[i]); ++i) {
+diff --git a/drivers/staging/crystalhd/crystalhd_hw.c b/drivers/staging/c=
+rystalhd/crystalhd_hw.c
+index f631857..153ddbf 100644
+--- a/drivers/staging/crystalhd/crystalhd_hw.c
++++ b/drivers/staging/crystalhd/crystalhd_hw.c
+@@ -1711,7 +1711,7 @@ enum BC_STATUS crystalhd_download_fw(struct crystal=
+hd_adp *adp, void *buffer, ui
+       }
+=20
+       BCMLOG(BCMLOG_INFO, "Firmware Downloaded Successfully\n");
+-      return BC_STS_SUCCESS;;
++      return BC_STS_SUCCESS;
+ }
+=20
+ enum BC_STATUS crystalhd_do_fw_cmd(struct crystalhd_hw *hw,
+diff --git a/drivers/staging/go7007/go7007-driver.c b/drivers/staging/go7=
+007/go7007-driver.c
+index b3f42f3..8426a02 100644
+--- a/drivers/staging/go7007/go7007-driver.c
++++ b/drivers/staging/go7007/go7007-driver.c
+@@ -624,7 +624,7 @@ struct go7007 *go7007_alloc(struct go7007_board_info =
+*board, struct device *dev)
+       go->dvd_mode =3D 0;
+       go->interlace_coding =3D 0;
+       for (i =3D 0; i < 4; ++i)
+-              go->modet[i].enable =3D 0;;
++              go->modet[i].enable =3D 0;
+       for (i =3D 0; i < 1624; ++i)
+               go->modet_map[i] =3D 0;
+       go->audio_deliver =3D NULL;
+diff --git a/drivers/staging/iio/accel/lis3l02dq_ring.c b/drivers/staging=
+/iio/accel/lis3l02dq_ring.c
+index 330d5d6..1fd088a 100644
+--- a/drivers/staging/iio/accel/lis3l02dq_ring.c
++++ b/drivers/staging/iio/accel/lis3l02dq_ring.c
+@@ -517,7 +517,7 @@ int lis3l02dq_configure_ring(struct iio_dev *indio_de=
+v)
+=20
+       ret =3D iio_alloc_pollfunc(indio_dev, NULL, &lis3l02dq_poll_func_th);
+       if (ret)
+-              goto error_iio_sw_rb_free;;
++              goto error_iio_sw_rb_free;
+       indio_dev->modes |=3D INDIO_RING_TRIGGERED;
+       return 0;
+=20
+diff --git a/drivers/staging/intel_sst/intel_sst_drv_interface.c b/driver=
+s/staging/intel_sst/intel_sst_drv_interface.c
+index 669e298..6443fbd 100644
+--- a/drivers/staging/intel_sst/intel_sst_drv_interface.c
++++ b/drivers/staging/intel_sst/intel_sst_drv_interface.c
+@@ -171,9 +171,9 @@ static int sst_get_sfreq(struct snd_sst_params *str_p=
+aram)
+       case SST_CODEC_TYPE_MP3:
+               return str_param->sparams.uc.mp3_params.sfreq;
+       case SST_CODEC_TYPE_AAC:
+-              return str_param->sparams.uc.aac_params.sfreq;;
++              return str_param->sparams.uc.aac_params.sfreq;
+       case SST_CODEC_TYPE_WMA9:
+-              return str_param->sparams.uc.wma_params.sfreq;;
++              return str_param->sparams.uc.wma_params.sfreq;
+       default:
+               return 0;
+       }
+diff --git a/drivers/staging/keucr/smilmain.c b/drivers/staging/keucr/smi=
+lmain.c
+index bdfbf76..2cbe9f8 100644
+--- a/drivers/staging/keucr/smilmain.c
++++ b/drivers/staging/keucr/smilmain.c
+@@ -153,9 +153,9 @@ int Media_D_ReadSector(struct us_data *us, DWORD star=
+t,WORD count,BYTE *buf)
+       WORD len, bn;
+=20
+       //if (Check_D_MediaPower())        ; =A6b 6250 don't care
+-      //    return(ErrCode);             ;
++      //    return(ErrCode);
+       //if (Check_D_MediaFmt(fdoExt))    ;
+-      //    return(ErrCode);             ;
++      //    return(ErrCode);
+       if (Conv_D_MediaAddr(us, start))
+               return(ErrCode);
+=20
+diff --git a/drivers/staging/keucr/smilsub.c b/drivers/staging/keucr/smil=
+sub.c
+index 1b52535..ce10cf2 100644
+--- a/drivers/staging/keucr/smilsub.c
++++ b/drivers/staging/keucr/smilsub.c
+@@ -763,8 +763,8 @@ int Ssfdc_D_WriteSectForCopy(struct us_data *us, BYTE=
+ *buf, BYTE *redundant)
+       bcb->CDB[7]                     =3D (BYTE)addr;
+       bcb->CDB[6]                     =3D (BYTE)(addr/0x0100);
+       bcb->CDB[5]                     =3D Media.Zone/2;
+-      bcb->CDB[8]                     =3D *(redundant+REDT_ADDR1H);;
+-      bcb->CDB[9]                     =3D *(redundant+REDT_ADDR1L);;
++      bcb->CDB[8]                     =3D *(redundant+REDT_ADDR1H);
++      bcb->CDB[9]                     =3D *(redundant+REDT_ADDR1L);
+=20
+       result =3D ENE_SendScsiCmd(us, FDIR_WRITE, buf, 0);
+       if (result !=3D USB_STOR_XFER_GOOD)
+diff --git a/drivers/staging/msm/lcdc_toshiba_wvga_pt.c b/drivers/staging=
+/msm/lcdc_toshiba_wvga_pt.c
+index 864d7c1..edba78a 100644
+--- a/drivers/staging/msm/lcdc_toshiba_wvga_pt.c
++++ b/drivers/staging/msm/lcdc_toshiba_wvga_pt.c
+@@ -77,7 +77,7 @@ static void toshiba_spi_write(char cmd, uint32 data, in=
+t num)
+=20
+       /* followed by parameter bytes */
+       if (num) {
+-              bp =3D (char *)&data;;
++              bp =3D (char *)&data;
+               bp +=3D (num - 1);
+               while (num) {
+                       toshiba_spi_write_byte(1, *bp);
+diff --git a/drivers/staging/rt2860/common/cmm_data_pci.c b/drivers/stagi=
+ng/rt2860/common/cmm_data_pci.c
+index 43d73a0..7af59ff 100644
+--- a/drivers/staging/rt2860/common/cmm_data_pci.c
++++ b/drivers/staging/rt2860/common/cmm_data_pci.c
+@@ -137,7 +137,7 @@ u16 RtmpPCI_WriteSingleTxResource(struct rt_rtmp_adap=
+ter *pAd,
+=20
+       pTxD->SDPtr0 =3D BufBasePaLow;
+       pTxD->SDLen0 =3D TXINFO_SIZE + TXWI_SIZE + hwHeaderLen; /* include padd=
+ing */
+-      pTxD->SDPtr1 =3D PCI_MAP_SINGLE(pAd, pTxBlk, 0, 1, PCI_DMA_TODEVICE);;
++      pTxD->SDPtr1 =3D PCI_MAP_SINGLE(pAd, pTxBlk, 0, 1, PCI_DMA_TODEVICE);
+       pTxD->SDLen1 =3D pTxBlk->SrcBufLen;
+       pTxD->LastSec0 =3D 0;
+       pTxD->LastSec1 =3D (bIsLast) ? 1 : 0;
+@@ -215,7 +215,7 @@ u16 RtmpPCI_WriteMultiTxResource(struct rt_rtmp_adapt=
+er *pAd,
+=20
+       pTxD->SDPtr0 =3D BufBasePaLow;
+       pTxD->SDLen0 =3D firstDMALen;   /* include padding */
+-      pTxD->SDPtr1 =3D PCI_MAP_SINGLE(pAd, pTxBlk, 0, 1, PCI_DMA_TODEVICE);;
++      pTxD->SDPtr1 =3D PCI_MAP_SINGLE(pAd, pTxBlk, 0, 1, PCI_DMA_TODEVICE);
+       pTxD->SDLen1 =3D pTxBlk->SrcBufLen;
+       pTxD->LastSec0 =3D 0;
+       pTxD->LastSec1 =3D (bIsLast) ? 1 : 0;
+diff --git a/drivers/staging/rt2860/rt_linux.c b/drivers/staging/rt2860/r=
+t_linux.c
+index abfeea1..7dad6ee 100644
+--- a/drivers/staging/rt2860/rt_linux.c
++++ b/drivers/staging/rt2860/rt_linux.c
+@@ -854,7 +854,7 @@ void send_monitor_packets(struct rt_rtmp_adapter *pAd=
+, struct rt_rx_blk *pRxBlk)
+                                                                        RSSI1,
+                                                                        RSSI_1),
+                                   ConvertToRssi(pAd, pRxBlk->pRxWI->RSSI2,
+-                                                RSSI_2));;
++                                                RSSI_2));
+=20
+       ph->signal.did =3D DIDmsg_lnxind_wlansniffrm_signal;
+       ph->signal.status =3D 0;
+diff --git a/drivers/staging/rt2860/rtmp.h b/drivers/staging/rt2860/rtmp.=
+h
+index ca54e53..26cc823 100644
+--- a/drivers/staging/rt2860/rtmp.h
++++ b/drivers/staging/rt2860/rtmp.h
+@@ -2978,7 +2978,7 @@ void LinkDown(struct rt_rtmp_adapter *pAd, IN BOOLE=
+AN IsReqFromAP);
+=20
+ void IterateOnBssTab(struct rt_rtmp_adapter *pAd);
+=20
+-void IterateOnBssTab2(struct rt_rtmp_adapter *pAd);;
++void IterateOnBssTab2(struct rt_rtmp_adapter *pAd);
+=20
+ void JoinParmFill(struct rt_rtmp_adapter *pAd,
+                 struct rt_mlme_join_req *JoinReq, unsigned long BssIdx);
+diff --git a/drivers/staging/rtl8192e/ieee80211/ieee80211_tx.c b/drivers/=
+staging/rtl8192e/ieee80211/ieee80211_tx.c
+index dd8a221..b26b5a8 100644
+--- a/drivers/staging/rtl8192e/ieee80211/ieee80211_tx.c
++++ b/drivers/staging/rtl8192e/ieee80211/ieee80211_tx.c
+@@ -822,7 +822,7 @@ int ieee80211_rtl_xmit(struct sk_buff *skb, struct ne=
+t_device *dev)
+               {
+                       txb->queue_index =3D UP2AC(skb->priority);
+               } else {
+-                      txb->queue_index =3D WME_AC_BK;;
++                      txb->queue_index =3D WME_AC_BK;
+               }
+=20
+=20
+diff --git a/drivers/staging/rtl8192e/r819xE_phy.c b/drivers/staging/rtl8=
+192e/r819xE_phy.c
+index d83bcbc..50cd0e5 100644
+--- a/drivers/staging/rtl8192e/r819xE_phy.c
++++ b/drivers/staging/rtl8192e/r819xE_phy.c
+@@ -2596,7 +2596,7 @@ u8 rtl8192_phy_ConfigRFWithHeaderFile(struct net_de=
+vice* dev, RF90_RADIO_PATH_E
+                       break;
+       }
+=20
+-      return ret;;
++      return ret;
+=20
+ }
+ /***********************************************************************=
+*******
+diff --git a/drivers/staging/rtl8192u/ieee80211/ieee80211_tx.c b/drivers/=
+staging/rtl8192u/ieee80211/ieee80211_tx.c
+index 81aa2ed..ec7845e 100644
+--- a/drivers/staging/rtl8192u/ieee80211/ieee80211_tx.c
++++ b/drivers/staging/rtl8192u/ieee80211/ieee80211_tx.c
+@@ -754,7 +754,7 @@ int ieee80211_xmit(struct sk_buff *skb, struct net_de=
+vice *dev)
+               {
+                       txb->queue_index =3D UP2AC(skb->priority);
+               } else {
+-                      txb->queue_index =3D WME_AC_BK;;
++                      txb->queue_index =3D WME_AC_BK;
+               }
+=20
+=20
+diff --git a/drivers/staging/rtl8192u/r8192U_core.c b/drivers/staging/rtl=
+8192u/r8192U_core.c
+index 494f180..1139a27 100644
+--- a/drivers/staging/rtl8192u/r8192U_core.c
++++ b/drivers/staging/rtl8192u/r8192U_core.c
+@@ -5085,7 +5085,7 @@ static void rtl8192_query_rxphystatus(
+                       //Get Rx snr value in DB
+                       tmp_rxsnr =3D   pofdm_buf->rxsnr_X[i];
+                       rx_snrX =3D (char)(tmp_rxsnr);
+-                      //rx_snrX >>=3D 1;;
++                      //rx_snrX >>=3D 1;
+                       rx_snrX /=3D 2;
+                       priv->stats.rxSNRdB[i] =3D (long)rx_snrX;
+=20
+diff --git a/drivers/staging/rtl8192u/r819xU_phy.c b/drivers/staging/rtl8=
+192u/r819xU_phy.c
+index a3adaed..8e10992 100644
+--- a/drivers/staging/rtl8192u/r819xU_phy.c
++++ b/drivers/staging/rtl8192u/r819xU_phy.c
+@@ -1011,7 +1011,7 @@ u8 rtl8192_phy_ConfigRFWithHeaderFile(struct net_de=
+vice* dev, RF90_RADIO_PATH_E
+                       break;
+       }
+=20
+-      return ret;;
++      return ret;
+=20
+ }
+ /***********************************************************************=
+*******
+diff --git a/drivers/staging/rtl8712/rtl8712_efuse.c b/drivers/staging/rt=
+l8712/rtl8712_efuse.c
+index 9730ae1..1dc12b7 100644
+--- a/drivers/staging/rtl8712/rtl8712_efuse.c
++++ b/drivers/staging/rtl8712/rtl8712_efuse.c
+@@ -428,7 +428,7 @@ u8 r8712_efuse_access(struct _adapter *padapter, u8 b=
+Read, u16 start_addr,
+                     u16 cnts, u8 *data)
+ {
+       int i;
+-      u8 res =3D true;;
++      u8 res =3D true;
+=20
+       if (start_addr > EFUSE_MAX_SIZE)
+               return false;
+diff --git a/drivers/staging/rtl8712/rtl8712_xmit.c b/drivers/staging/rtl=
+8712/rtl8712_xmit.c
+index 8edc518..88a1504 100644
+--- a/drivers/staging/rtl8712/rtl8712_xmit.c
++++ b/drivers/staging/rtl8712/rtl8712_xmit.c
+@@ -148,7 +148,7 @@ static u32 get_ff_hwaddr(struct xmit_frame *pxmitfram=
+e)
+               case 0x11:
+               case 0x12:
+               case 0x13:
+-                      addr =3D RTL8712_DMA_H2CCMD;;
++                      addr =3D RTL8712_DMA_H2CCMD;
+                       break;
+               default:
+                       addr =3D RTL8712_DMA_BEQ;/*RTL8712_EP_LO;*/
+diff --git a/drivers/staging/rtl8712/rtl871x_xmit.c b/drivers/staging/rtl=
+8712/rtl871x_xmit.c
+index b8195e3..75f1a6b 100644
+--- a/drivers/staging/rtl8712/rtl871x_xmit.c
++++ b/drivers/staging/rtl8712/rtl871x_xmit.c
+@@ -372,7 +372,7 @@ static sint xmitframe_addmic(struct _adapter *padapte=
+r,
+                                          0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+                                          0x0, 0x0};
+                       datalen =3D pattrib->pktlen - pattrib->hdrlen;
+-                      pframe =3D pxmitframe->buf_addr + TXDESC_OFFSET;;
++                      pframe =3D pxmitframe->buf_addr + TXDESC_OFFSET;
+                       if (bmcst) {
+                               if (!memcmp(psecuritypriv->XGrptxmickey
+                                  [psecuritypriv->XGrpKeyid].skey,
+diff --git a/drivers/staging/tidspbridge/core/tiomap3430.c b/drivers/stag=
+ing/tidspbridge/core/tiomap3430.c
+index 1be081f..a3b0a18 100644
+--- a/drivers/staging/tidspbridge/core/tiomap3430.c
++++ b/drivers/staging/tidspbridge/core/tiomap3430.c
+@@ -596,7 +596,7 @@ static int bridge_brd_start(struct bridge_dev_context=
+ *dev_ctxt,
+               dev_dbg(bridge, "DSP c_int00 Address =3D  0x%x\n", dsp_addr);
+               if (dsp_debug)
+                       while (__raw_readw(dw_sync_addr))
+-                              ;;
++                              ;
+=20
+               /* Wait for DSP to clear word in shared memory */
+               /* Read the Location */
+@@ -1671,7 +1671,7 @@ static int pte_set(struct pg_table_attrs *pt, u32 p=
+a, u32 va,
+                       /* Find a free L2 PT. */
+                       for (i =3D 0; (i < pt->l2_num_pages) &&
+                            (pt->pg_info[i].num_entries !=3D 0); i++)
+-                              ;;
++                              ;
+                       if (i < pt->l2_num_pages) {
+                               l2_page_num =3D i;
+                               l2_base_pa =3D pt->l2_base_pa + (l2_page_num *
+diff --git a/drivers/staging/tidspbridge/rmgr/nldr.c b/drivers/staging/ti=
+dspbridge/rmgr/nldr.c
+index a6ae007..28354bb 100644
+--- a/drivers/staging/tidspbridge/rmgr/nldr.c
++++ b/drivers/staging/tidspbridge/rmgr/nldr.c
+@@ -943,7 +943,7 @@ static int add_ovly_info(void *handle, struct dbll_se=
+ct_info *sect_info,
+=20
+       /* Determine which phase this section belongs to */
+       for (pch =3D sect_name + 1; *pch && *pch !=3D seps; pch++)
+-              ;;
++              ;
+=20
+       if (*pch) {
+               pch++;          /* Skip over the ':' */
+diff --git a/drivers/staging/vt6655/card.c b/drivers/staging/vt6655/card.=
+c
+index 32d095c..951a3a8 100644
+--- a/drivers/staging/vt6655/card.c
++++ b/drivers/staging/vt6655/card.c
+@@ -2058,7 +2058,7 @@ bool CARDbSoftwareReset (void *pDeviceHandler)
+ QWORD CARDqGetTSFOffset (unsigned char byRxRate, QWORD qwTSF1, QWORD qwT=
+SF2)
+ {
+     QWORD   qwTSFOffset;
+-    unsigned short wRxBcnTSFOffst=3D 0;;
++    unsigned short wRxBcnTSFOffst=3D 0;
+=20
+     HIDWORD(qwTSFOffset) =3D 0;
+     LODWORD(qwTSFOffset) =3D 0;
+diff --git a/drivers/staging/vt6655/iwctl.c b/drivers/staging/vt6655/iwct=
+l.c
+index 92e3399..5e425d1 100644
+--- a/drivers/staging/vt6655/iwctl.c
++++ b/drivers/staging/vt6655/iwctl.c
+@@ -2073,7 +2073,7 @@ int iwctl_giwencodeext(struct net_device *dev,
+              struct iw_point *wrq,
+              char *extra)
+ {
+-              return -EOPNOTSUPP;;
++              return -EOPNOTSUPP;
+ }
+=20
+ int iwctl_siwmlme(struct net_device *dev,
+diff --git a/drivers/staging/vt6655/wpa2.c b/drivers/staging/vt6655/wpa2.=
+c
+index 805164b..744799c 100644
+--- a/drivers/staging/vt6655/wpa2.c
++++ b/drivers/staging/vt6655/wpa2.c
+@@ -216,7 +216,7 @@ WPA2vParseRSN (
+         m =3D *((unsigned short *) &(pRSN->abyRSN[4]));
+=20
+         if (pRSN->len >=3D 10+m*4) { // ver(2) + GK(4) + PK count(2) + P=
+KS(4*m) + AKMSS count(2)
+-            pBSSNode->wAKMSSAuthCount =3D *((unsigned short *) &(pRSN->a=
+byRSN[6+4*m]));;
++            pBSSNode->wAKMSSAuthCount =3D *((unsigned short *) &(pRSN->a=
+byRSN[6+4*m]));
+             j =3D 0;
+             pbyOUI =3D &(pRSN->abyRSN[8+4*m]);
+             for (i =3D 0; (i < pBSSNode->wAKMSSAuthCount) && (j < sizeof=
+(pBSSNode->abyAKMSSAuthType)/sizeof(unsigned char)); i++) {
+@@ -235,7 +235,7 @@ WPA2vParseRSN (
+             pBSSNode->wAKMSSAuthCount =3D (unsigned short)j;
+             DBG_PRT(MSG_LEVEL_DEBUG, KERN_INFO"wAKMSSAuthCount: %d\n", p=
+BSSNode->wAKMSSAuthCount);
+=20
+-            n =3D *((unsigned short *) &(pRSN->abyRSN[6+4*m]));;
++            n =3D *((unsigned short *) &(pRSN->abyRSN[6+4*m]));
+             if (pRSN->len >=3D 12+4*m+4*n) { // ver(2)+GK(4)+PKCnt(2)+PK=
+S(4*m)+AKMSSCnt(2)+AKMSS(4*n)+Cap(2)
+                 pBSSNode->sRSNCapObj.bRSNCapExist =3D true;
+                 pBSSNode->sRSNCapObj.wRSNCap =3D *((unsigned short *) &(=
+pRSN->abyRSN[8+4*m+4*n]));
+diff --git a/drivers/staging/vt6656/baseband.c b/drivers/staging/vt6656/b=
+aseband.c
+index e5add20..0d11147 100644
+--- a/drivers/staging/vt6656/baseband.c
++++ b/drivers/staging/vt6656/baseband.c
+@@ -963,7 +963,7 @@ BBvSetAntennaMode (PSDevice pDevice, BYTE byAntennaMo=
+de)
+             break;
+         case ANT_RXB:
+             pDevice->byBBRxConf &=3D 0xFE;
+-            pDevice->byBBRxConf |=3D 0x02;;
++            pDevice->byBBRxConf |=3D 0x02;
+             break;
+     }
+=20
+diff --git a/drivers/staging/vt6656/iwctl.c b/drivers/staging/vt6656/iwct=
+l.c
+index 0004be8..2121205 100644
+--- a/drivers/staging/vt6656/iwctl.c
++++ b/drivers/staging/vt6656/iwctl.c
+@@ -1883,7 +1883,7 @@ int iwctl_giwencodeext(struct net_device *dev,
+              struct iw_point *wrq,
+              char *extra)
+ {
+-              return -EOPNOTSUPP;;
++              return -EOPNOTSUPP;
+ }
+=20
+ int iwctl_siwmlme(struct net_device *dev,
+diff --git a/drivers/staging/vt6656/power.c b/drivers/staging/vt6656/powe=
+r.c
+index 0c12fd3..e8c1b35 100644
+--- a/drivers/staging/vt6656/power.c
++++ b/drivers/staging/vt6656/power.c
+@@ -192,7 +192,7 @@ BOOL PSbConsiderPowerDown(void *hDeviceContext,
+     // check if already in Doze mode
+     ControlvReadByte(pDevice, MESSAGE_REQUEST_MACREG, MAC_REG_PSCTL, &by=
+Data);
+     if ( (byData & PSCTL_PS) !=3D 0 )
+-        return TRUE;;
++        return TRUE;
+=20
+     if (pMgmt->eCurrMode !=3D WMAC_MODE_IBSS_STA) {
+         // check if in TIM wake period
+diff --git a/drivers/staging/vt6656/wpa2.c b/drivers/staging/vt6656/wpa2.=
+c
+index 6d13190..d4f3f75 100644
+--- a/drivers/staging/vt6656/wpa2.c
++++ b/drivers/staging/vt6656/wpa2.c
+@@ -215,7 +215,7 @@ WPA2vParseRSN (
+         m =3D *((PWORD) &(pRSN->abyRSN[4]));
+=20
+         if (pRSN->len >=3D 10+m*4) { // ver(2) + GK(4) + PK count(2) + P=
+KS(4*m) + AKMSS count(2)
+-            pBSSNode->wAKMSSAuthCount =3D *((PWORD) &(pRSN->abyRSN[6+4*m=
+]));;
++            pBSSNode->wAKMSSAuthCount =3D *((PWORD) &(pRSN->abyRSN[6+4*m=
+]));
+             j =3D 0;
+             pbyOUI =3D &(pRSN->abyRSN[8+4*m]);
+             for (i =3D 0; (i < pBSSNode->wAKMSSAuthCount) && (j < sizeof=
+(pBSSNode->abyAKMSSAuthType)/sizeof(BYTE)); i++) {
+@@ -234,7 +234,7 @@ WPA2vParseRSN (
+             pBSSNode->wAKMSSAuthCount =3D (WORD)j;
+             DBG_PRT(MSG_LEVEL_DEBUG, KERN_INFO"wAKMSSAuthCount: %d\n", p=
+BSSNode->wAKMSSAuthCount);
+=20
+-            n =3D *((PWORD) &(pRSN->abyRSN[6+4*m]));;
++            n =3D *((PWORD) &(pRSN->abyRSN[6+4*m]));
+             if (pRSN->len >=3D 12+4*m+4*n) { // ver(2)+GK(4)+PKCnt(2)+PK=
+S(4*m)+AKMSSCnt(2)+AKMSS(4*n)+Cap(2)
+                 pBSSNode->sRSNCapObj.bRSNCapExist =3D TRUE;
+                 pBSSNode->sRSNCapObj.wRSNCap =3D *((PWORD) &(pRSN->abyRS=
+N[8+4*m+4*n]));
+--=20
+1.7.3.1.g432b3.dirty
+
+
+--===============1088501263==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+devel mailing list
+devel@linuxdriverproject.org
+http://driverdev.linuxdriverproject.org/mailman/listinfo/devel
+
+--===============1088501263==--
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002268:2, b/test/corpora/lkml/cur/1382298793.002268:2,
new file mode 100644 (file)
index 0000000..116de34
--- /dev/null
@@ -0,0 +1,66 @@
+From: Joe Perches <joe-6d6DIl74uiNBDgjK7y7TUQ@public.gmane.org>
+Subject: [PATCH 42/44] net/sunrpc/addr.c: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:05:01 -0800
+Lines: 26
+Message-ID: <aca92092a705e0d21176b5ac7d3581c4f9140dbc.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: "J. Bruce Fields" <bfields-uC3wQj2KruNg9hUCZPvPmw@public.gmane.org>,
+       Neil Brown <neilb-l3A5Bk7waGM@public.gmane.org>,
+       Trond Myklebust <Trond.Myklebust-HgOvQuBEEgTQT0dZR+AlfA@public.gmane.org>,
+       "David S. Miller" <davem-fT/PcQaiUtIeIZ0/mPfg9Q@public.gmane.org>, linux-nfs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org,
+       netdev-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Jiri Kosina <trivial-DgEjT+Ai2ygdnm+yROfE0A@public.gmane.org>
+X-From: linux-nfs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Mon Nov 15 04:07:25 2010
+Return-path: <linux-nfs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glN-linux-nfs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-nfs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1PHpPh-0001H8-D9
+       for glN-linux-nfs-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Mon, 15 Nov 2010 04:07:25 +0100
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S932828Ab0KODGd (ORCPT <rfc822;glN-linux-nfs@m.gmane.org>);
+       Sun, 14 Nov 2010 22:06:33 -0500
+Received: from mail.perches.com ([173.55.12.10]:1267 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932988Ab0KODGD (ORCPT <rfc822;linux-nfs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Sun, 14 Nov 2010 22:06:03 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 3EB452436E;
+       Sun, 14 Nov 2010 19:04:30 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe-6d6DIl74uiNBDgjK7y7TUQ@public.gmane.org>
+Sender: linux-nfs-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-nfs.vger.kernel.org>
+X-Mailing-List: linux-nfs-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062287>
+
+Signed-off-by: Joe Perches <joe-6d6DIl74uiNBDgjK7y7TUQ@public.gmane.org>
+---
+ net/sunrpc/addr.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/net/sunrpc/addr.c b/net/sunrpc/addr.c
+index 1419d0c..4195233 100644
+--- a/net/sunrpc/addr.c
++++ b/net/sunrpc/addr.c
+@@ -151,7 +151,7 @@ static size_t rpc_pton4(const char *buf, const size_t buflen,
+               return 0;
+       sin->sin_family = AF_INET;
+-      return sizeof(struct sockaddr_in);;
++      return sizeof(struct sockaddr_in);
+ }
+ #if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
+-- 
+1.7.3.1.g432b3.dirty
+
+--
+To unsubscribe from this list: send the line "unsubscribe linux-nfs" in
+the body of a message to majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002269:2, b/test/corpora/lkml/cur/1382298793.002269:2,
new file mode 100644 (file)
index 0000000..2e2652a
--- /dev/null
@@ -0,0 +1,74 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 32/44] fs/9p: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:51 -0800
+Lines: 35
+Message-ID: <f6ae29dfdd0b9ad401f5eb77ae670212689ee921.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Eric Van Hensbergen <ericvh@gmail.com>,
+       Ron Minnich <rminnich@sandia.gov>,
+       Latchesar Ionkov <lucho@ionkov.net>,
+       v9fs-developer@lists.sourceforge.net, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:07:26 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpPg-0001H8-Sy
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:07:25 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932992Ab0KODGD (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:06:03 -0500
+Received: from mail.perches.com ([173.55.12.10]:1227 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932846Ab0KODF4 (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:56 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id C9F2E2436F;
+       Sun, 14 Nov 2010 19:04:23 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062288>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ fs/9p/acl.c   |    2 +-
+ fs/9p/xattr.c |    2 +-
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/fs/9p/acl.c b/fs/9p/acl.c
+index 12d6023..bc4da9a 100644
+--- a/fs/9p/acl.c
++++ b/fs/9p/acl.c
+@@ -28,7 +28,7 @@ static struct posix_acl *__v9fs_get_acl(struct p9_fid *fid, char *name)
+ {
+       ssize_t size;
+       void *value = NULL;
+-      struct posix_acl *acl = NULL;;
++      struct posix_acl *acl = NULL;
+       size = v9fs_fid_xattr_get(fid, name, NULL, 0);
+       if (size > 0) {
+diff --git a/fs/9p/xattr.c b/fs/9p/xattr.c
+index 43ec7df..d288773 100644
+--- a/fs/9p/xattr.c
++++ b/fs/9p/xattr.c
+@@ -133,7 +133,7 @@ int v9fs_xattr_set(struct dentry *dentry, const char *name,
+                       "p9_client_xattrcreate failed %d\n", retval);
+               goto error;
+       }
+-      msize = fid->clnt->msize;;
++      msize = fid->clnt->msize;
+       while (value_len) {
+               if (value_len > (msize - P9_IOHDRSZ))
+                       write_count = msize - P9_IOHDRSZ;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002270:2, b/test/corpora/lkml/cur/1382298793.002270:2,
new file mode 100644 (file)
index 0000000..8e09306
--- /dev/null
@@ -0,0 +1,68 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 41/44] net/ipv6/mcast.c: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:05:00 -0800
+Lines: 26
+Message-ID: <1f3e1f7e454f3c62b66fc5f3e1e1ed90d62b7fb0.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: "David S. Miller" <davem@davemloft.net>,
+       Alexey Kuznetsov <kuznet@ms2.inr.ac.ru>,
+       "Pekka Savola (ipv6)" <pekkas@netcore.fi>,
+       James Morris <jmorris@namei.org>,
+       Hideaki YOSHIFUJI <yoshfuji@linux-ipv6.org>,
+       Patrick McHardy <kaber@trash.net>, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: netdev-owner@vger.kernel.org Mon Nov 15 04:07:30 2010
+Return-path: <netdev-owner@vger.kernel.org>
+Envelope-to: linux-netdev-2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <netdev-owner@vger.kernel.org>)
+       id 1PHpPi-0001H8-UT
+       for linux-netdev-2@lo.gmane.org; Mon, 15 Nov 2010 04:07:27 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933051Ab0KODGq (ORCPT <rfc822;linux-netdev-2@m.gmane.org>);
+       Sun, 14 Nov 2010 22:06:46 -0500
+Received: from mail.perches.com ([173.55.12.10]:1258 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932980Ab0KODGC (ORCPT <rfc822;netdev@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:06:02 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 8F37A24377;
+       Sun, 14 Nov 2010 19:04:29 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: netdev-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <netdev.vger.kernel.org>
+X-Mailing-List: netdev@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062289>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ net/ipv6/mcast.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/net/ipv6/mcast.c b/net/ipv6/mcast.c
+index d1444b9..9c50745 100644
+--- a/net/ipv6/mcast.c
++++ b/net/ipv6/mcast.c
+@@ -257,7 +257,7 @@ static struct inet6_dev *ip6_mc_find_dev_rcu(struct net *net,
+               return NULL;
+       idev = __in6_dev_get(dev);
+       if (!idev)
+-              return NULL;;
++              return NULL;
+       read_lock_bh(&idev->lock);
+       if (idev->dead) {
+               read_unlock_bh(&idev->lock);
+-- 
+1.7.3.1.g432b3.dirty
+
+--
+To unsubscribe from this list: send the line "unsubscribe netdev" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002271:2, b/test/corpora/lkml/cur/1382298793.002271:2,
new file mode 100644 (file)
index 0000000..144fc89
--- /dev/null
@@ -0,0 +1,58 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 38/44] include/linux/if_macvlan.h: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:57 -0800
+Lines: 21
+Message-ID: <186ca914f887b2354ea3178696edc81cacbb28c6.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Patrick McHardy <kaber@trash.net>, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:08:18 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpQX-0001fQ-U4
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:08:18 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933080Ab0KODHz (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:07:55 -0500
+Received: from mail.perches.com ([173.55.12.10]:1249 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932730Ab0KODGA (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:06:00 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id AAEDA24375;
+       Sun, 14 Nov 2010 19:04:27 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062290>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ include/linux/if_macvlan.h |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/include/linux/if_macvlan.h b/include/linux/if_macvlan.h
+index 8a2fd66..ac96a2d 100644
+--- a/include/linux/if_macvlan.h
++++ b/include/linux/if_macvlan.h
+@@ -69,7 +69,7 @@ static inline void macvlan_count_rx(const struct macvlan_dev *vlan,
+       rx_stats = this_cpu_ptr(vlan->rx_stats);
+       if (likely(success)) {
+               u64_stats_update_begin(&rx_stats->syncp);
+-              rx_stats->rx_packets++;;
++              rx_stats->rx_packets++;
+               rx_stats->rx_bytes += len;
+               if (multicast)
+                       rx_stats->rx_multicast++;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002272:2, b/test/corpora/lkml/cur/1382298793.002272:2,
new file mode 100644 (file)
index 0000000..f276bb4
--- /dev/null
@@ -0,0 +1,59 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 37/44] fs/ubifs: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:56 -0800
+Lines: 21
+Message-ID: <902d76520da2f65e5dc44339dccb07159947f23d.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Artem Bityutskiy <dedekind1@gmail.com>,
+       Adrian Hunter <adrian.hunter@nokia.com>,
+       linux-mtd@lists.infradead.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:08:19 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpQY-0001fQ-EH
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:08:18 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933129Ab0KODH5 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:07:57 -0500
+Received: from mail.perches.com ([173.55.12.10]:1247 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932907Ab0KODF7 (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:59 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 0CF9424374;
+       Sun, 14 Nov 2010 19:04:27 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062291>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ fs/ubifs/scan.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/fs/ubifs/scan.c b/fs/ubifs/scan.c
+index 3e1ee57..36216b4 100644
+--- a/fs/ubifs/scan.c
++++ b/fs/ubifs/scan.c
+@@ -328,7 +328,7 @@ struct ubifs_scan_leb *ubifs_scan(const struct ubifs_info *c, int lnum,
+               if (!quiet)
+                       ubifs_err("empty space starts at non-aligned offset %d",
+                                 offs);
+-              goto corrupted;;
++              goto corrupted;
+       }
+       ubifs_end_scan(c, sleb, lnum, offs);
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002273:2, b/test/corpora/lkml/cur/1382298793.002273:2,
new file mode 100644 (file)
index 0000000..d5eca3b
--- /dev/null
@@ -0,0 +1,64 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 39/44] include/net/caif/cfctrl.h: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:58 -0800
+Lines: 26
+Message-ID: <35914cfea1bd0ab3963e632d02b1fdd52a9d2bc8.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Sjur Braendeland <sjur.brandeland@stericsson.com>,
+       "David S. Miller" <davem@davemloft.net>, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: netdev-owner@vger.kernel.org Mon Nov 15 04:08:21 2010
+Return-path: <netdev-owner@vger.kernel.org>
+Envelope-to: linux-netdev-2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <netdev-owner@vger.kernel.org>)
+       id 1PHpQW-0001fQ-TC
+       for linux-netdev-2@lo.gmane.org; Mon, 15 Nov 2010 04:08:17 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932988Ab0KODHd (ORCPT <rfc822;linux-netdev-2@m.gmane.org>);
+       Sun, 14 Nov 2010 22:07:33 -0500
+Received: from mail.perches.com ([173.55.12.10]:1252 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932944Ab0KODGB (ORCPT <rfc822;netdev@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:06:01 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 4F8E124376;
+       Sun, 14 Nov 2010 19:04:28 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: netdev-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <netdev.vger.kernel.org>
+X-Mailing-List: netdev@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062292>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ include/net/caif/cfctrl.h |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/include/net/caif/cfctrl.h b/include/net/caif/cfctrl.h
+index 9402543..e54f639 100644
+--- a/include/net/caif/cfctrl.h
++++ b/include/net/caif/cfctrl.h
+@@ -51,7 +51,7 @@ struct cfctrl_rsp {
+       void (*restart_rsp)(void);
+       void (*radioset_rsp)(void);
+       void (*reject_rsp)(struct cflayer *layer, u8 linkid,
+-                              struct cflayer *client_layer);;
++                              struct cflayer *client_layer);
+ };
+ /* Link Setup Parameters for CAIF-Links. */
+-- 
+1.7.3.1.g432b3.dirty
+
+--
+To unsubscribe from this list: send the line "unsubscribe netdev" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002274:2, b/test/corpora/lkml/cur/1382298793.002274:2,
new file mode 100644 (file)
index 0000000..087ddf2
--- /dev/null
@@ -0,0 +1,59 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 30/44] drivers/usb/gadget: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:49 -0800
+Lines: 21
+Message-ID: <cdc48b6ab9446585f304c801cca45a2a9d1e37ec.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: David Brownell <dbrownell@users.sourceforge.net>,
+       Greg Kroah-Hartman <gregkh@suse.de>, linux-usb@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:08:56 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpR9-0001sn-P6
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:08:56 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932917Ab0KODF7 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:05:59 -0500
+Received: from mail.perches.com ([173.55.12.10]:1216 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932842Ab0KODFz (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:55 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 6C4A92436C;
+       Sun, 14 Nov 2010 19:04:22 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062293>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/usb/gadget/f_fs.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/usb/gadget/f_fs.c b/drivers/usb/gadget/f_fs.c
+index 4a830df..38bb200 100644
+--- a/drivers/usb/gadget/f_fs.c
++++ b/drivers/usb/gadget/f_fs.c
+@@ -2096,7 +2096,7 @@ static int __ffs_func_bind_do_descs(enum ffs_entity_type type, u8 *valuep,
+               ep = usb_ep_autoconfig(func->gadget, ds);
+               if (unlikely(!ep))
+                       return -ENOTSUPP;
+-              ep->driver_data = func->eps + idx;;
++              ep->driver_data = func->eps + idx;
+               req = usb_ep_alloc_request(ep, GFP_KERNEL);
+               if (unlikely(!req))
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002275:2, b/test/corpora/lkml/cur/1382298793.002275:2,
new file mode 100644 (file)
index 0000000..32fa526
--- /dev/null
@@ -0,0 +1,59 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 36/44] fs/ocfs2: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:55 -0800
+Lines: 21
+Message-ID: <e32409b17aaa1a54fec85f3654583ef08fcf851c.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Mark Fasheh <mfasheh@suse.com>,
+       Joel Becker <joel.becker@oracle.com>,
+       ocfs2-devel@oss.oracle.com, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:08:57 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpRA-0001sn-Pm
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:08:57 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933007Ab0KODI2 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:08:28 -0500
+Received: from mail.perches.com ([173.55.12.10]:1239 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932884Ab0KODF7 (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:59 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 54CEB24372;
+       Sun, 14 Nov 2010 19:04:26 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062294>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ fs/ocfs2/refcounttree.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/fs/ocfs2/refcounttree.c b/fs/ocfs2/refcounttree.c
+index b5f9160..da14a42 100644
+--- a/fs/ocfs2/refcounttree.c
++++ b/fs/ocfs2/refcounttree.c
+@@ -3704,7 +3704,7 @@ int ocfs2_refcount_cow_xattr(struct inode *inode,
+       context->cow_start = cow_start;
+       context->cow_len = cow_len;
+       context->ref_tree = ref_tree;
+-      context->ref_root_bh = ref_root_bh;;
++      context->ref_root_bh = ref_root_bh;
+       context->cow_object = xv;
+       context->cow_duplicate_clusters = ocfs2_duplicate_clusters_by_jbd;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002276:2, b/test/corpora/lkml/cur/1382298793.002276:2,
new file mode 100644 (file)
index 0000000..9e318aa
--- /dev/null
@@ -0,0 +1,58 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 35/44] fs/nfs: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:54 -0800
+Lines: 21
+Message-ID: <ec29c2321578915d1d219f5ad00b876a3ff1a105.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Trond Myklebust <Trond.Myklebust@netapp.com>,
+       linux-nfs@vger.kernel.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:09:03 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpRB-0001sn-9x
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:08:57 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933153Ab0KODIl (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:08:41 -0500
+Received: from mail.perches.com ([173.55.12.10]:1236 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932876Ab0KODF6 (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:58 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 8DCD324371;
+       Sun, 14 Nov 2010 19:04:25 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062295>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ fs/nfs/getroot.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/fs/nfs/getroot.c b/fs/nfs/getroot.c
+index ac7b814..e17f628 100644
+--- a/fs/nfs/getroot.c
++++ b/fs/nfs/getroot.c
+@@ -190,7 +190,7 @@ struct dentry *nfs4_get_root(struct super_block *sb, struct nfs_fh *mntfh)
+       fattr = nfs_alloc_fattr();
+       if (fattr == NULL)
+-              return ERR_PTR(-ENOMEM);;
++              return ERR_PTR(-ENOMEM);
+       /* get the actual root for this mount */
+       error = server->nfs_client->rpc_ops->getattr(server, mntfh, fattr);
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002277:2, b/test/corpora/lkml/cur/1382298793.002277:2,
new file mode 100644 (file)
index 0000000..7fb2d9a
--- /dev/null
@@ -0,0 +1,58 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 34/44] fs/logfs: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:53 -0800
+Lines: 21
+Message-ID: <0c990bdacb2f9bf256acbb06ca59130585f600b7.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Joern Engel <joern@logfs.org>, logfs@logfs.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:09:34 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpRl-000297-1j
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:09:33 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933187Ab0KODI4 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:08:56 -0500
+Received: from mail.perches.com ([173.55.12.10]:1234 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932859Ab0KODF5 (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:57 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 0339024370;
+       Sun, 14 Nov 2010 19:04:25 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062296>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ fs/logfs/readwrite.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/fs/logfs/readwrite.c b/fs/logfs/readwrite.c
+index 6127baf..b4304f2 100644
+--- a/fs/logfs/readwrite.c
++++ b/fs/logfs/readwrite.c
+@@ -481,7 +481,7 @@ static int inode_write_alias(struct super_block *sb,
+                       val = inode_val0(inode);
+                       break;
+               case INODE_USED_OFS:
+-                      val = cpu_to_be64(li->li_used_bytes);;
++                      val = cpu_to_be64(li->li_used_bytes);
+                       break;
+               case INODE_SIZE_OFS:
+                       val = cpu_to_be64(i_size_read(inode));
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002278:2, b/test/corpora/lkml/cur/1382298793.002278:2,
new file mode 100644 (file)
index 0000000..3048c16
--- /dev/null
@@ -0,0 +1,63 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 33/44] fs/ceph: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:52 -0800
+Lines: 26
+Message-ID: <e01252afc842668a94fb0549e2d1833d77a6411f.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Sage Weil <sage@newdream.net>, ceph-devel@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: ceph-devel-owner@vger.kernel.org Mon Nov 15 04:09:34 2010
+Return-path: <ceph-devel-owner@vger.kernel.org>
+Envelope-to: gcfcd-ceph-devel3-2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <ceph-devel-owner@vger.kernel.org>)
+       id 1PHpRl-000297-Hx
+       for gcfcd-ceph-devel3-2@lo.gmane.org; Mon, 15 Nov 2010 04:09:33 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932690Ab0KODJN (ORCPT <rfc822;gcfcd-ceph-devel3-2@m.gmane.org>);
+       Sun, 14 Nov 2010 22:09:13 -0500
+Received: from mail.perches.com ([173.55.12.10]:1231 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932849Ab0KODF5 (ORCPT <rfc822;ceph-devel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:57 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 69DEF2436B;
+       Sun, 14 Nov 2010 19:04:24 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: ceph-devel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <ceph-devel.vger.kernel.org>
+X-Mailing-List: ceph-devel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062297>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ fs/ceph/mds_client.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/fs/ceph/mds_client.c b/fs/ceph/mds_client.c
+index 3142b15..931124c 100644
+--- a/fs/ceph/mds_client.c
++++ b/fs/ceph/mds_client.c
+@@ -2023,7 +2023,7 @@ static void handle_reply(struct ceph_mds_session *session, struct ceph_msg *msg)
+               } else  {
+                       struct ceph_inode_info *ci = ceph_inode(req->r_inode);
+                       struct ceph_cap *cap =
+-                              ceph_get_cap_for_mds(ci, req->r_mds);;
++                              ceph_get_cap_for_mds(ci, req->r_mds);
+                       dout("already using auth");
+                       if ((!cap || cap != ci->i_auth_cap) ||
+-- 
+1.7.3.1.g432b3.dirty
+
+--
+To unsubscribe from this list: send the line "unsubscribe ceph-devel" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002279:2, b/test/corpora/lkml/cur/1382298793.002279:2,
new file mode 100644 (file)
index 0000000..1dbe4c3
--- /dev/null
@@ -0,0 +1,88 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 26/44] drivers/scsi/qla2xxx: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:45 -0800
+Lines: 49
+Message-ID: <40854ce1b1e958e2c0bb9b79911d89b45b5e5f27.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Andrew Vasquez <andrew.vasquez@qlogic.com>,
+       linux-driver@qlogic.com,
+       "James E.J. Bottomley" <James.Bottomley@suse.de>,
+       linux-scsi@vger.kernel.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-scsi-owner@vger.kernel.org Mon Nov 15 04:10:09 2010
+Return-path: <linux-scsi-owner@vger.kernel.org>
+Envelope-to: lnx-linux-scsi@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-scsi-owner@vger.kernel.org>)
+       id 1PHpSK-0002RT-EP
+       for lnx-linux-scsi@lo.gmane.org; Mon, 15 Nov 2010 04:10:08 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932860Ab0KODJt (ORCPT <rfc822;lnx-linux-scsi@m.gmane.org>);
+       Sun, 14 Nov 2010 22:09:49 -0500
+Received: from mail.perches.com ([173.55.12.10]:1202 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932797Ab0KODFo (ORCPT <rfc822;linux-scsi@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:44 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id E65A02436B;
+       Sun, 14 Nov 2010 19:04:11 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-scsi-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-scsi.vger.kernel.org>
+X-Mailing-List: linux-scsi@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062298>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/scsi/qla2xxx/qla_isr.c |    4 ++--
+ drivers/scsi/qla2xxx/qla_nx.c  |    2 +-
+ 2 files changed, 3 insertions(+), 3 deletions(-)
+
+diff --git a/drivers/scsi/qla2xxx/qla_isr.c b/drivers/scsi/qla2xxx/qla_isr.c
+index 1f06ddd..59c4870 100644
+--- a/drivers/scsi/qla2xxx/qla_isr.c
++++ b/drivers/scsi/qla2xxx/qla_isr.c
+@@ -1049,7 +1049,7 @@ qla2x00_ct_entry(scsi_qla_host_t *vha, struct req_que *req,
+               }
+               DEBUG2(qla2x00_dump_buffer((uint8_t *)pkt, sizeof(*pkt)));
+       } else {
+-              bsg_job->reply->result =  DID_OK << 16;;
++              bsg_job->reply->result =  DID_OK << 16;
+               bsg_job->reply->reply_payload_rcv_len =
+                   bsg_job->reply_payload.payload_len;
+               bsg_job->reply_len = 0;
+@@ -1144,7 +1144,7 @@ qla24xx_els_ct_entry(scsi_qla_host_t *vha, struct req_que *req,
+               DEBUG2(qla2x00_dump_buffer((uint8_t *)pkt, sizeof(*pkt)));
+       }
+       else {
+-              bsg_job->reply->result =  DID_OK << 16;;
++              bsg_job->reply->result =  DID_OK << 16;
+               bsg_job->reply->reply_payload_rcv_len = bsg_job->reply_payload.payload_len;
+               bsg_job->reply_len = 0;
+       }
+diff --git a/drivers/scsi/qla2xxx/qla_nx.c b/drivers/scsi/qla2xxx/qla_nx.c
+index 8d9edfb..de2d1eb 100644
+--- a/drivers/scsi/qla2xxx/qla_nx.c
++++ b/drivers/scsi/qla2xxx/qla_nx.c
+@@ -1022,7 +1022,7 @@ ql82xx_rom_lock_d(struct qla_hw_data *ha)
+               qla_printk(KERN_WARNING, ha, "ROM lock failed\n");
+               return -1;
+       }
+-      return 0;;
++      return 0;
+ }
+ static int
+-- 
+1.7.3.1.g432b3.dirty
+
+--
+To unsubscribe from this list: send the line "unsubscribe linux-scsi" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002280:2, b/test/corpora/lkml/cur/1382298793.002280:2,
new file mode 100644 (file)
index 0000000..f8961e7
--- /dev/null
@@ -0,0 +1,57 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 27/44] drivers/serial: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:46 -0800
+Lines: 21
+Message-ID: <57c2393ee99b62bca43fa1c510cae832795069ba.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:10:10 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpSJ-0002RT-DE
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:10:07 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932970Ab0KODJh (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:09:37 -0500
+Received: from mail.perches.com ([173.55.12.10]:1204 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932802Ab0KODFp (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:45 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 7FB8224371;
+       Sun, 14 Nov 2010 19:04:12 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062299>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/serial/mrst_max3110.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/serial/mrst_max3110.c b/drivers/serial/mrst_max3110.c
+index b62857b..00d284c 100644
+--- a/drivers/serial/mrst_max3110.c
++++ b/drivers/serial/mrst_max3110.c
+@@ -56,7 +56,7 @@ struct uart_max3110 {
+       wait_queue_head_t wq;
+       struct task_struct *main_thread;
+       struct task_struct *read_thread;
+-      struct mutex thread_mutex;;
++      struct mutex thread_mutex;
+       u32 baud;
+       u16 cur_conf;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002281:2, b/test/corpora/lkml/cur/1382298793.002281:2,
new file mode 100644 (file)
index 0000000..a1349a9
--- /dev/null
@@ -0,0 +1,59 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 24/44] drivers/scsi/lpfc: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:43 -0800
+Lines: 21
+Message-ID: <1a0612305d3b141f85a6aeef6d91cd6829a80bae.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: James Smart <james.smart@emulex.com>,
+       "James E.J. Bottomley" <James.Bottomley@suse.de>,
+       linux-scsi@vger.kernel.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:10:40 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpSn-0002jF-BC
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:10:37 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933110Ab0KODKE (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:10:04 -0500
+Received: from mail.perches.com ([173.55.12.10]:1195 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932782Ab0KODFn (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:43 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 86AD42436E;
+       Sun, 14 Nov 2010 19:04:10 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062300>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/scsi/lpfc/lpfc_bsg.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/scsi/lpfc/lpfc_bsg.c b/drivers/scsi/lpfc/lpfc_bsg.c
+index 7260c3a..9e2c652 100644
+--- a/drivers/scsi/lpfc/lpfc_bsg.c
++++ b/drivers/scsi/lpfc/lpfc_bsg.c
+@@ -617,7 +617,7 @@ lpfc_bsg_rport_els(struct fc_bsg_job *job)
+       dd_data->context_un.iocb.cmdiocbq = cmdiocbq;
+       dd_data->context_un.iocb.rspiocbq = rspiocbq;
+       dd_data->context_un.iocb.set_job = job;
+-      dd_data->context_un.iocb.bmp = NULL;;
++      dd_data->context_un.iocb.bmp = NULL;
+       dd_data->context_un.iocb.ndlp = ndlp;
+       if (phba->cfg_poll & DISABLE_FCP_RING_INT) {
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002282:2, b/test/corpora/lkml/cur/1382298793.002282:2,
new file mode 100644 (file)
index 0000000..49db122
--- /dev/null
@@ -0,0 +1,59 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 23/44] drivers/scsi/bfa: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:42 -0800
+Lines: 21
+Message-ID: <71d0d7db4197f7b6f6b946a295648dc18bd559e0.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Jing Huang <huangj@brocade.com>,
+       "James E.J. Bottomley" <James.Bottomley@suse.de>,
+       linux-scsi@vger.kernel.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:10:53 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpT0-0002p1-2I
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:10:50 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932981Ab0KODKh (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:10:37 -0500
+Received: from mail.perches.com ([173.55.12.10]:1192 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932746Ab0KODFm (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:42 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id EFB872436B;
+       Sun, 14 Nov 2010 19:04:09 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062301>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/scsi/bfa/bfa_fcs_lport.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/scsi/bfa/bfa_fcs_lport.c b/drivers/scsi/bfa/bfa_fcs_lport.c
+index 377cbff..55b3f74 100644
+--- a/drivers/scsi/bfa/bfa_fcs_lport.c
++++ b/drivers/scsi/bfa/bfa_fcs_lport.c
+@@ -1683,7 +1683,7 @@ bfa_fcs_lport_fdmi_build_rhba_pyld(struct bfa_fcs_lport_fdmi_s *fdmi, u8 *pyld)
+       memcpy(attr->value, fcs_hba_attr->driver_version, attr->len);
+       attr->len = fc_roundup(attr->len, sizeof(u32));
+       curr_ptr += sizeof(attr->type) + sizeof(attr->len) + attr->len;
+-      len += attr->len;;
++      len += attr->len;
+       count++;
+       attr->len = cpu_to_be16(attr->len + sizeof(attr->type) +
+                            sizeof(attr->len));
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002283:2, b/test/corpora/lkml/cur/1382298793.002283:2,
new file mode 100644 (file)
index 0000000..6b6c939
--- /dev/null
@@ -0,0 +1,68 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 22/44] drivers/scsi/be2iscsi: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:41 -0800
+Lines: 30
+Message-ID: <7d4fbb8d3ac34861808dac24efeebe05011f6b0c.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Jayamohan Kallickal <jayamohank@serverengines.com>,
+       "James E.J. Bottomley" <James.Bottomley@suse.de>,
+       linux-scsi@vger.kernel.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:10:52 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpSz-0002p1-Hv
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:10:49 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932862Ab0KODKi (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:10:38 -0500
+Received: from mail.perches.com ([173.55.12.10]:1188 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932757Ab0KODFm (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:42 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 610FF24371;
+       Sun, 14 Nov 2010 19:04:09 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062302>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/scsi/be2iscsi/be_main.c |    4 ++--
+ 1 files changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/drivers/scsi/be2iscsi/be_main.c b/drivers/scsi/be2iscsi/be_main.c
+index 75a85aa..db60563 100644
+--- a/drivers/scsi/be2iscsi/be_main.c
++++ b/drivers/scsi/be2iscsi/be_main.c
+@@ -618,7 +618,7 @@ static void beiscsi_get_params(struct beiscsi_hba *phba)
+                                   + BE2_NOPOUT_REQ));
+       phba->params.cxns_per_ctrl = phba->fw_config.iscsi_cid_count;
+       phba->params.asyncpdus_per_ctrl = phba->fw_config.iscsi_cid_count * 2;
+-      phba->params.icds_per_ctrl = phba->fw_config.iscsi_icd_count;;
++      phba->params.icds_per_ctrl = phba->fw_config.iscsi_icd_count;
+       phba->params.num_sge_per_io = BE2_SGE;
+       phba->params.defpdu_hdr_sz = BE2_DEFPDU_HDR_SZ;
+       phba->params.defpdu_data_sz = BE2_DEFPDU_DATA_SZ;
+@@ -781,7 +781,7 @@ static irqreturn_t be_isr(int irq, void *dev_id)
+       int isr;
+       phba = dev_id;
+-      ctrl = &phba->ctrl;;
++      ctrl = &phba->ctrl;
+       isr = ioread32(ctrl->csr + CEV_ISR0_OFFSET +
+                      (PCI_FUNC(ctrl->pdev->devfn) * CEV_ISR_SIZE));
+       if (!isr)
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002284:2, b/test/corpora/lkml/cur/1382298793.002284:2,
new file mode 100644 (file)
index 0000000..315c1be
--- /dev/null
@@ -0,0 +1,61 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 15/44] drivers/net/vxge: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:34 -0800
+Lines: 21
+Message-ID: <e86e79a18106cc38715136bfb2e880b38f5ac764.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Ramkrishna Vepa <ramkrishna.vepa@exar.com>,
+       Sivakumar Subramani <sivakumar.subramani@exar.com>,
+       Sreenivasa Honnur <sreenivasa.honnur@exar.com>,
+       Jon Mason <jon.mason@exar.com>, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:11:16 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpTP-00032R-QK
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:11:16 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932740Ab0KODFk (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:05:40 -0500
+Received: from mail.perches.com ([173.55.12.10]:1166 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932690Ab0KODFh (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:37 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id BAAD824377;
+       Sun, 14 Nov 2010 19:04:04 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062303>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/net/vxge/vxge-main.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/net/vxge/vxge-main.c b/drivers/net/vxge/vxge-main.c
+index 813829f..93e80c5 100644
+--- a/drivers/net/vxge/vxge-main.c
++++ b/drivers/net/vxge/vxge-main.c
+@@ -2062,7 +2062,7 @@ static irqreturn_t vxge_isr_napi(int irq, void *dev_id)
+       struct __vxge_hw_device *hldev;
+       u64 reason;
+       enum vxge_hw_status status;
+-      struct vxgedev *vdev = (struct vxgedev *) dev_id;;
++      struct vxgedev *vdev = (struct vxgedev *)dev_id;
+       vxge_debug_intr(VXGE_TRACE, "%s:%d", __func__, __LINE__);
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002285:2, b/test/corpora/lkml/cur/1382298793.002285:2,
new file mode 100644 (file)
index 0000000..80ec0f9
--- /dev/null
@@ -0,0 +1,61 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 21/44] drivers/s390/net: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:40 -0800
+Lines: 21
+Message-ID: <ea09773876fb36a52a9a750110b381d20767ac12.1289789605.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Ursula Braun <ursula.braun@de.ibm.com>,
+       Frank Blaschka <blaschka@linux.vnet.ibm.com>,
+       linux390@de.ibm.com, Martin Schwidefsky <schwidefsky@de.ibm.com>,
+       Heiko Carstens <heiko.carstens@de.ibm.com>,
+       linux-s390@vger.kernel.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:11:19 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpTR-00032R-T3
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:11:18 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932825Ab0KODLK (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:11:10 -0500
+Received: from mail.perches.com ([173.55.12.10]:1186 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932749Ab0KODFl (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:41 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id C07A924370;
+       Sun, 14 Nov 2010 19:04:08 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062304>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/s390/net/qeth_core_sys.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/s390/net/qeth_core_sys.c b/drivers/s390/net/qeth_core_sys.c
+index 42fa783..b5e967c 100644
+--- a/drivers/s390/net/qeth_core_sys.c
++++ b/drivers/s390/net/qeth_core_sys.c
+@@ -372,7 +372,7 @@ static ssize_t qeth_dev_performance_stats_store(struct device *dev,
+       i = simple_strtoul(buf, &tmp, 16);
+       if ((i == 0) || (i == 1)) {
+               if (i == card->options.performance_stats)
+-                      goto out;;
++                      goto out;
+               card->options.performance_stats = i;
+               if (i == 0)
+                       memset(&card->perf_stats, 0,
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002286:2, b/test/corpora/lkml/cur/1382298793.002286:2,
new file mode 100644 (file)
index 0000000..9045476
--- /dev/null
@@ -0,0 +1,62 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 17/44] drivers/net/wireless/iwlwifi: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:36 -0800
+Lines: 21
+Message-ID: <6beaab935c2c511a5833e855db527976ef05e2dc.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Reinette Chatre <reinette.chatre@intel.com>,
+       Wey-Yi Guy <wey-yi.w.guy@intel.com>,
+       Intel Linux Wireless <ilw@linux.intel.com>,
+       "John W. Linville" <linville@tuxdriver.com>,
+       linux-wireless@vger.kernel.org, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:11:41 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpTn-0003Ei-3m
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:11:39 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933222Ab0KODLe (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:11:34 -0500
+Received: from mail.perches.com ([173.55.12.10]:1174 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932718Ab0KODFj (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:39 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 3C0E524379;
+       Sun, 14 Nov 2010 19:04:06 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062305>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/net/wireless/iwlwifi/iwl-agn.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/net/wireless/iwlwifi/iwl-agn.c b/drivers/net/wireless/iwlwifi/iwl-agn.c
+index c2636a7..f293fb6 100644
+--- a/drivers/net/wireless/iwlwifi/iwl-agn.c
++++ b/drivers/net/wireless/iwlwifi/iwl-agn.c
+@@ -2420,7 +2420,7 @@ static const char *desc_lookup(u32 num)
+       max = ARRAY_SIZE(advanced_lookup) - 1;
+       for (i = 0; i < max; i++) {
+               if (advanced_lookup[i].num == num)
+-                      break;;
++                      break;
+       }
+       return advanced_lookup[i].name;
+ }
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002287:2, b/test/corpora/lkml/cur/1382298793.002287:2,
new file mode 100644 (file)
index 0000000..4fa3cef
--- /dev/null
@@ -0,0 +1,57 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 18/44] drivers/net/cnic.c: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:37 -0800
+Lines: 21
+Message-ID: <950331e47b16c2ad28d73502f30f5a0f017b5493.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: netdev@vger.kernel.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:12:07 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpUF-0003Pl-09
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:12:07 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933052Ab0KODLd (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:11:33 -0500
+Received: from mail.perches.com ([173.55.12.10]:1176 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932652Ab0KODFj (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:39 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id D093A24378;
+       Sun, 14 Nov 2010 19:04:06 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062306>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/net/cnic.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/net/cnic.c b/drivers/net/cnic.c
+index 92bac19..594ca9c 100644
+--- a/drivers/net/cnic.c
++++ b/drivers/net/cnic.c
+@@ -1695,7 +1695,7 @@ static int cnic_bnx2x_iscsi_ofld1(struct cnic_dev *dev, struct kwqe *wqes[],
+               *work = num;
+               return -EINVAL;
+       }
+-      *work = 2 + req2->num_additional_wqes;;
++      *work = 2 + req2->num_additional_wqes;
+       l5_cid = req1->iscsi_conn_id;
+       if (l5_cid >= MAX_ISCSI_TBL_SZ)
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002288:2, b/test/corpora/lkml/cur/1382298793.002288:2,
new file mode 100644 (file)
index 0000000..5b1515a
--- /dev/null
@@ -0,0 +1,63 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 16/44] drivers/net/wireless/ath: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:35 -0800
+Lines: 21
+Message-ID: <c375cdc1175018f00066e2220f1d659ca70cde16.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: "Luis R. Rodriguez" <lrodriguez@atheros.com>,
+       Jouni Malinen <jmalinen@atheros.com>,
+       Vasanthakumar Thiagarajan <vasanth@atheros.com>,
+       Senthil Balasubramanian <senthilkumar@atheros.com>,
+       "John W. Linville" <linville@tuxdriver.com>,
+       linux-wireless@vger.kernel.org, ath9k-devel@lists.ath9k.org,
+       netdev@vger.kernel.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:12:48 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpUt-0003ie-NF
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:12:48 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932786Ab0KODMQ (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:12:16 -0500
+Received: from mail.perches.com ([173.55.12.10]:1169 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932695Ab0KODFi (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:38 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 6AD192437A;
+       Sun, 14 Nov 2010 19:04:05 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062307>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/net/wireless/ath/ath9k/htc.h |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/net/wireless/ath/ath9k/htc.h b/drivers/net/wireless/ath/ath9k/htc.h
+index 75ecf6a..4c98b93 100644
+--- a/drivers/net/wireless/ath/ath9k/htc.h
++++ b/drivers/net/wireless/ath/ath9k/htc.h
+@@ -434,7 +434,7 @@ void ath9k_htc_beaconep(void *drv_priv, struct sk_buff *skb,
+ void ath9k_htc_station_work(struct work_struct *work);
+ void ath9k_htc_aggr_work(struct work_struct *work);
+-void ath9k_ani_work(struct work_struct *work);;
++void ath9k_ani_work(struct work_struct *work);
+ int ath9k_tx_init(struct ath9k_htc_priv *priv);
+ void ath9k_tx_tasklet(unsigned long data);
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002289:2, b/test/corpora/lkml/cur/1382298793.002289:2,
new file mode 100644 (file)
index 0000000..a6111d4
--- /dev/null
@@ -0,0 +1,86 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 09/44] drivers/media/video: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:28 -0800
+Lines: 49
+Message-ID: <d7cec5e05200050ee2c7f624eef8c571193b4d92.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Mauro Carvalho Chehab <mchehab@infradead.org>,
+       linux-media@vger.kernel.org, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-media-owner@vger.kernel.org Mon Nov 15 04:13:25 2010
+Return-path: <linux-media-owner@vger.kernel.org>
+Envelope-to: gldv-linux-media@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-media-owner@vger.kernel.org>)
+       id 1PHpVT-00042L-DI
+       for gldv-linux-media@lo.gmane.org; Mon, 15 Nov 2010 04:13:23 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932630Ab0KODFd (ORCPT <rfc822;gldv-linux-media@m.gmane.org>);
+       Sun, 14 Nov 2010 22:05:33 -0500
+Received: from mail.perches.com ([173.55.12.10]:1148 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932627Ab0KODFd (ORCPT <rfc822;linux-media@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:33 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 873E324371;
+       Sun, 14 Nov 2010 19:04:00 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-media-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-media.vger.kernel.org>
+X-Mailing-List: linux-media@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062308>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/media/video/cx88/cx88-blackbird.c  |    2 +-
+ drivers/media/video/davinci/vpfe_capture.c |    2 +-
+ drivers/media/video/em28xx/em28xx-cards.c  |    2 +-
+ 3 files changed, 3 insertions(+), 3 deletions(-)
+
+diff --git a/drivers/media/video/cx88/cx88-blackbird.c b/drivers/media/video/cx88/cx88-blackbird.c
+index 417d1d5..14b2546 100644
+--- a/drivers/media/video/cx88/cx88-blackbird.c
++++ b/drivers/media/video/cx88/cx88-blackbird.c
+@@ -1065,7 +1065,7 @@ static int mpeg_open(struct file *file)
+               err = drv->request_acquire(drv);
+               if(err != 0) {
+                       dprintk(1,"%s: Unable to acquire hardware, %d\n", __func__, err);
+-                      mutex_unlock(&dev->core->lock);;
++                      mutex_unlock(&dev->core->lock);
+                       return err;
+               }
+       }
+diff --git a/drivers/media/video/davinci/vpfe_capture.c b/drivers/media/video/davinci/vpfe_capture.c
+index d8e38cc..14f3d54 100644
+--- a/drivers/media/video/davinci/vpfe_capture.c
++++ b/drivers/media/video/davinci/vpfe_capture.c
+@@ -1276,7 +1276,7 @@ static int vpfe_videobuf_prepare(struct videobuf_queue *vq,
+               vb->size = vpfe_dev->fmt.fmt.pix.sizeimage;
+               vb->field = field;
+-              ret = videobuf_iolock(vq, vb, NULL);;
++              ret = videobuf_iolock(vq, vb, NULL);
+               if (ret < 0)
+                       return ret;
+diff --git a/drivers/media/video/em28xx/em28xx-cards.c b/drivers/media/video/em28xx/em28xx-cards.c
+index 5485923..7aee7f0 100644
+--- a/drivers/media/video/em28xx/em28xx-cards.c
++++ b/drivers/media/video/em28xx/em28xx-cards.c
+@@ -2408,7 +2408,7 @@ void em28xx_register_i2c_ir(struct em28xx *dev)
+               dev->init_data.get_key = em28xx_get_key_em_haup;
+               dev->init_data.name = "i2c IR (EM2840 Hauppauge)";
+       case EM2820_BOARD_LEADTEK_WINFAST_USBII_DELUXE:
+-              dev->init_data.ir_codes = RC_MAP_WINFAST_USBII_DELUXE;;
++              dev->init_data.ir_codes = RC_MAP_WINFAST_USBII_DELUXE;
+               dev->init_data.get_key = em28xx_get_key_winfast_usbii_deluxe;
+               dev->init_data.name = "i2c IR (EM2820 Winfast TV USBII Deluxe)";
+               break;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002290:2, b/test/corpora/lkml/cur/1382298793.002290:2,
new file mode 100644 (file)
index 0000000..bf05cb1
--- /dev/null
@@ -0,0 +1,81 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 12/44] drivers/net/bnx2x: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:31 -0800
+Lines: 44
+Message-ID: <2bfaf1f1fe5d503a8a386a433b5187997819d771.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Eilon Greenstein <eilong@broadcom.com>, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:13:27 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpVW-00042L-1L
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:13:26 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932754Ab0KODMy (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:12:54 -0500
+Received: from mail.perches.com ([173.55.12.10]:1156 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932660Ab0KODFf (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:35 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id AB58824375;
+       Sun, 14 Nov 2010 19:04:02 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062309>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/net/bnx2x/bnx2x_link.c |    4 ++--
+ drivers/net/bnx2x/bnx2x_main.c |    2 +-
+ 2 files changed, 3 insertions(+), 3 deletions(-)
+
+diff --git a/drivers/net/bnx2x/bnx2x_link.c b/drivers/net/bnx2x/bnx2x_link.c
+index 5809196..38aeffe 100644
+--- a/drivers/net/bnx2x/bnx2x_link.c
++++ b/drivers/net/bnx2x/bnx2x_link.c
+@@ -3904,7 +3904,7 @@ static u8 bnx2x_8726_read_sfp_module_eeprom(struct bnx2x_phy *phy,
+                             MDIO_PMA_REG_SFP_TWO_WIRE_CTRL, &val);
+               if ((val & MDIO_PMA_REG_SFP_TWO_WIRE_CTRL_STATUS_MASK) ==
+                   MDIO_PMA_REG_SFP_TWO_WIRE_STATUS_IDLE)
+-                      return 0;;
++                      return 0;
+               msleep(1);
+       }
+       return -EINVAL;
+@@ -3988,7 +3988,7 @@ static u8 bnx2x_8727_read_sfp_module_eeprom(struct bnx2x_phy *phy,
+                             MDIO_PMA_REG_SFP_TWO_WIRE_CTRL, &val);
+               if ((val & MDIO_PMA_REG_SFP_TWO_WIRE_CTRL_STATUS_MASK) ==
+                   MDIO_PMA_REG_SFP_TWO_WIRE_STATUS_IDLE)
+-                      return 0;;
++                      return 0;
+               msleep(1);
+       }
+diff --git a/drivers/net/bnx2x/bnx2x_main.c b/drivers/net/bnx2x/bnx2x_main.c
+index e9ad16f..7ffcb08 100644
+--- a/drivers/net/bnx2x/bnx2x_main.c
++++ b/drivers/net/bnx2x/bnx2x_main.c
+@@ -8078,7 +8078,7 @@ static void __devinit bnx2x_get_port_hwinfo(struct bnx2x *bp)
+       int port = BP_PORT(bp);
+       u32 val, val2;
+       u32 config;
+-      u32 ext_phy_type, ext_phy_config;;
++      u32 ext_phy_type, ext_phy_config;
+       bp->link_params.bp = bp;
+       bp->link_params.port = port;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002292:2, b/test/corpora/lkml/cur/1382298793.002292:2,
new file mode 100644 (file)
index 0000000..3aceed7
--- /dev/null
@@ -0,0 +1,71 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 10/44] drivers/misc: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:29 -0800
+Lines: 35
+Message-ID: <f1f1ff72045c075062d3fbe8d2bfcf67bdb1571d.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:14:21 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpWA-0004SF-ER
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:14:06 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932800Ab0KODNW (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:13:22 -0500
+Received: from mail.perches.com ([173.55.12.10]:1151 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932629Ab0KODFd (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:33 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 4E7E024372;
+       Sun, 14 Nov 2010 19:04:01 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062311>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/misc/bmp085.c   |    2 +-
+ drivers/misc/isl29020.c |    2 +-
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/drivers/misc/bmp085.c b/drivers/misc/bmp085.c
+index 63ee4c1..6b8a394 100644
+--- a/drivers/misc/bmp085.c
++++ b/drivers/misc/bmp085.c
+@@ -216,7 +216,7 @@ static s32 bmp085_get_temperature(struct bmp085_data *data, int *temperature)
+               *temperature = (x1+x2+8) >> 4;
+ exit:
+-      return status;;
++      return status;
+ }
+ /*
+diff --git a/drivers/misc/isl29020.c b/drivers/misc/isl29020.c
+index ca47e62..e6bbf13 100644
+--- a/drivers/misc/isl29020.c
++++ b/drivers/misc/isl29020.c
+@@ -158,7 +158,7 @@ static int als_set_default_config(struct i2c_client *client)
+               dev_err(&client->dev, "default write failed.");
+               return retval;
+       }
+-      return 0;;
++      return 0;
+ }
+ static int  isl29020_probe(struct i2c_client *client,
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002293:2, b/test/corpora/lkml/cur/1382298793.002293:2,
new file mode 100644 (file)
index 0000000..a547a9b
--- /dev/null
@@ -0,0 +1,57 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 08/44] drivers/leds: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:27 -0800
+Lines: 21
+Message-ID: <054f6857b7472d9f4c540c298cef0aa77bce6962.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Richard Purdie <rpurdie@rpsys.net>, linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:14:24 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpWB-0004SF-F9
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:14:07 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932958Ab0KODNw (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:13:52 -0500
+Received: from mail.perches.com ([173.55.12.10]:1145 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932602Ab0KODFc (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:32 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 92C3F24370;
+       Sun, 14 Nov 2010 19:03:59 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062312>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/leds/leds-mc13783.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/leds/leds-mc13783.c b/drivers/leds/leds-mc13783.c
+index f05bb08..f369e56 100644
+--- a/drivers/leds/leds-mc13783.c
++++ b/drivers/leds/leds-mc13783.c
+@@ -234,7 +234,7 @@ static int __devinit mc13783_leds_prepare(struct platform_device *pdev)
+                                                       MC13783_LED_Cx_PERIOD;
+       if (pdata->flags & MC13783_LED_TRIODE_TC3)
+-              reg |= MC13783_LED_Cx_TRIODE_TC_BIT;;
++              reg |= MC13783_LED_Cx_TRIODE_TC_BIT;
+       ret = mc13783_reg_write(dev, MC13783_REG_LED_CONTROL_5, reg);
+       if (ret)
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002294:2, b/test/corpora/lkml/cur/1382298793.002294:2,
new file mode 100644 (file)
index 0000000..aa5821b
--- /dev/null
@@ -0,0 +1,63 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 04/44] drivers/cpufreq: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:23 -0800
+Lines: 26
+Message-ID: <a4bef9c18ce34e80870a07c728ee25e8efac6d9d.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Dave Jones <davej@redhat.com>, cpufreq@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: cpufreq-owner@vger.kernel.org Mon Nov 15 04:16:07 2010
+Return-path: <cpufreq-owner@vger.kernel.org>
+Envelope-to: glkc-cpufreq2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <cpufreq-owner@vger.kernel.org>)
+       id 1PHpY5-0005lg-O4
+       for glkc-cpufreq2@lo.gmane.org; Mon, 15 Nov 2010 04:16:06 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932680Ab0KODO4 (ORCPT <rfc822;glkc-cpufreq2@m.gmane.org>);
+       Sun, 14 Nov 2010 22:14:56 -0500
+Received: from mail.perches.com ([173.55.12.10]:1127 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1757376Ab0KODF3 (ORCPT <rfc822;cpufreq@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:29 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 1F49D2436E;
+       Sun, 14 Nov 2010 19:03:57 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: cpufreq-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <cpufreq.vger.kernel.org>
+X-Mailing-List: cpufreq@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062313>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/cpufreq/cpufreq_conservative.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/cpufreq/cpufreq_conservative.c b/drivers/cpufreq/cpufreq_conservative.c
+index 526bfbf..b0d8f3d 100644
+--- a/drivers/cpufreq/cpufreq_conservative.c
++++ b/drivers/cpufreq/cpufreq_conservative.c
+@@ -118,7 +118,7 @@ static inline cputime64_t get_cpu_idle_time_jiffy(unsigned int cpu,
+       if (wall)
+               *wall = (cputime64_t)jiffies_to_usecs(cur_wall_time);
+-      return (cputime64_t)jiffies_to_usecs(idle_time);;
++      return (cputime64_t)jiffies_to_usecs(idle_time);
+ }
+ static inline cputime64_t get_cpu_idle_time(unsigned int cpu, cputime64_t *wall)
+-- 
+1.7.3.1.g432b3.dirty
+
+--
+To unsubscribe from this list: send the line "unsubscribe cpufreq" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002296:2, b/test/corpora/lkml/cur/1382298793.002296:2,
new file mode 100644 (file)
index 0000000..4d337c7
--- /dev/null
@@ -0,0 +1,148 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 01/44] arch/arm: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:20 -0800
+Lines: 105
+Message-ID: <b6d517c8da3ca0d50c836736e76059c89d692b6e.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Russell King <linux@arm.linux.org.uk>,
+       Wan ZongShun <mcuos.com@gmail.com>,
+       Colin Cross <ccross@android.com>,
+       Erik Gilling <konkers@android.com>,
+       Olof Johansson <olof@lixom.net>,
+       Sascha Hauer <kernel@pengutronix.de>,
+       linux-arm-kernel@lists.infradead.org, linux-kernel@vger.kernel.org,
+       linux-tegra@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:16:10 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpY9-0005lg-2R
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:16:09 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933240Ab0KODPt (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:15:49 -0500
+Received: from mail.perches.com ([173.55.12.10]:1125 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1757314Ab0KODF2 (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:28 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 4C0C32436B;
+       Sun, 14 Nov 2010 19:03:55 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062315>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ arch/arm/mach-at91/at91cap9_devices.c    |    2 +-
+ arch/arm/mach-at91/at91sam9g45_devices.c |    2 +-
+ arch/arm/mach-at91/at91sam9rl_devices.c  |    2 +-
+ arch/arm/mach-nuc93x/time.c              |    2 +-
+ arch/arm/mach-tegra/tegra2_clocks.c      |    2 +-
+ arch/arm/mach-w90x900/cpu.c              |    2 +-
+ arch/arm/plat-mxc/irq.c                  |    2 +-
+ 7 files changed, 7 insertions(+), 7 deletions(-)
+
+diff --git a/arch/arm/mach-at91/at91cap9_devices.c b/arch/arm/mach-at91/at91cap9_devices.c
+index d1f775e..308ce7a 100644
+--- a/arch/arm/mach-at91/at91cap9_devices.c
++++ b/arch/arm/mach-at91/at91cap9_devices.c
+@@ -171,7 +171,7 @@ void __init at91_add_device_usba(struct usba_platform_data *data)
+        */
+       usba_udc_data.pdata.vbus_pin = -EINVAL;
+       usba_udc_data.pdata.num_ep = ARRAY_SIZE(usba_udc_ep);
+-      memcpy(usba_udc_data.ep, usba_udc_ep, sizeof(usba_udc_ep));;
++      memcpy(usba_udc_data.ep, usba_udc_ep, sizeof(usba_udc_ep));
+       if (data && data->vbus_pin > 0) {
+               at91_set_gpio_input(data->vbus_pin, 0);
+diff --git a/arch/arm/mach-at91/at91sam9g45_devices.c b/arch/arm/mach-at91/at91sam9g45_devices.c
+index 1e8f275..5e9f8a4 100644
+--- a/arch/arm/mach-at91/at91sam9g45_devices.c
++++ b/arch/arm/mach-at91/at91sam9g45_devices.c
+@@ -256,7 +256,7 @@ void __init at91_add_device_usba(struct usba_platform_data *data)
+ {
+       usba_udc_data.pdata.vbus_pin = -EINVAL;
+       usba_udc_data.pdata.num_ep = ARRAY_SIZE(usba_udc_ep);
+-      memcpy(usba_udc_data.ep, usba_udc_ep, sizeof(usba_udc_ep));;
++      memcpy(usba_udc_data.ep, usba_udc_ep, sizeof(usba_udc_ep));
+       if (data && data->vbus_pin > 0) {
+               at91_set_gpio_input(data->vbus_pin, 0);
+diff --git a/arch/arm/mach-at91/at91sam9rl_devices.c b/arch/arm/mach-at91/at91sam9rl_devices.c
+index 53aaa94..c49262b 100644
+--- a/arch/arm/mach-at91/at91sam9rl_devices.c
++++ b/arch/arm/mach-at91/at91sam9rl_devices.c
+@@ -145,7 +145,7 @@ void __init at91_add_device_usba(struct usba_platform_data *data)
+        */
+       usba_udc_data.pdata.vbus_pin = -EINVAL;
+       usba_udc_data.pdata.num_ep = ARRAY_SIZE(usba_udc_ep);
+-      memcpy(usba_udc_data.ep, usba_udc_ep, sizeof(usba_udc_ep));;
++      memcpy(usba_udc_data.ep, usba_udc_ep, sizeof(usba_udc_ep));
+       if (data && data->vbus_pin > 0) {
+               at91_set_gpio_input(data->vbus_pin, 0);
+diff --git a/arch/arm/mach-nuc93x/time.c b/arch/arm/mach-nuc93x/time.c
+index 2f90f9d..f9807c0 100644
+--- a/arch/arm/mach-nuc93x/time.c
++++ b/arch/arm/mach-nuc93x/time.c
+@@ -82,7 +82,7 @@ static void nuc93x_timer_setup(void)
+       timer0_load = (rate / TICKS_PER_SEC);
+       __raw_writel(timer0_load, REG_TICR0);
+-      val |= (PERIOD | COUNTEN | INTEN | PRESCALE);;
++      val |= (PERIOD | COUNTEN | INTEN | PRESCALE);
+       __raw_writel(val, REG_TCSR0);
+ }
+diff --git a/arch/arm/mach-tegra/tegra2_clocks.c b/arch/arm/mach-tegra/tegra2_clocks.c
+index ae3b308..7f9d2252 100644
+--- a/arch/arm/mach-tegra/tegra2_clocks.c
++++ b/arch/arm/mach-tegra/tegra2_clocks.c
+@@ -293,7 +293,7 @@ static int tegra2_super_clk_set_parent(struct clk *c, struct clk *p)
+       const struct clk_mux_sel *sel;
+       int shift;
+-      val = clk_readl(c->reg + SUPER_CLK_MUX);;
++      val = clk_readl(c->reg + SUPER_CLK_MUX);
+       BUG_ON(((val & SUPER_STATE_MASK) != SUPER_STATE_RUN) &&
+               ((val & SUPER_STATE_MASK) != SUPER_STATE_IDLE));
+       shift = ((val & SUPER_STATE_MASK) == SUPER_STATE_IDLE) ?
+diff --git a/arch/arm/mach-w90x900/cpu.c b/arch/arm/mach-w90x900/cpu.c
+index 83c5632..0a235e5 100644
+--- a/arch/arm/mach-w90x900/cpu.c
++++ b/arch/arm/mach-w90x900/cpu.c
+@@ -60,7 +60,7 @@ static DEFINE_CLK(emc, 7);
+ static DEFINE_SUBCLK(rmii, 2);
+ static DEFINE_CLK(usbd, 8);
+ static DEFINE_CLK(usbh, 9);
+-static DEFINE_CLK(g2d, 10);;
++static DEFINE_CLK(g2d, 10);
+ static DEFINE_CLK(pwm, 18);
+ static DEFINE_CLK(ps2, 24);
+ static DEFINE_CLK(kpi, 25);
+diff --git a/arch/arm/plat-mxc/irq.c b/arch/arm/plat-mxc/irq.c
+index 7331f2a..d7809d0 100644
+--- a/arch/arm/plat-mxc/irq.c
++++ b/arch/arm/plat-mxc/irq.c
+@@ -53,7 +53,7 @@ int imx_irq_set_priority(unsigned char irq, unsigned char prio)
+       unsigned int mask = 0x0F << irq % 8 * 4;
+       if (irq >= MXC_INTERNAL_IRQS)
+-              return -EINVAL;;
++              return -EINVAL;
+       temp = __raw_readl(avic_base + AVIC_NIPRIORITY(irq / 8));
+       temp &= ~mask;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002297:2, b/test/corpora/lkml/cur/1382298793.002297:2,
new file mode 100644 (file)
index 0000000..48a49c4
--- /dev/null
@@ -0,0 +1,58 @@
+From: Joe Perches <joe-6d6DIl74uiNBDgjK7y7TUQ@public.gmane.org>
+Subject: [PATCH 06/44] drivers/i2c: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:25 -0800
+Lines: 21
+Message-ID: <04cfa2beee1ed9656e550bb13076b9c57899542e.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: "Jean Delvare (PC drivers, core)" <khali-PUYAD+kWke1g9hUCZPvPmw@public.gmane.org>,
+       "Ben Dooks (embedded platforms)" <ben-linux-elnMNo+KYs3YtjvyW6yDsg@public.gmane.org>,
+       linux-i2c-u79uwXL29TY76Z2rM5mHXA@public.gmane.org, linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+To: Jiri Kosina <trivial-DgEjT+Ai2ygdnm+yROfE0A@public.gmane.org>
+X-From: linux-i2c-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Mon Nov 15 04:15:07 2010
+Return-path: <linux-i2c-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: gldi-i2c-1-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-i2c-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1PHpX8-0005AO-Oy
+       for gldi-i2c-1-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Mon, 15 Nov 2010 04:15:07 +0100
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S932641Ab0KODOb (ORCPT <rfc822;gldi-i2c-1@m.gmane.org>);
+       Sun, 14 Nov 2010 22:14:31 -0500
+Received: from mail.perches.com ([173.55.12.10]:1138 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932562Ab0KODFa (ORCPT <rfc822;linux-i2c-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Sun, 14 Nov 2010 22:05:30 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 39B6A2436C;
+       Sun, 14 Nov 2010 19:03:58 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe-6d6DIl74uiNBDgjK7y7TUQ@public.gmane.org>
+Sender: linux-i2c-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-i2c.vger.kernel.org>
+X-Mailing-List: linux-i2c-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+
+Signed-off-by: Joe Perches <joe-6d6DIl74uiNBDgjK7y7TUQ@public.gmane.org>
+---
+ drivers/i2c/busses/i2c-designware.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/i2c/busses/i2c-designware.c b/drivers/i2c/busses/i2c-designware.c
+index b664ed8..a93922d 100644
+--- a/drivers/i2c/busses/i2c-designware.c
++++ b/drivers/i2c/busses/i2c-designware.c
+@@ -390,7 +390,7 @@ i2c_dw_xfer_msg(struct dw_i2c_dev *dev)
+       int tx_limit, rx_limit;
+       u32 addr = msgs[dev->msg_write_idx].addr;
+       u32 buf_len = dev->tx_buf_len;
+-      u8 *buf = dev->tx_buf;;
++      u8 *buf = dev->tx_buf;
+       intr_mask = DW_IC_INTR_DEFAULT_MASK;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002298:2, b/test/corpora/lkml/cur/1382298793.002298:2,
new file mode 100644 (file)
index 0000000..4aa3f38
--- /dev/null
@@ -0,0 +1,85 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 07/44] drivers/isdn: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:26 -0800
+Lines: 49
+Message-ID: <c7a38f65340aafb208d50fc3a781602c07aebb0c.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: Karsten Keil <isdn@linux-pingi.de>, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: netdev-owner@vger.kernel.org Mon Nov 15 04:15:06 2010
+Return-path: <netdev-owner@vger.kernel.org>
+Envelope-to: linux-netdev-2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <netdev-owner@vger.kernel.org>)
+       id 1PHpX7-0005AO-Lc
+       for linux-netdev-2@lo.gmane.org; Mon, 15 Nov 2010 04:15:06 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932642Ab0KODOJ (ORCPT <rfc822;linux-netdev-2@m.gmane.org>);
+       Sun, 14 Nov 2010 22:14:09 -0500
+Received: from mail.perches.com ([173.55.12.10]:1142 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932587Ab0KODFb (ORCPT <rfc822;netdev@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:31 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 036B82436F;
+       Sun, 14 Nov 2010 19:03:59 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: netdev-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <netdev.vger.kernel.org>
+X-Mailing-List: netdev@vger.kernel.org
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/isdn/hardware/mISDN/mISDNinfineon.c |    4 ++--
+ drivers/isdn/hardware/mISDN/mISDNisar.c     |    2 +-
+ 2 files changed, 3 insertions(+), 3 deletions(-)
+
+diff --git a/drivers/isdn/hardware/mISDN/mISDNinfineon.c b/drivers/isdn/hardware/mISDN/mISDNinfineon.c
+index e90db88..bc0529a 100644
+--- a/drivers/isdn/hardware/mISDN/mISDNinfineon.c
++++ b/drivers/isdn/hardware/mISDN/mISDNinfineon.c
+@@ -420,7 +420,7 @@ enable_hwirq(struct inf_hw *hw)
+               break;
+       case INF_NICCY:
+               val = inl((u32)hw->cfg.start + NICCY_IRQ_CTRL_REG);
+-              val |= NICCY_IRQ_ENABLE;;
++              val |= NICCY_IRQ_ENABLE;
+               outl(val, (u32)hw->cfg.start + NICCY_IRQ_CTRL_REG);
+               break;
+       case INF_SCT_1:
+@@ -924,7 +924,7 @@ setup_instance(struct inf_hw *card)
+               mISDNipac_init(&card->ipac, card);
+       if (card->ipac.isac.dch.dev.Bprotocols == 0)
+-              goto error_setup;;
++              goto error_setup;
+       err = mISDN_register_device(&card->ipac.isac.dch.dev,
+               &card->pdev->dev, card->name);
+diff --git a/drivers/isdn/hardware/mISDN/mISDNisar.c b/drivers/isdn/hardware/mISDN/mISDNisar.c
+index 38eb314..d13fa5b 100644
+--- a/drivers/isdn/hardware/mISDN/mISDNisar.c
++++ b/drivers/isdn/hardware/mISDN/mISDNisar.c
+@@ -264,7 +264,7 @@ load_firmware(struct isar_hw *isar, const u8 *buf, int size)
+                       while (noc) {
+                               val = le16_to_cpu(*sp++);
+                               *mp++ = val >> 8;
+-                              *mp++ = val & 0xFF;;
++                              *mp++ = val & 0xFF;
+                               noc--;
+                       }
+                       spin_lock_irqsave(isar->hwlock, flags);
+-- 
+1.7.3.1.g432b3.dirty
+
+--
+To unsubscribe from this list: send the line "unsubscribe netdev" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002299:2, b/test/corpora/lkml/cur/1382298793.002299:2,
new file mode 100644 (file)
index 0000000..dc35b93
--- /dev/null
@@ -0,0 +1,56 @@
+From: Joe Perches <joe@perches.com>
+Subject: [PATCH 05/44] drivers/gpio: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 19:04:24 -0800
+Lines: 21
+Message-ID: <a04f2c16a94e214f0a1828c4cea95f815a816853.1289789604.git.joe@perches.com>
+References: <cover.1289789604.git.joe@perches.com>
+Cc: linux-kernel@vger.kernel.org
+To: Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:15:13 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHpX9-0005AO-Pv
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:15:08 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933055Ab0KODOp (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:14:45 -0500
+Received: from mail.perches.com ([173.55.12.10]:1136 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932550Ab0KODFa (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:05:30 -0500
+Received: from Joe-Laptop.home (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id A81642436F;
+       Sun, 14 Nov 2010 19:03:57 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1.g432b3.dirty
+In-Reply-To: <cover.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+ drivers/gpio/langwell_gpio.c |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/drivers/gpio/langwell_gpio.c b/drivers/gpio/langwell_gpio.c
+index 64db9dc..ed05ecb 100644
+--- a/drivers/gpio/langwell_gpio.c
++++ b/drivers/gpio/langwell_gpio.c
+@@ -122,7 +122,7 @@ static int lnw_gpio_direction_output(struct gpio_chip *chip,
+       lnw_gpio_set(chip, offset, value);
+       spin_lock_irqsave(&lnw->lock, flags);
+       value = readl(gpdr);
+-      value |= BIT(offset % 32);;
++      value |= BIT(offset % 32);
+       writel(value, gpdr);
+       spin_unlock_irqrestore(&lnw->lock, flags);
+       return 0;
+-- 
+1.7.3.1.g432b3.dirty
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002302:2, b/test/corpora/lkml/cur/1382298793.002302:2,
new file mode 100644 (file)
index 0000000..7ee679f
--- /dev/null
@@ -0,0 +1,85 @@
+From: "Jack Wang" <jack_wang@usish.com>
+Subject: RE: [PATCH 25/44] drivers/scsi/pm8001: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:27:32 +0800
+Lines: 33
+Message-ID: <1671200DA80140558ED0D17FB55585AD@usish.com.cn>
+References: <cover.1289789604.git.joe@perches.com> <20b352f91642ca45ad730d8eeec0bbd323d26626.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=gb2312
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: <lindar_liu@usish.com>,
+       "'James E.J. Bottomley'" <James.Bottomley@suse.de>,
+       <linux-scsi@vger.kernel.org>, <linux-kernel@vger.kernel.org>
+To: "'Joe Perches'" <joe@perches.com>,
+       "'Jiri Kosina'" <trivial@kernel.org>
+X-From: linux-scsi-owner@vger.kernel.org Mon Nov 15 04:28:10 2010
+Return-path: <linux-scsi-owner@vger.kernel.org>
+Envelope-to: lnx-linux-scsi@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-scsi-owner@vger.kernel.org>)
+       id 1PHpjl-0002I8-68
+       for lnx-linux-scsi@lo.gmane.org; Mon, 15 Nov 2010 04:28:09 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932669Ab0KOD15 convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;lnx-linux-scsi@m.gmane.org>); Sun, 14 Nov 2010 22:27:57 -0500
+Received: from sr-smtp.usish.com ([210.5.144.203]:58240 "EHLO
+       sr-smtp.usish.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S932514Ab0KOD14 convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-scsi@vger.kernel.org>); Sun, 14 Nov 2010 22:27:56 -0500
+Received: from outbound.usish.com (unknown [192.168.40.103])
+       by sr-smtp.usish.com (Postfix) with ESMTP id 782BE778067;
+       Mon, 15 Nov 2010 11:20:06 +0800 (CST)
+Received: from outbound.usish.com (outbound.usish.com [127.0.0.1])
+       by postfix.imss70 (Postfix) with ESMTP id 8E538428070;
+       Mon, 15 Nov 2010 11:27:48 +0800 (CST)
+Received: from usishe7a1977d2 (unknown [192.168.58.33])
+       (using TLSv1 with cipher RC4-MD5 (128/128 bits))
+       (No client certificate requested)
+       by outbound.usish.com (Postfix) with ESMTP id 5437142807A;
+       Mon, 15 Nov 2010 11:27:48 +0800 (CST)
+X-Mailer: Microsoft Office Outlook 11
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.5994
+Thread-Index: AcuEcgXhz8cjF31qQU6VXNE8zpGc0wAAnu4g
+In-Reply-To: <20b352f91642ca45ad730d8eeec0bbd323d26626.1289789605.git.joe@perches.com>
+Sender: linux-scsi-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-scsi.vger.kernel.org>
+X-Mailing-List: linux-scsi@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062321>
+
+>=20
+> Signed-off-by: Joe Perches <joe@perches.com>
+> ---
+>  drivers/scsi/pm8001/pm8001_init.c |    2 +-
+>  1 files changed, 1 insertions(+), 1 deletions(-)
+>=20
+> diff --git a/drivers/scsi/pm8001/pm8001_init.c
+> b/drivers/scsi/pm8001/pm8001_init.c
+> index f8c86b2..be210dd 100644
+> --- a/drivers/scsi/pm8001/pm8001_init.c
+> +++ b/drivers/scsi/pm8001/pm8001_init.c
+> @@ -160,7 +160,7 @@ static void pm8001_free(struct pm8001_hba_info
+*pm8001_ha)
+>  static void pm8001_tasklet(unsigned long opaque)
+>  {
+>      struct pm8001_hba_info *pm8001_ha;
+> -    pm8001_ha =3D (struct pm8001_hba_info *)opaque;;
+> +    pm8001_ha =3D (struct pm8001_hba_info *)opaque;
+>      if (unlikely(!pm8001_ha))
+>              BUG_ON(1);
+>      PM8001_CHIP_DISP->isr(pm8001_ha);
+> --
+> 1.7.3.1.g432b3.dirty
+[Jack Wang] Acked-by: Jack Wang <jack_wang@usish.com>
+Thanks=A3=A1
+
+
+--
+To unsubscribe from this list: send the line "unsubscribe linux-scsi" i=
+n
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002309:2, b/test/corpora/lkml/cur/1382298793.002309:2,
new file mode 100644 (file)
index 0000000..64153b5
--- /dev/null
@@ -0,0 +1,93 @@
+From: Grant Likely <grant.likely@secretlab.ca>
+Subject: Re: [PATCH 28/44] drivers/spi: Remove unnecessary semicolons
+Date: Sun, 14 Nov 2010 20:57:42 -0700
+Lines: 41
+Message-ID: <20101115035742.GA19965@angua.secretlab.ca>
+References: <cover.1289789604.git.joe@perches.com>
+ <fe5e5e0efbd97eaa32530eef5ed47efdc3252dad.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Cc: Jiri Kosina <trivial@kernel.org>,
+       David Brownell <dbrownell@users.sourceforge.net>,
+       Wan ZongShun <mcuos.com@gmail.com>,
+       spi-devel-general@lists.sourceforge.net,
+       linux-kernel@vger.kernel.org, linux-arm-kernel@lists.infradead.org
+To: Joe Perches <joe@perches.com>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 04:58:26 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHqD3-0006P9-7p
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 04:58:25 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1757510Ab0KOD5r (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 14 Nov 2010 22:57:47 -0500
+Received: from mail-yx0-f174.google.com ([209.85.213.174]:43928 "EHLO
+       mail-yx0-f174.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1754294Ab0KOD5q (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 14 Nov 2010 22:57:46 -0500
+Received: by yxn35 with SMTP id 35so916234yxn.19
+        for <linux-kernel@vger.kernel.org>; Sun, 14 Nov 2010 19:57:46 -0800 (PST)
+Received: by 10.91.10.20 with SMTP id n20mr7068735agi.56.1289793465914;
+        Sun, 14 Nov 2010 19:57:45 -0800 (PST)
+Received: from angua (S01060002b3d79728.cg.shawcable.net [70.72.87.49])
+        by mx.google.com with ESMTPS id d15sm3276149ana.20.2010.11.14.19.57.43
+        (version=TLSv1/SSLv3 cipher=RC4-MD5);
+        Sun, 14 Nov 2010 19:57:45 -0800 (PST)
+Received: by angua (Postfix, from userid 1000)
+       id 238853C00E5; Sun, 14 Nov 2010 20:57:42 -0700 (MST)
+Content-Disposition: inline
+In-Reply-To: <fe5e5e0efbd97eaa32530eef5ed47efdc3252dad.1289789605.git.joe@perches.com>
+User-Agent: Mutt/1.5.20 (2009-06-14)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062328>
+
+On Sun, Nov 14, 2010 at 07:04:47PM -0800, Joe Perches wrote:
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+applied, thanks.
+
+g.
+
+> ---
+>  drivers/spi/amba-pl022.c |    2 +-
+>  drivers/spi/spi_nuc900.c |    2 +-
+>  2 files changed, 2 insertions(+), 2 deletions(-)
+> 
+> diff --git a/drivers/spi/amba-pl022.c b/drivers/spi/amba-pl022.c
+> index fb3d1b3..2e50631 100644
+> --- a/drivers/spi/amba-pl022.c
+> +++ b/drivers/spi/amba-pl022.c
+> @@ -956,7 +956,7 @@ static int configure_dma(struct pl022 *pl022)
+>              tx_conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES;
+>              break;
+>      case WRITING_U32:
+> -            tx_conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;;
+> +            tx_conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
+>              break;
+>      }
+>  
+> diff --git a/drivers/spi/spi_nuc900.c b/drivers/spi/spi_nuc900.c
+> index dff63be..d5be18b 100644
+> --- a/drivers/spi/spi_nuc900.c
+> +++ b/drivers/spi/spi_nuc900.c
+> @@ -449,7 +449,7 @@ err_iomap:
+>      release_mem_region(hw->res->start, resource_size(hw->res));
+>      kfree(hw->ioarea);
+>  err_pdata:
+> -    spi_master_put(hw->master);;
+> +    spi_master_put(hw->master);
+>  
+>  err_nomem:
+>      return err;
+> -- 
+> 1.7.3.1.g432b3.dirty
+> 
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002329:2, b/test/corpora/lkml/cur/1382298793.002329:2,
new file mode 100644 (file)
index 0000000..d91006a
--- /dev/null
@@ -0,0 +1,79 @@
+From: Michal Simek <monstr@monstr.eu>
+Subject: Re: [PATCH 02/44] arch/microblaze: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 07:37:39 +0100
+Lines: 32
+Message-ID: <4CE0D533.1010407@monstr.eu>
+References: <cover.1289789604.git.joe@perches.com> <5d57b90b488b4338bcdc3f0fbf5f6996842bd44d.1289789604.git.joe@perches.com>
+Reply-To: monstr@monstr.eu
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Cc: Jiri Kosina <trivial@kernel.org>,
+       microblaze-uclinux@itee.uq.edu.au, linux-kernel@vger.kernel.org
+To: Joe Perches <joe@perches.com>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 07:38:12 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHshf-0005Kt-RF
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 07:38:12 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755918Ab0KOGhs (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 01:37:48 -0500
+Received: from mail-fx0-f46.google.com ([209.85.161.46]:39130 "EHLO
+       mail-fx0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1755187Ab0KOGhp (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 01:37:45 -0500
+Received: by fxm6 with SMTP id 6so1494962fxm.19
+        for <linux-kernel@vger.kernel.org>; Sun, 14 Nov 2010 22:37:43 -0800 (PST)
+Received: by 10.223.70.131 with SMTP id d3mr4100646faj.73.1289803062970;
+        Sun, 14 Nov 2010 22:37:42 -0800 (PST)
+Received: from monstr.eu ([178.23.216.97])
+        by mx.google.com with ESMTPS id l14sm735429fan.33.2010.11.14.22.37.40
+        (version=SSLv3 cipher=RC4-MD5);
+        Sun, 14 Nov 2010 22:37:41 -0800 (PST)
+User-Agent: Thunderbird 2.0.0.22 (X11/20090625)
+In-Reply-To: <5d57b90b488b4338bcdc3f0fbf5f6996842bd44d.1289789604.git.joe@perches.com>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062348>
+
+Joe Perches wrote:
+> Signed-off-by: Joe Perches <joe@perches.com>
+> ---
+>  arch/microblaze/lib/memmove.c |    2 +-
+>  1 files changed, 1 insertions(+), 1 deletions(-)
+
+Applied.
+
+Thanks,
+Michal
+
+> 
+> diff --git a/arch/microblaze/lib/memmove.c b/arch/microblaze/lib/memmove.c
+> index 123e361..810fd68 100644
+> --- a/arch/microblaze/lib/memmove.c
+> +++ b/arch/microblaze/lib/memmove.c
+> @@ -182,7 +182,7 @@ void *memmove(void *v_dst, const void *v_src, __kernel_size_t c)
+>                      for (; c >= 4; c -= 4) {
+>                              value = *--i_src;
+>                              *--i_dst = buf_hold | ((value & 0xFF000000)>> 24);
+> -                            buf_hold = (value & 0xFFFFFF) << 8;;
+> +                            buf_hold = (value & 0xFFFFFF) << 8;
+>                      }
+>  #endif
+>                      /* Realign the source */
+
+
+-- 
+Michal Simek, Ing. (M.Eng)
+w: www.monstr.eu p: +42-0-721842854
+Maintainer of Linux kernel 2.6 Microblaze Linux - http://www.monstr.eu/fdt/
+Microblaze U-BOOT custodian
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002340:2, b/test/corpora/lkml/cur/1382298793.002340:2,
new file mode 100644 (file)
index 0000000..cee8581
--- /dev/null
@@ -0,0 +1,71 @@
+From: Sjur BRENDELAND <sjur.brandeland@stericsson.com>
+Subject: RE: [PATCH 39/44] include/net/caif/cfctrl.h: Remove unnecessary
+ semicolons
+Date: Mon, 15 Nov 2010 08:12:02 +0100
+Lines: 7
+Message-ID: <81C3A93C17462B4BBD7E272753C105791945C0C9AA@EXDCVYMBSTM005.EQ1STM.local>
+References: <cover.1289789604.git.joe@perches.com>
+ <35914cfea1bd0ab3963e632d02b1fdd52a9d2bc8.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 8BIT
+Cc: "David S. Miller" <davem@davemloft.net>,
+       "netdev@vger.kernel.org" <netdev@vger.kernel.org>,
+       "linux-kernel@vger.kernel.org" <linux-kernel@vger.kernel.org>
+To: Joe Perches <joe@perches.com>, Jiri Kosina <trivial@kernel.org>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 08:12:47 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PHtF8-0003Ia-E9
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 08:12:46 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1757561Ab0KOHMn (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 02:12:43 -0500
+Received: from eu1sys200aog110.obsmtp.com ([207.126.144.129]:34651 "EHLO
+       eu1sys200aog110.obsmtp.com" rhost-flags-OK-OK-OK-OK)
+       by vger.kernel.org with ESMTP id S1755276Ab0KOHMk convert rfc822-to-8bit
+       (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 02:12:40 -0500
+Received: from source ([138.198.100.35]) (using TLSv1) by eu1sys200aob110.postini.com ([207.126.147.11]) with SMTP
+       ID DSNKTODdXnF0LEMxKFzys6wWldAszZ/h5aGS@postini.com; Mon, 15 Nov 2010 07:12:40 UTC
+Received: from zeta.dmz-ap.st.com (ns6.st.com [138.198.234.13])
+       by beta.dmz-ap.st.com (STMicroelectronics) with ESMTP id B201FFC;
+       Mon, 15 Nov 2010 07:12:06 +0000 (GMT)
+Received: from relay1.stm.gmessaging.net (unknown [10.230.100.17])
+       by zeta.dmz-ap.st.com (STMicroelectronics) with ESMTP id C1093569;
+       Mon, 15 Nov 2010 07:12:05 +0000 (GMT)
+Received: from exdcvycastm022.EQ1STM.local (alteon-source-exch [10.230.100.61])
+       (using TLSv1 with cipher RC4-MD5 (128/128 bits))
+       (Client CN "exdcvycastm022", Issuer "exdcvycastm022" (not verified))
+       by relay1.stm.gmessaging.net (Postfix) with ESMTPS id 2017C24C080;
+       Mon, 15 Nov 2010 08:12:01 +0100 (CET)
+Received: from EXDCVYMBSTM005.EQ1STM.local ([10.230.100.3]) by
+ exdcvycastm022.EQ1STM.local ([10.230.100.30]) with mapi; Mon, 15 Nov 2010
+ 08:12:04 +0100
+Thread-Topic: [PATCH 39/44] include/net/caif/cfctrl.h: Remove unnecessary
+ semicolons
+Thread-Index: AcuEcgmvXg9N7FIzS1KHGoGQuVf68gAIhhTQ
+In-Reply-To: <35914cfea1bd0ab3963e632d02b1fdd52a9d2bc8.1289789605.git.joe@perches.com>
+Accept-Language: en-US
+Content-Language: en-US
+X-MS-Has-Attach: 
+X-MS-TNEF-Correlator: 
+acceptlanguage: en-US
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062359>
+
+> 
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+
+Looks good to me.
+Acked-by: Sjur Braendeland <sjur.brandeland@stericsson.com>
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002400:2, b/test/corpora/lkml/cur/1382298793.002400:2,
new file mode 100644 (file)
index 0000000..a2eab51
--- /dev/null
@@ -0,0 +1,102 @@
+From: Mel Gorman <mel@csn.ul.ie>
+Subject: Re: [PATCH 40/44] mm/hugetlb.c: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 09:52:44 +0000
+Lines: 44
+Message-ID: <20101115095244.GI27362@csn.ul.ie>
+References: <cover.1289789604.git.joe@perches.com> <59705f848d35b12ace640f92afcffea02cee0976.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15
+Cc: Jiri Kosina <trivial@kernel.org>, linux-mm@kvack.org,
+       linux-kernel@vger.kernel.org
+To: Joe Perches <joe@perches.com>
+X-From: owner-linux-mm@kvack.org Mon Nov 15 10:53:02 2010
+Return-path: <owner-linux-mm@kvack.org>
+Envelope-to: glkm-linux-mm-2@m.gmane.org
+Received: from kanga.kvack.org ([205.233.56.17])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <owner-linux-mm@kvack.org>)
+       id 1PHvkD-0001DG-RE
+       for glkm-linux-mm-2@m.gmane.org; Mon, 15 Nov 2010 10:53:02 +0100
+Received: by kanga.kvack.org (Postfix)
+       id BA7DD8D0050; Mon, 15 Nov 2010 04:53:00 -0500 (EST)
+Delivered-To: linux-mm-outgoing@kvack.org
+Received: by kanga.kvack.org (Postfix, from userid 40)
+       id B3E4F8D0017; Mon, 15 Nov 2010 04:53:00 -0500 (EST)
+X-Original-To: int-list-linux-mm@kvack.org
+Delivered-To: int-list-linux-mm@kvack.org
+Received: by kanga.kvack.org (Postfix, from userid 63042)
+       id 985338D0050; Mon, 15 Nov 2010 04:53:00 -0500 (EST)
+X-Original-To: linux-mm@kvack.org
+Delivered-To: linux-mm@kvack.org
+Received: from mail144.messagelabs.com (mail144.messagelabs.com [216.82.254.51])
+       by kanga.kvack.org (Postfix) with ESMTP id 3FA8F8D0017
+       for <linux-mm@kvack.org>; Mon, 15 Nov 2010 04:53:00 -0500 (EST)
+X-VirusChecked: Checked
+X-Env-Sender: mel@csn.ul.ie
+X-Msg-Ref: server-6.tower-144.messagelabs.com!1289814777!96428158!1
+X-StarScan-Version: 6.2.9; banners=-,-,-
+X-Originating-IP: [193.1.99.77]
+X-SpamReason: No, hits=0.5 required=7.0 tests=BODY_RANDOM_LONG
+Received: (qmail 13284 invoked from network); 15 Nov 2010 09:52:59 -0000
+Received: from gir.skynet.ie (HELO gir.skynet.ie) (193.1.99.77)
+  by server-6.tower-144.messagelabs.com with DHE-RSA-AES256-SHA encrypted SMTP; 15 Nov 2010 09:52:59 -0000
+Received: from skynet.skynet.ie (skynet.skynet.ie [193.1.99.74])
+       by gir.skynet.ie (Postfix) with ESMTP id E3A5E1244B;
+       Mon, 15 Nov 2010 09:52:44 +0000 (GMT)
+Received: by skynet.skynet.ie (Postfix, from userid 2391)
+       id D7AF750911; Mon, 15 Nov 2010 09:52:44 +0000 (GMT)
+Content-Disposition: inline
+In-Reply-To: <59705f848d35b12ace640f92afcffea02cee0976.1289789605.git.joe@perches.com>
+User-Agent: Mutt/1.5.17+20080114 (2008-01-14)
+X-Bogosity: Ham, tests=bogofilter, spamicity=0.000000, version=1.2.2
+Sender: owner-linux-mm@kvack.org
+Precedence: bulk
+X-Loop: owner-majordomo@kvack.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062420>
+
+On Sun, Nov 14, 2010 at 07:04:59PM -0800, Joe Perches wrote:
+> Signed-off-by: Joe Perches <joe@perches.com>
+> ---
+>  mm/hugetlb.c |    2 +-
+>  1 files changed, 1 insertions(+), 1 deletions(-)
+> 
+
+Acked-by: Mel Gorman <mel@csn.ul.ie>
+
+> diff --git a/mm/hugetlb.c b/mm/hugetlb.c
+> index c4a3558..8875242 100644
+> --- a/mm/hugetlb.c
+> +++ b/mm/hugetlb.c
+> @@ -540,7 +540,7 @@ static struct page *dequeue_huge_page_vma(struct hstate *h,
+>  
+>      /* If reserves cannot be used, ensure enough pages are in the pool */
+>      if (avoid_reserve && h->free_huge_pages - h->resv_huge_pages == 0)
+> -            goto err;;
+> +            goto err;
+>  
+>      for_each_zone_zonelist_nodemask(zone, z, zonelist,
+>                                              MAX_NR_ZONES - 1, nodemask) {
+> -- 
+> 1.7.3.1.g432b3.dirty
+> 
+> --
+> To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
+> the body of a message to majordomo@vger.kernel.org
+> More majordomo info at  http://vger.kernel.org/majordomo-info.html
+> Please read the FAQ at  http://www.tux.org/lkml/
+> 
+
+-- 
+Mel Gorman
+Part-time Phd Student                          Linux Technology Center
+University of Limerick                         IBM Dublin Software Lab
+
+--
+To unsubscribe, send a message with 'unsubscribe linux-mm' in
+the body to majordomo@kvack.org.  For more info on Linux MM,
+see: http://www.linux-mm.org/ .
+Fight unfair telecom policy in Canada: sign http://dissolvethecrtc.ca/
+Don't email: <a href=mailto:"dont@kvack.org"> email@kvack.org </a>
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002432:2, b/test/corpora/lkml/cur/1382298793.002432:2,
new file mode 100644 (file)
index 0000000..f8b72b6
--- /dev/null
@@ -0,0 +1,85 @@
+From: Liam Girdwood <lrg@slimlogic.co.uk>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary
+       semicolons
+Date: Mon, 15 Nov 2010 11:09:20 +0000
+Lines: 14
+Message-ID: <1289819360.3377.15.camel@odin>
+References: <cover.1289789604.git.joe@perches.com>
+       <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       Iwai <tiwai@suse.de>, linux-kernel@vger.kernel.org,
+       Takashi@alsa-project.org,
+       Mark Brown <broonie@opensource.wolfsonmicro.com>,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Mon Nov 15 12:09:44 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PHwwS-0002x6-6X
+       for glad-alsa-devel-2@m.gmane.org; Mon, 15 Nov 2010 12:09:44 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id D0E09103835; Mon, 15 Nov 2010 12:09:43 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: *
+X-Spam-Status: No, score=1.0 required=5.0 tests=PRX_BODY_40 autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 0AE19103845;
+       Mon, 15 Nov 2010 12:09:35 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id EA398103845; Mon, 15 Nov 2010 12:09:33 +0100 (CET)
+Received: from mail-wy0-f179.google.com (mail-wy0-f179.google.com
+       [74.125.82.179])
+       by alsa0.perex.cz (Postfix) with ESMTP id 8FD12103849
+       for <alsa-devel@alsa-project.org>; Mon, 15 Nov 2010 12:09:26 +0100 (CET)
+Received: by mail-wy0-f179.google.com with SMTP id 36so3482678wyg.38
+       for <alsa-devel@alsa-project.org>; Mon, 15 Nov 2010 03:09:26 -0800 (PST)
+Received: by 10.216.64.139 with SMTP id c11mr5392190wed.81.1289819366153;
+       Mon, 15 Nov 2010 03:09:26 -0800 (PST)
+Received: from [192.168.1.6] (host81-136-218-57.in-addr.btopenworld.com
+       [81.136.218.57])
+       by mx.google.com with ESMTPS id 7sm3626925wet.24.2010.11.15.03.09.21
+       (version=SSLv3 cipher=RC4-MD5); Mon, 15 Nov 2010 03:09:24 -0800 (PST)
+In-Reply-To: <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+X-Mailer: Evolution 2.30.3 
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062453>
+
+On Sun, 2010-11-14 at 19:05 -0800, Joe Perches wrote:
+> Signed-off-by: Joe Perches <joe@perches.com>
+> ---
+>  sound/soc/codecs/wm8904.c  |    2 +-
+>  sound/soc/codecs/wm8940.c  |    1 -
+>  sound/soc/codecs/wm8993.c  |    2 +-
+>  sound/soc/codecs/wm_hubs.c |    2 +-
+>  4 files changed, 3 insertions(+), 4 deletions(-)
+
+Acked-by: Liam Girdwood <lrg@slimlogic.co.uk>
+-- 
+Freelance Developer, SlimLogic Ltd
+ASoC and Voltage Regulator Maintainer.
+http://www.slimlogic.co.uk
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002468:2, b/test/corpora/lkml/cur/1382298793.002468:2,
new file mode 100644 (file)
index 0000000..e06d389
--- /dev/null
@@ -0,0 +1,75 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary
+       semicolons
+Date: Mon, 15 Nov 2010 13:49:39 +0000
+Lines: 5
+Message-ID: <20101115134939.GC12986@rakim.wolfsonmicro.main>
+References: <cover.1289789604.git.joe@perches.com>
+       <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       Takashi Iwai <tiwai@suse.de>, linux-kernel@vger.kernel.org,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Mon Nov 15 14:49:49 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PHzRN-0003qZ-C2
+       for glad-alsa-devel-2@m.gmane.org; Mon, 15 Nov 2010 14:49:49 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id D24A2103851; Mon, 15 Nov 2010 14:49:48 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 53C91103853;
+       Mon, 15 Nov 2010 14:49:44 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 15C62103853; Mon, 15 Nov 2010 14:49:42 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id 8DE08103851
+       for <alsa-devel@alsa-project.org>; Mon, 15 Nov 2010 14:49:41 +0100 (CET)
+Received: from rakim.wolfsonmicro.main (lumison.wolfsonmicro.com
+       [87.246.78.27])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id E613E788028; 
+       Mon, 15 Nov 2010 13:49:40 +0000 (GMT)
+Received: from broonie by rakim.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@rakim.wolfsonmicro.main>)
+       id 1PHzRD-0004Lh-OM; Mon, 15 Nov 2010 13:49:39 +0000
+Content-Disposition: inline
+In-Reply-To: <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+X-Cookie: I like your SNOOPY POSTER!!
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062489>
+
+On Sun, Nov 14, 2010 at 07:05:03PM -0800, Joe Perches wrote:
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+This doesn't apply against current -next, could you please regenerate
+against that?
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002543:2, b/test/corpora/lkml/cur/1382298793.002543:2,
new file mode 100644 (file)
index 0000000..aebfe25
--- /dev/null
@@ -0,0 +1,132 @@
+From: "Rose, Gregory V" <gregory.v.rose@intel.com>
+Subject: Re: [PATCH 14/44] drivers/net/ixgbe: Remove
+       unnecessary semicolons
+Date: Mon, 15 Nov 2010 08:24:22 -0800
+Lines: 48
+Message-ID: <43F901BD926A4E43B106BF17856F0755013080DEFF@orsmsx508.amr.corp.intel.com>
+References: <cover.1289789604.git.joe@perches.com>
+       <7d2c334daa75c5221946a17d45c9de1901cf06e7.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: "e1000-devel@lists.sourceforge.net" <e1000-devel@lists.sourceforge.net>,
+       "Allan, Bruce W" <bruce.w.allan@intel.com>, "Brandeburg,
+       Jesse" <jesse.brandeburg@intel.com>,
+       "linux-kernel@vger.kernel.org" <linux-kernel@vger.kernel.org>,
+       "Ronciak, John" <john.ronciak@intel.com>, "Kirsher,
+       Jeffrey T" <jeffrey.t.kirsher@intel.com>,
+       "netdev@vger.kernel.org" <netdev@vger.kernel.org>
+To: Joe Perches <joe@perches.com>, Jiri Kosina <trivial@kernel.org>
+X-From: e1000-devel-bounces@lists.sourceforge.net Mon Nov 15 17:25:50 2010
+Return-path: <e1000-devel-bounces@lists.sourceforge.net>
+Envelope-to: glded-e1000-devel@m.gmane.org
+Received: from lists.sourceforge.net ([216.34.181.88])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <e1000-devel-bounces@lists.sourceforge.net>)
+       id 1PI1sL-0003p5-2h
+       for glded-e1000-devel@m.gmane.org; Mon, 15 Nov 2010 17:25:49 +0100
+Received: from localhost ([127.0.0.1] helo=sfs-ml-4.v29.ch3.sourceforge.com)
+       by sfs-ml-4.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <e1000-devel-bounces@lists.sourceforge.net>)
+       id 1PI1sG-0006Jq-KT; Mon, 15 Nov 2010 16:25:44 +0000
+Received: from sog-mx-4.v43.ch3.sourceforge.com ([172.29.43.194]
+       helo=mx.sourceforge.net)
+       by sfs-ml-4.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <gregory.v.rose@intel.com>) id 1PI1sF-0006Jk-IV
+       for e1000-devel@lists.sourceforge.net; Mon, 15 Nov 2010 16:25:43 +0000
+X-ACL-Warn: 
+Received: from mga09.intel.com ([134.134.136.24])
+       by sog-mx-4.v43.ch3.sourceforge.com with esmtp (Exim 4.69)
+       id 1PI1sA-0001hP-Vk
+       for e1000-devel@lists.sourceforge.net; Mon, 15 Nov 2010 16:25:43 +0000
+Received: from orsmga001.jf.intel.com ([10.7.209.18])
+       by orsmga102.jf.intel.com with ESMTP; 15 Nov 2010 08:25:33 -0800
+X-ExtLoop1: 1
+X-IronPort-AV: E=Sophos;i="4.59,200,1288594800"; d="scan'208";a="677619042"
+Received: from orsmsx604.amr.corp.intel.com ([10.22.226.87])
+       by orsmga001.jf.intel.com with ESMTP; 15 Nov 2010 08:25:33 -0800
+Received: from orsmsx606.amr.corp.intel.com (10.22.226.128) by
+       orsmsx604.amr.corp.intel.com (10.22.226.87) with Microsoft SMTP Server
+       (TLS) id 8.2.254.0; Mon, 15 Nov 2010 08:24:25 -0800
+Received: from orsmsx508.amr.corp.intel.com ([10.22.226.46]) by
+       orsmsx606.amr.corp.intel.com ([10.22.226.128]) with mapi;
+       Mon, 15 Nov 2010 08:24:24 -0800
+Thread-Topic: [PATCH 14/44] drivers/net/ixgbe: Remove unnecessary semicolons
+Thread-Index: AcuEcftvdxmC6VgnRT2RlEslHutcHgAb4Qcg
+In-Reply-To: <7d2c334daa75c5221946a17d45c9de1901cf06e7.1289789604.git.joe@perches.com>
+Accept-Language: en-US
+Content-Language: en-US
+X-MS-Has-Attach: 
+X-MS-TNEF-Correlator: 
+acceptlanguage: en-US
+X-Spam-Score: -0.0 (/)
+X-Spam-Report: Spam Filtering performed by mx.sourceforge.net.
+       See http://spamassassin.org/tag/ for more details.
+       -0.0 T_RP_MATCHES_RCVD Envelope sender domain matches handover relay
+       domain
+X-Headers-End: 1PI1sA-0001hP-Vk
+X-BeenThere: e1000-devel@lists.sourceforge.net
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "e100/e1000\(e\)/ixgb/igb/ixgbe development and discussion"
+       <e1000-devel.lists.sourceforge.net>
+List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/e1000-devel>, 
+       <mailto:e1000-devel-request@lists.sourceforge.net?subject=unsubscribe>
+List-Archive: <http://sourceforge.net/mailarchive/forum.php?forum_name=e1000-devel>
+List-Post: <mailto:e1000-devel@lists.sourceforge.net>
+List-Help: <mailto:e1000-devel-request@lists.sourceforge.net?subject=help>
+List-Subscribe: <https://lists.sourceforge.net/lists/listinfo/e1000-devel>,
+       <mailto:e1000-devel-request@lists.sourceforge.net?subject=subscribe>
+Errors-To: e1000-devel-bounces@lists.sourceforge.net
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062564>
+
+> -----Original Message-----
+> From: Joe Perches [mailto:joe@perches.com]
+> Sent: Sunday, November 14, 2010 7:05 PM
+> To: Jiri Kosina
+> Cc: Kirsher, Jeffrey T; Brandeburg, Jesse; Allan, Bruce W; Wyborny,
+> Carolyn; Skidmore, Donald C; Rose, Gregory V; Waskiewicz Jr, Peter P;
+> Duyck, Alexander H; Ronciak, John; e1000-devel@lists.sourceforge.net;
+> netdev@vger.kernel.org; linux-kernel@vger.kernel.org
+> Subject: [PATCH 14/44] drivers/net/ixgbe: Remove unnecessary semicolons
+> 
+> Signed-off-by: Joe Perches <joe@perches.com>
+> ---
+>  drivers/net/ixgbe/ixgbe_sriov.c |    2 +-
+>  1 files changed, 1 insertions(+), 1 deletions(-)
+> 
+> diff --git a/drivers/net/ixgbe/ixgbe_sriov.c
+> b/drivers/net/ixgbe/ixgbe_sriov.c
+> index 5428153..93f40bc 100644
+> --- a/drivers/net/ixgbe/ixgbe_sriov.c
+> +++ b/drivers/net/ixgbe/ixgbe_sriov.c
+> @@ -68,7 +68,7 @@ static int ixgbe_set_vf_multicasts(struct ixgbe_adapter
+> *adapter,
+>       * addresses
+>       */
+>      for (i = 0; i < entries; i++) {
+> -            vfinfo->vf_mc_hashes[i] = hash_list[i];;
+> +            vfinfo->vf_mc_hashes[i] = hash_list[i];
+>      }
+> 
+>      for (i = 0; i < vfinfo->num_vf_mc_hashes; i++) {
+> --
+> 1.7.3.1.g432b3.dirty
+
+Acked By: Greg Rose <Gregory.v.rose@intel.com>
+
+
+------------------------------------------------------------------------------
+Centralized Desktop Delivery: Dell and VMware Reference Architecture
+Simplifying enterprise desktop deployment and management using
+Dell EqualLogic storage and VMware View: A highly scalable, end-to-end
+client virtualization framework. Read more!
+http://p.sf.net/sfu/dell-eql-dev2dev
+_______________________________________________
+E1000-devel mailing list
+E1000-devel@lists.sourceforge.net
+https://lists.sourceforge.net/lists/listinfo/e1000-devel
+To learn more about Intel&#174; Ethernet, visit http://communities.intel.com/community/wired
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002557:2, b/test/corpora/lkml/cur/1382298793.002557:2,
new file mode 100644 (file)
index 0000000..723f9d6
--- /dev/null
@@ -0,0 +1,109 @@
+From: Joe Perches <joe@perches.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 09:09:17 -0800
+Lines: 63
+Message-ID: <1289840957.16461.138.camel@Joe-Laptop>
+References: <cover.1289789604.git.joe@perches.com>
+        <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+        <20101115134939.GC12986@rakim.wolfsonmicro.main>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+Cc: Jiri Kosina <trivial@kernel.org>,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>,
+       Jaroslav Kysela <perex@perex.cz>, Takashi Iwai <tiwai@suse.de>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 18:09:51 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI2Yr-0005ly-Pc
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 18:09:46 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932795Ab0KORJV (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 12:09:21 -0500
+Received: from mail.perches.com ([173.55.12.10]:1293 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932132Ab0KORJU (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 12:09:20 -0500
+Received: from [192.168.1.162] (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 93B872436B;
+       Mon, 15 Nov 2010 09:07:32 -0800 (PST)
+In-Reply-To: <20101115134939.GC12986@rakim.wolfsonmicro.main>
+X-Mailer: Evolution 2.30.3 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062578>
+
+Signed-off-by: Joe Perches <joe@perches.com>
+---
+V2: against -next
+
+ sound/soc/codecs/wm8904.c  |    2 +-
+ sound/soc/codecs/wm8940.c  |    1 -
+ sound/soc/codecs/wm8993.c  |    2 +-
+ sound/soc/codecs/wm_hubs.c |    2 +-
+ 4 files changed, 3 insertions(+), 4 deletions(-)
+
+diff --git a/sound/soc/codecs/wm8904.c b/sound/soc/codecs/wm8904.c
+index be90399..5e57bd2 100644
+--- a/sound/soc/codecs/wm8904.c
++++ b/sound/soc/codecs/wm8904.c
+@@ -1591,7 +1591,7 @@ static int wm8904_hw_params(struct snd_pcm_substream *substream,
+                      - wm8904->fs);
+       for (i = 1; i < ARRAY_SIZE(clk_sys_rates); i++) {
+               cur_val = abs((wm8904->sysclk_rate /
+-                             clk_sys_rates[i].ratio) - wm8904->fs);;
++                             clk_sys_rates[i].ratio) - wm8904->fs);
+               if (cur_val < best_val) {
+                       best = i;
+                       best_val = cur_val;
+diff --git a/sound/soc/codecs/wm8940.c b/sound/soc/codecs/wm8940.c
+index c2def1b..caed084 100644
+--- a/sound/soc/codecs/wm8940.c
++++ b/sound/soc/codecs/wm8940.c
+@@ -736,7 +736,6 @@ static int wm8940_probe(struct snd_soc_codec *codec)
+               return ret;
+       return ret;
+-;
+ }
+ static int wm8940_remove(struct snd_soc_codec *codec)
+diff --git a/sound/soc/codecs/wm8993.c b/sound/soc/codecs/wm8993.c
+index bcc54be..991d90c 100644
+--- a/sound/soc/codecs/wm8993.c
++++ b/sound/soc/codecs/wm8993.c
+@@ -1227,7 +1227,7 @@ static int wm8993_hw_params(struct snd_pcm_substream *substream,
+                      - wm8993->fs);
+       for (i = 1; i < ARRAY_SIZE(clk_sys_rates); i++) {
+               cur_val = abs((wm8993->sysclk_rate /
+-                             clk_sys_rates[i].ratio) - wm8993->fs);;
++                             clk_sys_rates[i].ratio) - wm8993->fs);
+               if (cur_val < best_val) {
+                       best = i;
+                       best_val = cur_val;
+diff --git a/sound/soc/codecs/wm_hubs.c b/sound/soc/codecs/wm_hubs.c
+index 8aff0ef..422c7fb 100644
+--- a/sound/soc/codecs/wm_hubs.c
++++ b/sound/soc/codecs/wm_hubs.c
+@@ -119,7 +119,7 @@ static void calibrate_dc_servo(struct snd_soc_codec *codec)
+       switch (hubs->dcs_readback_mode) {
+       case 0:
+               reg_l = snd_soc_read(codec, WM8993_DC_SERVO_READBACK_1)
+-                      & WM8993_DCS_INTEG_CHAN_0_MASK;;
++                      & WM8993_DCS_INTEG_CHAN_0_MASK;
+               reg_r = snd_soc_read(codec, WM8993_DC_SERVO_READBACK_2)
+                       & WM8993_DCS_INTEG_CHAN_1_MASK;
+               break;
+
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002575:2, b/test/corpora/lkml/cur/1382298793.002575:2,
new file mode 100644 (file)
index 0000000..981c1c9
--- /dev/null
@@ -0,0 +1,79 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary
+       semicolons
+Date: Mon, 15 Nov 2010 17:30:31 +0000
+Lines: 7
+Message-ID: <20101115173031.GI12986@rakim.wolfsonmicro.main>
+References: <cover.1289789604.git.joe@perches.com>
+       <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+       <20101115134939.GC12986@rakim.wolfsonmicro.main>
+       <1289840957.16461.138.camel@Joe-Laptop>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       Takashi Iwai <tiwai@suse.de>, linux-kernel@vger.kernel.org,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Mon Nov 15 18:30:46 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PI2t9-0000lr-Er
+       for glad-alsa-devel-2@m.gmane.org; Mon, 15 Nov 2010 18:30:43 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 5B6F8244F9; Mon, 15 Nov 2010 18:30:41 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id AE3D8244FB;
+       Mon, 15 Nov 2010 18:30:36 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id EF7B7244FC; Mon, 15 Nov 2010 18:30:34 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id 8B247244F9
+       for <alsa-devel@alsa-project.org>; Mon, 15 Nov 2010 18:30:34 +0100 (CET)
+Received: from rakim.wolfsonmicro.main (lumison.wolfsonmicro.com
+       [87.246.78.27])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id C748C788028; 
+       Mon, 15 Nov 2010 17:30:32 +0000 (GMT)
+Received: from broonie by rakim.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@rakim.wolfsonmicro.main>)
+       id 1PI2sx-0000C1-Jf; Mon, 15 Nov 2010 17:30:31 +0000
+Content-Disposition: inline
+In-Reply-To: <1289840957.16461.138.camel@Joe-Laptop>
+X-Cookie: I like your SNOOPY POSTER!!
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062597>
+
+On Mon, Nov 15, 2010 at 09:09:17AM -0800, Joe Perches wrote:
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Applied, thanks.  
+
+Please try to use changelog formats consistent with the code you're
+modifying.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002576:2, b/test/corpora/lkml/cur/1382298793.002576:2,
new file mode 100644 (file)
index 0000000..6963356
--- /dev/null
@@ -0,0 +1,63 @@
+From: Joe Perches <joe@perches.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 09:34:04 -0800
+Lines: 15
+Message-ID: <1289842444.16461.140.camel@Joe-Laptop>
+References: <cover.1289789604.git.joe@perches.com>
+        <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+        <20101115134939.GC12986@rakim.wolfsonmicro.main>
+        <1289840957.16461.138.camel@Joe-Laptop>
+        <20101115173031.GI12986@rakim.wolfsonmicro.main>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+Cc: Jiri Kosina <trivial@kernel.org>,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>,
+       Jaroslav Kysela <perex@perex.cz>, Takashi Iwai <tiwai@suse.de>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 18:34:20 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI2wd-0002wj-Br
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 18:34:19 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933091Ab0KOReI (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 12:34:08 -0500
+Received: from mail.perches.com ([173.55.12.10]:1304 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S933013Ab0KOReH (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 12:34:07 -0500
+Received: from [192.168.1.162] (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 9F71A24368;
+       Mon, 15 Nov 2010 09:32:18 -0800 (PST)
+In-Reply-To: <20101115173031.GI12986@rakim.wolfsonmicro.main>
+X-Mailer: Evolution 2.30.3 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062599>
+
+On Mon, 2010-11-15 at 17:30 +0000, Mark Brown wrote:
+> On Mon, Nov 15, 2010 at 09:09:17AM -0800, Joe Perches wrote:
+> > Signed-off-by: Joe Perches <joe@perches.com>
+> Applied, thanks.
+> Please try to use changelog formats consistent with the code you're
+> modifying.
+
+I think it's more important to use consistent changelogs
+for a patch series.
+
+If you want your own subsystem changelog consistency, I
+think you should change the format to what you desire.
+
+cheers, Joe
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002639:2, b/test/corpora/lkml/cur/1382298793.002639:2,
new file mode 100644 (file)
index 0000000..450509d
--- /dev/null
@@ -0,0 +1,90 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary
+       semicolons
+Date: Mon, 15 Nov 2010 18:27:08 +0000
+Lines: 16
+Message-ID: <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+References: <cover.1289789604.git.joe@perches.com>
+       <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+       <20101115134939.GC12986@rakim.wolfsonmicro.main>
+       <1289840957.16461.138.camel@Joe-Laptop>
+       <20101115173031.GI12986@rakim.wolfsonmicro.main>
+       <1289842444.16461.140.camel@Joe-Laptop>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       Takashi Iwai <tiwai@suse.de>, linux-kernel@vger.kernel.org,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Mon Nov 15 19:27:21 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PI3lw-0007AT-Pq
+       for glad-alsa-devel-2@m.gmane.org; Mon, 15 Nov 2010 19:27:20 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 35136103873; Mon, 15 Nov 2010 19:27:17 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id B554724525;
+       Mon, 15 Nov 2010 19:27:11 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id D599A24527; Mon, 15 Nov 2010 19:27:10 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id 5719224414
+       for <alsa-devel@alsa-project.org>; Mon, 15 Nov 2010 19:27:10 +0100 (CET)
+Received: from rakim.wolfsonmicro.main (lumison.wolfsonmicro.com
+       [87.246.78.27])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id C97D2788028; 
+       Mon, 15 Nov 2010 18:27:09 +0000 (GMT)
+Received: from broonie by rakim.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@rakim.wolfsonmicro.main>)
+       id 1PI3lk-00053D-RZ; Mon, 15 Nov 2010 18:27:08 +0000
+Content-Disposition: inline
+In-Reply-To: <1289842444.16461.140.camel@Joe-Laptop>
+X-Cookie: I like your SNOOPY POSTER!!
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062664>
+
+On Mon, Nov 15, 2010 at 09:34:04AM -0800, Joe Perches wrote:
+> On Mon, 2010-11-15 at 17:30 +0000, Mark Brown wrote:
+
+> > Please try to use changelog formats consistent with the code you're
+> > modifying.
+
+> I think it's more important to use consistent changelogs
+> for a patch series.
+
+...since...?
+
+> If you want your own subsystem changelog consistency, I
+> think you should change the format to what you desire.
+
+Which is what I'm doing but it's annoying to have to constantly hand
+edit changelogs.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002642:2, b/test/corpora/lkml/cur/1382298793.002642:2,
new file mode 100644 (file)
index 0000000..1bd4e32
--- /dev/null
@@ -0,0 +1,66 @@
+From: Joe Perches <joe@perches.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 10:30:29 -0800
+Lines: 16
+Message-ID: <1289845830.16461.149.camel@Joe-Laptop>
+References: <cover.1289789604.git.joe@perches.com>
+        <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+        <20101115134939.GC12986@rakim.wolfsonmicro.main>
+        <1289840957.16461.138.camel@Joe-Laptop>
+        <20101115173031.GI12986@rakim.wolfsonmicro.main>
+        <1289842444.16461.140.camel@Joe-Laptop>
+        <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+Cc: Jiri Kosina <trivial@kernel.org>,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>,
+       Jaroslav Kysela <perex@perex.cz>, Takashi Iwai <tiwai@suse.de>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 19:31:55 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI3qN-0000mO-6u
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 19:31:55 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1758106Ab0KOSad (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 13:30:33 -0500
+Received: from mail.perches.com ([173.55.12.10]:1314 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1756843Ab0KOSac (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 13:30:32 -0500
+Received: from [192.168.1.162] (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 2BC7724368;
+       Mon, 15 Nov 2010 10:28:43 -0800 (PST)
+In-Reply-To: <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+X-Mailer: Evolution 2.30.3 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062667>
+
+On Mon, 2010-11-15 at 18:27 +0000, Mark Brown wrote:
+> On Mon, Nov 15, 2010 at 09:34:04AM -0800, Joe Perches wrote:
+> > On Mon, 2010-11-15 at 17:30 +0000, Mark Brown wrote:
+> > > Please try to use changelog formats consistent with the code you're
+> > > modifying.
+> > I think it's more important to use consistent changelogs
+> > for a patch series.
+> ...since...?
+
+1995...
+
+Since there isn't a consistent standard for subsystems
+changelogs and automating scripts for the desires of
+individual subsystem maintainers is not feasible.
+
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002661:2, b/test/corpora/lkml/cur/1382298793.002661:2,
new file mode 100644 (file)
index 0000000..36f8752
--- /dev/null
@@ -0,0 +1,49 @@
+From: David Miller <davem@davemloft.net>
+Subject: Re: [PATCH 39/44] include/net/caif/cfctrl.h: Remove unnecessary
+ semicolons
+Date: Mon, 15 Nov 2010 11:07:32 -0800 (PST)
+Lines: 6
+Message-ID: <20101115.110732.27814339.davem@davemloft.net>
+References: <cover.1289789604.git.joe@perches.com>
+       <35914cfea1bd0ab3963e632d02b1fdd52a9d2bc8.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: Text/Plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Cc: trivial@kernel.org, sjur.brandeland@stericsson.com,
+       netdev@vger.kernel.org, linux-kernel@vger.kernel.org
+To: joe@perches.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 20:07:42 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI4Oy-0002G6-Lp
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 20:07:41 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933101Ab0KOTHJ (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 14:07:09 -0500
+Received: from 74-93-104-97-Washington.hfc.comcastbusiness.net ([74.93.104.97]:51782
+       "EHLO sunset.davemloft.net" rhost-flags-OK-OK-OK-OK)
+       by vger.kernel.org with ESMTP id S932513Ab0KOTHI (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 14:07:08 -0500
+Received: from localhost (localhost [127.0.0.1])
+       by sunset.davemloft.net (Postfix) with ESMTP id 71B5924C088;
+       Mon, 15 Nov 2010 11:07:33 -0800 (PST)
+In-Reply-To: <35914cfea1bd0ab3963e632d02b1fdd52a9d2bc8.1289789605.git.joe@perches.com>
+X-Mailer: Mew version 6.3 on Emacs 23.1 / Mule 6.0 (HANACHIRUSATO)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062686>
+
+From: Joe Perches <joe@perches.com>
+Date: Sun, 14 Nov 2010 19:04:58 -0800
+
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Applied.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002662:2, b/test/corpora/lkml/cur/1382298793.002662:2,
new file mode 100644 (file)
index 0000000..2fbb7e7
--- /dev/null
@@ -0,0 +1,49 @@
+From: David Miller <davem@davemloft.net>
+Subject: Re: [PATCH 41/44] net/ipv6/mcast.c: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:07:39 -0800 (PST)
+Lines: 6
+Message-ID: <20101115.110739.191407854.davem@davemloft.net>
+References: <cover.1289789604.git.joe@perches.com>
+       <1f3e1f7e454f3c62b66fc5f3e1e1ed90d62b7fb0.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: Text/Plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Cc: trivial@kernel.org, kuznet@ms2.inr.ac.ru, pekkas@netcore.fi,
+       jmorris@namei.org, yoshfuji@linux-ipv6.org, kaber@trash.net,
+       netdev@vger.kernel.org, linux-kernel@vger.kernel.org
+To: joe@perches.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 20:07:42 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI4Oz-0002G6-63
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 20:07:41 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933351Ab0KOTHS (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 14:07:18 -0500
+Received: from 74-93-104-97-Washington.hfc.comcastbusiness.net ([74.93.104.97]:51792
+       "EHLO sunset.davemloft.net" rhost-flags-OK-OK-OK-OK)
+       by vger.kernel.org with ESMTP id S933109Ab0KOTHP (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 14:07:15 -0500
+Received: from localhost (localhost [127.0.0.1])
+       by sunset.davemloft.net (Postfix) with ESMTP id 53FFA24C088;
+       Mon, 15 Nov 2010 11:07:40 -0800 (PST)
+In-Reply-To: <1f3e1f7e454f3c62b66fc5f3e1e1ed90d62b7fb0.1289789605.git.joe@perches.com>
+X-Mailer: Mew version 6.3 on Emacs 23.1 / Mule 6.0 (HANACHIRUSATO)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062687>
+
+From: Joe Perches <joe@perches.com>
+Date: Sun, 14 Nov 2010 19:05:00 -0800
+
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Applied.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002663:2, b/test/corpora/lkml/cur/1382298793.002663:2,
new file mode 100644 (file)
index 0000000..c888922
--- /dev/null
@@ -0,0 +1,50 @@
+From: David Miller <davem@davemloft.net>
+Subject: Re: [PATCH 15/44] drivers/net/vxge: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:07:55 -0800 (PST)
+Lines: 6
+Message-ID: <20101115.110755.98889745.davem@davemloft.net>
+References: <cover.1289789604.git.joe@perches.com>
+       <e86e79a18106cc38715136bfb2e880b38f5ac764.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: Text/Plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Cc: trivial@kernel.org, ramkrishna.vepa@exar.com,
+       sivakumar.subramani@exar.com, sreenivasa.honnur@exar.com,
+       jon.mason@exar.com, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: joe@perches.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 20:07:42 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI4P0-0002G6-7S
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 20:07:42 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933487Ab0KOTHe (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 14:07:34 -0500
+Received: from 74-93-104-97-Washington.hfc.comcastbusiness.net ([74.93.104.97]:51805
+       "EHLO sunset.davemloft.net" rhost-flags-OK-OK-OK-OK)
+       by vger.kernel.org with ESMTP id S933109Ab0KOTHb (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 14:07:31 -0500
+Received: from localhost (localhost [127.0.0.1])
+       by sunset.davemloft.net (Postfix) with ESMTP id 185A124C08A;
+       Mon, 15 Nov 2010 11:07:56 -0800 (PST)
+In-Reply-To: <e86e79a18106cc38715136bfb2e880b38f5ac764.1289789604.git.joe@perches.com>
+X-Mailer: Mew version 6.3 on Emacs 23.1 / Mule 6.0 (HANACHIRUSATO)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062688>
+
+From: Joe Perches <joe@perches.com>
+Date: Sun, 14 Nov 2010 19:04:34 -0800
+
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Not applicable to net-next-2.6
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002664:2, b/test/corpora/lkml/cur/1382298793.002664:2,
new file mode 100644 (file)
index 0000000..ec584cb
--- /dev/null
@@ -0,0 +1,49 @@
+From: David Miller <davem@davemloft.net>
+Subject: Re: [PATCH 38/44] include/linux/if_macvlan.h: Remove unnecessary
+ semicolons
+Date: Mon, 15 Nov 2010 11:07:46 -0800 (PST)
+Lines: 6
+Message-ID: <20101115.110746.241931394.davem@davemloft.net>
+References: <cover.1289789604.git.joe@perches.com>
+       <186ca914f887b2354ea3178696edc81cacbb28c6.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: Text/Plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Cc: trivial@kernel.org, kaber@trash.net, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: joe@perches.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 20:07:42 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI4Oz-0002G6-MR
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 20:07:42 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933465Ab0KOTHZ (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 14:07:25 -0500
+Received: from 74-93-104-97-Washington.hfc.comcastbusiness.net ([74.93.104.97]:51798
+       "EHLO sunset.davemloft.net" rhost-flags-OK-OK-OK-OK)
+       by vger.kernel.org with ESMTP id S933109Ab0KOTHV (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 14:07:21 -0500
+Received: from localhost (localhost [127.0.0.1])
+       by sunset.davemloft.net (Postfix) with ESMTP id 665FB24C08A;
+       Mon, 15 Nov 2010 11:07:46 -0800 (PST)
+In-Reply-To: <186ca914f887b2354ea3178696edc81cacbb28c6.1289789605.git.joe@perches.com>
+X-Mailer: Mew version 6.3 on Emacs 23.1 / Mule 6.0 (HANACHIRUSATO)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062689>
+
+From: Joe Perches <joe@perches.com>
+Date: Sun, 14 Nov 2010 19:04:57 -0800
+
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Applied.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002665:2, b/test/corpora/lkml/cur/1382298793.002665:2,
new file mode 100644 (file)
index 0000000..7af81e8
--- /dev/null
@@ -0,0 +1,98 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary
+       semicolons
+Date: Mon, 15 Nov 2010 19:07:38 +0000
+Lines: 22
+Message-ID: <20101115190738.GF3338@sirena.org.uk>
+References: <cover.1289789604.git.joe@perches.com>
+       <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+       <20101115134939.GC12986@rakim.wolfsonmicro.main>
+       <1289840957.16461.138.camel@Joe-Laptop>
+       <20101115173031.GI12986@rakim.wolfsonmicro.main>
+       <1289842444.16461.140.camel@Joe-Laptop>
+       <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+       <1289845830.16461.149.camel@Joe-Laptop>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       Takashi Iwai <tiwai@suse.de>, linux-kernel@vger.kernel.org,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Mon Nov 15 20:07:53 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PI4PA-0002MQ-AO
+       for glad-alsa-devel-2@m.gmane.org; Mon, 15 Nov 2010 20:07:52 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 78DB02453D; Mon, 15 Nov 2010 20:07:51 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 07D122453A;
+       Mon, 15 Nov 2010 20:07:47 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 4AB082453B; Mon, 15 Nov 2010 20:07:45 +0100 (CET)
+Received: from cassiel.sirena.org.uk (cassiel.sirena.org.uk [80.68.93.111])
+       by alsa0.perex.cz (Postfix) with ESMTP id D864524538
+       for <alsa-devel@alsa-project.org>; Mon, 15 Nov 2010 20:07:44 +0100 (CET)
+Received: from broonie by cassiel.sirena.org.uk with local (Exim 4.69)
+       (envelope-from <broonie@sirena.org.uk>)
+       id 1PI4Ow-0007qS-V5; Mon, 15 Nov 2010 19:07:38 +0000
+Content-Disposition: inline
+In-Reply-To: <1289845830.16461.149.camel@Joe-Laptop>
+X-Cookie: Who messed with my anti-paranoia shot?
+User-Agent: Mutt/1.5.18 (2008-05-17)
+X-SA-Exim-Connect-IP: <locally generated>
+X-SA-Exim-Mail-From: broonie@sirena.org.uk
+X-SA-Exim-Scanned: No (on cassiel.sirena.org.uk);
+       SAEximRunCond expanded to false
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062690>
+
+On Mon, Nov 15, 2010 at 10:30:29AM -0800, Joe Perches wrote:
+> On Mon, 2010-11-15 at 18:27 +0000, Mark Brown wrote:
+> > On Mon, Nov 15, 2010 at 09:34:04AM -0800, Joe Perches wrote:
+
+> > > I think it's more important to use consistent changelogs
+> > > for a patch series.
+
+> > ...since...?
+
+> 1995...
+
+That's not really a reason.  It seems that...
+
+> Since there isn't a consistent standard for subsystems
+> changelogs and automating scripts for the desires of
+> individual subsystem maintainers is not feasible.
+
+...you mean that you wish to do this since it makes your life as a
+script author easier.  I'd suggest using pattern matching to look up the
+rules for generating the prefixes (it's pretty much entirely prefixes)
+in the same way you're handling figuring out who to mail - that'd
+probably cover it in an automatable fashion.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002666:2, b/test/corpora/lkml/cur/1382298793.002666:2,
new file mode 100644 (file)
index 0000000..8907c25
--- /dev/null
@@ -0,0 +1,87 @@
+From: David Miller <davem@davemloft.net>
+Subject: Re: [PATCH 13/44] drivers/net/e1000e: Remove
+       unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:08:27 -0800 (PST)
+Lines: 19
+Message-ID: <20101115.110827.58428696.davem@davemloft.net>
+References: <cover.1289789604.git.joe@perches.com>
+       <e5cf92d50de7924930d660a5865c3d60d9cd9dc5.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: trivial@kernel.org, e1000-devel@lists.sourceforge.net,
+       bruce.w.allan@intel.com, jesse.brandeburg@intel.com,
+       linux-kernel@vger.kernel.org, gregory.v.rose@intel.com,
+       john.ronciak@intel.com, jeffrey.t.kirsher@intel.com, netdev@vger.kernel.org
+To: joe@perches.com
+X-From: e1000-devel-bounces@lists.sourceforge.net Mon Nov 15 20:08:15 2010
+Return-path: <e1000-devel-bounces@lists.sourceforge.net>
+Envelope-to: glded-e1000-devel@m.gmane.org
+Received: from lists.sourceforge.net ([216.34.181.88])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <e1000-devel-bounces@lists.sourceforge.net>)
+       id 1PI4PW-0002W0-GR
+       for glded-e1000-devel@m.gmane.org; Mon, 15 Nov 2010 20:08:14 +0100
+Received: from localhost ([127.0.0.1] helo=sfs-ml-2.v29.ch3.sourceforge.com)
+       by sfs-ml-2.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <e1000-devel-bounces@lists.sourceforge.net>)
+       id 1PI4PV-0002fQ-W8; Mon, 15 Nov 2010 19:08:13 +0000
+Received: from sog-mx-3.v43.ch3.sourceforge.com ([172.29.43.193]
+       helo=mx.sourceforge.net)
+       by sfs-ml-2.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <davem@davemloft.net>) id 1PI4PU-0002fJ-Ct
+       for e1000-devel@lists.sourceforge.net; Mon, 15 Nov 2010 19:08:12 +0000
+X-ACL-Warn: 
+Received: from 74-93-104-97-washington.hfc.comcastbusiness.net ([74.93.104.97]
+       helo=sunset.davemloft.net)
+       by sog-mx-3.v43.ch3.sourceforge.com with esmtp (Exim 4.69)
+       id 1PI4PQ-0006vK-9a
+       for e1000-devel@lists.sourceforge.net; Mon, 15 Nov 2010 19:08:12 +0000
+Received: from localhost (localhost [127.0.0.1])
+       by sunset.davemloft.net (Postfix) with ESMTP id 0BCD224C088;
+       Mon, 15 Nov 2010 11:08:28 -0800 (PST)
+In-Reply-To: <e5cf92d50de7924930d660a5865c3d60d9cd9dc5.1289789604.git.joe@perches.com>
+X-Mailer: Mew version 6.3 on Emacs 23.1 / Mule 6.0 (HANACHIRUSATO)
+X-Spam-Score: 0.8 (/)
+X-Spam-Report: Spam Filtering performed by mx.sourceforge.net.
+       See http://spamassassin.org/tag/ for more details.
+       1.0 RDNS_DYNAMIC           Delivered to internal network by host with
+       dynamic-looking rDNS
+       -0.2 AWL AWL: From: address is in the auto white-list
+X-Headers-End: 1PI4PQ-0006vK-9a
+X-BeenThere: e1000-devel@lists.sourceforge.net
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "e100/e1000\(e\)/ixgb/igb/ixgbe development and discussion"
+       <e1000-devel.lists.sourceforge.net>
+List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/e1000-devel>, 
+       <mailto:e1000-devel-request@lists.sourceforge.net?subject=unsubscribe>
+List-Archive: <http://sourceforge.net/mailarchive/forum.php?forum_name=e1000-devel>
+List-Post: <mailto:e1000-devel@lists.sourceforge.net>
+List-Help: <mailto:e1000-devel-request@lists.sourceforge.net?subject=help>
+List-Subscribe: <https://lists.sourceforge.net/lists/listinfo/e1000-devel>,
+       <mailto:e1000-devel-request@lists.sourceforge.net?subject=subscribe>
+Errors-To: e1000-devel-bounces@lists.sourceforge.net
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062691>
+
+From: Joe Perches <joe@perches.com>
+Date: Sun, 14 Nov 2010 19:04:32 -0800
+
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Applied.
+
+------------------------------------------------------------------------------
+Centralized Desktop Delivery: Dell and VMware Reference Architecture
+Simplifying enterprise desktop deployment and management using
+Dell EqualLogic storage and VMware View: A highly scalable, end-to-end
+client virtualization framework. Read more!
+http://p.sf.net/sfu/dell-eql-dev2dev
+_______________________________________________
+E1000-devel mailing list
+E1000-devel@lists.sourceforge.net
+https://lists.sourceforge.net/lists/listinfo/e1000-devel
+To learn more about Intel&#174; Ethernet, visit http://communities.intel.com/community/wired
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002667:2, b/test/corpora/lkml/cur/1382298793.002667:2,
new file mode 100644 (file)
index 0000000..c85cbd5
--- /dev/null
@@ -0,0 +1,87 @@
+From: David Miller <davem@davemloft.net>
+Subject: Re: [PATCH 14/44] drivers/net/ixgbe: Remove
+       unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:08:21 -0800 (PST)
+Lines: 19
+Message-ID: <20101115.110821.13743893.davem@davemloft.net>
+References: <cover.1289789604.git.joe@perches.com>
+       <7d2c334daa75c5221946a17d45c9de1901cf06e7.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: trivial@kernel.org, e1000-devel@lists.sourceforge.net,
+       bruce.w.allan@intel.com, jesse.brandeburg@intel.com,
+       linux-kernel@vger.kernel.org, gregory.v.rose@intel.com,
+       john.ronciak@intel.com, jeffrey.t.kirsher@intel.com, netdev@vger.kernel.org
+To: joe@perches.com
+X-From: e1000-devel-bounces@lists.sourceforge.net Mon Nov 15 20:08:15 2010
+Return-path: <e1000-devel-bounces@lists.sourceforge.net>
+Envelope-to: glded-e1000-devel@m.gmane.org
+Received: from lists.sourceforge.net ([216.34.181.88])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <e1000-devel-bounces@lists.sourceforge.net>)
+       id 1PI4PW-0002Vz-9H
+       for glded-e1000-devel@m.gmane.org; Mon, 15 Nov 2010 20:08:14 +0100
+Received: from localhost ([127.0.0.1] helo=sfs-ml-4.v29.ch3.sourceforge.com)
+       by sfs-ml-4.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <e1000-devel-bounces@lists.sourceforge.net>)
+       id 1PI4PQ-0008VG-9t; Mon, 15 Nov 2010 19:08:08 +0000
+Received: from sog-mx-1.v43.ch3.sourceforge.com ([172.29.43.191]
+       helo=mx.sourceforge.net)
+       by sfs-ml-4.v29.ch3.sourceforge.com with esmtp (Exim 4.69)
+       (envelope-from <davem@davemloft.net>) id 1PI4PO-0008V9-I9
+       for e1000-devel@lists.sourceforge.net; Mon, 15 Nov 2010 19:08:06 +0000
+X-ACL-Warn: 
+Received: from 74-93-104-97-washington.hfc.comcastbusiness.net ([74.93.104.97]
+       helo=sunset.davemloft.net)
+       by sog-mx-1.v43.ch3.sourceforge.com with esmtp (Exim 4.69)
+       id 1PI4PK-0004Ab-D0
+       for e1000-devel@lists.sourceforge.net; Mon, 15 Nov 2010 19:08:06 +0000
+Received: from localhost (localhost [127.0.0.1])
+       by sunset.davemloft.net (Postfix) with ESMTP id E0AE124C08A;
+       Mon, 15 Nov 2010 11:08:21 -0800 (PST)
+In-Reply-To: <7d2c334daa75c5221946a17d45c9de1901cf06e7.1289789604.git.joe@perches.com>
+X-Mailer: Mew version 6.3 on Emacs 23.1 / Mule 6.0 (HANACHIRUSATO)
+X-Spam-Score: 0.7 (/)
+X-Spam-Report: Spam Filtering performed by mx.sourceforge.net.
+       See http://spamassassin.org/tag/ for more details.
+       1.0 RDNS_DYNAMIC           Delivered to internal network by host with
+       dynamic-looking rDNS
+       -0.3 AWL AWL: From: address is in the auto white-list
+X-Headers-End: 1PI4PK-0004Ab-D0
+X-BeenThere: e1000-devel@lists.sourceforge.net
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "e100/e1000\(e\)/ixgb/igb/ixgbe development and discussion"
+       <e1000-devel.lists.sourceforge.net>
+List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/e1000-devel>, 
+       <mailto:e1000-devel-request@lists.sourceforge.net?subject=unsubscribe>
+List-Archive: <http://sourceforge.net/mailarchive/forum.php?forum_name=e1000-devel>
+List-Post: <mailto:e1000-devel@lists.sourceforge.net>
+List-Help: <mailto:e1000-devel-request@lists.sourceforge.net?subject=help>
+List-Subscribe: <https://lists.sourceforge.net/lists/listinfo/e1000-devel>,
+       <mailto:e1000-devel-request@lists.sourceforge.net?subject=subscribe>
+Errors-To: e1000-devel-bounces@lists.sourceforge.net
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062692>
+
+From: Joe Perches <joe@perches.com>
+Date: Sun, 14 Nov 2010 19:04:33 -0800
+
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Applied.
+
+------------------------------------------------------------------------------
+Centralized Desktop Delivery: Dell and VMware Reference Architecture
+Simplifying enterprise desktop deployment and management using
+Dell EqualLogic storage and VMware View: A highly scalable, end-to-end
+client virtualization framework. Read more!
+http://p.sf.net/sfu/dell-eql-dev2dev
+_______________________________________________
+E1000-devel mailing list
+E1000-devel@lists.sourceforge.net
+https://lists.sourceforge.net/lists/listinfo/e1000-devel
+To learn more about Intel&#174; Ethernet, visit http://communities.intel.com/community/wired
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002668:2, b/test/corpora/lkml/cur/1382298793.002668:2,
new file mode 100644 (file)
index 0000000..a453744
--- /dev/null
@@ -0,0 +1,50 @@
+From: David Miller <davem@davemloft.net>
+Subject: Re: [PATCH 21/44] drivers/s390/net: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:08:10 -0800 (PST)
+Lines: 6
+Message-ID: <20101115.110810.241442235.davem@davemloft.net>
+References: <cover.1289789604.git.joe@perches.com>
+       <ea09773876fb36a52a9a750110b381d20767ac12.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: Text/Plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Cc: trivial@kernel.org, ursula.braun@de.ibm.com,
+       blaschka@linux.vnet.ibm.com, linux390@de.ibm.com,
+       schwidefsky@de.ibm.com, heiko.carstens@de.ibm.com,
+       linux-s390@vger.kernel.org, linux-kernel@vger.kernel.org
+To: joe@perches.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 20:09:11 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI4QQ-00030Y-IU
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 20:09:10 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933525Ab0KOTHt (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 14:07:49 -0500
+Received: from 74-93-104-97-Washington.hfc.comcastbusiness.net ([74.93.104.97]:51810
+       "EHLO sunset.davemloft.net" rhost-flags-OK-OK-OK-OK)
+       by vger.kernel.org with ESMTP id S933071Ab0KOTHp (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 14:07:45 -0500
+Received: from localhost (localhost [127.0.0.1])
+       by sunset.davemloft.net (Postfix) with ESMTP id 79DC124C088;
+       Mon, 15 Nov 2010 11:08:10 -0800 (PST)
+In-Reply-To: <ea09773876fb36a52a9a750110b381d20767ac12.1289789605.git.joe@perches.com>
+X-Mailer: Mew version 6.3 on Emacs 23.1 / Mule 6.0 (HANACHIRUSATO)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062693>
+
+From: Joe Perches <joe@perches.com>
+Date: Sun, 14 Nov 2010 19:04:40 -0800
+
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+I'll let the s390 folks take this one.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002669:2, b/test/corpora/lkml/cur/1382298793.002669:2,
new file mode 100644 (file)
index 0000000..f595d0a
--- /dev/null
@@ -0,0 +1,48 @@
+From: David Miller <davem@davemloft.net>
+Subject: Re: [PATCH 18/44] drivers/net/cnic.c: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:08:15 -0800 (PST)
+Lines: 6
+Message-ID: <20101115.110815.52192986.davem@davemloft.net>
+References: <cover.1289789604.git.joe@perches.com>
+       <950331e47b16c2ad28d73502f30f5a0f017b5493.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: Text/Plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Cc: trivial@kernel.org, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: joe@perches.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 20:09:11 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI4QR-00030Y-3C
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 20:09:11 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933539Ab0KOTHy (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 14:07:54 -0500
+Received: from 74-93-104-97-Washington.hfc.comcastbusiness.net ([74.93.104.97]:51817
+       "EHLO sunset.davemloft.net" rhost-flags-OK-OK-OK-OK)
+       by vger.kernel.org with ESMTP id S933054Ab0KOTHv (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 14:07:51 -0500
+Received: from localhost (localhost [127.0.0.1])
+       by sunset.davemloft.net (Postfix) with ESMTP id 0297924C08A;
+       Mon, 15 Nov 2010 11:08:16 -0800 (PST)
+In-Reply-To: <950331e47b16c2ad28d73502f30f5a0f017b5493.1289789604.git.joe@perches.com>
+X-Mailer: Mew version 6.3 on Emacs 23.1 / Mule 6.0 (HANACHIRUSATO)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062694>
+
+From: Joe Perches <joe@perches.com>
+Date: Sun, 14 Nov 2010 19:04:37 -0800
+
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Applied.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002670:2, b/test/corpora/lkml/cur/1382298793.002670:2,
new file mode 100644 (file)
index 0000000..964ba85
--- /dev/null
@@ -0,0 +1,52 @@
+From: David Miller <davem@davemloft.net>
+Subject: Re: [PATCH 07/44] drivers/isdn: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:08:40 -0800 (PST)
+Lines: 11
+Message-ID: <20101115.110840.45901337.davem@davemloft.net>
+References: <cover.1289789604.git.joe@perches.com>
+       <c7a38f65340aafb208d50fc3a781602c07aebb0c.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: Text/Plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Cc: trivial@kernel.org, isdn@linux-pingi.de, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: joe@perches.com
+X-From: netdev-owner@vger.kernel.org Mon Nov 15 20:09:14 2010
+Return-path: <netdev-owner@vger.kernel.org>
+Envelope-to: linux-netdev-2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <netdev-owner@vger.kernel.org>)
+       id 1PI4QU-00030Y-5P
+       for linux-netdev-2@lo.gmane.org; Mon, 15 Nov 2010 20:09:14 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933584Ab0KOTIR (ORCPT <rfc822;linux-netdev-2@m.gmane.org>);
+       Mon, 15 Nov 2010 14:08:17 -0500
+Received: from 74-93-104-97-Washington.hfc.comcastbusiness.net ([74.93.104.97]:51842
+       "EHLO sunset.davemloft.net" rhost-flags-OK-OK-OK-OK)
+       by vger.kernel.org with ESMTP id S933494Ab0KOTIP (ORCPT
+       <rfc822;netdev@vger.kernel.org>); Mon, 15 Nov 2010 14:08:15 -0500
+Received: from localhost (localhost [127.0.0.1])
+       by sunset.davemloft.net (Postfix) with ESMTP id 88D8924C088;
+       Mon, 15 Nov 2010 11:08:40 -0800 (PST)
+In-Reply-To: <c7a38f65340aafb208d50fc3a781602c07aebb0c.1289789604.git.joe@perches.com>
+X-Mailer: Mew version 6.3 on Emacs 23.1 / Mule 6.0 (HANACHIRUSATO)
+Sender: netdev-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <netdev.vger.kernel.org>
+X-Mailing-List: netdev@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062695>
+
+From: Joe Perches <joe@perches.com>
+Date: Sun, 14 Nov 2010 19:04:26 -0800
+
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Applied.
+--
+To unsubscribe from this list: send the line "unsubscribe netdev" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002671:2, b/test/corpora/lkml/cur/1382298793.002671:2,
new file mode 100644 (file)
index 0000000..1fe0cc5
--- /dev/null
@@ -0,0 +1,48 @@
+From: David Miller <davem@davemloft.net>
+Subject: Re: [PATCH 12/44] drivers/net/bnx2x: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:08:34 -0800 (PST)
+Lines: 6
+Message-ID: <20101115.110834.91340564.davem@davemloft.net>
+References: <cover.1289789604.git.joe@perches.com>
+       <2bfaf1f1fe5d503a8a386a433b5187997819d771.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: Text/Plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+Cc: trivial@kernel.org, eilong@broadcom.com, netdev@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: joe@perches.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 20:09:14 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI4QT-00030Y-LK
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 20:09:13 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933575Ab0KOTIN (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 14:08:13 -0500
+Received: from 74-93-104-97-Washington.hfc.comcastbusiness.net ([74.93.104.97]:51836
+       "EHLO sunset.davemloft.net" rhost-flags-OK-OK-OK-OK)
+       by vger.kernel.org with ESMTP id S933449Ab0KOTIJ (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 14:08:09 -0500
+Received: from localhost (localhost [127.0.0.1])
+       by sunset.davemloft.net (Postfix) with ESMTP id 8FAC824C08A;
+       Mon, 15 Nov 2010 11:08:34 -0800 (PST)
+In-Reply-To: <2bfaf1f1fe5d503a8a386a433b5187997819d771.1289789604.git.joe@perches.com>
+X-Mailer: Mew version 6.3 on Emacs 23.1 / Mule 6.0 (HANACHIRUSATO)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062696>
+
+From: Joe Perches <joe@perches.com>
+Date: Sun, 14 Nov 2010 19:04:31 -0800
+
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Applied.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002679:2, b/test/corpora/lkml/cur/1382298793.002679:2,
new file mode 100644 (file)
index 0000000..8e1234b
--- /dev/null
@@ -0,0 +1,61 @@
+From: Joe Perches <joe@perches.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:14:18 -0800
+Lines: 9
+Message-ID: <1289848458.16461.150.camel@Joe-Laptop>
+References: <cover.1289789604.git.joe@perches.com>
+        <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+        <20101115134939.GC12986@rakim.wolfsonmicro.main>
+        <1289840957.16461.138.camel@Joe-Laptop>
+        <20101115173031.GI12986@rakim.wolfsonmicro.main>
+        <1289842444.16461.140.camel@Joe-Laptop>
+        <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+        <1289845830.16461.149.camel@Joe-Laptop>
+        <20101115190738.GF3338@sirena.org.uk>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+Cc: Jiri Kosina <trivial@kernel.org>,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>,
+       Jaroslav Kysela <perex@perex.cz>, Takashi Iwai <tiwai@suse.de>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 20:14:49 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI4Vp-0006HI-NH
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 20:14:46 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933566Ab0KOTOW (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 14:14:22 -0500
+Received: from mail.perches.com ([173.55.12.10]:1319 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S933505Ab0KOTOV (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 14:14:21 -0500
+Received: from [192.168.1.162] (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 4087C24368;
+       Mon, 15 Nov 2010 11:12:31 -0800 (PST)
+In-Reply-To: <20101115190738.GF3338@sirena.org.uk>
+X-Mailer: Evolution 2.30.3 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062704>
+
+On Mon, 2010-11-15 at 19:07 +0000, Mark Brown wrote:
+> I'd suggest using pattern matching to look up the
+> rules for generating the prefixes (it's pretty much entirely prefixes)
+> in the same way you're handling figuring out who to mail - that'd
+> probably cover it in an automatable fashion.
+
+Publish a tool that works and I'll use it.
+
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002688:2, b/test/corpora/lkml/cur/1382298793.002688:2,
new file mode 100644 (file)
index 0000000..7d573a0
--- /dev/null
@@ -0,0 +1,94 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary
+       semicolons
+Date: Mon, 15 Nov 2010 19:34:07 +0000
+Lines: 16
+Message-ID: <20101115193407.GK12986@rakim.wolfsonmicro.main>
+References: <cover.1289789604.git.joe@perches.com>
+       <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+       <20101115134939.GC12986@rakim.wolfsonmicro.main>
+       <1289840957.16461.138.camel@Joe-Laptop>
+       <20101115173031.GI12986@rakim.wolfsonmicro.main>
+       <1289842444.16461.140.camel@Joe-Laptop>
+       <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+       <1289845830.16461.149.camel@Joe-Laptop>
+       <20101115190738.GF3338@sirena.org.uk>
+       <1289848458.16461.150.camel@Joe-Laptop>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       Takashi Iwai <tiwai@suse.de>, linux-kernel@vger.kernel.org,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Mon Nov 15 20:34:23 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PI4oo-0008LE-En
+       for glad-alsa-devel-2@m.gmane.org; Mon, 15 Nov 2010 20:34:22 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 9B9BF24551; Mon, 15 Nov 2010 20:34:21 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 7F6F424547;
+       Mon, 15 Nov 2010 20:34:16 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 4D4F724548; Mon, 15 Nov 2010 20:34:14 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id 9C6502453F
+       for <alsa-devel@alsa-project.org>; Mon, 15 Nov 2010 20:34:09 +0100 (CET)
+Received: from rakim.wolfsonmicro.main (lumison.wolfsonmicro.com
+       [87.246.78.27])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id 992AA788028; 
+       Mon, 15 Nov 2010 19:34:08 +0000 (GMT)
+Received: from broonie by rakim.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@rakim.wolfsonmicro.main>)
+       id 1PI4oZ-0005Qk-Q4; Mon, 15 Nov 2010 19:34:07 +0000
+Content-Disposition: inline
+In-Reply-To: <1289848458.16461.150.camel@Joe-Laptop>
+X-Cookie: I like your SNOOPY POSTER!!
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062713>
+
+On Mon, Nov 15, 2010 at 11:14:18AM -0800, Joe Perches wrote:
+> On Mon, 2010-11-15 at 19:07 +0000, Mark Brown wrote:
+
+> > I'd suggest using pattern matching to look up the
+> > rules for generating the prefixes (it's pretty much entirely prefixes)
+> > in the same way you're handling figuring out who to mail - that'd
+> > probably cover it in an automatable fashion.
+
+> Publish a tool that works and I'll use it.
+
+It appears your scripts are already hooked into get_maintainers.pl which
+would seem the obvious place to do this?  Sadly I don't do perl, though
+it looks like you're doing pretty much all the work on that anyway.
+
+The main thing here is to avoid your patches sticking out - as well as
+the hassle applying them stuff like this is also a red flag on review.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.002699:2, b/test/corpora/lkml/cur/1382298793.002699:2,
new file mode 100644 (file)
index 0000000..3fdfaf1
--- /dev/null
@@ -0,0 +1,106 @@
+From: Joe Perches <joe@perches.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary semicolons
+Date: Mon, 15 Nov 2010 11:52:53 -0800
+Lines: 52
+Message-ID: <1289850773.16461.166.camel@Joe-Laptop>
+References: <cover.1289789604.git.joe@perches.com>
+        <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+        <20101115134939.GC12986@rakim.wolfsonmicro.main>
+        <1289840957.16461.138.camel@Joe-Laptop>
+        <20101115173031.GI12986@rakim.wolfsonmicro.main>
+        <1289842444.16461.140.camel@Joe-Laptop>
+        <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+        <1289845830.16461.149.camel@Joe-Laptop>
+        <20101115190738.GF3338@sirena.org.uk>
+        <1289848458.16461.150.camel@Joe-Laptop>
+        <20101115193407.GK12986@rakim.wolfsonmicro.main>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+Cc: Jiri Kosina <trivial@kernel.org>,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>,
+       Jaroslav Kysela <perex@perex.cz>, Takashi Iwai <tiwai@suse.de>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: linux-kernel-owner@vger.kernel.org Mon Nov 15 20:53:21 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PI57A-0001v9-CG
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 15 Nov 2010 20:53:20 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932871Ab0KOTw5 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 15 Nov 2010 14:52:57 -0500
+Received: from mail.perches.com ([173.55.12.10]:1328 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1758222Ab0KOTw4 (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 15 Nov 2010 14:52:56 -0500
+Received: from [192.168.1.162] (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id CE13524368;
+       Mon, 15 Nov 2010 11:51:05 -0800 (PST)
+In-Reply-To: <20101115193407.GK12986@rakim.wolfsonmicro.main>
+X-Mailer: Evolution 2.30.3 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1062724>
+
+On Mon, 2010-11-15 at 19:34 +0000, Mark Brown wrote:
+> On Mon, Nov 15, 2010 at 11:14:18AM -0800, Joe Perches wrote:
+> > On Mon, 2010-11-15 at 19:07 +0000, Mark Brown wrote:
+> > > I'd suggest using pattern matching to look up the
+> > > rules for generating the prefixes (it's pretty much entirely prefixes)
+> > > in the same way you're handling figuring out who to mail - that'd
+> > > probably cover it in an automatable fashion.
+> > Publish a tool that works and I'll use it.
+> It appears your scripts are already hooked into get_maintainers.pl which
+> would seem the obvious place to do this?  Sadly I don't do perl, though
+> it looks like you're doing pretty much all the work on that anyway.
+
+Sadly, no it's not the right place.
+
+That script just generates cc email addresses
+for pre-formatted commit patches.
+
+It'd have to be a script that modifies the git commit subject line
+to the taste of the subsystem maintainer.
+
+Right now, I use a commit script that's something like:
+
+#!/bin/bash
+echo "$1: Remove unnecessary semicolons" > msg
+echo >> msg
+#cat >> msg <<EOF
+#Unnecessary semicolons should not exist.
+#EOF
+git commit -s -F msg $1
+
+There could be a modification to $1 (path)
+or some such.
+
+Maybe a script like
+./scripts/convert_commit_subject_to_subsystem_maintainer_taste
+or something.
+
+Care to write one in sh/bash/perl/python/c/ocaml/c#?
+
+As far as I know, the only subsystem pedants^H^H^H^H^Hople
+that care much about the commit subject style are
+arch/x86 and sound.
+
+I can understand the desire of these subsystem maintainers
+to have a consistent style.  I think though that requiring
+a subject header style without providing more than a
+general guideline is a but much.
+
+I'd use any other automated tool you want to provide.
+
+cheers, Joe
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003013:2, b/test/corpora/lkml/cur/1382298793.003013:2,
new file mode 100644 (file)
index 0000000..945ce7c
--- /dev/null
@@ -0,0 +1,128 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary
+       semicolons
+Date: Tue, 16 Nov 2010 10:49:22 +0000
+Lines: 50
+Message-ID: <20101116104921.GL12986@rakim.wolfsonmicro.main>
+References: <20101115134939.GC12986@rakim.wolfsonmicro.main>
+       <1289840957.16461.138.camel@Joe-Laptop>
+       <20101115173031.GI12986@rakim.wolfsonmicro.main>
+       <1289842444.16461.140.camel@Joe-Laptop>
+       <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+       <1289845830.16461.149.camel@Joe-Laptop>
+       <20101115190738.GF3338@sirena.org.uk>
+       <1289848458.16461.150.camel@Joe-Laptop>
+       <20101115193407.GK12986@rakim.wolfsonmicro.main>
+       <1289850773.16461.166.camel@Joe-Laptop>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       Takashi Iwai <tiwai@suse.de>, linux-kernel@vger.kernel.org,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Tue Nov 16 11:49:29 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PIJ6O-0003Hl-Gx
+       for glad-alsa-devel-2@m.gmane.org; Tue, 16 Nov 2010 11:49:28 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id C3B89243EB; Tue, 16 Nov 2010 11:49:27 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 5204E243EB;
+       Tue, 16 Nov 2010 11:49:26 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id D1E2E243EC; Tue, 16 Nov 2010 11:49:24 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id 16268243EA
+       for <alsa-devel@alsa-project.org>; Tue, 16 Nov 2010 11:49:24 +0100 (CET)
+Received: from rakim.wolfsonmicro.main (lumison.wolfsonmicro.com
+       [87.246.78.27])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id 4AC4E3B438A; 
+       Tue, 16 Nov 2010 10:49:23 +0000 (GMT)
+Received: from broonie by rakim.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@rakim.wolfsonmicro.main>)
+       id 1PIJ6I-0001Kz-Cf; Tue, 16 Nov 2010 10:49:22 +0000
+Content-Disposition: inline
+In-Reply-To: <1289850773.16461.166.camel@Joe-Laptop>
+X-Cookie: I like your SNOOPY POSTER!!
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063040>
+
+On Mon, Nov 15, 2010 at 11:52:53AM -0800, Joe Perches wrote:
+> On Mon, 2010-11-15 at 19:34 +0000, Mark Brown wrote:
+
+> > It appears your scripts are already hooked into get_maintainers.pl which
+> > would seem the obvious place to do this?  Sadly I don't do perl, though
+> > it looks like you're doing pretty much all the work on that anyway.
+
+> Sadly, no it's not the right place.
+
+To query MAINTAINERS?  I'd assume that's where you'd want to put that
+stuff?
+
+> There could be a modification to $1 (path)
+> or some such.
+> 
+> Maybe a script like
+> ./scripts/convert_commit_subject_to_subsystem_maintainer_taste
+> or something.
+
+> Care to write one in sh/bash/perl/python/c/ocaml/c#?
+
+Like I say I'd expect this to be a get_maintainers based lookup to dump
+some data out?
+
+> As far as I know, the only subsystem pedants^H^H^H^H^Hople
+> that care much about the commit subject style are
+> arch/x86 and sound.
+
+If you look at the kernel you'll see quite a few subsystems which have
+some sort of standard practice which they do try to enforce, you
+shouldn't take silence as people being happy here - it's taken me some
+considerable time to get round to mentioning this, for example, and I
+might not have bothered if the patch had applied first time around.
+Like working against -next it's one of these things that would make your
+patches easier to deal with.
+
+> I can understand the desire of these subsystem maintainers
+> to have a consistent style.  I think though that requiring
+> a subject header style without providing more than a
+> general guideline is a but much.
+
+The general guideline I tend to go with is that if what you're doing
+looks odd for the code you're submitting against for some reason you're
+doing something wrong unless you understand why you're doing something
+different and there's a good reason.
+
+> I'd use any other automated tool you want to provide.
+
+Like I say, I'd expect the lookup from the database to be handled by
+get_maintainers.pl.  Having a separate database would seem odd.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003145:2, b/test/corpora/lkml/cur/1382298793.003145:2,
new file mode 100644 (file)
index 0000000..7418171
--- /dev/null
@@ -0,0 +1,89 @@
+From: Joe Perches <joe@perches.com>
+Subject: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Tue, 16 Nov 2010 06:51:17 -0800
+Lines: 36
+Message-ID: <1289919077.28741.50.camel@Joe-Laptop>
+References: <20101115134939.GC12986@rakim.wolfsonmicro.main>
+        <1289840957.16461.138.camel@Joe-Laptop>
+        <20101115173031.GI12986@rakim.wolfsonmicro.main>
+        <1289842444.16461.140.camel@Joe-Laptop>
+        <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+        <1289845830.16461.149.camel@Joe-Laptop>
+        <20101115190738.GF3338@sirena.org.uk>
+        <1289848458.16461.150.camel@Joe-Laptop>
+        <20101115193407.GK12986@rakim.wolfsonmicro.main>
+        <1289850773.16461.166.camel@Joe-Laptop>
+        <20101116104921.GL12986@rakim.wolfsonmicro.main>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+Cc: Jiri Kosina <trivial@kernel.org>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       Florian Mickler <florian@mickler.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Nov 16 15:51:41 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PIMsn-0003tR-Ee
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 16 Nov 2010 15:51:41 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1758690Ab0KPOvV (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 16 Nov 2010 09:51:21 -0500
+Received: from mail.perches.com ([173.55.12.10]:1433 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1755918Ab0KPOvU (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 16 Nov 2010 09:51:20 -0500
+Received: from [192.168.1.162] (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 38D5C24368;
+       Tue, 16 Nov 2010 06:49:10 -0800 (PST)
+In-Reply-To: <20101116104921.GL12986@rakim.wolfsonmicro.main>
+X-Mailer: Evolution 2.30.3 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063175>
+
+On Tue, 2010-11-16 at 10:49 +0000, Mark Brown wrote:
+> On Mon, Nov 15, 2010 at 11:52:53AM -0800, Joe Perches wrote:
+> > On Mon, 2010-11-15 at 19:34 +0000, Mark Brown wrote:
+> > > It appears your scripts are already hooked into get_maintainers.pl which
+> > > would seem the obvious place to do this?  Sadly I don't do perl, though
+> > > it looks like you're doing pretty much all the work on that anyway.
+> > Sadly, no it's not the right place.
+> To query MAINTAINERS?  I'd assume that's where you'd want to put that
+> stuff?
+
+I trimmed cc's and added Andrew Morton and Florian Mickler.
+First thread link for them: http://lkml.org/lkml/2010/11/15/262
+
+I use get_maintainer to find email addresses with
+"git send-email --cc-cmd=" but sure it could be extended
+to find some other new information in the MAINTAINERS file.
+
+Anyway, I think that get_maintainers isn't the proper tool
+to rewrite commit subject lines, though it could certainly
+do the lookup of a key in the MAINTAINERS file.
+
+Maybe add a new MAINTAINERS section line something like:
+       "C:     CommitSubjectGrammarStyle"
+where CommitSubjectGrammarStyle is something more
+information rich than "style 1", "style 2".
+
+Perhaps you'll propose a grammar to convert path to header
+and go through and add these "C:" style entries to the
+sections you maintain.
+
+Also, what would you expect the output to be when a single
+patch modified files from 2 subsystems that use different
+styles?
+
+cheers, Joe
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003148:2, b/test/corpora/lkml/cur/1382298793.003148:2,
new file mode 100644 (file)
index 0000000..bd56e87
--- /dev/null
@@ -0,0 +1,92 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem
+ maintainer preference tool
+Date: Tue, 16 Nov 2010 15:04:51 +0000
+Lines: 15
+Message-ID: <20101116150451.GA26239@rakim.wolfsonmicro.main>
+References: <20101115173031.GI12986@rakim.wolfsonmicro.main>
+       <1289842444.16461.140.camel@Joe-Laptop>
+       <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+       <1289845830.16461.149.camel@Joe-Laptop>
+       <20101115190738.GF3338@sirena.org.uk>
+       <1289848458.16461.150.camel@Joe-Laptop>
+       <20101115193407.GK12986@rakim.wolfsonmicro.main>
+       <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Florian Mickler <florian@mickler.org>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       Jiri Kosina <trivial@kernel.org>, alsa-devel@alsa-project.org,
+       linux-kernel@vger.kernel.org
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Tue Nov 16 16:05:00 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PIN5f-0004ME-Rp
+       for glad-alsa-devel-2@m.gmane.org; Tue, 16 Nov 2010 16:04:59 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id E869810380D; Tue, 16 Nov 2010 16:04:57 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 159A924457;
+       Tue, 16 Nov 2010 16:04:57 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 43E4E24458; Tue, 16 Nov 2010 16:04:55 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id CED8D243CD
+       for <alsa-devel@alsa-project.org>; Tue, 16 Nov 2010 16:04:54 +0100 (CET)
+Received: from rakim.wolfsonmicro.main (lumison.wolfsonmicro.com
+       [87.246.78.27])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id A251D3B445E; 
+       Tue, 16 Nov 2010 15:04:52 +0000 (GMT)
+Received: from broonie by rakim.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@rakim.wolfsonmicro.main>)
+       id 1PIN5X-0007x2-Qm; Tue, 16 Nov 2010 15:04:51 +0000
+Content-Disposition: inline
+In-Reply-To: <1289919077.28741.50.camel@Joe-Laptop>
+X-Cookie: Onward through the fog.
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063178>
+
+On Tue, Nov 16, 2010 at 06:51:17AM -0800, Joe Perches wrote:
+
+> Maybe add a new MAINTAINERS section line something like:
+>      "C:     CommitSubjectGrammarStyle"
+> where CommitSubjectGrammarStyle is something more
+> information rich than "style 1", "style 2".
+
+Something printfish would seem reasonable?
+
+> Also, what would you expect the output to be when a single
+> patch modified files from 2 subsystems that use different
+> styles?
+
+The traditional thing is "ThingX/ThingY: blah" but as with anything else
+you need to be sensible.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003216:2, b/test/corpora/lkml/cur/1382298793.003216:2,
new file mode 100644 (file)
index 0000000..655f450
--- /dev/null
@@ -0,0 +1,113 @@
+From: Florian Mickler <florian@mickler.org>
+Subject: Re: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Tue, 16 Nov 2010 18:37:07 +0100
+Lines: 59
+Message-ID: <20101116183707.179964dd@schatten.dmk.lab>
+References: <20101115134939.GC12986@rakim.wolfsonmicro.main>
+       <1289840957.16461.138.camel@Joe-Laptop>
+       <20101115173031.GI12986@rakim.wolfsonmicro.main>
+       <1289842444.16461.140.camel@Joe-Laptop>
+       <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+       <1289845830.16461.149.camel@Joe-Laptop>
+       <20101115190738.GF3338@sirena.org.uk>
+       <1289848458.16461.150.camel@Joe-Laptop>
+       <20101115193407.GK12986@rakim.wolfsonmicro.main>
+       <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: Mark Brown <broonie@opensource.wolfsonmicro.com>,
+       Jiri Kosina <trivial@kernel.org>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Joe Perches <joe@perches.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Nov 16 18:37:57 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PIPTh-0007Ey-5Q
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 16 Nov 2010 18:37:57 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1756054Ab0KPRhi (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 16 Nov 2010 12:37:38 -0500
+Received: from ist.d-labs.de ([213.239.218.44]:44291 "EHLO mx01.d-labs.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1754101Ab0KPRhh (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 16 Nov 2010 12:37:37 -0500
+Received: from schatten.dmk.lab (f053209081.adsl.alicedsl.de [78.53.209.81])
+       by mx01.d-labs.de (Postfix) with ESMTPSA id 1EB9E7FFD4;
+       Tue, 16 Nov 2010 18:36:55 +0100 (CET)
+In-Reply-To: <1289919077.28741.50.camel@Joe-Laptop>
+X-Mailer: Claws Mail 3.7.6cvs31 (GTK+ 2.20.1; x86_64-unknown-linux-gnu)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063246>
+
+On Tue, 16 Nov 2010 06:51:17 -0800
+Joe Perches <joe@perches.com> wrote:
+
+> On Tue, 2010-11-16 at 10:49 +0000, Mark Brown wrote:
+> > On Mon, Nov 15, 2010 at 11:52:53AM -0800, Joe Perches wrote:
+> > > On Mon, 2010-11-15 at 19:34 +0000, Mark Brown wrote:
+> > > > It appears your scripts are already hooked into get_maintainers.pl which
+> > > > would seem the obvious place to do this?  Sadly I don't do perl, though
+> > > > it looks like you're doing pretty much all the work on that anyway.
+> > > Sadly, no it's not the right place.
+> > To query MAINTAINERS?  I'd assume that's where you'd want to put that
+> > stuff?
+> 
+> I trimmed cc's and added Andrew Morton and Florian Mickler.
+> First thread link for them: http://lkml.org/lkml/2010/11/15/262
+> 
+> I use get_maintainer to find email addresses with
+> "git send-email --cc-cmd=" but sure it could be extended
+> to find some other new information in the MAINTAINERS file.
+> 
+> Anyway, I think that get_maintainers isn't the proper tool
+> to rewrite commit subject lines, though it could certainly
+> do the lookup of a key in the MAINTAINERS file.
+> 
+> Maybe add a new MAINTAINERS section line something like:
+>      "C:     CommitSubjectGrammarStyle"
+> where CommitSubjectGrammarStyle is something more
+> information rich than "style 1", "style 2".
+> 
+> Perhaps you'll propose a grammar to convert path to header
+> and go through and add these "C:" style entries to the
+> sections you maintain.
+> 
+> Also, what would you expect the output to be when a single
+> patch modified files from 2 subsystems that use different
+> styles?
+> 
+> cheers, Joe
+> 
+
+My first reaction to this is, it's silly. Certainly a
+subsystem-maintainer is capable of hacking something together that
+suits his needs or may just use a good editor to get the job done.
+After all, he might want to edit the commit message anyway. Also he has
+to have his act together for all non-conforming submitters anyway,
+because shurely, telling people to re-edit their patches subject line
+is not what one would consider "welcoming to newbies",  or whatever it
+is kernel subsystem maintainers have to be nowadays *g*... 
+
+On second thought, if that facility existed, i think nobody would mind
+it either. So, why not. I don't see a way to specify what to do with
+cross-subsystem patches though. 
+
+(MAINTAINERS seems to be the logical place to put this
+information.)
+
+Regards,
+Flo
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003231:2, b/test/corpora/lkml/cur/1382298793.003231:2,
new file mode 100644 (file)
index 0000000..0f4a123
--- /dev/null
@@ -0,0 +1,82 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Tue, 16 Nov 2010 18:12:27 +0000
+Lines: 26
+Message-ID: <20101116181226.GB26239@rakim.wolfsonmicro.main>
+References: <1289842444.16461.140.camel@Joe-Laptop>
+ <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+ <1289845830.16461.149.camel@Joe-Laptop>
+ <20101115190738.GF3338@sirena.org.uk>
+ <1289848458.16461.150.camel@Joe-Laptop>
+ <20101115193407.GK12986@rakim.wolfsonmicro.main>
+ <1289850773.16461.166.camel@Joe-Laptop>
+ <20101116104921.GL12986@rakim.wolfsonmicro.main>
+ <1289919077.28741.50.camel@Joe-Laptop>
+ <20101116183707.179964dd@schatten.dmk.lab>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Cc: Joe Perches <joe@perches.com>, Jiri Kosina <trivial@kernel.org>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Florian Mickler <florian@mickler.org>
+X-From: linux-kernel-owner@vger.kernel.org Tue Nov 16 19:12:51 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PIQ1Q-0006KJ-Uw
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 16 Nov 2010 19:12:49 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1756467Ab0KPSMa (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 16 Nov 2010 13:12:30 -0500
+Received: from opensource.wolfsonmicro.com ([80.75.67.52]:42692 "EHLO
+       opensource2.wolfsonmicro.com" rhost-flags-OK-OK-OK-FAIL)
+       by vger.kernel.org with ESMTP id S1755686Ab0KPSM3 (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 16 Nov 2010 13:12:29 -0500
+Received: from rakim.wolfsonmicro.main (lumison.wolfsonmicro.com [87.246.78.27])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id 3E8603B44D5;
+       Tue, 16 Nov 2010 18:12:28 +0000 (GMT)
+Received: from broonie by rakim.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@rakim.wolfsonmicro.main>)
+       id 1PIQ15-0000oJ-5s; Tue, 16 Nov 2010 18:12:27 +0000
+Content-Disposition: inline
+In-Reply-To: <20101116183707.179964dd@schatten.dmk.lab>
+X-Cookie: Onward through the fog.
+User-Agent: Mutt/1.5.20 (2009-06-14)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063261>
+
+On Tue, Nov 16, 2010 at 06:37:07PM +0100, Florian Mickler wrote:
+
+> My first reaction to this is, it's silly. Certainly a
+> subsystem-maintainer is capable of hacking something together that
+> suits his needs or may just use a good editor to get the job done.
+> After all, he might want to edit the commit message anyway. Also he has
+> to have his act together for all non-conforming submitters anyway,
+> because shurely, telling people to re-edit their patches subject line
+> is not what one would consider "welcoming to newbies",  or whatever it
+> is kernel subsystem maintainers have to be nowadays *g*... 
+
+So, my general policy on this is that I tend to push back on patches
+which don't just work with the toolset (subject lines are just one part
+of it) to a variable extent depending on who's submitting and what
+they're submitting.  One of the factors is that the more patches are
+coming from someone the easier I expect their patches to be to work
+with.
+
+The reason this came up is that this is one of the issues with Joe's
+patches (which are rather frequent) but he is only willing to do things
+that he can automate.
+
+> (MAINTAINERS seems to be the logical place to put this
+> information.)
+
+Indeed.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003278:2, b/test/corpora/lkml/cur/1382298793.003278:2,
new file mode 100644 (file)
index 0000000..b3b0f0c
--- /dev/null
@@ -0,0 +1,89 @@
+From: Florian Mickler <florian@mickler.org>
+Subject: Re: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Tue, 16 Nov 2010 20:35:22 +0100
+Lines: 37
+Message-ID: <20101116203522.65240b18@schatten.dmk.lab>
+References: <1289842444.16461.140.camel@Joe-Laptop>
+       <20101115182708.GJ12986@rakim.wolfsonmicro.main>
+       <1289845830.16461.149.camel@Joe-Laptop>
+       <20101115190738.GF3338@sirena.org.uk>
+       <1289848458.16461.150.camel@Joe-Laptop>
+       <20101115193407.GK12986@rakim.wolfsonmicro.main>
+       <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: Joe Perches <joe@perches.com>, Jiri Kosina <trivial@kernel.org>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: linux-kernel-owner@vger.kernel.org Tue Nov 16 20:36:24 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PIRKK-0004cK-An
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 16 Nov 2010 20:36:24 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S932105Ab0KPTfy (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 16 Nov 2010 14:35:54 -0500
+Received: from ist.d-labs.de ([213.239.218.44]:46199 "EHLO mx01.d-labs.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1756324Ab0KPTfw (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 16 Nov 2010 14:35:52 -0500
+Received: from schatten.dmk.lab (f053209081.adsl.alicedsl.de [78.53.209.81])
+       by mx01.d-labs.de (Postfix) with ESMTPSA id 8CEAA7FAFE;
+       Tue, 16 Nov 2010 20:35:09 +0100 (CET)
+In-Reply-To: <20101116181226.GB26239@rakim.wolfsonmicro.main>
+X-Mailer: Claws Mail 3.7.6cvs31 (GTK+ 2.20.1; x86_64-unknown-linux-gnu)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063309>
+
+On Tue, 16 Nov 2010 18:12:27 +0000
+Mark Brown <broonie@opensource.wolfsonmicro.com> wrote:
+
+> On Tue, Nov 16, 2010 at 06:37:07PM +0100, Florian Mickler wrote:
+> 
+> > My first reaction to this is, it's silly. Certainly a
+> > subsystem-maintainer is capable of hacking something together that
+> > suits his needs or may just use a good editor to get the job done.
+> > After all, he might want to edit the commit message anyway. Also he has
+> > to have his act together for all non-conforming submitters anyway,
+> > because shurely, telling people to re-edit their patches subject line
+> > is not what one would consider "welcoming to newbies",  or whatever it
+> > is kernel subsystem maintainers have to be nowadays *g*... 
+> 
+> So, my general policy on this is that I tend to push back on patches
+> which don't just work with the toolset (subject lines are just one part
+> of it) to a variable extent depending on who's submitting and what
+> they're submitting.  One of the factors is that the more patches are
+> coming from someone the easier I expect their patches to be to work
+> with.
+> 
+> The reason this came up is that this is one of the issues with Joe's
+> patches (which are rather frequent) but he is only willing to do things
+> that he can automate.
+
+Hehe, I know that I wouldn't want to hand edit every autogenerated patch
+people throw at me... What about just dropping everything before the
+last "]" or ":" and putting an autogenerated prefix before it in a
+pre-commit hook on your side?  
+
+That should work most of the time... don't know... maybe other
+subsystem maintainers have some more suggestions on reducing the
+workload... this could even be an interesting topic for some summit...
+
+Regards,
+Flo
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003295:2, b/test/corpora/lkml/cur/1382298793.003295:2,
new file mode 100644 (file)
index 0000000..802eca7
--- /dev/null
@@ -0,0 +1,92 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem
+ maintainer preference tool
+Date: Tue, 16 Nov 2010 19:55:31 +0000
+Lines: 16
+Message-ID: <20101116195530.GA7523@rakim.wolfsonmicro.main>
+References: <1289845830.16461.149.camel@Joe-Laptop>
+       <20101115190738.GF3338@sirena.org.uk>
+       <1289848458.16461.150.camel@Joe-Laptop>
+       <20101115193407.GK12986@rakim.wolfsonmicro.main>
+       <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Joe Perches <joe@perches.com>, Andrew Morton <akpm@linux-foundation.org>,
+       Jiri Kosina <trivial@kernel.org>, alsa-devel@alsa-project.org,
+       linux-kernel@vger.kernel.org
+To: Florian Mickler <florian@mickler.org>
+X-From: alsa-devel-bounces@alsa-project.org Tue Nov 16 20:55:48 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PIRd2-0000Zr-GV
+       for glad-alsa-devel-2@m.gmane.org; Tue, 16 Nov 2010 20:55:44 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 02BEA2417E; Tue, 16 Nov 2010 20:55:36 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 8AFD424159;
+       Tue, 16 Nov 2010 20:55:35 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 32ADC24179; Tue, 16 Nov 2010 20:55:34 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id 913CA24158
+       for <alsa-devel@alsa-project.org>; Tue, 16 Nov 2010 20:55:33 +0100 (CET)
+Received: from rakim.wolfsonmicro.main (lumison.wolfsonmicro.com
+       [87.246.78.27])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id C4CDE3B4538; 
+       Tue, 16 Nov 2010 19:55:31 +0000 (GMT)
+Received: from broonie by rakim.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@rakim.wolfsonmicro.main>)
+       id 1PIRcp-0004nx-60; Tue, 16 Nov 2010 19:55:31 +0000
+Content-Disposition: inline
+In-Reply-To: <20101116203522.65240b18@schatten.dmk.lab>
+X-Cookie: Killing turkeys causes winter.
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063326>
+
+On Tue, Nov 16, 2010 at 08:35:22PM +0100, Florian Mickler wrote:
+
+> Hehe, I know that I wouldn't want to hand edit every autogenerated patch
+> people throw at me... What about just dropping everything before the
+> last "]" or ":" and putting an autogenerated prefix before it in a
+> pre-commit hook on your side?  
+
+> That should work most of the time... don't know... maybe other
+
+It's the most of the time bit that worries me, I'm generally reluctant
+to script things like this when the scripts aren't very widely used and
+it's a pain to get hooks distributed over all my systems and working for
+all the things I need to apply patches for.
+
+From my point of view my current approach is actually working pretty
+well with most submitters, even people doing similar janitorial stuff.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003316:2, b/test/corpora/lkml/cur/1382298793.003316:2,
new file mode 100644 (file)
index 0000000..7b09d35
--- /dev/null
@@ -0,0 +1,105 @@
+From: Randy Dunlap <rdunlap@xenotime.net>
+Subject: Re: rfc: rewrite commit subject line for subsystem
+ maintainer preference tool
+Date: Tue, 16 Nov 2010 12:21:02 -0800
+Organization: YPO4
+Lines: 34
+Message-ID: <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+References: <1289845830.16461.149.camel@Joe-Laptop>
+       <20101115190738.GF3338@sirena.org.uk>
+       <1289848458.16461.150.camel@Joe-Laptop>
+       <20101115193407.GK12986@rakim.wolfsonmicro.main>
+       <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+       <20101116195530.GA7523@rakim.wolfsonmicro.main>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       linux-kernel@vger.kernel.org, Florian Mickler <florian@mickler.org>,
+       Joe Perches <joe@perches.com>, Andrew Morton <akpm@linux-foundation.org>
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: alsa-devel-bounces@alsa-project.org Tue Nov 16 21:21:20 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PIS1m-0001Kj-Rd
+       for glad-alsa-devel-2@m.gmane.org; Tue, 16 Nov 2010 21:21:18 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 2FC2024371; Tue, 16 Nov 2010 21:21:18 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id ACBEB24368;
+       Tue, 16 Nov 2010 21:21:16 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 6AFBB24369; Tue, 16 Nov 2010 21:21:15 +0100 (CET)
+Received: from xenotime.net (xenotime.net [72.52.115.56])
+       by alsa0.perex.cz (Postfix) with SMTP id 6FDFD24363
+       for <alsa-devel@alsa-project.org>; Tue, 16 Nov 2010 21:21:14 +0100 (CET)
+Received: from chimera.site ([173.50.240.230]) by xenotime.net for
+       <alsa-devel@alsa-project.org>; Tue, 16 Nov 2010 12:21:06 -0800
+In-Reply-To: <20101116195530.GA7523@rakim.wolfsonmicro.main>
+X-Mailer: Sylpheed 2.7.1 (GTK+ 2.16.6; x86_64-unknown-linux-gnu)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063347>
+
+On Tue, 16 Nov 2010 19:55:31 +0000 Mark Brown wrote:
+
+> On Tue, Nov 16, 2010 at 08:35:22PM +0100, Florian Mickler wrote:
+> 
+> > Hehe, I know that I wouldn't want to hand edit every autogenerated patch
+> > people throw at me... What about just dropping everything before the
+> > last "]" or ":" and putting an autogenerated prefix before it in a
+> > pre-commit hook on your side?  
+> 
+> > That should work most of the time... don't know... maybe other
+> 
+> It's the most of the time bit that worries me, I'm generally reluctant
+> to script things like this when the scripts aren't very widely used and
+> it's a pain to get hooks distributed over all my systems and working for
+> all the things I need to apply patches for.
+> 
+> From my point of view my current approach is actually working pretty
+> well with most submitters, even people doing similar janitorial stuff.
+
+I don't know what you asked Joe to change, but asking someone to use
+the documented canonical patch format:
+
+<quote>
+The canonical patch subject line is:
+
+    Subject: [PATCH 001/123] subsystem: summary phrase
+</quote>
+
+should be fine.  And there is no need for printf-ish templates
+for this in MAINTAINERS either.
+
+---
+~Randy
+*** Remember to use Documentation/SubmitChecklist when testing your code ***
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003334:2, b/test/corpora/lkml/cur/1382298793.003334:2,
new file mode 100644 (file)
index 0000000..9a58840
--- /dev/null
@@ -0,0 +1,106 @@
+From: Joe Perches <joe@perches.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Tue, 16 Nov 2010 12:42:36 -0800
+Lines: 51
+Message-ID: <1289940156.28741.207.camel@Joe-Laptop>
+References: <1289845830.16461.149.camel@Joe-Laptop>
+        <20101115190738.GF3338@sirena.org.uk>
+        <1289848458.16461.150.camel@Joe-Laptop>
+        <20101115193407.GK12986@rakim.wolfsonmicro.main>
+        <1289850773.16461.166.camel@Joe-Laptop>
+        <20101116104921.GL12986@rakim.wolfsonmicro.main>
+        <1289919077.28741.50.camel@Joe-Laptop>
+        <20101116183707.179964dd@schatten.dmk.lab>
+        <20101116181226.GB26239@rakim.wolfsonmicro.main>
+        <20101116203522.65240b18@schatten.dmk.lab>
+        <20101116195530.GA7523@rakim.wolfsonmicro.main>
+        <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+Cc: Mark Brown <broonie@opensource.wolfsonmicro.com>,
+       Florian Mickler <florian@mickler.org>,
+       Jiri Kosina <trivial@kernel.org>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Randy Dunlap <rdunlap@xenotime.net>
+X-From: linux-kernel-owner@vger.kernel.org Tue Nov 16 21:43:01 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PISMm-00074k-9X
+       for glk-linux-kernel-3@lo.gmane.org; Tue, 16 Nov 2010 21:43:00 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1757174Ab0KPUmj (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 16 Nov 2010 15:42:39 -0500
+Received: from mail.perches.com ([173.55.12.10]:1476 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1754409Ab0KPUmi (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 16 Nov 2010 15:42:38 -0500
+Received: from [192.168.1.162] (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 17CC824368;
+       Tue, 16 Nov 2010 12:40:23 -0800 (PST)
+In-Reply-To: <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+X-Mailer: Evolution 2.30.3 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063365>
+
+On Tue, 2010-11-16 at 12:21 -0800, Randy Dunlap wrote:
+> On Tue, 16 Nov 2010 19:55:31 +0000 Mark Brown wrote:
+> I don't know what you asked Joe to change, but asking someone to use
+> the documented canonical patch format:
+> <quote>
+> The canonical patch subject line is:
+>     Subject: [PATCH 001/123] subsystem: summary phrase
+> </quote>
+> should be fine.  And there is no need for printf-ish templates
+> for this in MAINTAINERS either.
+
+I've never read that before.  Learn something new etc...
+It seems path prefixes aren't good nor even commonly used.
+
+A review of kernel patch subjects:
+
+$ git log --no-merges --pretty=oneline | \
+       cut -f2- -d" " | cut -f1 -d: | sort | uniq -c | sort -rn
+
+is interesting.  Here's the head:
+   5007 x86
+   3943 Staging
+   3220 USB
+   2790 sh
+   2707 KVM
+   2624 ARM
+   2449 ALSA
+   1571 Input
+   1549 ASoC
+   1470 iwlwifi
+   1423 ACPI
+   1397 mac80211
+   1384 V4L/DVB
+   1226 sched
+   1200 Btrfs
+   1184 powerpc
+   1106 [NETFILTER]
+   1080 MIPS
+   1049 net
+   1047 ide
+   1014 drm/i915
+    993 staging
+    921 ath9k
+
+Some subsystem maintainers like upper case, some mixed, some lower.
+Some aren't consistent.  (Staging/staging)
+
+It doesn't seem a rule can be pregenerated so maybe adding these
+"C:" lines to MAINTAINERS has some value.
+
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003340:2, b/test/corpora/lkml/cur/1382298793.003340:2,
new file mode 100644 (file)
index 0000000..ff520cb
--- /dev/null
@@ -0,0 +1,138 @@
+From: Randy Dunlap <rdunlap@xenotime.net>
+Subject: Re: rfc: rewrite commit subject line for subsystem
+ maintainer preference tool
+Date: Tue, 16 Nov 2010 12:46:09 -0800
+Organization: YPO4
+Lines: 64
+Message-ID: <20101116124609.382e42fb.rdunlap@xenotime.net>
+References: <1289845830.16461.149.camel@Joe-Laptop>
+       <20101115190738.GF3338@sirena.org.uk>
+       <1289848458.16461.150.camel@Joe-Laptop>
+       <20101115193407.GK12986@rakim.wolfsonmicro.main>
+       <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+       <20101116195530.GA7523@rakim.wolfsonmicro.main>
+       <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+       <1289940156.28741.207.camel@Joe-Laptop>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       Mark Brown <broonie@opensource.wolfsonmicro.com>,
+       linux-kernel@vger.kernel.org, Florian Mickler <florian@mickler.org>,
+       Andrew Morton <akpm@linux-foundation.org>
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Tue Nov 16 21:46:23 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PISQ1-0000rd-2s
+       for glad-alsa-devel-2@m.gmane.org; Tue, 16 Nov 2010 21:46:21 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 5421B2438C; Tue, 16 Nov 2010 21:46:20 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: *
+X-Spam-Status: No, score=1.0 required=5.0 tests=PRX_BODY_29 autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 774FE24390;
+       Tue, 16 Nov 2010 21:46:19 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 6051924391; Tue, 16 Nov 2010 21:46:17 +0100 (CET)
+Received: from xenotime.net (xenotime.net [72.52.115.56])
+       by alsa0.perex.cz (Postfix) with SMTP id 4F17D2438C
+       for <alsa-devel@alsa-project.org>; Tue, 16 Nov 2010 21:46:15 +0100 (CET)
+Received: from chimera.site ([173.50.240.230]) by xenotime.net for
+       <alsa-devel@alsa-project.org>; Tue, 16 Nov 2010 12:46:10 -0800
+In-Reply-To: <1289940156.28741.207.camel@Joe-Laptop>
+X-Mailer: Sylpheed 2.7.1 (GTK+ 2.16.6; x86_64-unknown-linux-gnu)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063371>
+
+On Tue, 16 Nov 2010 12:42:36 -0800 Joe Perches wrote:
+
+> On Tue, 2010-11-16 at 12:21 -0800, Randy Dunlap wrote:
+> > On Tue, 16 Nov 2010 19:55:31 +0000 Mark Brown wrote:
+> > I don't know what you asked Joe to change, but asking someone to use
+> > the documented canonical patch format:
+> > <quote>
+> > The canonical patch subject line is:
+> >     Subject: [PATCH 001/123] subsystem: summary phrase
+> > </quote>
+> > should be fine.  And there is no need for printf-ish templates
+> > for this in MAINTAINERS either.
+> 
+> I've never read that before.  Learn something new etc...
+> It seems path prefixes aren't good nor even commonly used.
+> 
+> A review of kernel patch subjects:
+> 
+> $ git log --no-merges --pretty=oneline | \
+>      cut -f2- -d" " | cut -f1 -d: | sort | uniq -c | sort -rn
+> 
+> is interesting.  Here's the head:
+>    5007 x86
+>    3943 Staging
+>    3220 USB
+>    2790 sh
+>    2707 KVM
+>    2624 ARM
+>    2449 ALSA
+>    1571 Input
+>    1549 ASoC
+>    1470 iwlwifi
+>    1423 ACPI
+>    1397 mac80211
+>    1384 V4L/DVB
+>    1226 sched
+>    1200 Btrfs
+>    1184 powerpc
+>    1106 [NETFILTER]
+>    1080 MIPS
+>    1049 net
+>    1047 ide
+>    1014 drm/i915
+>     993 staging
+>     921 ath9k
+
+$ARCH is a commonly accepted substitute for subsystem.
+
+And yes, lots of people use <drivername> there as well.
+
+
+> Some subsystem maintainers like upper case, some mixed, some lower.
+> Some aren't consistent.  (Staging/staging)
+
+Case usually doesn't matter to most of us.
+
+> It doesn't seem a rule can be pregenerated so maybe adding these
+> "C:" lines to MAINTAINERS has some value.
+
+Hopefully it won't go that far.
+
+---
+~Randy
+*** Remember to use Documentation/SubmitChecklist when testing your code ***
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003448:2, b/test/corpora/lkml/cur/1382298793.003448:2,
new file mode 100644 (file)
index 0000000..d9ecd6a
--- /dev/null
@@ -0,0 +1,100 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem
+ maintainer preference tool
+Date: Tue, 16 Nov 2010 23:01:26 +0000
+Lines: 24
+Message-ID: <20101116230126.GB24623@opensource.wolfsonmicro.com>
+References: <1289848458.16461.150.camel@Joe-Laptop>
+       <20101115193407.GK12986@rakim.wolfsonmicro.main>
+       <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+       <20101116195530.GA7523@rakim.wolfsonmicro.main>
+       <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       linux-kernel@vger.kernel.org, Florian Mickler <florian@mickler.org>,
+       Joe Perches <joe@perches.com>, Andrew Morton <akpm@linux-foundation.org>
+To: Randy Dunlap <rdunlap@xenotime.net>
+X-From: alsa-devel-bounces@alsa-project.org Wed Nov 17 00:01:43 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PIUWr-0004yP-6J
+       for glad-alsa-devel-2@m.gmane.org; Wed, 17 Nov 2010 00:01:33 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id A434B103882; Wed, 17 Nov 2010 00:01:26 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 9B9A81037FB;
+       Wed, 17 Nov 2010 00:01:23 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 47D7B1037FB; Wed, 17 Nov 2010 00:01:22 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id F24201037E3
+       for <alsa-devel@alsa-project.org>; Wed, 17 Nov 2010 00:01:20 +0100 (CET)
+Received: from finisterre.wolfsonmicro.main
+       (cpc3-sgyl4-0-0-cust125.sgyl.cable.virginmedia.com [82.41.240.126])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id F2D407881C9; 
+       Tue, 16 Nov 2010 23:01:18 +0000 (GMT)
+Received: from broonie by finisterre.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@opensource.wolfsonmicro.com>)
+       id 1PIUWk-0007m9-B8; Tue, 16 Nov 2010 23:01:26 +0000
+Content-Disposition: inline
+In-Reply-To: <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+X-Cookie: Beware of Bigfoot!
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063481>
+
+On Tue, Nov 16, 2010 at 12:21:02PM -0800, Randy Dunlap wrote:
+
+> I don't know what you asked Joe to change, but asking someone to use
+> the documented canonical patch format:
+
+> <quote>
+> The canonical patch subject line is:
+
+>     Subject: [PATCH 001/123] subsystem: summary phrase
+> </quote>
+
+> should be fine.  And there is no need for printf-ish templates
+> for this in MAINTAINERS either.
+
+That's exactly what I asked him to do.  He said he's not willing to use
+anything for "subsystem" which can't be automatically generated.
+
+The formats I mentioned because some subsystems have their own things
+within this format like "subsystem: driver:" or whatever.  While it's
+probably not an issue for the sort of patch Joe generates if we do have
+a tool for this I'd expect it'll go the same way that checkpatch does
+and get used by people doing more specific work.  It'd be good to try to
+head off the friction that may cause by at least having an idea how we
+might cope with that.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003459:2, b/test/corpora/lkml/cur/1382298793.003459:2,
new file mode 100644 (file)
index 0000000..8fddae8
--- /dev/null
@@ -0,0 +1,88 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem
+ maintainer preference tool
+Date: Tue, 16 Nov 2010 23:22:58 +0000
+Lines: 12
+Message-ID: <20101116232258.GC24623@opensource.wolfsonmicro.com>
+References: <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+       <20101116195530.GA7523@rakim.wolfsonmicro.main>
+       <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+       <1289940156.28741.207.camel@Joe-Laptop>
+       <20101116124609.382e42fb.rdunlap@xenotime.net>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       linux-kernel@vger.kernel.org, Florian Mickler <florian@mickler.org>,
+       Joe Perches <joe@perches.com>, Andrew Morton <akpm@linux-foundation.org>
+To: Randy Dunlap <rdunlap@xenotime.net>
+X-From: alsa-devel-bounces@alsa-project.org Wed Nov 17 00:22:58 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PIUrZ-0001Km-F5
+       for glad-alsa-devel-2@m.gmane.org; Wed, 17 Nov 2010 00:22:57 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 28F9610381A; Wed, 17 Nov 2010 00:22:55 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 1CC8D10381B;
+       Wed, 17 Nov 2010 00:22:55 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id B1B8B10381B; Wed, 17 Nov 2010 00:22:52 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id CB03810381A
+       for <alsa-devel@alsa-project.org>; Wed, 17 Nov 2010 00:22:51 +0100 (CET)
+Received: from finisterre.wolfsonmicro.main
+       (cpc3-sgyl4-0-0-cust125.sgyl.cable.virginmedia.com [82.41.240.126])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id 39A957881C9; 
+       Tue, 16 Nov 2010 23:22:51 +0000 (GMT)
+Received: from broonie by finisterre.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@opensource.wolfsonmicro.com>)
+       id 1PIUra-0001BL-NS; Tue, 16 Nov 2010 23:22:58 +0000
+Content-Disposition: inline
+In-Reply-To: <20101116124609.382e42fb.rdunlap@xenotime.net>
+X-Cookie: Beware of Bigfoot!
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063492>
+
+On Tue, Nov 16, 2010 at 12:46:09PM -0800, Randy Dunlap wrote:
+> On Tue, 16 Nov 2010 12:42:36 -0800 Joe Perches wrote:
+
+> > Some subsystem maintainers like upper case, some mixed, some lower.
+> > Some aren't consistent.  (Staging/staging)
+
+> Case usually doesn't matter to most of us.
+
+Given that we're working in case sensitive languages here it's probably
+safe to assume that a reasonable proportion of people will care; being
+reasonably consistent with existing practice for the subsystem seems
+sensible.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003462:2, b/test/corpora/lkml/cur/1382298793.003462:2,
new file mode 100644 (file)
index 0000000..4f6ba5a
--- /dev/null
@@ -0,0 +1,97 @@
+From: Randy Dunlap <rdunlap@xenotime.net>
+Subject: Re: rfc: rewrite commit subject line for subsystem
+ maintainer preference tool
+Date: Tue, 16 Nov 2010 15:28:35 -0800
+Organization: YPO4
+Lines: 26
+Message-ID: <20101116152835.b0ab571c.rdunlap@xenotime.net>
+References: <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+       <20101116195530.GA7523@rakim.wolfsonmicro.main>
+       <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+       <1289940156.28741.207.camel@Joe-Laptop>
+       <20101116124609.382e42fb.rdunlap@xenotime.net>
+       <20101116232258.GC24623@opensource.wolfsonmicro.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       linux-kernel@vger.kernel.org, Florian Mickler <florian@mickler.org>,
+       Joe Perches <joe@perches.com>, Andrew Morton <akpm@linux-foundation.org>
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: alsa-devel-bounces@alsa-project.org Wed Nov 17 00:29:00 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PIUxP-0004iI-Hq
+       for glad-alsa-devel-2@m.gmane.org; Wed, 17 Nov 2010 00:28:59 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id AD65410388C; Wed, 17 Nov 2010 00:28:58 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 21296103822;
+       Wed, 17 Nov 2010 00:28:57 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 58AE7103822; Wed, 17 Nov 2010 00:28:54 +0100 (CET)
+Received: from xenotime.net (xenotime.net [72.52.115.56])
+       by alsa0.perex.cz (Postfix) with SMTP id 1947B10381B
+       for <alsa-devel@alsa-project.org>; Wed, 17 Nov 2010 00:28:52 +0100 (CET)
+Received: from chimera.site ([173.50.240.230]) by xenotime.net for
+       <alsa-devel@alsa-project.org>; Tue, 16 Nov 2010 15:28:36 -0800
+In-Reply-To: <20101116232258.GC24623@opensource.wolfsonmicro.com>
+X-Mailer: Sylpheed 2.7.1 (GTK+ 2.16.6; x86_64-unknown-linux-gnu)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063495>
+
+On Tue, 16 Nov 2010 23:22:58 +0000 Mark Brown wrote:
+
+> On Tue, Nov 16, 2010 at 12:46:09PM -0800, Randy Dunlap wrote:
+> > On Tue, 16 Nov 2010 12:42:36 -0800 Joe Perches wrote:
+> 
+> > > Some subsystem maintainers like upper case, some mixed, some lower.
+> > > Some aren't consistent.  (Staging/staging)
+> 
+> > Case usually doesn't matter to most of us.
+> 
+> Given that we're working in case sensitive languages here it's probably
+> safe to assume that a reasonable proportion of people will care; being
+> reasonably consistent with existing practice for the subsystem seems
+> sensible.
+
+Greg takes patches that say STAGING or Staging or staging.
+
+DaveM takes patches that say net: or netdev: or network: or NET:
+
+The sound maintainers take patches that say sound: or alsa: or ALSA:
+
+etc.
+
+---
+~Randy
+*** Remember to use Documentation/SubmitChecklist when testing your code ***
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003468:2, b/test/corpora/lkml/cur/1382298793.003468:2,
new file mode 100644 (file)
index 0000000..a26128d
--- /dev/null
@@ -0,0 +1,99 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem
+ maintainer preference tool
+Date: Tue, 16 Nov 2010 23:50:26 +0000
+Lines: 23
+Message-ID: <20101116235025.GA7256@opensource.wolfsonmicro.com>
+References: <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+       <20101116195530.GA7523@rakim.wolfsonmicro.main>
+       <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+       <1289940156.28741.207.camel@Joe-Laptop>
+       <20101116124609.382e42fb.rdunlap@xenotime.net>
+       <20101116232258.GC24623@opensource.wolfsonmicro.com>
+       <20101116152835.b0ab571c.rdunlap@xenotime.net>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       linux-kernel@vger.kernel.org, Florian Mickler <florian@mickler.org>,
+       Joe Perches <joe@perches.com>, Andrew Morton <akpm@linux-foundation.org>
+To: Randy Dunlap <rdunlap@xenotime.net>
+X-From: alsa-devel-bounces@alsa-project.org Wed Nov 17 00:50:24 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PIVI7-00006s-O3
+       for glad-alsa-devel-2@m.gmane.org; Wed, 17 Nov 2010 00:50:23 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id F007910388B; Wed, 17 Nov 2010 00:50:22 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 9E0B9103822;
+       Wed, 17 Nov 2010 00:50:21 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id AD918103822; Wed, 17 Nov 2010 00:50:19 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id 39C5A10381B
+       for <alsa-devel@alsa-project.org>; Wed, 17 Nov 2010 00:50:19 +0100 (CET)
+Received: from finisterre.wolfsonmicro.main
+       (cpc3-sgyl4-0-0-cust125.sgyl.cable.virginmedia.com [82.41.240.126])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id B19503B4628; 
+       Tue, 16 Nov 2010 23:50:18 +0000 (GMT)
+Received: from broonie by finisterre.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@opensource.wolfsonmicro.com>)
+       id 1PIVIA-0003UP-7d; Tue, 16 Nov 2010 23:50:26 +0000
+Content-Disposition: inline
+In-Reply-To: <20101116152835.b0ab571c.rdunlap@xenotime.net>
+X-Cookie: You enjoy the company of other people.
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063501>
+
+On Tue, Nov 16, 2010 at 03:28:35PM -0800, Randy Dunlap wrote:
+> On Tue, 16 Nov 2010 23:22:58 +0000 Mark Brown wrote:
+> > On Tue, Nov 16, 2010 at 12:46:09PM -0800, Randy Dunlap wrote:
+
+> > > Case usually doesn't matter to most of us.
+
+> > Given that we're working in case sensitive languages here it's probably
+> > safe to assume that a reasonable proportion of people will care; being
+> > reasonably consistent with existing practice for the subsystem seems
+> > sensible.
+
+> Greg takes patches that say STAGING or Staging or staging.
+
+> DaveM takes patches that say net: or netdev: or network: or NET:
+
+> The sound maintainers take patches that say sound: or alsa: or ALSA:
+
+> etc.
+
+...and best practice would be to pay attention to what the standard
+thing is for the subsystem and follow that.  We shouldn't be suggesting
+that people just ignore the case, though obviously if it's not clear
+then it's not worth worrying too much about it.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003471:2, b/test/corpora/lkml/cur/1382298793.003471:2,
new file mode 100644 (file)
index 0000000..1e1fc4a
--- /dev/null
@@ -0,0 +1,79 @@
+From: Joe Perches <joe@perches.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Tue, 16 Nov 2010 15:57:57 -0800
+Lines: 25
+Message-ID: <1289951877.28741.262.camel@Joe-Laptop>
+References: <1289850773.16461.166.camel@Joe-Laptop>
+        <20101116104921.GL12986@rakim.wolfsonmicro.main>
+        <1289919077.28741.50.camel@Joe-Laptop>
+        <20101116183707.179964dd@schatten.dmk.lab>
+        <20101116181226.GB26239@rakim.wolfsonmicro.main>
+        <20101116203522.65240b18@schatten.dmk.lab>
+        <20101116195530.GA7523@rakim.wolfsonmicro.main>
+        <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+        <1289940156.28741.207.camel@Joe-Laptop>
+        <20101116124609.382e42fb.rdunlap@xenotime.net>
+        <20101116232258.GC24623@opensource.wolfsonmicro.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+Cc: Randy Dunlap <rdunlap@xenotime.net>,
+       Florian Mickler <florian@mickler.org>,
+       Jiri Kosina <trivial@kernel.org>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: linux-kernel-owner@vger.kernel.org Wed Nov 17 00:58:25 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PIVPr-0004pn-RQ
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 17 Nov 2010 00:58:24 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1757881Ab0KPX6A (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 16 Nov 2010 18:58:00 -0500
+Received: from mail.perches.com ([173.55.12.10]:1493 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1757143Ab0KPX57 (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 16 Nov 2010 18:57:59 -0500
+Received: from [192.168.1.162] (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id B87C82436B;
+       Tue, 16 Nov 2010 15:55:40 -0800 (PST)
+In-Reply-To: <20101116232258.GC24623@opensource.wolfsonmicro.com>
+X-Mailer: Evolution 2.30.3 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063504>
+
+On Tue, 2010-11-16 at 23:22 +0000, Mark Brown wrote:
+> On Tue, Nov 16, 2010 at 12:46:09PM -0800, Randy Dunlap wrote:
+> > On Tue, 16 Nov 2010 12:42:36 -0800 Joe Perches wrote:
+> > > Some subsystem maintainers like upper case, some mixed, some lower.
+> > > Some aren't consistent.  (Staging/staging)
+> > Case usually doesn't matter to most of us.
+> Given that we're working in case sensitive languages here it's probably
+> safe to assume that a reasonable proportion of people will care; being
+> reasonably consistent with existing practice for the subsystem seems
+> sensible.
+
+Presumably the tool would also have to traverse up the tree
+to find the appropriate style so every MAINTAINERS section
+would not need a C entry.
+
+ie: sound/soc/codecs/foo could use the C: entry for sound/soc/
+
+Perhaps something like:
+       C:      ASoC basename:
+
+and for arch/x86/:
+       C:      x86, dirname:
+
+etc.
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003472:2, b/test/corpora/lkml/cur/1382298793.003472:2,
new file mode 100644 (file)
index 0000000..0e360fd
--- /dev/null
@@ -0,0 +1,84 @@
+From: Joe Perches <joe@perches.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Tue, 16 Nov 2010 15:57:55 -0800
+Lines: 29
+Message-ID: <1289951875.28741.261.camel@Joe-Laptop>
+References: <1289850773.16461.166.camel@Joe-Laptop>
+        <20101116104921.GL12986@rakim.wolfsonmicro.main>
+        <1289919077.28741.50.camel@Joe-Laptop>
+        <20101116183707.179964dd@schatten.dmk.lab>
+        <20101116181226.GB26239@rakim.wolfsonmicro.main>
+        <20101116203522.65240b18@schatten.dmk.lab>
+        <20101116195530.GA7523@rakim.wolfsonmicro.main>
+        <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+        <1289940156.28741.207.camel@Joe-Laptop>
+        <20101116124609.382e42fb.rdunlap@xenotime.net>
+        <20101116232258.GC24623@opensource.wolfsonmicro.com>
+        <20101116152835.b0ab571c.rdunlap@xenotime.net>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+Cc: Mark Brown <broonie@opensource.wolfsonmicro.com>,
+       Florian Mickler <florian@mickler.org>,
+       Jiri Kosina <trivial@kernel.org>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Randy Dunlap <rdunlap@xenotime.net>
+X-From: linux-kernel-owner@vger.kernel.org Wed Nov 17 00:58:26 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PIVPr-0004pn-AW
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 17 Nov 2010 00:58:23 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1757099Ab0KPX56 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 16 Nov 2010 18:57:58 -0500
+Received: from mail.perches.com ([173.55.12.10]:1485 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751238Ab0KPX55 (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 16 Nov 2010 18:57:57 -0500
+Received: from [192.168.1.162] (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 18B3A24368;
+       Tue, 16 Nov 2010 15:55:38 -0800 (PST)
+In-Reply-To: <20101116152835.b0ab571c.rdunlap@xenotime.net>
+X-Mailer: Evolution 2.30.3 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063505>
+
+On Tue, 2010-11-16 at 15:28 -0800, Randy Dunlap wrote:
+> On Tue, 16 Nov 2010 23:22:58 +0000 Mark Brown wrote:
+> > On Tue, Nov 16, 2010 at 12:46:09PM -0800, Randy Dunlap wrote:
+> > > On Tue, 16 Nov 2010 12:42:36 -0800 Joe Perches wrote:
+> > > > Some subsystem maintainers like upper case, some mixed, some lower.
+> > > > Some aren't consistent.  (Staging/staging)
+> > > Case usually doesn't matter to most of us.
+> > Given that we're working in case sensitive languages here it's probably
+> > safe to assume that a reasonable proportion of people will care; being
+> > reasonably consistent with existing practice for the subsystem seems
+> > sensible.
+> Greg takes patches that say STAGING or Staging or staging.
+
+Greg seems to rewrite patch subjects and is inconsistent
+about case, so he might be doing that by hand.
+
+> DaveM takes patches that say net: or netdev: or network: or NET:
+
+DaveM doesn't appear to be choosy about patch subject lines.
+He seems to take any sensible patch and as far as I know he
+doesn't edit the subject lines.  He does reject inferior
+patches outright.
+
+> The sound maintainers take patches that say sound: or alsa: or ALSA:
+
+The sound maintainers seem to rewrite patch subjects on an
+ad-hoc basis.
+
+
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003478:2, b/test/corpora/lkml/cur/1382298793.003478:2,
new file mode 100644 (file)
index 0000000..c95e3f8
--- /dev/null
@@ -0,0 +1,113 @@
+From: Joel Becker <Joel.Becker@oracle.com>
+Subject: Re: [PATCH 36/44] fs/ocfs2: Remove unnecessary
+       semicolons
+Date: Tue, 16 Nov 2010 16:10:47 -0800
+Lines: 38
+Message-ID: <20101117001046.GE10237@mail.oracle.com>
+References: <cover.1289789604.git.joe@perches.com>
+       <e32409b17aaa1a54fec85f3654583ef08fcf851c.1289789605.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Mark Fasheh <mfasheh@suse.com>, Jiri Kosina <trivial@kernel.org>,
+        linux-kernel@vger.kernel.org, ocfs2-devel@oss.oracle.com
+To: Joe Perches <joe@perches.com>
+X-From: ocfs2-devel-bounces@oss.oracle.com Wed Nov 17 01:11:36 2010
+Return-path: <ocfs2-devel-bounces@oss.oracle.com>
+Envelope-to: gcfod-ocfs2-devel@gmane.org
+Received: from rcsinet10.oracle.com ([148.87.113.121])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <ocfs2-devel-bounces@oss.oracle.com>)
+       id 1PIVcY-0003qc-VC
+       for gcfod-ocfs2-devel@gmane.org; Wed, 17 Nov 2010 01:11:31 +0100
+Received: from rcsinet15.oracle.com (rcsinet15.oracle.com [148.87.113.117])
+       by rcsinet10.oracle.com (Switch-3.4.2/Switch-3.4.2) with ESMTP id oAH0B96Y007820
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK);
+       Wed, 17 Nov 2010 00:11:10 GMT
+Received: from oss.oracle.com (oss.oracle.com [141.146.12.120])
+       by rcsinet15.oracle.com (Switch-3.4.2/Switch-3.4.1) with ESMTP id oAH0B6rM032434
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=NO);
+       Wed, 17 Nov 2010 00:11:07 GMT
+Received: from localhost ([127.0.0.1] helo=oss.oracle.com)
+       by oss.oracle.com with esmtp (Exim 4.63)
+       (envelope-from <ocfs2-devel-bounces@oss.oracle.com>)
+       id 1PIVcA-0001bs-K0; Tue, 16 Nov 2010 16:11:06 -0800
+Received: from rcsinet15.oracle.com ([148.87.113.117])
+       by oss.oracle.com with esmtp (Exim 4.63)
+       (envelope-from <joel.becker@oracle.com>) id 1PIVc7-0001bl-Td
+       for ocfs2-devel@oss.oracle.com; Tue, 16 Nov 2010 16:11:04 -0800
+Received: from acsmt354.oracle.com (acsmt354.oracle.com [141.146.40.154])
+       by rcsinet15.oracle.com (Switch-3.4.2/Switch-3.4.1) with ESMTP id
+       oAH0B1gV032165; Wed, 17 Nov 2010 00:11:01 GMT
+Received: from ca-server1.us.oracle.com by acsmt353.oracle.com
+       with ESMTP id 784380701289952654; Tue, 16 Nov 2010 16:10:54 -0800
+Received: from jlbec by ca-server1.us.oracle.com with local (Exim 4.69)
+       (envelope-from <joel.becker@oracle.com>)
+       id 1PIVbx-0007gG-4G; Tue, 16 Nov 2010 16:10:53 -0800
+Mail-Followup-To: Joe Perches <joe@perches.com>,
+       Jiri Kosina <trivial@kernel.org>, Mark Fasheh <mfasheh@suse.com>,
+       ocfs2-devel@oss.oracle.com, linux-kernel@vger.kernel.org
+Content-Disposition: inline
+In-Reply-To: <e32409b17aaa1a54fec85f3654583ef08fcf851c.1289789605.git.joe@perches.com>
+X-Burt-Line: Trees are cool.
+X-Red-Smith: Ninety feet between bases is perhaps as close as man has ever
+       come to perfection.
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: ocfs2-devel@oss.oracle.com
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: <ocfs2-devel.oss.oracle.com>
+List-Unsubscribe: <http://oss.oracle.com/mailman/listinfo/ocfs2-devel>,
+       <mailto:ocfs2-devel-request@oss.oracle.com?subject=unsubscribe>
+List-Archive: <http://oss.oracle.com/pipermail/ocfs2-devel>
+List-Post: <mailto:ocfs2-devel@oss.oracle.com>
+List-Help: <mailto:ocfs2-devel-request@oss.oracle.com?subject=help>
+List-Subscribe: <http://oss.oracle.com/mailman/listinfo/ocfs2-devel>,
+       <mailto:ocfs2-devel-request@oss.oracle.com?subject=subscribe>
+Sender: ocfs2-devel-bounces@oss.oracle.com
+Errors-To: ocfs2-devel-bounces@oss.oracle.com
+X-Source-IP: oss.oracle.com [141.146.12.120]
+X-Auth-Type: Internal IP
+X-CT-RefId: str=0001.0A090207.4CE31D9C.007A:SCFSTAT3865452,ss=1,fgs=0
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063511>
+
+On Sun, Nov 14, 2010 at 07:04:55PM -0800, Joe Perches wrote:
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Acked-by: Joel Becker <joel.becker@oracle.com>
+
+
+> ---
+>  fs/ocfs2/refcounttree.c |    2 +-
+>  1 files changed, 1 insertions(+), 1 deletions(-)
+> 
+> diff --git a/fs/ocfs2/refcounttree.c b/fs/ocfs2/refcounttree.c
+> index b5f9160..da14a42 100644
+> --- a/fs/ocfs2/refcounttree.c
+> +++ b/fs/ocfs2/refcounttree.c
+> @@ -3704,7 +3704,7 @@ int ocfs2_refcount_cow_xattr(struct inode *inode,
+>      context->cow_start = cow_start;
+>      context->cow_len = cow_len;
+>      context->ref_tree = ref_tree;
+> -    context->ref_root_bh = ref_root_bh;;
+> +    context->ref_root_bh = ref_root_bh;
+>      context->cow_object = xv;
+>  
+>      context->cow_duplicate_clusters = ocfs2_duplicate_clusters_by_jbd;
+> -- 
+> 1.7.3.1.g432b3.dirty
+> 
+
+-- 
+
+Life's Little Instruction Book #237
+
+       "Seek out the good in people."
+
+Joel Becker
+Senior Development Manager
+Oracle
+E-mail: joel.becker@oracle.com
+Phone: (650) 506-8127
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003497:2, b/test/corpora/lkml/cur/1382298793.003497:2,
new file mode 100644 (file)
index 0000000..ab47886
--- /dev/null
@@ -0,0 +1,94 @@
+From: Stefan Richter <stefanr@s5r6.in-berlin.de>
+Subject: Re: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Wed, 17 Nov 2010 01:44:27 +0100
+Lines: 34
+Message-ID: <20101117014427.41d85b13@stein>
+References: <1289848458.16461.150.camel@Joe-Laptop>
+       <20101115193407.GK12986@rakim.wolfsonmicro.main>
+       <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+       <20101116195530.GA7523@rakim.wolfsonmicro.main>
+       <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+       <20101116230126.GB24623@opensource.wolfsonmicro.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: Randy Dunlap <rdunlap@xenotime.net>,
+       Florian Mickler <florian@mickler.org>,
+       Joe Perches <joe@perches.com>,
+       Jiri Kosina <trivial@kernel.org>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: linux-kernel-owner@vger.kernel.org Wed Nov 17 01:45:44 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PIW9d-0005et-Ft
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 17 Nov 2010 01:45:41 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933190Ab0KQApU (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 16 Nov 2010 19:45:20 -0500
+Received: from einhorn.in-berlin.de ([192.109.42.8]:40608 "EHLO
+       einhorn.in-berlin.de" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S932265Ab0KQApS (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 16 Nov 2010 19:45:18 -0500
+X-Envelope-From: stefanr@s5r6.in-berlin.de
+Received: from stein ([83.221.231.7])
+       (authenticated bits=0)
+       by einhorn.in-berlin.de (8.13.6/8.13.6/Debian-1) with ESMTP id oAH0iRjV025917
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES128-SHA bits=128 verify=NOT);
+       Wed, 17 Nov 2010 01:44:28 +0100
+In-Reply-To: <20101116230126.GB24623@opensource.wolfsonmicro.com>
+X-Mailer: Claws Mail 3.7.6 (GTK+ 2.20.1; x86_64-pc-linux-gnu)
+X-Scanned-By: MIMEDefang_at_IN-Berlin_e.V. on 192.109.42.8
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063530>
+
+On Nov 16 Mark Brown wrote:
+> On Tue, Nov 16, 2010 at 12:21:02PM -0800, Randy Dunlap wrote:
+> 
+> > I don't know what you asked Joe to change, but asking someone to use
+> > the documented canonical patch format:
+> 
+> > <quote>
+> > The canonical patch subject line is:
+> 
+> >     Subject: [PATCH 001/123] subsystem: summary phrase
+> > </quote>
+> 
+> > should be fine.  And there is no need for printf-ish templates
+> > for this in MAINTAINERS either.
+> 
+> That's exactly what I asked him to do.  He said he's not willing to use
+> anything for "subsystem" which can't be automatically generated.
+
+Why should we codify our conventions in MAINTAINERS to accommodate the
+specific problem of virtually a _single_ patch author?
+
+Conventions are living and are being adjusted all the time, as code
+organization changes, people join and go, projects start and cease.
+
+Said author please looks the conventions up in the git history.  If he
+finds that this decelerates his patch generation rate, he can surely
+code a script that looks into git for him and suggests plausible
+prefixes for his patch titles to him.  Or he can collect a kind of
+database (a config file) locally for his own use in which he records
+conventional prefixes on the go.
+-- 
+Stefan Richter
+-=====-==-=- =-== =---=
+http://arcgraph.de/sr/
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003501:2, b/test/corpora/lkml/cur/1382298793.003501:2,
new file mode 100644 (file)
index 0000000..67668d1
--- /dev/null
@@ -0,0 +1,89 @@
+From: Jiri Kosina <jkosina@suse.cz>
+Subject: Re: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Wed, 17 Nov 2010 01:53:35 +0100 (CET)
+Lines: 43
+Message-ID: <alpine.LNX.2.00.1011170150060.7420@pobox.suse.cz>
+References: <1289848458.16461.150.camel@Joe-Laptop> <20101115193407.GK12986@rakim.wolfsonmicro.main> <1289850773.16461.166.camel@Joe-Laptop> <20101116104921.GL12986@rakim.wolfsonmicro.main> <1289919077.28741.50.camel@Joe-Laptop> <20101116183707.179964dd@schatten.dmk.lab>
+ <20101116181226.GB26239@rakim.wolfsonmicro.main> <20101116203522.65240b18@schatten.dmk.lab> <20101116195530.GA7523@rakim.wolfsonmicro.main> <20101116122102.86e7e0b9.rdunlap@xenotime.net> <20101116230126.GB24623@opensource.wolfsonmicro.com>
+ <20101117014427.41d85b13@stein>
+Mime-Version: 1.0
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+Cc: Mark Brown <broonie@opensource.wolfsonmicro.com>,
+       Randy Dunlap <rdunlap@xenotime.net>,
+       Florian Mickler <florian@mickler.org>,
+       Joe Perches <joe@perches.com>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Stefan Richter <stefanr@s5r6.in-berlin.de>
+X-From: linux-kernel-owner@vger.kernel.org Wed Nov 17 01:53:55 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PIWHa-0001VG-H4
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 17 Nov 2010 01:53:54 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S933245Ab0KQAxi (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Tue, 16 Nov 2010 19:53:38 -0500
+Received: from cantor2.suse.de ([195.135.220.15]:45188 "EHLO mx2.suse.de"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S932265Ab0KQAxh (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Tue, 16 Nov 2010 19:53:37 -0500
+Received: from relay2.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       by mx2.suse.de (Postfix) with ESMTP id 1C9DD867E2;
+       Wed, 17 Nov 2010 01:53:36 +0100 (CET)
+In-Reply-To: <20101117014427.41d85b13@stein>
+User-Agent: Alpine 2.00 (LNX 1167 2008-08-23)
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063534>
+
+On Wed, 17 Nov 2010, Stefan Richter wrote:
+
+> > > I don't know what you asked Joe to change, but asking someone to use
+> > > the documented canonical patch format:
+> > 
+> > > <quote>
+> > > The canonical patch subject line is:
+> > 
+> > >     Subject: [PATCH 001/123] subsystem: summary phrase
+> > > </quote>
+> > 
+> > > should be fine.  And there is no need for printf-ish templates
+> > > for this in MAINTAINERS either.
+> > 
+> > That's exactly what I asked him to do.  He said he's not willing to use
+> > anything for "subsystem" which can't be automatically generated.
+> 
+> Why should we codify our conventions in MAINTAINERS to accommodate the
+> specific problem of virtually a _single_ patch author?
+> 
+> Conventions are living and are being adjusted all the time, as code
+> organization changes, people join and go, projects start and cease.
+> 
+> Said author please looks the conventions up in the git history.  If he
+> finds that this decelerates his patch generation rate, he can surely
+> code a script that looks into git for him and suggests plausible
+> prefixes for his patch titles to him.  Or he can collect a kind of
+> database (a config file) locally for his own use in which he records
+> conventional prefixes on the go.
+
+Come on guys, this debate is really horribly boring.
+
+Either the maintainer wants the patch. Then he is certainly able to apply 
+it no matter the subject line (I personally am getting a lot of patches 
+which don't follow the format I am using in my tree ... converting 
+Subject: lines is so trivial that I have never felt like bothering anyone 
+about it ... it's basically single condition in a shellscript). Or the 
+maintainer doesn't feel like the patch is worth it, and then the 
+subject-line format really doesn't matter.
+
+-- 
+Jiri Kosina
+SUSE Labs, Novell Inc.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003503:2, b/test/corpora/lkml/cur/1382298793.003503:2,
new file mode 100644 (file)
index 0000000..0ea87ec
--- /dev/null
@@ -0,0 +1,110 @@
+From: Randy Dunlap <rdunlap@xenotime.net>
+Subject: Re: rfc: rewrite commit subject line for subsystem
+ maintainer preference tool
+Date: Tue, 16 Nov 2010 16:55:56 -0800
+Organization: YPO4
+Lines: 36
+Message-ID: <20101116165556.3ee8e236.rdunlap@xenotime.net>
+References: <1289850773.16461.166.camel@Joe-Laptop>
+       <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+       <20101116195530.GA7523@rakim.wolfsonmicro.main>
+       <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+       <1289940156.28741.207.camel@Joe-Laptop>
+       <20101116124609.382e42fb.rdunlap@xenotime.net>
+       <20101116232258.GC24623@opensource.wolfsonmicro.com>
+       <20101116152835.b0ab571c.rdunlap@xenotime.net>
+       <1289951875.28741.261.camel@Joe-Laptop>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       Mark Brown <broonie@opensource.wolfsonmicro.com>,
+       linux-kernel@vger.kernel.org, Florian Mickler <florian@mickler.org>,
+       Andrew Morton <akpm@linux-foundation.org>
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Wed Nov 17 01:56:19 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PIWJv-0002k1-9C
+       for glad-alsa-devel-2@m.gmane.org; Wed, 17 Nov 2010 01:56:19 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 9953B1038A4; Wed, 17 Nov 2010 01:56:17 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id C25DC24439;
+       Wed, 17 Nov 2010 01:56:09 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 316CC24439; Wed, 17 Nov 2010 01:56:08 +0100 (CET)
+Received: from xenotime.net (xenotime.net [72.52.115.56])
+       by alsa0.perex.cz (Postfix) with SMTP id 043F124436
+       for <alsa-devel@alsa-project.org>; Wed, 17 Nov 2010 01:56:06 +0100 (CET)
+Received: from chimera.site ([173.50.240.230]) by xenotime.net for
+       <alsa-devel@alsa-project.org>; Tue, 16 Nov 2010 16:55:57 -0800
+In-Reply-To: <1289951875.28741.261.camel@Joe-Laptop>
+X-Mailer: Sylpheed 2.7.1 (GTK+ 2.16.6; x86_64-unknown-linux-gnu)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1063536>
+
+On Tue, 16 Nov 2010 15:57:55 -0800 Joe Perches wrote:
+
+> On Tue, 2010-11-16 at 15:28 -0800, Randy Dunlap wrote:
+> > On Tue, 16 Nov 2010 23:22:58 +0000 Mark Brown wrote:
+> > > On Tue, Nov 16, 2010 at 12:46:09PM -0800, Randy Dunlap wrote:
+> > > > On Tue, 16 Nov 2010 12:42:36 -0800 Joe Perches wrote:
+> > > > > Some subsystem maintainers like upper case, some mixed, some lower.
+> > > > > Some aren't consistent.  (Staging/staging)
+> > > > Case usually doesn't matter to most of us.
+> > > Given that we're working in case sensitive languages here it's probably
+> > > safe to assume that a reasonable proportion of people will care; being
+> > > reasonably consistent with existing practice for the subsystem seems
+> > > sensible.
+> > Greg takes patches that say STAGING or Staging or staging.
+> 
+> Greg seems to rewrite patch subjects and is inconsistent
+> about case, so he might be doing that by hand.
+> 
+> > DaveM takes patches that say net: or netdev: or network: or NET:
+> 
+> DaveM doesn't appear to be choosy about patch subject lines.
+> He seems to take any sensible patch and as far as I know he
+> doesn't edit the subject lines.  He does reject inferior
+> patches outright.
+> 
+> > The sound maintainers take patches that say sound: or alsa: or ALSA:
+> 
+> The sound maintainers seem to rewrite patch subjects on an
+> ad-hoc basis.
+
+OK, I can accept your summary.
+However, I can't tell that we are making any progress.
+
+---
+~Randy
+*** Remember to use Documentation/SubmitChecklist when testing your code ***
+
+
diff --git a/test/corpora/lkml/cur/1382298793.003971:2, b/test/corpora/lkml/cur/1382298793.003971:2,
new file mode 100644 (file)
index 0000000..4b510ad
--- /dev/null
@@ -0,0 +1,106 @@
+From: Mark Brown <broonie@opensource.wolfsonmicro.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem
+ maintainer preference tool
+Date: Wed, 17 Nov 2010 17:07:47 +0000
+Lines: 29
+Message-ID: <20101117170746.GB19488@rakim.wolfsonmicro.main>
+References: <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+       <20101116195530.GA7523@rakim.wolfsonmicro.main>
+       <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+       <20101116230126.GB24623@opensource.wolfsonmicro.com>
+       <20101117014427.41d85b13@stein>
+       <alpine.LNX.2.00.1011170150060.7420@pobox.suse.cz>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org,
+       Florian Mickler <florian@mickler.org>, Randy Dunlap <rdunlap@xenotime.net>,
+       Stefan Richter <stefanr@s5r6.in-berlin.de>, Joe Perches <joe@perches.com>,
+       Andrew Morton <akpm@linux-foundation.org>
+To: Jiri Kosina <jkosina@suse.cz>
+X-From: alsa-devel-bounces@alsa-project.org Wed Nov 17 18:07:59 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PIlUD-000436-I4
+       for glad-alsa-devel-2@m.gmane.org; Wed, 17 Nov 2010 18:07:57 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id B1000245CE; Wed, 17 Nov 2010 18:07:54 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id C35471037EC;
+       Wed, 17 Nov 2010 18:07:52 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 4EE8F1037EC; Wed, 17 Nov 2010 18:07:51 +0100 (CET)
+Received: from opensource2.wolfsonmicro.com (opensource.wolfsonmicro.com
+       [80.75.67.52]) by alsa0.perex.cz (Postfix) with ESMTP id A8A16245CE
+       for <alsa-devel@alsa-project.org>; Wed, 17 Nov 2010 18:07:50 +0100 (CET)
+Received: from rakim.wolfsonmicro.main (lumison.wolfsonmicro.com
+       [87.246.78.27])
+       by opensource2.wolfsonmicro.com (Postfix) with ESMTPSA id 9E3591147AC; 
+       Wed, 17 Nov 2010 17:07:48 +0000 (GMT)
+Received: from broonie by rakim.wolfsonmicro.main with local (Exim 4.72)
+       (envelope-from <broonie@rakim.wolfsonmicro.main>)
+       id 1PIlU3-00061C-Bl; Wed, 17 Nov 2010 17:07:47 +0000
+Content-Disposition: inline
+In-Reply-To: <alpine.LNX.2.00.1011170150060.7420@pobox.suse.cz>
+X-Cookie: Killing turkeys causes winter.
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1064007>
+
+On Wed, Nov 17, 2010 at 01:53:35AM +0100, Jiri Kosina wrote:
+> On Wed, 17 Nov 2010, Stefan Richter wrote:
+
+> > Why should we codify our conventions in MAINTAINERS to accommodate the
+> > specific problem of virtually a _single_ patch author?
+
+It seems to be the way we're heading in general - look at all the recent
+work on MAINTAINERS and get_maintainer.pl.  There seems to be a genral
+push to make all this stuff automatable.
+
+> Either the maintainer wants the patch. Then he is certainly able to apply 
+> it no matter the subject line (I personally am getting a lot of patches 
+> which don't follow the format I am using in my tree ... converting 
+> Subject: lines is so trivial that I have never felt like bothering anyone 
+> about it ... it's basically single condition in a shellscript). Or the 
+
+It's slightly more than that if you're dealing with more than one area,
+and I also find this sort of stuff is a good flag for scrubbing the
+patch in greater detail - when patches stand out from a 1000ft visual
+overview there's a fair chance that there's other issues so if people
+regularly submit good patches that have only cosmetic issues I find it's
+worth guiding them away from that.
+
+> maintainer doesn't feel like the patch is worth it, and then the 
+> subject-line format really doesn't matter.
+
+In this case if I don't apply it it's likely to end up going in via your
+tree and then I'll still have to deal with the merge conflicts which are
+more annoying.
+
+
diff --git a/test/corpora/lkml/cur/1382298793.004059:2, b/test/corpora/lkml/cur/1382298793.004059:2,
new file mode 100644 (file)
index 0000000..646ac83
--- /dev/null
@@ -0,0 +1,88 @@
+From: Pavel Machek <pavel@ucw.cz>
+Subject: Re: [PATCH 44/44] sound/soc/codecs: Remove unnecessary
+       semicolons
+Date: Wed, 17 Nov 2010 20:32:56 +0100
+Lines: 19
+Message-ID: <20101117193256.GA28010@ucw.cz>
+References: <cover.1289789604.git.joe@perches.com>
+       <97fd199b7dac50613f6843156687223928cce44a.1289789605.git.joe@perches.com>
+       <20101115134939.GC12986@rakim.wolfsonmicro.main>
+       <1289840957.16461.138.camel@Joe-Laptop>
+       <20101115173031.GI12986@rakim.wolfsonmicro.main>
+       <1289842444.16461.140.camel@Joe-Laptop>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: Dimitris Papastamos <dp@opensource.wolfsonmicro.com>,
+       alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       Takashi Iwai <tiwai@suse.de>, linux-kernel@vger.kernel.org,
+       Mark Brown <broonie@opensource.wolfsonmicro.com>,
+       Ian Lartey <ian@opensource.wolfsonmicro.com>,
+       Liam Girdwood <lrg@slimlogic.co.uk>
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Wed Nov 17 20:33:19 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PInkr-0005bs-LY
+       for glad-alsa-devel-2@m.gmane.org; Wed, 17 Nov 2010 20:33:17 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id D7C501038BF; Wed, 17 Nov 2010 20:33:14 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=disabled
+       version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 193061038BD;
+       Wed, 17 Nov 2010 20:33:08 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 664E91038BD; Wed, 17 Nov 2010 20:33:06 +0100 (CET)
+Received: from atrey.karlin.mff.cuni.cz (ksp.mff.cuni.cz [195.113.26.206])
+       by alsa0.perex.cz (Postfix) with ESMTP id 9E5621038BA
+       for <alsa-devel@alsa-project.org>; Wed, 17 Nov 2010 20:33:05 +0100 (CET)
+Received: by atrey.karlin.mff.cuni.cz (Postfix, from userid 512)
+       id AC6A1F23E1; Wed, 17 Nov 2010 20:33:04 +0100 (CET)
+Content-Disposition: inline
+In-Reply-To: <1289842444.16461.140.camel@Joe-Laptop>
+User-Agent: Mutt/1.5.20 (2009-06-14)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1064096>
+
+On Mon 2010-11-15 09:34:04, Joe Perches wrote:
+> On Mon, 2010-11-15 at 17:30 +0000, Mark Brown wrote:
+> > On Mon, Nov 15, 2010 at 09:09:17AM -0800, Joe Perches wrote:
+> > > Signed-off-by: Joe Perches <joe@perches.com>
+> > Applied, thanks.
+> > Please try to use changelog formats consistent with the code you're
+> > modifying.
+> 
+> I think it's more important to use consistent changelogs
+> for a patch series.
+
+And I agree here. Having to learn code-style quirks for patches is
+bad, having to learn new changelog style for each subsystem is very
+bad.
+                                                               Pavel
+
+-- 
+(english) http://www.livejournal.com/~pavelmachek
+(cesky, pictures) http://atrey.karlin.mff.cuni.cz/~pavel/picture/horses/blog.html
+
+
diff --git a/test/corpora/lkml/cur/1382298793.004091:2, b/test/corpora/lkml/cur/1382298793.004091:2,
new file mode 100644 (file)
index 0000000..1abc297
--- /dev/null
@@ -0,0 +1,133 @@
+From: Stefan Richter <stefanr@s5r6.in-berlin.de>
+Subject: Re: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Wed, 17 Nov 2010 22:07:37 +0100
+Lines: 74
+Message-ID: <20101117220737.2d3d7356@stein>
+References: <20101116104921.GL12986@rakim.wolfsonmicro.main>
+       <1289919077.28741.50.camel@Joe-Laptop>
+       <20101116183707.179964dd@schatten.dmk.lab>
+       <20101116181226.GB26239@rakim.wolfsonmicro.main>
+       <20101116203522.65240b18@schatten.dmk.lab>
+       <20101116195530.GA7523@rakim.wolfsonmicro.main>
+       <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+       <20101116230126.GB24623@opensource.wolfsonmicro.com>
+       <20101117014427.41d85b13@stein>
+       <alpine.LNX.2.00.1011170150060.7420@pobox.suse.cz>
+       <20101117170746.GB19488@rakim.wolfsonmicro.main>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=US-ASCII
+Content-Transfer-Encoding: 7bit
+Cc: Jiri Kosina <jkosina@suse.cz>, Randy Dunlap <rdunlap@xenotime.net>,
+       Florian Mickler <florian@mickler.org>,
+       Joe Perches <joe@perches.com>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Mark Brown <broonie@opensource.wolfsonmicro.com>
+X-From: linux-kernel-owner@vger.kernel.org Wed Nov 17 22:08:15 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PIpEk-0006ge-Hz
+       for glk-linux-kernel-3@lo.gmane.org; Wed, 17 Nov 2010 22:08:14 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1758632Ab0KQVHz (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Wed, 17 Nov 2010 16:07:55 -0500
+Received: from einhorn.in-berlin.de ([192.109.42.8]:48630 "EHLO
+       einhorn.in-berlin.de" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751563Ab0KQVHy (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Wed, 17 Nov 2010 16:07:54 -0500
+X-Envelope-From: stefanr@s5r6.in-berlin.de
+Received: from stein ([83.221.231.7])
+       (authenticated bits=0)
+       by einhorn.in-berlin.de (8.13.6/8.13.6/Debian-1) with ESMTP id oAHL7dhi014114
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES128-SHA bits=128 verify=NOT);
+       Wed, 17 Nov 2010 22:07:39 +0100
+In-Reply-To: <20101117170746.GB19488@rakim.wolfsonmicro.main>
+X-Mailer: Claws Mail 3.7.6 (GTK+ 2.20.1; x86_64-pc-linux-gnu)
+X-Scanned-By: MIMEDefang_at_IN-Berlin_e.V. on 192.109.42.8
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1064128>
+
+On Nov 17 Mark Brown wrote:
+> On Wed, Nov 17, 2010 at 01:53:35AM +0100, Jiri Kosina wrote:
+> > On Wed, 17 Nov 2010, Stefan Richter wrote:
+> 
+> > > Why should we codify our conventions in MAINTAINERS to accommodate the
+> > > specific problem of virtually a _single_ patch author?
+> 
+> It seems to be the way we're heading in general - look at all the recent
+> work on MAINTAINERS and get_maintainer.pl.  There seems to be a genral
+> push to make all this stuff automatable.
+
+get_maintainer.pl, used with judgment and together with "gitk
+the/patched/source.c" is nice not only for people like Joe who
+regularly work tree-wide but also for ones like me who only rarely want
+to submit a bug report or patch for a subsystem with they are
+unfamiliar with.
+
+But the thought of a database of "how to start a good patch title" is
+far-fetched.  Really, as a patch author, just look how other people
+write patch titles and judge whether this is good for your work too or
+not.
+
+> > Either the maintainer wants the patch. Then he is certainly able to apply 
+> > it no matter the subject line (I personally am getting a lot of patches 
+> > which don't follow the format I am using in my tree ... converting 
+> > Subject: lines is so trivial that I have never felt like bothering anyone 
+> > about it ... it's basically single condition in a shellscript). Or the 
+> 
+> It's slightly more than that if you're dealing with more than one area,
+> and I also find this sort of stuff is a good flag for scrubbing the
+> patch in greater detail - when patches stand out from a 1000ft visual
+> overview there's a fair chance that there's other issues so if people
+> regularly submit good patches that have only cosmetic issues I find it's
+> worth guiding them away from that.
+
+On one hand Jiri is right that maintainers can adjust title prefixes ad
+hoc.  (Downside:  Weaker connection to mailinglist archives.)  On the
+other hand, in the case of long-term prolific authors like Joe it is
+more optimal if there is a good patch title right from the outset.
+
+So, if this boring thread does at least yield the conclusion that
+${path}/${filename}: is a bad title prefix, at least something was
+won. :-)
+
+Another thought:  Whether a typical part of a mass conversion, e.g. to
+use a new helper macro without change of functionality, is named
+
+       [PATCH] [subsystem] driver: use foo_bar helper
+or
+       [PATCH] use foo_bar helper in subsystem, driver
+
+does not really matter, does it?  This change is more about the helper
+than about the driver.  It is really a different kind of changeset than
+a functional change that we want to be called
+
+       [PATCH] [subsystem] driver: fix crash at disconnection
+
+or so.  This is something that those who look for release notes of
+that driver or subsystem want to grep in the changelog.
+
+Or in other words:  If you as patch author wonder what would be a good
+title for your patch, then ask yourself:  How should this change show up
+in kernel release notes that are constructed from the git shortlog?
+Sometimes the answer to this question includes among else a prefix with
+a canonical subsystem name (even case sensitive, with brackets or
+colon), whereas other times such formalities are utterly pointless.
+
+[Sorry for the spent electrons.  But OTOH, issues like (1.) optimum
+use of reviewer bandwidth, (2.) kernel changelog alias release
+notes /do/ matter.]
+-- 
+Stefan Richter
+-=====-==-=- =-== =---=
+http://arcgraph.de/sr/
+
+
diff --git a/test/corpora/lkml/cur/1382298793.004190:2, b/test/corpora/lkml/cur/1382298793.004190:2,
new file mode 100644 (file)
index 0000000..10a54af
--- /dev/null
@@ -0,0 +1,67 @@
+From: Joe Perches <joe@perches.com>
+Subject: Re: rfc: rewrite commit subject line for subsystem maintainer
+ preference tool
+Date: Wed, 17 Nov 2010 15:49:19 -0800
+Lines: 11
+Message-ID: <1290037759.28741.313.camel@Joe-Laptop>
+References: <20101116104921.GL12986@rakim.wolfsonmicro.main>
+        <1289919077.28741.50.camel@Joe-Laptop>
+        <20101116183707.179964dd@schatten.dmk.lab>
+        <20101116181226.GB26239@rakim.wolfsonmicro.main>
+        <20101116203522.65240b18@schatten.dmk.lab>
+        <20101116195530.GA7523@rakim.wolfsonmicro.main>
+        <20101116122102.86e7e0b9.rdunlap@xenotime.net>
+        <20101116230126.GB24623@opensource.wolfsonmicro.com>
+        <20101117014427.41d85b13@stein>
+        <alpine.LNX.2.00.1011170150060.7420@pobox.suse.cz>
+        <20101117170746.GB19488@rakim.wolfsonmicro.main>
+        <20101117220737.2d3d7356@stein>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: 7bit
+Cc: Mark Brown <broonie@opensource.wolfsonmicro.com>,
+       Jiri Kosina <jkosina@suse.cz>,
+       Randy Dunlap <rdunlap@xenotime.net>,
+       Florian Mickler <florian@mickler.org>,
+       Andrew Morton <akpm@linux-foundation.org>,
+       alsa-devel@alsa-project.org, linux-kernel@vger.kernel.org
+To: Stefan Richter <stefanr@s5r6.in-berlin.de>
+X-From: linux-kernel-owner@vger.kernel.org Thu Nov 18 00:49:42 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PIrkz-0000bE-Ro
+       for glk-linux-kernel-3@lo.gmane.org; Thu, 18 Nov 2010 00:49:42 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752426Ab0KQXtX (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Wed, 17 Nov 2010 18:49:23 -0500
+Received: from mail.perches.com ([173.55.12.10]:1507 "EHLO mail.perches.com"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1750850Ab0KQXtW (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Wed, 17 Nov 2010 18:49:22 -0500
+Received: from [192.168.1.162] (unknown [192.168.1.162])
+       by mail.perches.com (Postfix) with ESMTP id 7D75724368;
+       Wed, 17 Nov 2010 15:49:17 -0800 (PST)
+In-Reply-To: <20101117220737.2d3d7356@stein>
+X-Mailer: Evolution 2.30.3 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1064228>
+
+On Wed, 2010-11-17 at 22:07 +0100, Stefan Richter wrote:
+> So, if this boring thread does at least yield the conclusion that
+> ${path}/${filename}: is a bad title prefix, at least something was
+> won. :-)
+
+I've changed my scripts to use this style:
+
+Subject: [PATCH] $(basename $(dirname $file)): commit desc... 
+
+until a better tool is available.
+
+
+
diff --git a/test/corpora/lkml/cur/1382298795.000299:2, b/test/corpora/lkml/cur/1382298795.000299:2,
new file mode 100644 (file)
index 0000000..ea8522f
--- /dev/null
@@ -0,0 +1,79 @@
+From: Artem Bityutskiy <dedekind1@gmail.com>
+Subject: Re: [PATCH 37/44] fs/ubifs: Remove unnecessary semicolons
+Date: Fri, 19 Nov 2010 15:21:08 +0200
+Lines: 13
+Message-ID: <1290172868.4768.2.camel@localhost>
+References: <cover.1289789604.git.joe@perches.com>
+        <902d76520da2f65e5dc44339dccb07159947f23d.1289789605.git.joe@perches.com>
+Reply-To: dedekind1@gmail.com
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: Jiri Kosina <trivial@kernel.org>,
+       Adrian Hunter <adrian.hunter@nokia.com>,
+       linux-mtd@lists.infradead.org, linux-kernel@vger.kernel.org
+To: Joe Perches <joe@perches.com>
+X-From: linux-kernel-owner@vger.kernel.org Fri Nov 19 14:22:08 2010
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PJQuj-00008T-0Z
+       for glk-linux-kernel-3@lo.gmane.org; Fri, 19 Nov 2010 14:22:05 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1754099Ab0KSNVg convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Fri, 19 Nov 2010 08:21:36 -0500
+Received: from mail-bw0-f46.google.com ([209.85.214.46]:60443 "EHLO
+       mail-bw0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1753270Ab0KSNVf (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Fri, 19 Nov 2010 08:21:35 -0500
+Received: by bwz15 with SMTP id 15so3864554bwz.19
+        for <linux-kernel@vger.kernel.org>; Fri, 19 Nov 2010 05:21:33 -0800 (PST)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:received:received:subject:from:reply-to:to:cc
+         :in-reply-to:references:content-type:date:message-id:mime-version
+         :x-mailer:content-transfer-encoding;
+        bh=I+JFeMB9svh3ZIIGdVlHav4mOQmOa+QTh4VZGVL0a/0=;
+        b=hLukn/U4YkodFZ8CEkuJJmYvpTDXhavKiL1YZ12QApXyCb9xBeYORheXEIQUygjUpL
+         Fy9zyFIWzw3YAiLEa4WUJnC4L+VWq4Nhtua9a1XBQBCK8HZuDITUcmtYcobib1kBg4KE
+         0nBOF7IL6d17HN8QGC+Nn+YTu7JxHSq4cHy8Y=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=subject:from:reply-to:to:cc:in-reply-to:references:content-type
+         :date:message-id:mime-version:x-mailer:content-transfer-encoding;
+        b=WiSerhUAnUL/tGRi8PqwwkXymz8N1Uf58rludUDWzSk+L3KSJqGtAvdv8xYqW8x4PQ
+         Vla/qoLPjcqQQmaLgLKtDvGIL9BFQ86dXrgokJhcrT1qSs2xN3VKGdhm49MOd/HrYZ/F
+         v4wespzjVohrMt2tOP38nOKZpPeu4t1wUJYOQ=
+Received: by 10.204.79.142 with SMTP id p14mr1988895bkk.175.1290172893650;
+        Fri, 19 Nov 2010 05:21:33 -0800 (PST)
+Received: from ?IPv6:::1? (shutemov.name [188.40.19.243])
+        by mx.google.com with ESMTPS id v25sm850549bkt.6.2010.11.19.05.21.30
+        (version=SSLv3 cipher=RC4-MD5);
+        Fri, 19 Nov 2010 05:21:31 -0800 (PST)
+In-Reply-To: <902d76520da2f65e5dc44339dccb07159947f23d.1289789605.git.joe@perches.com>
+X-Mailer: Evolution 2.32.0 (2.32.0-2.fc14) 
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1065301>
+
+On Sun, 2010-11-14 at 19:04 -0800, Joe Perches wrote:
+> Signed-off-by: Joe Perches <joe@perches.com>
+> ---
+>  fs/ubifs/scan.c |    2 +-
+>  1 files changed, 1 insertions(+), 1 deletions(-)
+
+Thanks, I'll pick this up.
+
+--=20
+Best Regards,
+Artem Bityutskiy (=D0=90=D1=80=D1=82=D1=91=D0=BC =D0=91=D0=B8=D1=82=D1=8E=
+=D1=86=D0=BA=D0=B8=D0=B9)
+
+
+
diff --git a/test/corpora/lkml/cur/1382298795.001362:2, b/test/corpora/lkml/cur/1382298795.001362:2,
new file mode 100644 (file)
index 0000000..a723d58
--- /dev/null
@@ -0,0 +1,96 @@
+From: Takashi Iwai <tiwai@suse.de>
+Subject: Re: [PATCH 43/44] sound/core/pcm_lib.c: Remove
+       unnecessary semicolons
+Date: Mon, 22 Nov 2010 07:44:21 +0100
+Lines: 31
+Message-ID: <s5h4ob9svqy.wl%tiwai@suse.de>
+References: <cover.1289789604.git.joe@perches.com>
+       <9fa8e193ce125ef4fd19a952792629c5ee84953f.1289789605.git.joe@perches.com>
+Mime-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Cc: alsa-devel@alsa-project.org, Jiri Kosina <trivial@kernel.org>,
+       linux-kernel@vger.kernel.org
+To: Joe Perches <joe@perches.com>
+X-From: alsa-devel-bounces@alsa-project.org Mon Nov 22 07:44:31 2010
+Return-path: <alsa-devel-bounces@alsa-project.org>
+Envelope-to: glad-alsa-devel-2@m.gmane.org
+Received: from alsa0.perex.cz ([212.20.107.51])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <alsa-devel-bounces@alsa-project.org>)
+       id 1PKQ8Y-0002qf-F0
+       for glad-alsa-devel-2@m.gmane.org; Mon, 22 Nov 2010 07:44:26 +0100
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 26BFF103863; Mon, 22 Nov 2010 07:44:26 +0100 (CET)
+X-Spam-Checker-Version: SpamAssassin 3.2.4 (2008-01-01) on mail1.perex.cz
+X-Spam-Level: 
+X-Spam-Status: No, score=-8.0 required=5.0 tests=RCVD_IN_DNSWL_HI
+       autolearn=disabled version=3.2.4
+Received: from alsa0.perex.cz (localhost [127.0.0.1])
+       by alsa0.perex.cz (Postfix) with ESMTP id 1E4EF103848;
+       Mon, 22 Nov 2010 07:44:24 +0100 (CET)
+X-Original-To: alsa-devel@alsa-project.org
+Delivered-To: alsa-devel@alsa-project.org
+Received: by alsa0.perex.cz (Postfix, from userid 1000)
+       id 0861E103851; Mon, 22 Nov 2010 07:44:22 +0100 (CET)
+Received: from mx1.suse.de (cantor.suse.de [195.135.220.2])
+       by alsa0.perex.cz (Postfix) with ESMTP id 7007710383C
+       for <alsa-devel@alsa-project.org>; Mon, 22 Nov 2010 07:44:21 +0100 (CET)
+Received: from relay2.suse.de (charybdis-ext.suse.de [195.135.221.2])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by mx1.suse.de (Postfix) with ESMTP id 53C1B947B6;
+       Mon, 22 Nov 2010 07:44:21 +0100 (CET)
+In-Reply-To: <9fa8e193ce125ef4fd19a952792629c5ee84953f.1289789605.git.joe@perches.com>
+User-Agent: Wanderlust/2.15.6 (Almost Unreal) SEMI/1.14.6 (Maruoka)
+       FLIM/1.14.9 (=?UTF-8?B?R29qxY0=?=) APEL/10.7 Emacs/23.1
+       (x86_64-suse-linux-gnu) MULE/6.0 (HANACHIRUSATO)
+X-BeenThere: alsa-devel@alsa-project.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+List-Id: "Alsa-devel mailing list for ALSA developers -
+       http://www.alsa-project.org" <alsa-devel.alsa-project.org>
+List-Unsubscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=unsubscribe>
+List-Archive: <http://mailman.alsa-project.org/pipermail/alsa-devel>
+List-Post: <mailto:alsa-devel@alsa-project.org>
+List-Help: <mailto:alsa-devel-request@alsa-project.org?subject=help>
+List-Subscribe: <http://mailman.alsa-project.org/mailman/listinfo/alsa-devel>, 
+       <mailto:alsa-devel-request@alsa-project.org?subject=subscribe>
+Sender: alsa-devel-bounces@alsa-project.org
+Errors-To: alsa-devel-bounces@alsa-project.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1066369>
+
+At Sun, 14 Nov 2010 19:05:02 -0800,
+Joe Perches wrote:
+> 
+> Signed-off-by: Joe Perches <joe@perches.com>
+
+Applied now.  Thanks.
+
+
+Takashi
+
+
+> ---
+>  sound/core/pcm_lib.c |    2 +-
+>  1 files changed, 1 insertions(+), 1 deletions(-)
+> 
+> diff --git a/sound/core/pcm_lib.c b/sound/core/pcm_lib.c
+> index a1707cc..b75db8e 100644
+> --- a/sound/core/pcm_lib.c
+> +++ b/sound/core/pcm_lib.c
+> @@ -223,7 +223,7 @@ static void xrun_log(struct snd_pcm_substream *substream,
+>      entry->jiffies = jiffies;
+>      entry->pos = pos;
+>      entry->period_size = runtime->period_size;
+> -    entry->buffer_size = runtime->buffer_size;;
+> +    entry->buffer_size = runtime->buffer_size;
+>      entry->old_hw_ptr = runtime->status->hw_ptr;
+>      entry->hw_ptr_base = runtime->hw_ptr_base;
+>      log->idx = (log->idx + 1) % XRUN_LOG_CNT;
+> -- 
+> 1.7.3.1.g432b3.dirty
+> 
+
+
diff --git a/test/corpora/lkml/cur/1382298795.002635:2, b/test/corpora/lkml/cur/1382298795.002635:2,
new file mode 100644 (file)
index 0000000..3c04b86
--- /dev/null
@@ -0,0 +1,54 @@
+From: Matthew Garrett <mjg59@srcf.ucam.org>
+Subject: Re: [PATCH 19/44] drivers/platform/x86: Remove unnecessary
+       semicolons
+Date: Wed, 24 Nov 2010 16:52:46 +0000
+Lines: 4
+Message-ID: <20101124165246.GH2150@srcf.ucam.org>
+References: <cover.1289789604.git.joe@perches.com> <eda82bcfaad265fc5cd3901bc4f41bfcfac2403b.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Cc: Jiri Kosina <trivial@kernel.org>,
+       Thadeu Lima de Souza Cascardo <cascardo@holoscopio.com>,
+       Daniel Oliveira Nascimento <don@syst.com.br>,
+       Henrique de Moraes Holschuh <ibm-acpi@hmh.eng.br>,
+       platform-driver-x86@vger.kernel.org, linux-kernel@vger.kernel.org,
+       ibm-acpi-devel@lists.sourceforge.net
+To: Joe Perches <joe@perches.com>
+X-From: platform-driver-x86-owner@vger.kernel.org Wed Nov 24 17:53:05 2010
+Return-path: <platform-driver-x86-owner@vger.kernel.org>
+Envelope-to: gldpxd-platform-driver-x86@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <platform-driver-x86-owner@vger.kernel.org>)
+       id 1PLIae-0001zN-Vf
+       for gldpxd-platform-driver-x86@lo.gmane.org; Wed, 24 Nov 2010 17:53:05 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1754012Ab0KXQxE (ORCPT
+       <rfc822;gldpxd-platform-driver-x86@m.gmane.org>);
+       Wed, 24 Nov 2010 11:53:04 -0500
+Received: from cavan.codon.org.uk ([93.93.128.6]:37338 "EHLO
+       cavan.codon.org.uk" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1753985Ab0KXQxD (ORCPT
+       <rfc822;platform-driver-x86@vger.kernel.org>);
+       Wed, 24 Nov 2010 11:53:03 -0500
+Received: from mjg59 by cavan.codon.org.uk with local (Exim 4.69)
+       (envelope-from <mjg59@cavan.codon.org.uk>)
+       id 1PLIaM-0000fC-HX; Wed, 24 Nov 2010 16:52:46 +0000
+Content-Disposition: inline
+In-Reply-To: <eda82bcfaad265fc5cd3901bc4f41bfcfac2403b.1289789604.git.joe@perches.com>
+User-Agent: Mutt/1.5.18 (2008-05-17)
+X-SA-Exim-Connect-IP: <locally generated>
+X-SA-Exim-Mail-From: mjg59@cavan.codon.org.uk
+X-SA-Exim-Scanned: No (on cavan.codon.org.uk); SAEximRunCond expanded to false
+Sender: platform-driver-x86-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <platform-driver-x86.vger.kernel.org>
+X-Mailing-List: platform-driver-x86@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1067655>
+
+Applied, thanks.
+
+-- 
+Matthew Garrett | mjg59@srcf.ucam.org
+
+
diff --git a/test/corpora/lkml/cur/1382298796.001941:2, b/test/corpora/lkml/cur/1382298796.001941:2,
new file mode 100644 (file)
index 0000000..c65a72f
--- /dev/null
@@ -0,0 +1,73 @@
+From: Chris Ball <cjb@laptop.org>
+Subject: Re: [PATCH 11/44] drivers/mmc: Remove unnecessary semicolons
+Date: Sun, 5 Dec 2010 03:32:32 +0000
+Lines: 33
+Message-ID: <20101205033232.GD24000@void.printf.net>
+References: <cover.1289789604.git.joe@perches.com> <6391af02ba7ec4a76c5c5f462d8013fc1f52f999.1289789604.git.joe@perches.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Cc: Jiri Kosina <trivial@kernel.org>, linux-mmc@vger.kernel.org,
+       linux-kernel@vger.kernel.org
+To: Joe Perches <joe@perches.com>
+X-From: linux-mmc-owner@vger.kernel.org Sun Dec 05 04:32:38 2010
+Return-path: <linux-mmc-owner@vger.kernel.org>
+Envelope-to: glkm-linux-mmc@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-mmc-owner@vger.kernel.org>)
+       id 1PP5L3-0005AB-4b
+       for glkm-linux-mmc@lo.gmane.org; Sun, 05 Dec 2010 04:32:37 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752522Ab0LEDcg (ORCPT <rfc822;glkm-linux-mmc@m.gmane.org>);
+       Sat, 4 Dec 2010 22:32:36 -0500
+Received: from void.printf.net ([89.145.121.20]:42897 "EHLO void.printf.net"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1752473Ab0LEDcf (ORCPT <rfc822;linux-mmc@vger.kernel.org>);
+       Sat, 4 Dec 2010 22:32:35 -0500
+Received: from chris by void.printf.net with local (Exim 4.69)
+       (envelope-from <chris@void.printf.net>)
+       id 1PP5Ky-0006Ly-KC; Sun, 05 Dec 2010 03:32:32 +0000
+Content-Disposition: inline
+In-Reply-To: <6391af02ba7ec4a76c5c5f462d8013fc1f52f999.1289789604.git.joe@perches.com>
+User-Agent: Mutt/1.5.18 (2008-05-17)
+Sender: linux-mmc-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-mmc.vger.kernel.org>
+X-Mailing-List: linux-mmc@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1071959>
+
+Hi Joe,
+
+On Sun, Nov 14, 2010 at 07:04:30PM -0800, Joe Perches wrote:
+> Signed-off-by: Joe Perches <joe@perches.com>
+> ---
+>  drivers/mmc/host/davinci_mmc.c |    2 +-
+>  1 files changed, 1 insertions(+), 1 deletions(-)
+> 
+> diff --git a/drivers/mmc/host/davinci_mmc.c b/drivers/mmc/host/davinci_mmc.c
+> index e15547c..b643dde 100644
+> --- a/drivers/mmc/host/davinci_mmc.c
+> +++ b/drivers/mmc/host/davinci_mmc.c
+> @@ -480,7 +480,7 @@ static void mmc_davinci_send_dma_request(struct mmc_davinci_host *host,
+>      struct scatterlist      *sg;
+>      unsigned                sg_len;
+>      unsigned                bytes_left = host->bytes_left;
+> -    const unsigned          shift = ffs(rw_threshold) - 1;;
+> +    const unsigned          shift = ffs(rw_threshold) - 1;
+>  
+>      if (host->data_dir == DAVINCI_MMC_DATADIR_WRITE) {
+>              template = &host->tx_template;
+> -- 
+
+Pushed to mmc-next for .38, thanks.
+
+-- 
+Chris Ball   <cjb@laptop.org>   <http://printf.net/>
+One Laptop Per Child
+--
+To unsubscribe from this list: send the line "unsubscribe linux-mmc" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004526:2, b/test/corpora/lkml/cur/1382298805.004526:2,
new file mode 100644 (file)
index 0000000..05dbb9d
--- /dev/null
@@ -0,0 +1,89 @@
+From: Colin Cross <ccross@android.com>
+Subject: [PATCH] ARM: vfp: Always save VFP state in vfp_pm_suspend
+Date: Sun, 13 Feb 2011 15:13:33 -0800
+Lines: 45
+Message-ID: <1297638813-1315-1-git-send-email-ccross@android.com>
+Cc: Colin Cross <ccross@android.com>,
+       Catalin Marinas <catalin.marinas@arm.com>,
+       Russell King <linux@arm.linux.org.uk>,
+       linux-kernel@vger.kernel.org
+To: linux-arm-kernel@lists.infradead.org
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 00:14:16 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1Pol8x-0007RZ-Hw
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 00:14:15 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1755242Ab1BMXOA (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Sun, 13 Feb 2011 18:14:00 -0500
+Received: from smtp-out.google.com ([74.125.121.67]:10204 "EHLO
+       smtp-out.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1753669Ab1BMXN5 (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Sun, 13 Feb 2011 18:13:57 -0500
+Received: from hpaq1.eem.corp.google.com (hpaq1.eem.corp.google.com [172.25.149.1])
+       by smtp-out.google.com with ESMTP id p1DNDjFc030645;
+       Sun, 13 Feb 2011 15:13:45 -0800
+Received: from walnut.mtv.corp.google.com (walnut.mtv.corp.google.com [172.18.102.62])
+       by hpaq1.eem.corp.google.com with ESMTP id p1DNDdKg016468;
+       Sun, 13 Feb 2011 15:13:39 -0800
+Received: by walnut.mtv.corp.google.com (Postfix, from userid 99897)
+       id 1F65025772D; Sun, 13 Feb 2011 15:13:39 -0800 (PST)
+X-Mailer: git-send-email 1.7.3.1
+X-System-Of-Record: true
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099558>
+
+vfp_pm_suspend should save the VFP state any time there is
+a last_VFP_context.  If it only saves when the VFP is enabled,
+the state can get lost when, on a UP system:
+   Thread 1 uses the VFP
+   Context switch occurs to thread 2, VFP is disabled but the
+      VFP context is not saved to allow lazy save and restore
+   Thread 2 initiates suspend
+   vfp_pm_suspend is called with the VFP disabled, but the
+      context has not been saved.
+
+Modify vfp_pm_suspend to save the VFP context whenever
+last_VFP_context is set.
+
+Cc: Catalin Marinas <catalin.marinas@arm.com>
+Signed-off-by: Colin Cross <ccross@android.com>
+---
+ arch/arm/vfp/vfpmodule.c |   11 +++++------
+ 1 files changed, 5 insertions(+), 6 deletions(-)
+
+diff --git a/arch/arm/vfp/vfpmodule.c b/arch/arm/vfp/vfpmodule.c
+index 66bf8d1..7aea616 100644
+--- a/arch/arm/vfp/vfpmodule.c
++++ b/arch/arm/vfp/vfpmodule.c
+@@ -415,13 +415,12 @@ static int vfp_pm_suspend(struct sys_device *dev, pm_message_t state)
+       struct thread_info *ti = current_thread_info();
+       u32 fpexc = fmrx(FPEXC);
+-      /* if vfp is on, then save state for resumption */
+-      if (fpexc & FPEXC_EN) {
++      /* save state for resume */
++      if (last_VFP_context[ti->cpu]) {
+               printk(KERN_DEBUG "%s: saving vfp state\n", __func__);
+-              vfp_save_state(&ti->vfpstate, fpexc);
+-
+-              /* disable, just in case */
+-              fmxr(FPEXC, fmrx(FPEXC) & ~FPEXC_EN);
++              fmxr(FPEXC, fpexc | FPEXC_EN);
++              vfp_save_state(last_VFP_context[ti->cpu], fpexc);
++              fmxr(FPEXC, fpexc & ~FPEXC_EN);
+       }
+       /* clear any information we had about last context state */
+-- 
+1.7.3.1
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004551:2, b/test/corpora/lkml/cur/1382298805.004551:2,
new file mode 100644 (file)
index 0000000..0e21b16
--- /dev/null
@@ -0,0 +1,90 @@
+From: Justin Mattock <justinmattock-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
+Subject: bluetooth disabled with current 2.6.38-rc4
+Date: Sun, 13 Feb 2011 17:30:04 -0800
+Lines: 30
+Message-ID: <1A8743E5-65EA-4625-82FD-658C9722629F@gmail.com>
+Mime-Version: 1.0 (Apple Message framework v936)
+Content-Type: text/plain; charset=US-ASCII; format=flowed; delsp=yes
+Content-Transfer-Encoding: 7bit
+Cc: "linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Mailing List" 
+       <linux-kernel-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+To: linux-bluetooth-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+X-From: linux-bluetooth-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org Mon Feb 14 02:30:20 2011
+Return-path: <linux-bluetooth-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>
+Envelope-to: glbk-linux-bluetooth-1dZseelyfdZg9hUCZPvPmw@public.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-bluetooth-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>)
+       id 1PonGb-0000BP-Ns
+       for glbk-linux-bluetooth-1dZseelyfdZg9hUCZPvPmw@public.gmane.org; Mon, 14 Feb 2011 02:30:18 +0100
+Received: (majordomo-u79uwXL29TY76Z2rM5mHXA@public.gmane.org) by vger.kernel.org via listexpand
+       id S1755415Ab1BNBaO (ORCPT
+       <rfc822;glbk-linux-bluetooth@m.gmane.org>);
+       Sun, 13 Feb 2011 20:30:14 -0500
+Received: from mail-iw0-f174.google.com ([209.85.214.174]:33676 "EHLO
+       mail-iw0-f174.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1755315Ab1BNBaJ (ORCPT
+       <rfc822;linux-bluetooth-u79uwXL29TY76Z2rM5mHXA@public.gmane.org>);
+       Sun, 13 Feb 2011 20:30:09 -0500
+Received: by iwn9 with SMTP id 9so4315727iwn.19
+        for <multiple recipients>; Sun, 13 Feb 2011 17:30:08 -0800 (PST)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:message-id:from:to:content-type
+         :content-transfer-encoding:mime-version:subject:date:cc:x-mailer;
+        bh=Hm2xpT9F5uspvKowKW51PBMJXHVySz8oO68WD+15vw4=;
+        b=CYWbVChg8u/vPwCQijLtu4qwy88RnlkiXipfYaorEsKoqnL/riJzvgVjtYz3uoSWE5
+         m9IsBgZBGd2zuZMuDEfGiLQo1h5ReLbCsQ2FSLRM8dW15g3xENkK0Zd86EHNATbnU4CQ
+         YMF3gYQHr5BffWBu8xllNHnUKHzMGZz827BEk=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=message-id:from:to:content-type:content-transfer-encoding
+         :mime-version:subject:date:cc:x-mailer;
+        b=DU4h/EWn8O7B48JT0DuiMTjHma3v+7cSup8eYqLmOgYopjvr42kO9BACgxHMR/mpI/
+         sa4AtFAuWg3TkZkPOjJg2SiPiUGQcj7kqjycvHWvWiQHEE6tLEH7g6aGF7ojSTrsiJxN
+         zpjZBS0EvbDtIrQf8YAV9eFQJSkQ5yYXmK00c=
+Received: by 10.42.228.201 with SMTP id jf9mr3782213icb.471.1297647007933;
+        Sun, 13 Feb 2011 17:30:07 -0800 (PST)
+Received: from [10.0.0.13] ([76.89.133.205])
+        by mx.google.com with ESMTPS id y8sm1925328ica.2.2011.02.13.17.30.06
+        (version=TLSv1/SSLv3 cipher=OTHER);
+        Sun, 13 Feb 2011 17:30:07 -0800 (PST)
+X-Mailer: Apple Mail (2.936)
+Sender: linux-bluetooth-owner-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Precedence: bulk
+List-ID: <linux-bluetooth.vger.kernel.org>
+X-Mailing-List: linux-bluetooth-u79uwXL29TY76Z2rM5mHXA@public.gmane.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099583>
+
+maybe I missed something, but my bluetooth is just not functioning with
+2.6.38-rc4(works with 2.6.37-rc4)
+
+I've done a bisect on this, but was pointed to:
+c0e45c1ca3162acb2e77b3d9e152ce6e7b6fa3f5
+but doesn't look correct to me
+
+here is what I am seeing with the bluetooth-applet etc..:
+
+working correctly:
+http://www.flickr.com/photos/44066293@N08/5443727238/
+
+not working:
+http://www.flickr.com/photos/44066293@N08/5443124859/
+
+my /var/log/daemon.log shows:
+
+Feb 13 17:12:22 Linux-2 acpid: 1 client rule loaded
+Feb 13 17:12:23 Linux-2 bluetoothd[1950]: HCI dev 0 registered
+Feb 13 17:12:23 Linux-2 bluetoothd[1950]: Listening for HCI events on  
+hci0
+Feb 13 17:12:23 Linux-2 bluetoothd[1950]: HCI dev 0 up
+Feb 13 17:12:23 Linux-2 bluetoothd[1950]: Unable to find matching  
+adapter
+
+I can try at another bisect, but might take some time.. let me know if  
+there is something I can test
+or do.
+
+Justin P. Mattock
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004613:2, b/test/corpora/lkml/cur/1382298805.004613:2,
new file mode 100644 (file)
index 0000000..5a12c8b
--- /dev/null
@@ -0,0 +1,94 @@
+From: Henrik Kretzschmar <henne@nachtwindheim.de>
+Subject: [PATCH 1/6] x86: move ioapic_irq_destination_types
+Date: Mon, 14 Feb 2011 11:00:07 +0100
+Lines: 55
+Message-ID: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Cc: tglx@linutronix.de, hpa@zytor.com, x86@kernel.org, tj@kernel.org,
+       yinghai@kernel.org, ak@linux.intel.com, robert.richter@amd.com,
+       linux-kernel@vger.kernel.org, henne@nachtwindheim.de
+To: mingo@readhat.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 11:00:33 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PovEP-0006ED-4Z
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 11:00:33 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752832Ab1BNKAX (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 05:00:23 -0500
+Received: from server103.greatnet.de ([83.133.97.6]:38305 "EHLO
+       server103.greatnet.de" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752268Ab1BNKAW (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 05:00:22 -0500
+Received: from localhost.localdomain (cmnz-d9bab6be.pool.mediaWays.net [217.186.182.190])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by server103.greatnet.de (Postfix) with ESMTPSA id D2234950DB2;
+       Mon, 14 Feb 2011 10:59:03 +0100 (CET)
+X-Mailer: git-send-email 1.7.1
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099645>
+
+This enum is used also by non-ioapic code, e.g apic_noop,
+so its better kept in apicdef.h.
+
+Signed-off-by: Henrik Kretzschmar <henne@nachtwindheim.de>
+---
+ arch/x86/include/asm/apicdef.h |   12 ++++++++++++
+ arch/x86/include/asm/io_apic.h |   11 -----------
+ 2 files changed, 12 insertions(+), 11 deletions(-)
+
+diff --git a/arch/x86/include/asm/apicdef.h b/arch/x86/include/asm/apicdef.h
+index 47a30ff..2de3e95 100644
+--- a/arch/x86/include/asm/apicdef.h
++++ b/arch/x86/include/asm/apicdef.h
+@@ -426,4 +426,16 @@ struct local_apic {
+ #else
+  #define BAD_APICID 0xFFFFu
+ #endif
++
++enum ioapic_irq_destination_types {
++      dest_Fixed = 0,
++      dest_LowestPrio = 1,
++      dest_SMI = 2,
++      dest__reserved_1 = 3,
++      dest_NMI = 4,
++      dest_INIT = 5,
++      dest__reserved_2 = 6,
++      dest_ExtINT = 7
++};
++
+ #endif /* _ASM_X86_APICDEF_H */
+diff --git a/arch/x86/include/asm/io_apic.h b/arch/x86/include/asm/io_apic.h
+index f327d38..e1a9b0e 100644
+--- a/arch/x86/include/asm/io_apic.h
++++ b/arch/x86/include/asm/io_apic.h
+@@ -63,17 +63,6 @@ union IO_APIC_reg_03 {
+       } __attribute__ ((packed)) bits;
+ };
+-enum ioapic_irq_destination_types {
+-      dest_Fixed = 0,
+-      dest_LowestPrio = 1,
+-      dest_SMI = 2,
+-      dest__reserved_1 = 3,
+-      dest_NMI = 4,
+-      dest_INIT = 5,
+-      dest__reserved_2 = 6,
+-      dest_ExtINT = 7
+-};
+-
+ struct IO_APIC_route_entry {
+       __u32   vector          :  8,
+               delivery_mode   :  3,   /* 000: FIXED
+-- 
+1.7.2.3
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004614:2, b/test/corpora/lkml/cur/1382298805.004614:2,
new file mode 100644 (file)
index 0000000..ce850ff
--- /dev/null
@@ -0,0 +1,89 @@
+From: Henrik Kretzschmar <henne@nachtwindheim.de>
+Subject: [PATCH 2/6] x86: ifdef enable_IR_x2apic() out
+Date: Mon, 14 Feb 2011 11:00:08 +0100
+Lines: 48
+Message-ID: <1297677612-12405-2-git-send-email-henne@nachtwindheim.de>
+References: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Cc: tglx@linutronix.de, hpa@zytor.com, x86@kernel.org, tj@kernel.org,
+       yinghai@kernel.org, ak@linux.intel.com, robert.richter@amd.com,
+       linux-kernel@vger.kernel.org, henne@nachtwindheim.de
+To: mingo@readhat.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 11:00:34 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PovEP-0006ED-LT
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 11:00:34 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752908Ab1BNKA1 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 05:00:27 -0500
+Received: from server103.greatnet.de ([83.133.97.6]:38321 "EHLO
+       server103.greatnet.de" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752778Ab1BNKAX (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 05:00:23 -0500
+Received: from localhost.localdomain (cmnz-d9bab6be.pool.mediaWays.net [217.186.182.190])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by server103.greatnet.de (Postfix) with ESMTPSA id 1AFC7950DB3;
+       Mon, 14 Feb 2011 10:59:06 +0100 (CET)
+X-Mailer: git-send-email 1.7.1
+In-Reply-To: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099646>
+
+The only caller of enable_IR_x2apic() is probe_64.c, which is only
+compiled on x86-64 bit machines.
+This function causes compilation problems on 32-bit machines with no
+io-apic enabled, so we remove it from 32s and keep the way it was on 64s.
+
+Signed-off-by: Henrik Kretzschmar <henne@nachtwindheim.de>
+---
+ arch/x86/include/asm/apic.h |    2 ++
+ arch/x86/kernel/apic/apic.c |    2 +-
+ 2 files changed, 3 insertions(+), 1 deletions(-)
+
+diff --git a/arch/x86/include/asm/apic.h b/arch/x86/include/asm/apic.h
+index b8a3484..b1d77e1 100644
+--- a/arch/x86/include/asm/apic.h
++++ b/arch/x86/include/asm/apic.h
+@@ -216,7 +216,9 @@ static inline void x2apic_force_phys(void)
+ #define       x2apic_supported()      0
+ #endif
++#ifdef CONFIG_X86_64
+ extern void enable_IR_x2apic(void);
++#endif
+ extern int get_physical_broadcast(void);
+diff --git a/arch/x86/kernel/apic/apic.c b/arch/x86/kernel/apic/apic.c
+index 306386f..27a7497 100644
+--- a/arch/x86/kernel/apic/apic.c
++++ b/arch/x86/kernel/apic/apic.c
+@@ -1467,6 +1467,7 @@ int __init enable_IR(void)
+       return 0;
+ }
++#ifdef CONFIG_X86_64
+ void __init enable_IR_x2apic(void)
+ {
+       unsigned long flags;
+@@ -1540,7 +1541,6 @@ out:
+               pr_info("Not enabling x2apic, Intr-remapping init failed.\n");
+ }
+-#ifdef CONFIG_X86_64
+ /*
+  * Detect and enable local APICs on non-SMP boards.
+  * Original code written by Keir Fraser.
+-- 
+1.7.2.3
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004615:2, b/test/corpora/lkml/cur/1382298805.004615:2,
new file mode 100644 (file)
index 0000000..cceabb5
--- /dev/null
@@ -0,0 +1,64 @@
+From: Henrik Kretzschmar <henne@nachtwindheim.de>
+Subject: [PATCH 4/6] x86: add dummy mp_save_irq()
+Date: Mon, 14 Feb 2011 11:00:10 +0100
+Lines: 23
+Message-ID: <1297677612-12405-4-git-send-email-henne@nachtwindheim.de>
+References: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Cc: tglx@linutronix.de, hpa@zytor.com, x86@kernel.org, tj@kernel.org,
+       yinghai@kernel.org, ak@linux.intel.com, robert.richter@amd.com,
+       linux-kernel@vger.kernel.org, henne@nachtwindheim.de
+To: mingo@readhat.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 11:00:36 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PovEQ-0006ED-6t
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 11:00:34 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752958Ab1BNKA3 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 05:00:29 -0500
+Received: from server103.greatnet.de ([83.133.97.6]:38321 "EHLO
+       server103.greatnet.de" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752592Ab1BNKAY (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 05:00:24 -0500
+Received: from localhost.localdomain (cmnz-d9bab6be.pool.mediaWays.net [217.186.182.190])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by server103.greatnet.de (Postfix) with ESMTPSA id 4B47D950DB5;
+       Mon, 14 Feb 2011 10:59:08 +0100 (CET)
+X-Mailer: git-send-email 1.7.1
+In-Reply-To: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099647>
+
+mmparse.c needs a mp_save_irq() function, which is only available
+when CONFIG_X86_IO_APIC is defined. So here we give it a dummy one.
+
+Signed-off-by: Henrik Kretzschmar <henne@nachtwindheim.de>
+---
+ arch/x86/include/asm/io_apic.h |    1 +
+ 1 files changed, 1 insertions(+), 0 deletions(-)
+
+diff --git a/arch/x86/include/asm/io_apic.h b/arch/x86/include/asm/io_apic.h
+index e1a9b0e..7af0f5f 100644
+--- a/arch/x86/include/asm/io_apic.h
++++ b/arch/x86/include/asm/io_apic.h
+@@ -188,6 +188,7 @@ static inline int mp_find_ioapic(u32 gsi) { return 0; }
+ struct io_apic_irq_attr;
+ static inline int io_apic_set_pci_routing(struct device *dev, int irq,
+                struct io_apic_irq_attr *irq_attr) { return 0; }
++static inline void mp_save_irq(struct mpc_intsrc *m) { }
+ #endif
+ #endif /* _ASM_X86_IO_APIC_H */
+-- 
+1.7.2.3
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004617:2, b/test/corpora/lkml/cur/1382298805.004617:2,
new file mode 100644 (file)
index 0000000..71f2f1f
--- /dev/null
@@ -0,0 +1,67 @@
+From: Henrik Kretzschmar <henne@nachtwindheim.de>
+Subject: [PATCH 6/6] x86: makes X86_UP_IOAPIC work again
+Date: Mon, 14 Feb 2011 11:00:12 +0100
+Lines: 26
+Message-ID: <1297677612-12405-6-git-send-email-henne@nachtwindheim.de>
+References: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Cc: tglx@linutronix.de, hpa@zytor.com, x86@kernel.org, tj@kernel.org,
+       yinghai@kernel.org, ak@linux.intel.com, robert.richter@amd.com,
+       linux-kernel@vger.kernel.org, henne@nachtwindheim.de
+To: mingo@readhat.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 11:01:26 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PovFD-0006cV-Si
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 11:01:24 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753137Ab1BNKAo (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 05:00:44 -0500
+Received: from server103.greatnet.de ([83.133.97.6]:38361 "EHLO
+       server103.greatnet.de" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752877Ab1BNKA0 (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 05:00:26 -0500
+Received: from localhost.localdomain (cmnz-d9bab6be.pool.mediaWays.net [217.186.182.190])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by server103.greatnet.de (Postfix) with ESMTPSA id 26BC1950DB7;
+       Mon, 14 Feb 2011 10:59:10 +0100 (CET)
+X-Mailer: git-send-email 1.7.1
+In-Reply-To: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099649>
+
+This fixes a typo, which made CONFIG_X86_UP_IOAPIC defunctional,
+in commit 7cd92366a593246650cc7d6198e2c7d3af8c1d8a.
+
+This has been successfully tested.
+
+Signed-off-by: Henrik Kretzschmar <henne@nachtwindheim.de>
+---
+ arch/x86/Kconfig |    2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig
+index 95c36c4..66c6801 100644
+--- a/arch/x86/Kconfig
++++ b/arch/x86/Kconfig
+@@ -811,7 +811,7 @@ config X86_LOCAL_APIC
+ config X86_IO_APIC
+       def_bool y
+-      depends on X86_64 || SMP || X86_32_NON_STANDARD || X86_UP_APIC
++      depends on X86_64 || SMP || X86_32_NON_STANDARD || X86_UP_IOAPIC
+ config X86_VISWS_APIC
+       def_bool y
+-- 
+1.7.2.3
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004618:2, b/test/corpora/lkml/cur/1382298805.004618:2,
new file mode 100644 (file)
index 0000000..620bed1
--- /dev/null
@@ -0,0 +1,70 @@
+From: Henrik Kretzschmar <henne@nachtwindheim.de>
+Subject: [PATCH 5/6] x86: ifdef ioapic related function out
+Date: Mon, 14 Feb 2011 11:00:11 +0100
+Lines: 29
+Message-ID: <1297677612-12405-5-git-send-email-henne@nachtwindheim.de>
+References: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Cc: tglx@linutronix.de, hpa@zytor.com, x86@kernel.org, tj@kernel.org,
+       yinghai@kernel.org, ak@linux.intel.com, robert.richter@amd.com,
+       linux-kernel@vger.kernel.org, henne@nachtwindheim.de
+To: mingo@readhat.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 11:01:27 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PovFE-0006cV-Cn
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 11:01:24 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753212Ab1BNKAq (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 05:00:46 -0500
+Received: from server103.greatnet.de ([83.133.97.6]:38350 "EHLO
+       server103.greatnet.de" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752841Ab1BNKAZ (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 05:00:25 -0500
+Received: from localhost.localdomain (cmnz-d9bab6be.pool.mediaWays.net [217.186.182.190])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by server103.greatnet.de (Postfix) with ESMTPSA id 37BBE950DB6;
+       Mon, 14 Feb 2011 10:59:09 +0100 (CET)
+X-Mailer: git-send-email 1.7.1
+In-Reply-To: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099650>
+
+arch_disable_smp_config() is an IO-APIC related function on x86,
+and should only be needed if SMP is enabled.
+But the IO-APIC code calls it when the parameter "noapic" is given to
+the kernel, which doesn't mean SMP is enabled.
+
+Anyway this fixes compilation on x86_32 UP systems with APIC and no IO-APIC.
+
+Signed-off-by: Henrik Kretzschmar <henne@nachtwindheim.de>
+---
+ arch/x86/kernel/apic/apic.c |    2 ++
+ 1 files changed, 2 insertions(+), 0 deletions(-)
+
+diff --git a/arch/x86/kernel/apic/apic.c b/arch/x86/kernel/apic/apic.c
+index 999c531..4998f0a 100644
+--- a/arch/x86/kernel/apic/apic.c
++++ b/arch/x86/kernel/apic/apic.c
+@@ -1218,7 +1218,9 @@ void __cpuinit setup_local_APIC(void)
+               rdtscll(tsc);
+       if (disable_apic) {
++#ifdef CONFIG_X86_IO_APIC
+               arch_disable_smp_support();
++#endif
+               return;
+       }
+-- 
+1.7.2.3
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004619:2, b/test/corpora/lkml/cur/1382298805.004619:2,
new file mode 100644 (file)
index 0000000..f7895dd
--- /dev/null
@@ -0,0 +1,99 @@
+From: Henrik Kretzschmar <henne@nachtwindheim.de>
+Subject: [PATCH 3/6] x86: ifdef INTR_REMAP code out
+Date: Mon, 14 Feb 2011 11:00:09 +0100
+Lines: 58
+Message-ID: <1297677612-12405-3-git-send-email-henne@nachtwindheim.de>
+References: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Cc: tglx@linutronix.de, hpa@zytor.com, x86@kernel.org, tj@kernel.org,
+       yinghai@kernel.org, ak@linux.intel.com, robert.richter@amd.com,
+       linux-kernel@vger.kernel.org, henne@nachtwindheim.de
+To: mingo@readhat.com
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 11:01:30 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PovFF-0006cV-Te
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 11:01:26 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753126Ab1BNKBX (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 05:01:23 -0500
+Received: from server103.greatnet.de ([83.133.97.6]:38330 "EHLO
+       server103.greatnet.de" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752268Ab1BNKAX (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 05:00:23 -0500
+Received: from localhost.localdomain (cmnz-d9bab6be.pool.mediaWays.net [217.186.182.190])
+       (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
+       (No client certificate requested)
+       by server103.greatnet.de (Postfix) with ESMTPSA id 29517950DB4;
+       Mon, 14 Feb 2011 10:59:07 +0100 (CET)
+X-Mailer: git-send-email 1.7.1
+In-Reply-To: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099651>
+
+Interrupt remapping is only available on 64-bit machines,
+so it has no place in lapic_resume() for 32bit machines.
+
+Compilation on 32bit machines would produce errors here.
+
+Signed-off-by: Henrik Kretzschmar <henne@nachtwindheim.de>
+---
+ arch/x86/kernel/apic/apic.c |    8 +++++++-
+ 1 files changed, 7 insertions(+), 1 deletions(-)
+
+diff --git a/arch/x86/kernel/apic/apic.c b/arch/x86/kernel/apic/apic.c
+index 27a7497..999c531 100644
+--- a/arch/x86/kernel/apic/apic.c
++++ b/arch/x86/kernel/apic/apic.c
+@@ -2109,12 +2109,15 @@ static int lapic_resume(struct sys_device *dev)
+       unsigned long flags;
+       int maxlvt;
+       int ret = 0;
+-      struct IO_APIC_route_entry **ioapic_entries = NULL;
+       if (!apic_pm_state.active)
+               return 0;
+       local_irq_save(flags);
++
++#ifdef CONFIG_INTR_REMAP
++      struct IO_APIC_route_entry **ioapic_entries = NULL;
++
+       if (intr_remapping_enabled) {
+               ioapic_entries = alloc_ioapic_entries();
+               if (!ioapic_entries) {
+@@ -2133,6 +2136,7 @@ static int lapic_resume(struct sys_device *dev)
+               mask_IO_APIC_setup(ioapic_entries);
+               legacy_pic->mask_all();
+       }
++#endif
+       if (x2apic_mode)
+               enable_x2apic();
+@@ -2173,6 +2177,7 @@ static int lapic_resume(struct sys_device *dev)
+       apic_write(APIC_ESR, 0);
+       apic_read(APIC_ESR);
++#ifdef CONFIG_INTR_REMAP
+       if (intr_remapping_enabled) {
+               reenable_intr_remapping(x2apic_mode);
+               legacy_pic->restore_mask();
+@@ -2180,6 +2185,7 @@ static int lapic_resume(struct sys_device *dev)
+               free_ioapic_entries(ioapic_entries);
+       }
+ restore:
++#endif
+       local_irq_restore(flags);
+       return ret;
+-- 
+1.7.2.3
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004636:2, b/test/corpora/lkml/cur/1382298805.004636:2,
new file mode 100644 (file)
index 0000000..c14d42c
--- /dev/null
@@ -0,0 +1,93 @@
+From: Vasiliy Kulikov <segoon@openwall.com>
+Subject: [PATCH] core: dev: don't call BUG() on bad input
+Date: Mon, 14 Feb 2011 13:56:06 +0300
+Lines: 36
+Message-ID: <1297680967-11893-1-git-send-email-segoon@openwall.com>
+Cc: "David S. Miller" <davem@davemloft.net>,
+       Eric Dumazet <eric.dumazet@gmail.com>,
+       Tom Herbert <therbert@google.com>,
+       Changli Gao <xiaosuo@gmail.com>,
+       Jesse Gross <jesse@nicira.com>, netdev@vger.kernel.org
+To: linux-kernel@vger.kernel.org
+X-From: netdev-owner@vger.kernel.org Mon Feb 14 11:56:26 2011
+Return-path: <netdev-owner@vger.kernel.org>
+Envelope-to: linux-netdev-2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <netdev-owner@vger.kernel.org>)
+       id 1Pow6Q-0007p5-UJ
+       for linux-netdev-2@lo.gmane.org; Mon, 14 Feb 2011 11:56:23 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753243Ab1BNK4O (ORCPT <rfc822;linux-netdev-2@m.gmane.org>);
+       Mon, 14 Feb 2011 05:56:14 -0500
+Received: from mail-bw0-f46.google.com ([209.85.214.46]:60909 "EHLO
+       mail-bw0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1752645Ab1BNK4M (ORCPT
+       <rfc822;netdev@vger.kernel.org>); Mon, 14 Feb 2011 05:56:12 -0500
+Received: by bwz15 with SMTP id 15so5332720bwz.19
+        for <multiple recipients>; Mon, 14 Feb 2011 02:56:11 -0800 (PST)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:sender:from:to:cc:subject:date:message-id
+         :x-mailer;
+        bh=YQn7OCqAZuXaSsRtgaQYckH74o43k6Rppt54AR6UzDo=;
+        b=CxfBmTAbcMf7ySl3szqU/hLEMbY7aJ+FjefneMcTm/AmBnyihy20JuV2k0yYJzcIBi
+         9+2npC4H9oJn7/ocVARq88j9ZA/4firOi9ZddgGu6c8+o0tWoZylA1ehtHzzk+4I173l
+         H8guqK5rplkryj6+PStELYYt36SpAVfaL2EdY=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=sender:from:to:cc:subject:date:message-id:x-mailer;
+        b=G0AqVbcip2oFA2IqAQa6TWwQydu/mJFzt98tGkR1fVNl3m+HaKY433gNCR+Dqdv0gA
+         SGL/R3HRiBBBku/GM4x3gQ8SoAFZiREw6PDtkU55l/mk+yS+v+8YTq7/InPxHoHeTWsv
+         pX0mWUI2HtTXKALBiM+nLsnBWtcC8yInYtyeQ=
+Received: by 10.204.61.73 with SMTP id s9mr6247440bkh.185.1297680970948;
+        Mon, 14 Feb 2011 02:56:10 -0800 (PST)
+Received: from localhost (ppp91-77-40-235.pppoe.mtu-net.ru [91.77.40.235])
+        by mx.google.com with ESMTPS id u23sm1686152bkw.9.2011.02.14.02.56.09
+        (version=TLSv1/SSLv3 cipher=OTHER);
+        Mon, 14 Feb 2011 02:56:10 -0800 (PST)
+X-Mailer: git-send-email 1.7.0.4
+Sender: netdev-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <netdev.vger.kernel.org>
+X-Mailing-List: netdev@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099668>
+
+alloc_netdev() may be called with too long name (more that IFNAMSIZ bytes).
+Currently this leads to BUG().  Other insane inputs (bad txqs, rxqs) and
+even OOM don't lead to BUG().  Made alloc_netdev() return NULL, like on
+other errors.
+
+Signed-off-by: Vasiliy Kulikov <segoon@openwall.com>
+---
+ Compile tested.
+
+ net/core/dev.c |    5 ++++-
+ 1 files changed, 4 insertions(+), 1 deletions(-)
+
+diff --git a/net/core/dev.c b/net/core/dev.c
+index 6392ea0..12ef4b0 100644
+--- a/net/core/dev.c
++++ b/net/core/dev.c
+@@ -5761,7 +5761,10 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
+       size_t alloc_size;
+       struct net_device *p;
+-      BUG_ON(strlen(name) >= sizeof(dev->name));
++      if (strnlen(name, sizeof(dev->name)) >= sizeof(dev->name)) {
++              pr_err("alloc_netdev: Too long device name \n");
++              return NULL;
++      }
+       if (txqs < 1) {
+               pr_err("alloc_netdev: Unable to allocate device "
+-- 
+1.7.0.4
+
+--
+To unsubscribe from this list: send the line "unsubscribe netdev" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004638:2, b/test/corpora/lkml/cur/1382298805.004638:2,
new file mode 100644 (file)
index 0000000..551ce5b
--- /dev/null
@@ -0,0 +1,88 @@
+From: Ingo Molnar <mingo@elte.hu>
+Subject: Re: [PATCH 5/6] x86: ifdef ioapic related function out
+Date: Mon, 14 Feb 2011 12:00:39 +0100
+Lines: 34
+Message-ID: <20110214110039.GA7140@elte.hu>
+References: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+ <1297677612-12405-5-git-send-email-henne@nachtwindheim.de>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Cc: mingo@readhat.com, tglx@linutronix.de, hpa@zytor.com,
+       x86@kernel.org, tj@kernel.org, yinghai@kernel.org,
+       ak@linux.intel.com, robert.richter@amd.com,
+       linux-kernel@vger.kernel.org
+To: Henrik Kretzschmar <henne@nachtwindheim.de>
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 12:01:04 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PowAx-0001Lu-Lk
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 12:01:04 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752962Ab1BNLA5 (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 06:00:57 -0500
+Received: from mx3.mail.elte.hu ([157.181.1.138]:38470 "EHLO mx3.mail.elte.hu"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1752556Ab1BNLAz (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 06:00:55 -0500
+Received: from elvis.elte.hu ([157.181.1.14])
+       by mx3.mail.elte.hu with esmtp (Exim)
+       id 1PowAd-0003Lr-F7
+       from <mingo@elte.hu>; Mon, 14 Feb 2011 12:00:48 +0100
+Received: by elvis.elte.hu (Postfix, from userid 1004)
+       id 7726F3E236B; Mon, 14 Feb 2011 12:00:41 +0100 (CET)
+Content-Disposition: inline
+In-Reply-To: <1297677612-12405-5-git-send-email-henne@nachtwindheim.de>
+User-Agent: Mutt/1.5.20 (2009-08-17)
+Received-SPF: neutral (mx3: 157.181.1.14 is neither permitted nor denied by domain of elte.hu) client-ip=157.181.1.14; envelope-from=mingo@elte.hu; helo=elvis.elte.hu;
+X-ELTE-SpamScore: -2.0
+X-ELTE-SpamLevel: 
+X-ELTE-SpamCheck: no
+X-ELTE-SpamVersion: ELTE 2.0 
+X-ELTE-SpamCheck-Details: score=-2.0 required=5.9 tests=BAYES_00 autolearn=no SpamAssassin version=3.2.5
+       -2.0 BAYES_00               BODY: Bayesian spam probability is 0 to 1%
+       [score: 0.0000]
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099670>
+
+
+* Henrik Kretzschmar <henne@nachtwindheim.de> wrote:
+
+> arch_disable_smp_config() is an IO-APIC related function on x86,
+> and should only be needed if SMP is enabled.
+> But the IO-APIC code calls it when the parameter "noapic" is given to
+> the kernel, which doesn't mean SMP is enabled.
+> 
+> Anyway this fixes compilation on x86_32 UP systems with APIC and no IO-APIC.
+> 
+> Signed-off-by: Henrik Kretzschmar <henne@nachtwindheim.de>
+> ---
+>  arch/x86/kernel/apic/apic.c |    2 ++
+>  1 files changed, 2 insertions(+), 0 deletions(-)
+> 
+> diff --git a/arch/x86/kernel/apic/apic.c b/arch/x86/kernel/apic/apic.c
+> index 999c531..4998f0a 100644
+> --- a/arch/x86/kernel/apic/apic.c
+> +++ b/arch/x86/kernel/apic/apic.c
+> @@ -1218,7 +1218,9 @@ void __cpuinit setup_local_APIC(void)
+>              rdtscll(tsc);
+>  
+>      if (disable_apic) {
+> +#ifdef CONFIG_X86_IO_APIC
+>              arch_disable_smp_support();
+> +#endif
+
+Why not make the arch_disable_smp_support() call generic in the 
+arch/x86/include/asm/smp.h file (via an inline helper) and thus
+avoid an ugly #ifdef in the .c file?
+
+Thanks,
+
+       Ingo
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004639:2, b/test/corpora/lkml/cur/1382298805.004639:2,
new file mode 100644 (file)
index 0000000..56118aa
--- /dev/null
@@ -0,0 +1,95 @@
+From: Ingo Molnar <mingo@elte.hu>
+Subject: Re: [PATCH 3/6] x86: ifdef INTR_REMAP code out
+Date: Mon, 14 Feb 2011 12:02:31 +0100
+Lines: 41
+Message-ID: <20110214110231.GB7140@elte.hu>
+References: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+ <1297677612-12405-3-git-send-email-henne@nachtwindheim.de>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Cc: mingo@readhat.com, tglx@linutronix.de, hpa@zytor.com,
+       x86@kernel.org, tj@kernel.org, yinghai@kernel.org,
+       ak@linux.intel.com, robert.richter@amd.com,
+       linux-kernel@vger.kernel.org
+To: Henrik Kretzschmar <henne@nachtwindheim.de>
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 12:02:52 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PowCg-00022G-BR
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 12:02:50 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1752997Ab1BNLCn (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 06:02:43 -0500
+Received: from mx3.mail.elte.hu ([157.181.1.138]:38974 "EHLO mx3.mail.elte.hu"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1751314Ab1BNLCl (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 06:02:41 -0500
+Received: from elvis.elte.hu ([157.181.1.14])
+       by mx3.mail.elte.hu with esmtp (Exim)
+       id 1PowCQ-0003df-Gk
+       from <mingo@elte.hu>; Mon, 14 Feb 2011 12:02:34 +0100
+Received: by elvis.elte.hu (Postfix, from userid 1004)
+       id 0D9343E2369; Mon, 14 Feb 2011 12:02:32 +0100 (CET)
+Content-Disposition: inline
+In-Reply-To: <1297677612-12405-3-git-send-email-henne@nachtwindheim.de>
+User-Agent: Mutt/1.5.20 (2009-08-17)
+Received-SPF: neutral (mx3: 157.181.1.14 is neither permitted nor denied by domain of elte.hu) client-ip=157.181.1.14; envelope-from=mingo@elte.hu; helo=elvis.elte.hu;
+X-ELTE-SpamScore: -2.0
+X-ELTE-SpamLevel: 
+X-ELTE-SpamCheck: no
+X-ELTE-SpamVersion: ELTE 2.0 
+X-ELTE-SpamCheck-Details: score=-2.0 required=5.9 tests=BAYES_00 autolearn=no SpamAssassin version=3.2.5
+       -2.0 BAYES_00               BODY: Bayesian spam probability is 0 to 1%
+       [score: 0.0000]
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099671>
+
+
+* Henrik Kretzschmar <henne@nachtwindheim.de> wrote:
+
+> +#ifdef CONFIG_INTR_REMAP
+> +    struct IO_APIC_route_entry **ioapic_entries = NULL;
+> +
+>      if (intr_remapping_enabled) {
+>              ioapic_entries = alloc_ioapic_entries();
+>              if (!ioapic_entries) {
+> @@ -2133,6 +2136,7 @@ static int lapic_resume(struct sys_device *dev)
+>              mask_IO_APIC_setup(ioapic_entries);
+>              legacy_pic->mask_all();
+>      }
+> +#endif
+>  
+>      if (x2apic_mode)
+>              enable_x2apic();
+> @@ -2173,6 +2177,7 @@ static int lapic_resume(struct sys_device *dev)
+>      apic_write(APIC_ESR, 0);
+>      apic_read(APIC_ESR);
+>  
+> +#ifdef CONFIG_INTR_REMAP
+>      if (intr_remapping_enabled) {
+>              reenable_intr_remapping(x2apic_mode);
+>              legacy_pic->restore_mask();
+> @@ -2180,6 +2185,7 @@ static int lapic_resume(struct sys_device *dev)
+>              free_ioapic_entries(ioapic_entries);
+>      }
+>  restore:
+> +#endif
+
+Hm, these bits should be factored out in a cleaner fashion - by adding helper 
+functions, etc. The x2apic code's integration into the lapic code was done in a 
+pretty ugly fashion so it's not your fault - but if we want to reintroduce UP-IOAPIC 
+we need to do it cleanly.
+
+Do you still want to do it? :-)
+
+Thanks,
+
+       Ingo
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004640:2, b/test/corpora/lkml/cur/1382298805.004640:2,
new file mode 100644 (file)
index 0000000..af16a62
--- /dev/null
@@ -0,0 +1,69 @@
+From: Ingo Molnar <mingo@elte.hu>
+Subject: Re: [PATCH 2/6] x86: ifdef enable_IR_x2apic() out
+Date: Mon, 14 Feb 2011 12:03:40 +0100
+Lines: 15
+Message-ID: <20110214110340.GC7140@elte.hu>
+References: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+ <1297677612-12405-2-git-send-email-henne@nachtwindheim.de>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Cc: mingo@readhat.com, tglx@linutronix.de, hpa@zytor.com,
+       x86@kernel.org, tj@kernel.org, yinghai@kernel.org,
+       ak@linux.intel.com, robert.richter@amd.com,
+       linux-kernel@vger.kernel.org
+To: Henrik Kretzschmar <henne@nachtwindheim.de>
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 12:04:03 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PowDr-0002js-9K
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 12:04:03 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753399Ab1BNLDz (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 06:03:55 -0500
+Received: from mx2.mail.elte.hu ([157.181.151.9]:54341 "EHLO mx2.mail.elte.hu"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1753317Ab1BNLDx (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 06:03:53 -0500
+Received: from elvis.elte.hu ([157.181.1.14])
+       by mx2.mail.elte.hu with esmtp (Exim)
+       id 1PowDY-0002Mp-4N
+       from <mingo@elte.hu>; Mon, 14 Feb 2011 12:03:45 +0100
+Received: by elvis.elte.hu (Postfix, from userid 1004)
+       id 458B63E2369; Mon, 14 Feb 2011 12:03:40 +0100 (CET)
+Content-Disposition: inline
+In-Reply-To: <1297677612-12405-2-git-send-email-henne@nachtwindheim.de>
+User-Agent: Mutt/1.5.20 (2009-08-17)
+Received-SPF: neutral (mx2.mail.elte.hu: 157.181.1.14 is neither permitted nor denied by domain of elte.hu) client-ip=157.181.1.14; envelope-from=mingo@elte.hu; helo=elvis.elte.hu;
+X-ELTE-SpamScore: -2.0
+X-ELTE-SpamLevel: 
+X-ELTE-SpamCheck: no
+X-ELTE-SpamVersion: ELTE 2.0 
+X-ELTE-SpamCheck-Details: score=-2.0 required=5.9 tests=BAYES_00 autolearn=no SpamAssassin version=3.2.5
+       -2.0 BAYES_00               BODY: Bayesian spam probability is 0 to 1%
+       [score: 0.0000]
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099672>
+
+
+
+* Henrik Kretzschmar <henne@nachtwindheim.de> wrote:
+
+> +#ifdef CONFIG_X86_64
+>  extern void enable_IR_x2apic(void);
+> +#endif
+
+Cannot we use the CONFIG_X86_X2APIC Kconfig switch here, instead of CONFIG_X86_64?
+
+enable_IR_x2apic() is not a 64-bit CPU feature.
+
+Thanks,
+
+       Ingo
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004642:2, b/test/corpora/lkml/cur/1382298805.004642:2,
new file mode 100644 (file)
index 0000000..ce8ac11
--- /dev/null
@@ -0,0 +1,93 @@
+From: Ingo Molnar <mingo@elte.hu>
+Subject: Re: [PATCH 1/6] x86: move ioapic_irq_destination_types
+Date: Mon, 14 Feb 2011 12:05:28 +0100
+Lines: 40
+Message-ID: <20110214110528.GD7140@elte.hu>
+References: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Cc: mingo@readhat.com, tglx@linutronix.de, hpa@zytor.com,
+       x86@kernel.org, tj@kernel.org, yinghai@kernel.org,
+       ak@linux.intel.com, robert.richter@amd.com,
+       linux-kernel@vger.kernel.org
+To: Henrik Kretzschmar <henne@nachtwindheim.de>
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 12:06:00 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PowFj-0003bW-W9
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 12:06:00 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753317Ab1BNLFq (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 06:05:46 -0500
+Received: from mx3.mail.elte.hu ([157.181.1.138]:46158 "EHLO mx3.mail.elte.hu"
+       rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP
+       id S1752958Ab1BNLFn (ORCPT <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 06:05:43 -0500
+Received: from elvis.elte.hu ([157.181.1.14])
+       by mx3.mail.elte.hu with esmtp (Exim)
+       id 1PowFH-0003wc-2d
+       from <mingo@elte.hu>; Mon, 14 Feb 2011 12:05:36 +0100
+Received: by elvis.elte.hu (Postfix, from userid 1004)
+       id 30C323E2369; Mon, 14 Feb 2011 12:05:27 +0100 (CET)
+Content-Disposition: inline
+In-Reply-To: <1297677612-12405-1-git-send-email-henne@nachtwindheim.de>
+User-Agent: Mutt/1.5.20 (2009-08-17)
+Received-SPF: neutral (mx3: 157.181.1.14 is neither permitted nor denied by domain of elte.hu) client-ip=157.181.1.14; envelope-from=mingo@elte.hu; helo=elvis.elte.hu;
+X-ELTE-SpamScore: -2.0
+X-ELTE-SpamLevel: 
+X-ELTE-SpamCheck: no
+X-ELTE-SpamVersion: ELTE 2.0 
+X-ELTE-SpamCheck-Details: score=-2.0 required=5.9 tests=BAYES_00 autolearn=no SpamAssassin version=3.2.5
+       -2.0 BAYES_00               BODY: Bayesian spam probability is 0 to 1%
+       [score: 0.0000]
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099674>
+
+
+* Henrik Kretzschmar <henne@nachtwindheim.de> wrote:
+
+> +++ b/arch/x86/include/asm/apicdef.h
+> @@ -426,4 +426,16 @@ struct local_apic {
+>  #else
+>   #define BAD_APICID 0xFFFFu
+>  #endif
+> +
+> +enum ioapic_irq_destination_types {
+> +    dest_Fixed = 0,
+> +    dest_LowestPrio = 1,
+> +    dest_SMI = 2,
+> +    dest__reserved_1 = 3,
+> +    dest_NMI = 4,
+> +    dest_INIT = 5,
+> +    dest__reserved_2 = 6,
+> +    dest_ExtINT = 7
+> +};
+
+one very small request, while we are moving it could you please align the value
+enumeration vertically? Something like:
+
+enum ioapic_irq_destination_types {
+
+       dest_Fixed              = 0,
+       dest_LowestPrio         = 1,
+       dest_SMI                = 2,
+       dest__reserved_1        = 3,
+       dest_NMI                = 4,
+       dest_INIT               = 5,
+       dest__reserved_2        = 6,
+       dest_ExtINT             = 7
+};
+
+... would be much more readable, right?
+
+Thanks,
+
+       Ingo
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004653:2, b/test/corpora/lkml/cur/1382298805.004653:2,
new file mode 100644 (file)
index 0000000..d6bd8d1
--- /dev/null
@@ -0,0 +1,90 @@
+From: Catalin Marinas <catalin.marinas@arm.com>
+Subject: Re: [PATCH] ARM: vfp: Always save VFP state in vfp_pm_suspend
+Date: Mon, 14 Feb 2011 11:42:22 +0000
+Organization: ARM Limited
+Lines: 43
+Message-ID: <1297683742.30092.11.camel@e102109-lin.cambridge.arm.com>
+References: <1297638813-1315-1-git-send-email-ccross@android.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8BIT
+Cc: linux-arm-kernel@lists.infradead.org,
+       Russell King <linux@arm.linux.org.uk>,
+       linux-kernel@vger.kernel.org
+To: Colin Cross <ccross@android.com>
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 12:42:45 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1PowpJ-00069R-8c
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 12:42:45 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753535Ab1BNLmi (ORCPT <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 06:42:38 -0500
+Received: from service87.mimecast.com ([94.185.240.25]:56758 "HELO
+       service87.mimecast.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with SMTP id S1752997Ab1BNLmg convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 06:42:36 -0500
+Received: from cam-owa2.Emea.Arm.com (fw-tnat.cambridge.arm.com [217.140.96.21])
+       by service87.mimecast.com;
+       Mon, 14 Feb 2011 11:42:31 +0000
+Received: from [10.1.77.95] ([10.1.255.212]) by cam-owa2.Emea.Arm.com with Microsoft SMTPSVC(6.0.3790.3959);
+        Mon, 14 Feb 2011 11:42:28 +0000
+In-Reply-To: <1297638813-1315-1-git-send-email-ccross@android.com>
+X-Mailer: Evolution 2.28.1
+X-OriginalArrivalTime: 14 Feb 2011 11:42:28.0658 (UTC) FILETIME=[41F09120:01CBCC3C]
+X-MC-Unique: 111021411423105201
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099685>
+
+On Sun, 2011-02-13 at 23:13 +0000, Colin Cross wrote:
+> vfp_pm_suspend should save the VFP state any time there is
+> a last_VFP_context.  If it only saves when the VFP is enabled,
+> the state can get lost when, on a UP system:
+>    Thread 1 uses the VFP
+>    Context switch occurs to thread 2, VFP is disabled but the
+>       VFP context is not saved to allow lazy save and restore
+>    Thread 2 initiates suspend
+>    vfp_pm_suspend is called with the VFP disabled, but the
+>       context has not been saved.
+
+At this point is it guaranteed that the thread won't migrate to another
+CPU? If not, we should use get/put_cpu.
+
+> --- a/arch/arm/vfp/vfpmodule.c
+> +++ b/arch/arm/vfp/vfpmodule.c
+> @@ -415,13 +415,12 @@ static int vfp_pm_suspend(struct sys_device *dev, pm_message_t state)
+>         struct thread_info *ti = current_thread_info();
+>         u32 fpexc = fmrx(FPEXC);
+> 
+> -       /* if vfp is on, then save state for resumption */
+> -       if (fpexc & FPEXC_EN) {
+> +       /* save state for resume */
+> +       if (last_VFP_context[ti->cpu]) {
+>                 printk(KERN_DEBUG "%s: saving vfp state\n", __func__);
+> -               vfp_save_state(&ti->vfpstate, fpexc);
+> -
+> -               /* disable, just in case */
+> -               fmxr(FPEXC, fmrx(FPEXC) & ~FPEXC_EN);
+> +               fmxr(FPEXC, fpexc | FPEXC_EN);
+> +               vfp_save_state(last_VFP_context[ti->cpu], fpexc);
+> +               fmxr(FPEXC, fpexc & ~FPEXC_EN);
+>         }
+
+We may want to set the last_VFP_context to NULL so that after resuming
+(to the same thread) we force the VFP reload from the vfpstate
+structure. The vfp_support_entry code ignores the reloading if the
+last_VFP_context is the same as vfpstate.
+
+-- 
+Catalin
+
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004665:2, b/test/corpora/lkml/cur/1382298805.004665:2,
new file mode 100644 (file)
index 0000000..bfba805
--- /dev/null
@@ -0,0 +1,121 @@
+From: =?ISO-8859-1?Q?Nicolas_de_Peslo=FCan?= 
+       <nicolas.2p.debian@gmail.com>
+Subject: Re: [PATCH] core: dev: don't call BUG() on bad input
+Date: Mon, 14 Feb 2011 13:16:04 +0100
+Lines: 54
+Message-ID: <4D591D04.4050000@gmail.com>
+References: <1297680967-11893-1-git-send-email-segoon@openwall.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1;
+       format=flowed
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: linux-kernel@vger.kernel.org,
+       "David S. Miller" <davem@davemloft.net>,
+       Eric Dumazet <eric.dumazet@gmail.com>,
+       Tom Herbert <therbert@google.com>,
+       Changli Gao <xiaosuo@gmail.com>,
+       Jesse Gross <jesse@nicira.com>, netdev@vger.kernel.org
+To: Vasiliy Kulikov <segoon@openwall.com>
+X-From: netdev-owner@vger.kernel.org Mon Feb 14 13:16:23 2011
+Return-path: <netdev-owner@vger.kernel.org>
+Envelope-to: linux-netdev-2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <netdev-owner@vger.kernel.org>)
+       id 1PoxLn-0007s8-Rx
+       for linux-netdev-2@lo.gmane.org; Mon, 14 Feb 2011 13:16:20 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1753819Ab1BNMQQ convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;linux-netdev-2@m.gmane.org>); Mon, 14 Feb 2011 07:16:16 -0500
+Received: from mail-bw0-f46.google.com ([209.85.214.46]:53692 "EHLO
+       mail-bw0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1753690Ab1BNMQO (ORCPT
+       <rfc822;netdev@vger.kernel.org>); Mon, 14 Feb 2011 07:16:14 -0500
+Received: by bwz15 with SMTP id 15so5395788bwz.19
+        for <multiple recipients>; Mon, 14 Feb 2011 04:16:13 -0800 (PST)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:message-id:date:from:user-agent:mime-version:to
+         :cc:subject:references:in-reply-to:content-type
+         :content-transfer-encoding;
+        bh=xx3YxuXgqhBANV8wzcWyUMECJelpRfdmoRSp1AKdYdc=;
+        b=N0KOUEiWNDJjbwFsNkzabK7eGUNcoUNkqBGVRMNJFQ1jIgKtMC9sXdcmSFkLf2G3W0
+         zzRsPc9T6wnstCSnGFjIStR1GQK4bQ7o7SC+bmV0UqsBQAMW8sdDkT20PMZlBl2X7PkF
+         a2TGrPtJKfEcGFXay9Xo1ZMu1aYODhlRfsZKo=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=message-id:date:from:user-agent:mime-version:to:cc:subject
+         :references:in-reply-to:content-type:content-transfer-encoding;
+        b=vyCGwBn8dYUhOjmcV0am255nAUilQntfBxUI4yId3MocKlSBfEE0jJCJIVDySIAYOj
+         4g7FzXDeqZ5brwXgwA1derVfTXYvhKUC/60QA9/377l/PZ0vvRfqyPQcinMoOSCc+Kvv
+         vnBZtq/Kr+D4nUnefIwwjJ/dXS3dGjFBvis6w=
+Received: by 10.204.98.65 with SMTP id p1mr25300616bkn.198.1297685772974;
+        Mon, 14 Feb 2011 04:16:12 -0800 (PST)
+Received: from [192.168.0.101] (eab95-4-88-175-177-37.fbx.proxad.net [88.175.177.37])
+        by mx.google.com with ESMTPS id a17sm1733557bku.23.2011.02.14.04.16.07
+        (version=SSLv3 cipher=OTHER);
+        Mon, 14 Feb 2011 04:16:09 -0800 (PST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.16) Gecko/20101226 Icedove/3.0.11
+In-Reply-To: <1297680967-11893-1-git-send-email-segoon@openwall.com>
+Sender: netdev-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <netdev.vger.kernel.org>
+X-Mailing-List: netdev@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099697>
+
+Le 14/02/2011 11:56, Vasiliy Kulikov a =E9crit :
+> alloc_netdev() may be called with too long name (more that IFNAMSIZ b=
+ytes).
+> Currently this leads to BUG().  Other insane inputs (bad txqs, rxqs) =
+and
+> even OOM don't lead to BUG().  Made alloc_netdev() return NULL, like =
+on
+> other errors.
+>
+> Signed-off-by: Vasiliy Kulikov<segoon@openwall.com>
+> ---
+>   Compile tested.
+>
+>   net/core/dev.c |    5 ++++-
+>   1 files changed, 4 insertions(+), 1 deletions(-)
+>
+> diff --git a/net/core/dev.c b/net/core/dev.c
+> index 6392ea0..12ef4b0 100644
+> --- a/net/core/dev.c
+> +++ b/net/core/dev.c
+> @@ -5761,7 +5761,10 @@ struct net_device *alloc_netdev_mqs(int sizeof=
+_priv, const char *name,
+>      size_t alloc_size;
+>      struct net_device *p;
+>
+> -    BUG_ON(strlen(name)>=3D sizeof(dev->name));
+> +    if (strnlen(name, sizeof(dev->name))>=3D sizeof(dev->name)) {
+
+"size_t strnlen(const char *s, size_t maxlen) : The strnlen() function =
+returns strlen(s), if that is=20
+less than maxlen, or maxlen if there is no '\0' character among the fir=
+st maxlen characters pointed=20
+to by s."
+
+How can strnlen(name, sizeof(dev->name)) be greater than sizeof(dev->na=
+me)?
+
+Shouldn't it be "if (strnlen(name, sizeof(dev->name)) =3D=3D sizeof(dev=
+->name))" instead?
+
+         Nicolas.
+
+> +            pr_err("alloc_netdev: Too long device name \n");
+> +            return NULL;
+> +    }
+>
+>      if (txqs<  1) {
+>              pr_err("alloc_netdev: Unable to allocate device "
+
+--
+To unsubscribe from this list: send the line "unsubscribe netdev" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004680:2, b/test/corpora/lkml/cur/1382298805.004680:2,
new file mode 100644 (file)
index 0000000..e0e74ce
--- /dev/null
@@ -0,0 +1,103 @@
+From: Vasiliy Kulikov <segoon@openwall.com>
+Subject: Re: [PATCH] core: dev: don't call BUG() on bad input
+Date: Mon, 14 Feb 2011 15:23:13 +0300
+Lines: 34
+Message-ID: <20110214122313.GA10062@albatros>
+References: <1297680967-11893-1-git-send-email-segoon@openwall.com>
+ <4D591D04.4050000@gmail.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: linux-kernel@vger.kernel.org,
+       "David S. Miller" <davem@davemloft.net>,
+       Eric Dumazet <eric.dumazet@gmail.com>,
+       Tom Herbert <therbert@google.com>,
+       Changli Gao <xiaosuo@gmail.com>,
+       Jesse Gross <jesse@nicira.com>, netdev@vger.kernel.org
+To: Nicolas de =?iso-8859-1?Q?Peslo=FCan?= 
+       <nicolas.2p.debian@gmail.com>
+X-From: netdev-owner@vger.kernel.org Mon Feb 14 13:24:40 2011
+Return-path: <netdev-owner@vger.kernel.org>
+Envelope-to: linux-netdev-2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <netdev-owner@vger.kernel.org>)
+       id 1PoxTr-00046W-DE
+       for linux-netdev-2@lo.gmane.org; Mon, 14 Feb 2011 13:24:39 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1754719Ab1BNMXX convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;linux-netdev-2@m.gmane.org>); Mon, 14 Feb 2011 07:23:23 -0500
+Received: from mail-bw0-f46.google.com ([209.85.214.46]:64487 "EHLO
+       mail-bw0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1754086Ab1BNMXU (ORCPT
+       <rfc822;netdev@vger.kernel.org>); Mon, 14 Feb 2011 07:23:20 -0500
+Received: by bwz15 with SMTP id 15so5401470bwz.19
+        for <multiple recipients>; Mon, 14 Feb 2011 04:23:18 -0800 (PST)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:sender:date:from:to:cc:subject:message-id
+         :references:mime-version:content-type:content-disposition
+         :content-transfer-encoding:in-reply-to:user-agent;
+        bh=4cv3J11meWILtVQ6Drgqk74suEYFVRQKnvtS62ZKPMU=;
+        b=oson8MDOPhFFO5h9lGEmq3EcDJ7bfOy60AsJ8ka5Q45h/Fg5LvGyVGBWB48YesHBg+
+         51Kdb4VrtCWazj2/c1Eauv2jvrGXUjj1hZdo3Rq0jZb5eU+Nvf+7Gl8nWE1S47XmT8YW
+         ed9CdFnNDgvkVUw+Rg48e2nG79kDRNUGWlpKI=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=sender:date:from:to:cc:subject:message-id:references:mime-version
+         :content-type:content-disposition:content-transfer-encoding
+         :in-reply-to:user-agent;
+        b=lEIU6CLzqKxhpDXukIqjR49CQZ370NKAkb0Aah7A1uyEdLZ9ctYpgg1oPjsQX/V9IR
+         TqyIP0zocVjBhUgCi32M9DPe/qjiqe+YS+EXNGLMMrF1oEUY+yFfq1jChaHkk2xuf/EM
+         MaGyK3svEz1q2iV1bgkTLcXCLWyK+A/M1WFlg=
+Received: by 10.204.80.161 with SMTP id t33mr15020786bkk.121.1297686197182;
+        Mon, 14 Feb 2011 04:23:17 -0800 (PST)
+Received: from localhost (ppp91-77-40-235.pppoe.mtu-net.ru [91.77.40.235])
+        by mx.google.com with ESMTPS id w3sm1684029bkt.5.2011.02.14.04.23.14
+        (version=TLSv1/SSLv3 cipher=OTHER);
+        Mon, 14 Feb 2011 04:23:16 -0800 (PST)
+Content-Disposition: inline
+In-Reply-To: <4D591D04.4050000@gmail.com>
+User-Agent: Mutt/1.5.20 (2009-06-14)
+Sender: netdev-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <netdev.vger.kernel.org>
+X-Mailing-List: netdev@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099712>
+
+Hi Nicolas,
+
+On Mon, Feb 14, 2011 at 13:16 +0100, Nicolas de Peslo=FCan wrote:
+> >-   BUG_ON(strlen(name)>=3D sizeof(dev->name));
+> >+   if (strnlen(name, sizeof(dev->name))>=3D sizeof(dev->name)) {
+
+Ehh...  Space after ")" is needed :)
+
+> "size_t strnlen(const char *s, size_t maxlen) : The strnlen()
+> function returns strlen(s), if that is less than maxlen, or maxlen
+> if there is no '\0' character among the first maxlen characters
+> pointed to by s."
+>=20
+> How can strnlen(name, sizeof(dev->name)) be greater than sizeof(dev->=
+name)?
+>=20
+> Shouldn't it be "if (strnlen(name, sizeof(dev->name)) =3D=3D sizeof(d=
+ev->name))" instead?
+
+Not a big deal, but MO it's better to guard from everything that
+is not a good input by negating the check.  strnlen() < sizeof() is OK,
+strnlen() >=3D sizeof() is bad.  Is "=3D=3D" more preferable for net/ c=
+oding style?
+
+
+--=20
+Vasiliy Kulikov
+http://www.openwall.com - bringing security into open computing environ=
+ments
+--
+To unsubscribe from this list: send the line "unsubscribe netdev" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004688:2, b/test/corpora/lkml/cur/1382298805.004688:2,
new file mode 100644 (file)
index 0000000..dd935b6
--- /dev/null
@@ -0,0 +1,107 @@
+From: =?ISO-8859-1?Q?Nicolas_de_Peslo=FCan?= 
+       <nicolas.2p.debian@gmail.com>
+Subject: Re: [PATCH] core: dev: don't call BUG() on bad input
+Date: Mon, 14 Feb 2011 14:01:44 +0100
+Lines: 40
+Message-ID: <4D5927B8.2070704@gmail.com>
+References: <1297680967-11893-1-git-send-email-segoon@openwall.com> <4D591D04.4050000@gmail.com> <20110214122313.GA10062@albatros>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1;
+       format=flowed
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: linux-kernel@vger.kernel.org,
+       "David S. Miller" <davem@davemloft.net>,
+       Eric Dumazet <eric.dumazet@gmail.com>,
+       Tom Herbert <therbert@google.com>,
+       Changli Gao <xiaosuo@gmail.com>,
+       Jesse Gross <jesse@nicira.com>, netdev@vger.kernel.org
+To: Vasiliy Kulikov <segoon@openwall.com>
+X-From: netdev-owner@vger.kernel.org Mon Feb 14 14:02:23 2011
+Return-path: <netdev-owner@vger.kernel.org>
+Envelope-to: linux-netdev-2@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <netdev-owner@vger.kernel.org>)
+       id 1Poy4M-0006df-J5
+       for linux-netdev-2@lo.gmane.org; Mon, 14 Feb 2011 14:02:22 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1754233Ab1BNNBu convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;linux-netdev-2@m.gmane.org>); Mon, 14 Feb 2011 08:01:50 -0500
+Received: from mail-fx0-f46.google.com ([209.85.161.46]:54545 "EHLO
+       mail-fx0-f46.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1753647Ab1BNNBs (ORCPT
+       <rfc822;netdev@vger.kernel.org>); Mon, 14 Feb 2011 08:01:48 -0500
+Received: by fxm20 with SMTP id 20so5178314fxm.19
+        for <multiple recipients>; Mon, 14 Feb 2011 05:01:47 -0800 (PST)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=gmail.com; s=gamma;
+        h=domainkey-signature:message-id:date:from:user-agent:mime-version:to
+         :cc:subject:references:in-reply-to:content-type
+         :content-transfer-encoding;
+        bh=AbxlHNh3L+hBj6Vij/+GRK5xyYUXmvKoB1QZLa2ZBj0=;
+        b=b/GQO7FpiFoh6WrR9d9qEW2Q1ZOK0YtYzl/fLoXZS49QbuYiuExhWkohPnHsdH/n7s
+         liu8crpx1n3Ajna/7GX1mHBP6V4lfhH+NyF0Rmw3w+fx154lFiY9dbyPX7H9MZNdW60a
+         8TmPRR356gmV+7bijgKwyMN1FRVMPNV0Zg0i8=
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=gmail.com; s=gamma;
+        h=message-id:date:from:user-agent:mime-version:to:cc:subject
+         :references:in-reply-to:content-type:content-transfer-encoding;
+        b=C+hWB2Aof37xOLi8SWuN+D3QsDtf/f4yCxcLrNGhRYytyr/7CUmq/rS7PpgnfvBVBr
+         yaKwVZXs7QRxIWbPnzmV38e1K+eUwZ+dd9XuEFN1dnXd5KJVv4CjWr2N84NIHx/NvOBL
+         7QYK5+DuuRaccybcS4xWMNK8mujh9ebSBXTgM=
+Received: by 10.223.87.1 with SMTP id u1mr4464553fal.112.1297688507260;
+        Mon, 14 Feb 2011 05:01:47 -0800 (PST)
+Received: from [192.168.0.101] (eab95-4-88-175-177-37.fbx.proxad.net [88.175.177.37])
+        by mx.google.com with ESMTPS id y3sm1031898fai.38.2011.02.14.05.01.45
+        (version=SSLv3 cipher=OTHER);
+        Mon, 14 Feb 2011 05:01:46 -0800 (PST)
+User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.16) Gecko/20101226 Icedove/3.0.11
+In-Reply-To: <20110214122313.GA10062@albatros>
+Sender: netdev-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <netdev.vger.kernel.org>
+X-Mailing-List: netdev@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099723>
+
+Le 14/02/2011 13:23, Vasiliy Kulikov a =E9crit :
+> Hi Nicolas,
+
+Hi Vasiliy,
+
+> On Mon, Feb 14, 2011 at 13:16 +0100, Nicolas de Peslo=FCan wrote:
+>>> -  BUG_ON(strlen(name)>=3D sizeof(dev->name));
+>>> +  if (strnlen(name, sizeof(dev->name))>=3D sizeof(dev->name)) {
+>
+> Ehh...  Space after ")" is needed :)
+
+:-D
+
+>> "size_t strnlen(const char *s, size_t maxlen) : The strnlen()
+>> function returns strlen(s), if that is less than maxlen, or maxlen
+>> if there is no '\0' character among the first maxlen characters
+>> pointed to by s."
+>>
+>> How can strnlen(name, sizeof(dev->name)) be greater than sizeof(dev-=
+>name)?
+>>
+>> Shouldn't it be "if (strnlen(name, sizeof(dev->name)) =3D=3D sizeof(=
+dev->name))" instead?
+>
+> Not a big deal, but MO it's better to guard from everything that
+> is not a good input by negating the check.  strnlen()<  sizeof() is O=
+K,
+> strnlen()>=3D sizeof() is bad.  Is "=3D=3D" more preferable for net/ =
+coding style?
+
+Agreed, both cannot cause any troubles. =3D=3D is supposed to be better=
+ from the API point of view, but=20
+ >=3D is probably more readable.
+
+       Nicolas.
+--
+To unsubscribe from this list: send the line "unsubscribe netdev" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at  http://vger.kernel.org/majordomo-info.html
+
+
+
diff --git a/test/corpora/lkml/cur/1382298805.004906:2, b/test/corpora/lkml/cur/1382298805.004906:2,
new file mode 100644 (file)
index 0000000..fb3510a
--- /dev/null
@@ -0,0 +1,125 @@
+From: Colin Cross <ccross@android.com>
+Subject: Re: [PATCH] ARM: vfp: Always save VFP state in vfp_pm_suspend
+Date: Mon, 14 Feb 2011 10:35:37 -0800
+Lines: 50
+Message-ID: <AANLkTik_Jey_PtRmr530FVckA6RXHESeX+CyoJC=ZTkR@mail.gmail.com>
+References: <1297638813-1315-1-git-send-email-ccross@android.com>
+       <1297683742.30092.11.camel@e102109-lin.cambridge.arm.com>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Cc: linux-arm-kernel@lists.infradead.org,
+       Russell King <linux@arm.linux.org.uk>,
+       linux-kernel@vger.kernel.org
+To: Catalin Marinas <catalin.marinas@arm.com>
+X-From: linux-kernel-owner@vger.kernel.org Mon Feb 14 19:36:14 2011
+Return-path: <linux-kernel-owner@vger.kernel.org>
+Envelope-to: glk-linux-kernel-3@lo.gmane.org
+Received: from vger.kernel.org ([209.132.180.67])
+       by lo.gmane.org with esmtp (Exim 4.69)
+       (envelope-from <linux-kernel-owner@vger.kernel.org>)
+       id 1Pp3HR-0002ph-ME
+       for glk-linux-kernel-3@lo.gmane.org; Mon, 14 Feb 2011 19:36:14 +0100
+Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand
+       id S1751716Ab1BNSf7 convert rfc822-to-quoted-printable (ORCPT
+       <rfc822;glk-linux-kernel-3@m.gmane.org>);
+       Mon, 14 Feb 2011 13:35:59 -0500
+Received: from smtp-out.google.com ([74.125.121.67]:16138 "EHLO
+       smtp-out.google.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org
+       with ESMTP id S1751472Ab1BNSf5 convert rfc822-to-8bit (ORCPT
+       <rfc822;linux-kernel@vger.kernel.org>);
+       Mon, 14 Feb 2011 13:35:57 -0500
+Received: from kpbe14.cbf.corp.google.com (kpbe14.cbf.corp.google.com [172.25.105.78])
+       by smtp-out.google.com with ESMTP id p1EIZtnZ010066
+       for <linux-kernel@vger.kernel.org>; Mon, 14 Feb 2011 10:35:55 -0800
+DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; d=google.com; s=beta;
+       t=1297708556; bh=zHQvHco6EycqYjYMR9YZftpchts=;
+       h=MIME-Version:Sender:In-Reply-To:References:Date:Message-ID:
+        Subject:From:To:Cc:Content-Type:Content-Transfer-Encoding;
+       b=vMdLxEMc5VUxTbXeXOfO9iAKtgTlVwOwZrVZEhG7GNUReOYWZKzblAuPHuJUWHr5q
+        8s7bNLIdGKiRH4q28mv9w==
+Received: from vws17 (vws17.prod.google.com [10.241.21.145])
+       by kpbe14.cbf.corp.google.com with ESMTP id p1EIYaTC004311
+       (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT)
+       for <linux-kernel@vger.kernel.org>; Mon, 14 Feb 2011 10:35:54 -0800
+Received: by vws17 with SMTP id 17so3300387vws.2
+        for <linux-kernel@vger.kernel.org>; Mon, 14 Feb 2011 10:35:54 -0800 (PST)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+        d=google.com; s=beta;
+        h=domainkey-signature:mime-version:sender:in-reply-to:references:date
+         :x-google-sender-auth:message-id:subject:from:to:cc:content-type
+         :content-transfer-encoding;
+        bh=FyW95sl8WRQv1ev+TqjP0gzPcCabtWdW1//GAc6oFnY=;
+        b=MNkyYY1htITiUX23N5enGCjsYq2mGBCW4BadxXMha/29ZeIyVP6jrUHlViT88u79RG
+         SxLMzz7lijwW38xTiBfw==
+DomainKey-Signature: a=rsa-sha1; c=nofws;
+        d=google.com; s=beta;
+        h=mime-version:sender:in-reply-to:references:date
+         :x-google-sender-auth:message-id:subject:from:to:cc:content-type
+         :content-transfer-encoding;
+        b=Lio4jom4+RruCzH/a6zheXvUcTpZvSX7eMk6Ld1+PQQybP02I0+Cv4+PgGac2GoAYX
+         tXciu0c/KOGts2phbyMg==
+Received: by 10.220.94.201 with SMTP id a9mr1430388vcn.56.1297708537386; Mon,
+ 14 Feb 2011 10:35:37 -0800 (PST)
+Received: by 10.220.43.142 with HTTP; Mon, 14 Feb 2011 10:35:37 -0800 (PST)
+In-Reply-To: <1297683742.30092.11.camel@e102109-lin.cambridge.arm.com>
+X-Google-Sender-Auth: 47_39DbBbWrnAh28bpWvfpwmbLw
+X-System-Of-Record: true
+Sender: linux-kernel-owner@vger.kernel.org
+Precedence: bulk
+List-ID: <linux-kernel.vger.kernel.org>
+X-Mailing-List: linux-kernel@vger.kernel.org
+Archived-At: <http://permalink.gmane.org/gmane.linux.kernel/1099943>
+
+On Mon, Feb 14, 2011 at 3:42 AM, Catalin Marinas
+<catalin.marinas@arm.com> wrote:
+> On Sun, 2011-02-13 at 23:13 +0000, Colin Cross wrote:
+>> vfp_pm_suspend should save the VFP state any time there is
+>> a last_VFP_context. =A0If it only saves when the VFP is enabled,
+>> the state can get lost when, on a UP system:
+>> =A0 =A0Thread 1 uses the VFP
+>> =A0 =A0Context switch occurs to thread 2, VFP is disabled but the
+>> =A0 =A0 =A0 VFP context is not saved to allow lazy save and restore
+>> =A0 =A0Thread 2 initiates suspend
+>> =A0 =A0vfp_pm_suspend is called with the VFP disabled, but the
+>> =A0 =A0 =A0 context has not been saved.
+>
+> At this point is it guaranteed that the thread won't migrate to anoth=
+er
+> CPU? If not, we should use get/put_cpu.
+
+Yes, VFP suspend is implemented with a sysdev, which is suspended
+after disable_nonboot_cpus.
+
+>> --- a/arch/arm/vfp/vfpmodule.c
+>> +++ b/arch/arm/vfp/vfpmodule.c
+>> @@ -415,13 +415,12 @@ static int vfp_pm_suspend(struct sys_device *d=
+ev, pm_message_t state)
+>> =A0 =A0 =A0 =A0 struct thread_info *ti =3D current_thread_info();
+>> =A0 =A0 =A0 =A0 u32 fpexc =3D fmrx(FPEXC);
+>>
+>> - =A0 =A0 =A0 /* if vfp is on, then save state for resumption */
+>> - =A0 =A0 =A0 if (fpexc & FPEXC_EN) {
+>> + =A0 =A0 =A0 /* save state for resume */
+>> + =A0 =A0 =A0 if (last_VFP_context[ti->cpu]) {
+>> =A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0 printk(KERN_DEBUG "%s: saving vfp st=
+ate\n", __func__);
+>> - =A0 =A0 =A0 =A0 =A0 =A0 =A0 vfp_save_state(&ti->vfpstate, fpexc);
+>> -
+>> - =A0 =A0 =A0 =A0 =A0 =A0 =A0 /* disable, just in case */
+>> - =A0 =A0 =A0 =A0 =A0 =A0 =A0 fmxr(FPEXC, fmrx(FPEXC) & ~FPEXC_EN);
+>> + =A0 =A0 =A0 =A0 =A0 =A0 =A0 fmxr(FPEXC, fpexc | FPEXC_EN);
+>> + =A0 =A0 =A0 =A0 =A0 =A0 =A0 vfp_save_state(last_VFP_context[ti->cp=
+u], fpexc);
+>> + =A0 =A0 =A0 =A0 =A0 =A0 =A0 fmxr(FPEXC, fpexc & ~FPEXC_EN);
+>> =A0 =A0 =A0 =A0 }
+>
+> We may want to set the last_VFP_context to NULL so that after resumin=
+g
+> (to the same thread) we force the VFP reload from the vfpstate
+> structure. The vfp_support_entry code ignores the reloading if the
+> last_VFP_context is the same as vfpstate.
+
+Right, will fix.
+
+
diff --git a/test/corpora/mangling/mixed-up.eml b/test/corpora/mangling/mixed-up.eml
new file mode 100644 (file)
index 0000000..d470279
--- /dev/null
@@ -0,0 +1,29 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Here is the password
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <mixed-up@mangling.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: quoted-printable
+
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+Content-Transfer-Encoding: base64
+
+VmVyc2lvbjogMQ0K
+
+--=-=-=
+Content-Type: application/octet-stream
+Content-Transfer-Encoding: base64
+
+wV4DHXHP849rSK8SAQdA4L/4ntrLU9OGEHy0JIryOhNPoVCN1MgZieW5E+1wDjMw
+GZL3EGlGTYWPhAm6wmDXsMHWMtuBVawUN0lZDuUoQtWzXYZrn3HiKQZn2ahfIrrs
+0oMBLz48JurPlrHCIHfsVa/YbfZOTun/TPa5zcjX/Vi0+FgOHEFCmHzK/VqlqsdW
+PYgQPvn4rQL25GACLzzJGrDvvJKS4fEo2p3pf6SGDFtBeyKb0AdyXoBWfXelS+ST
+rMTVqEDTT+71MToOcmYPX2QJRIMEVix8tCmvMkUXni8AjurehQ==
+--=-=-=--
diff --git a/test/corpora/pkcs7/smime-onepart-signed.eml b/test/corpora/pkcs7/smime-onepart-signed.eml
new file mode 100644 (file)
index 0000000..070303b
--- /dev/null
@@ -0,0 +1,51 @@
+Received: from localhost (localhost [127.0.0.1]); Tue, 26 Nov 2019
+ 20:11:46 -0400 (UTC-04:00)
+Content-Transfer-Encoding: base64
+Content-Type: application/pkcs7-mime; name="smime.p7m";
+ smime-type="signed-data"
+MIME-Version: 1.0
+From: Alice Lovelace <alice@smime.example>
+To: Bob Babbage <bob@smime.example>
+Date: Tue, 26 Nov 2019 20:11:29 -0400
+Subject: The FooCorp contract
+Message-ID: <smime-onepart-signed@protected-headers.example>
+
+MIIHRQYJKoZIhvcNAQcCoIIHNjCCBzICAQExDTALBglghkgBZQMEAgEwggHJBgkq
+hkiG9w0BBwGgggG6BIIBtkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNl
+dD0idXMtYXNjaWkiDQpGcm9tOiBBbGljZSBMb3ZlbGFjZSA8YWxpY2VAc21pbWUu
+ZXhhbXBsZT4NClRvOiBCb2IgQmFiYmFnZSA8Ym9iQHNtaW1lLmV4YW1wbGU+DQpE
+YXRlOiBUdWUsIDI2IE5vdiAyMDE5IDIwOjExOjI5IC0wNDAwDQpTdWJqZWN0OiBU
+aGUgRm9vQ29ycCBjb250cmFjdA0KTWVzc2FnZS1JRDogPHNtaW1lLW9uZXBhcnQt
+c2lnbmVkQHByb3RlY3RlZC1oZWFkZXJzLmV4YW1wbGU+DQoNCkJvYiwgd2UgbmVl
+ZCB0byBjYW5jZWwgdGhpcyBjb250cmFjdC4NCg0KUGxlYXNlIHN0YXJ0IHRoZSBu
+ZWNlc3NhcnkgcHJvY2Vzc2VzIHRvIG1ha2UgdGhhdCBoYXBwZW4gdG9kYXkuDQoN
+ClRoYW5rcywgQWxpY2UNCi0tIA0KQWxpY2UgTG92ZWxhY2UNClByZXNpZGVudA0K
+T3BlblBHUCBFeGFtcGxlIENvcnANCqCCA3IwggNuMIICVqADAgECAhRngrRZc1JL
+wfRxRxlq8P0RiqpMCzANBgkqhkiG9w0BAQ0FADAtMSswKQYDVQQDEyJTYW1wbGUg
+TEFNUFMgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MCAXDTE5MTEyMDA2NTQxOFoYDzIw
+NTIwOTI3MDY1NDE4WjAZMRcwFQYDVQQDEw5BbGljZSBMb3ZlbGFjZTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMPurfll0bYkDPMkY1kNn2xXsAqHSGVF
++gWNNk3mbhF6BABhLJqDjei5aLXFE3Rq9/RRNivCMrTipF1XsbMIAKgQqr/GI1Q6
+yN8lfNsK5uU3d9kw5cOyEooGpOGUrvlKMD0LPGDt6MaiJj+KJ2TR73Wd4rfRIIJo
+FMmV9HZkOs+Tvcg8x6SzGhNq18X2HD10MD78eLXKm039obRD+z2JwWvGvrLbNBey
+O5A+aMxmCPXRoP1xrNZWBFgKB+WGYDRXW5CXXChthTwMBXFWf4aBpurKMZAyjK2E
+grQafn6h/DFddQz/NtT6Dr7UhJ2hfFFEW2rYbNsiqQAdllCb4FucWuECAwEAAaOB
+lzCBlDAMBgNVHRMBAf8EAjAAMB4GA1UdEQQXMBWBE2FsaWNlQHNtaW1lLmV4YW1w
+bGUwEwYDVR0lBAwwCgYIKwYBBQUHAwQwDwYDVR0PAQH/BAUDAwegADAdBgNVHQ4E
+FgQUrC5UWqT9VRivLuhmRDjRJdHXAHkwHwYDVR0jBBgwFoAUt1JNc8CIPbLDeloM
+85T394Cid9swDQYJKoZIhvcNAQENBQADggEBAHvqjhjPvKtVIVyleoutwa10jir3
+dooJcQIILM1AunjJ6yHpuuppkc0m3BhwnlOptTKb2EnvSIkTiMY037IBlHWW217Q
+cUpggEozgQm6Yb77aGptRovPi2XToEdpA8K//02I1jur1H1z8HqzVjMeHCqRaG3Z
+r4C2AngGSkb6D4yZkxBX8CjtHAsUon06UxYsGYRcVykgk3Qek9qxPScSX8yai1K7
+7xGcKUCLfIV/JMpv7ysPtXG7Jd62oNnp1T/3+KoP9JlLs5AiPLC13fjeYALPcHVG
+UXEwdIDp1AB/Zu0a6apHQqICncqRhEB4+hompiQHtlp3TqeAWXQbQUc437sxggHZ
+MIIB1QIBATBFMC0xKzApBgNVBAMTIlNhbXBsZSBMQU1QUyBDZXJ0aWZpY2F0ZSBB
+dXRob3JpdHkCFGeCtFlzUkvB9HFHGWrw/RGKqkwLMAsGCWCGSAFlAwQCAaBpMBgG
+CSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE5MTEyNzAw
+MTEyOVowLwYJKoZIhvcNAQkEMSIEILsI9kL3zfZiVOEDjAUWrbjHjGMLoGUwEqYH
+pOA9XZ+QMA0GCSqGSIb3DQEBAQUABIIBAGDat8UYN9MShlKEw3hYVVUk6HKO6Xjp
+rdgCBKpoyoWJy0VJis0xHxaT2gn/+TPu8a5l6RslgeALjMyflzyzAmrqnknQQG8K
+bvbt/MwpU/TxnmxT+2oP9TVmAx/IQOq4pQ35uK7peSPck2CcTvZjHTeVBWcsLVEk
+hELoSD8XFRBo34qdinBzW0/sMlyK1XnlN7khKry1g7uaXcurVqptRA1rWOvCOt72
+aElKG/Q7OoVgHxbUpdzV3Hqe9/UeTRDUqCs++on2pLlA0TA0Pq8RQ0hDHD/p0t41
+1RAT1/RbnGQiVfRilMan+VGT4shokb1RoANy/1rOO9ZKlyWToYdRl9E=
diff --git a/test/corpora/protected-headers/double-wrapped-with-phony-protected-header.eml b/test/corpora/protected-headers/double-wrapped-with-phony-protected-header.eml
new file mode 100644 (file)
index 0000000..115019b
--- /dev/null
@@ -0,0 +1,41 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Subject Unavailable
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <double-wrapped-with-phony-protected-header@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdAhuTX3AYQs007gCtcGsYQmtZg5DrD3ev6Dm+1Fq9vfjQw
+bHP9+UKDAmWL5XW2+zlFQVb1CcWn8QUXwjJBZvHEpwyxRWJNYqNlhsnXsX+5woGs
+0ukB6Jq0OJydXSKubEnI5Bxs7UC7rekUfxUH2ij6ZXmz5Xyc/h5rF2rJO1uaPVVx
+LuVOF+ZX0muBzegGMiUtunHmY0Akis7m3xDl2ZRVViRdW+LPoxb4oAGoPq5LHfHp
+5adDtrno13K6oO0NriVEPsSl6xFCLXOqTj600F8658o16yA8fiZj4kY8G8NunmMD
+lN4UNw/EQuWIe343dKaQo+iH52atihw90OG4ahmjvkhBC/iIiXZ3J5m10VoJ7Sxy
+G6O84UW06pCAqvPmkjnkgpflGdpM3Qv66Uu9ss6jAnRLP2LVv+IVWrwle/Yp6B8V
+1adoFUMphkp534vTR4v0xJ/6WaI1YzIwlT2zd2zCMoqwT5weuX509pTbae0iGLRG
+Q+l0ImN4AxsUdgWIW+GtMn3GVWCXk2BDLcr1xkl5lJVlOIbIvbpTwqYFMVAfnDGc
+s6JZS3Bwg9bfmxSkM4GhZlDiCdZE7f0IUBSnRRHPcMkaox0g7ibF8aG0himZRqhD
+3z6gtPmWO+c5i1iV6pQbntB7MY80vprz7gzmvmadbGFS+A4uCaH2afWr1eBb6nGx
+W6deJxHaCQrrxujz20y5osSMhdAk47WPkZhphU/8ERncZ1F8aYJhUprAbKFxNjt6
+I4p8lrfAe7iM4UxqtSfVx2rPV9/Nqm8rFVH5l4f/0P93qsBNdF7rVQl9Ry2kVqkk
+86ct9ZK2QrOtsnuSYFzhlaDo9pOHkhlMOmQXAgSb12VP87z65NTYRGbElypmQl8F
+MXMxrljqnCbPaQaPq/37GEHTRJ7rrUvCQ2L6Ljp+F0m555eYHNiZRaxYA17uABOf
+RIr5gfB5ICmnH2EGIYiEkSeX3k8+8xHffrzTpOAqv/5oJBDlRcu6qyVSAumNMPYX
+fKqlNDhKh9xmK7K88vWwTgeOBm/sOXcfuAkr9nXqDqdLHb+duLiH+xjvBdl+ewTx
+gpzIkoTSsd0fgmNqmmfwmmU4oIQu4QX3/zh5HiQS70xogRxxU3przSFrrtNkiSkI
+Oe5fIqQ3tlUSzutTEoaSxeU=
+=4vL7
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/encrypted-message-with-forwarded-attachment.eml b/test/corpora/protected-headers/encrypted-message-with-forwarded-attachment.eml
new file mode 100644 (file)
index 0000000..d43331b
--- /dev/null
@@ -0,0 +1,38 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <encrypted-message-with-forwarded-attachment@crypto.notmuchmail.org>
+Subject: Subject Unavailable
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdACh9CzQl7cqCVO0bpHfyqL0Kr/V2TtUO68yhOYuMMWXEw
+v+aH1aLYervLMNDSC8Fj6XY9myqaybbOfHnLZIQg0XkR3JIKhWR3LpZtfpFGSr/H
+0ukBgdihDN26YqD+UETkBN0lg/fd4+cvVyOJSwuU5DQFdsOk1znvEercz1UVpKxz
+gg1lniZzRYCyPSl9NcZVtNXggz6z9XqxT0cVE3WCH2IsVrJPfgnFu/P63pAhp43X
+IxJe5mB6y5eaX5Dcvj48Z2E+3tVVn1gKGwLzPH40WIy98KM4dv59I0ZghM8h1dVr
+LUUgdiz17fo5oWphzdgn57ERk9B/pApkzujrB4Sum9qy9y/Q2DWFKdOARNJSZ/Li
+KZVXZLVEyp1WeAU262blQRtqWXq30wU9PazFo3wsUUVoLCu5SWsRpircSIWrh3gH
+iBE/YdPYQNJ6kJoz17AdIq0a4jH3ae881P6eBW22zMxdqD4zfkT43iVQAtuki4JS
+iBJ2vHoMU3z+rJ59ea+P595QHCGNyfkgl50e0E8rh9j+9/CExyD6F4GwAZe9VA6o
+hYFQ3U089Cp/Cd5jVojUWIAOWqwb6nB7w3SabPZIbhhJSBdQEGe4Xh3TN3k5cpLr
+TxDJz4zf220EQ2pt0BJwDu8fdskMXC6ytQettvR46+UtFOOG9eClnAFkgeuqwKmv
+k1YyYMFWQ3Bv/IHOFrcu3negBQpP4ln5px6zggNoeNLzaZ9PN9Y2lHnoQTkw/93L
+qyjLD+xEnzgdPYZvVsNA1iI6LMrCandDwKySQyfZadj/G3/nLFcoThvn03NNUyCK
+Sh0Vy2gJhxD04HF8nkR/E5MLFCQ3x0qpGbOoanctgs/glxCpnzWRcSZERlilQ+3Y
+0r5UkO5XJU3Z3CP4uJnjykgB8Qjq/WzERgOtOQ/bVaWBWkKzxS/Pj1sJ47kKMtBJ
+3r5LXymlkA70RA0cJIv1F/At
+=vLN3
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/encrypted-signed-not-masked.eml b/test/corpora/protected-headers/encrypted-signed-not-masked.eml
new file mode 100644 (file)
index 0000000..9f0e51b
--- /dev/null
@@ -0,0 +1,33 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Rhinoceros dinner
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <encrypted-signed-not-masked@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdAhRhcdC3r33MMW+D1PgLAoIhstxnEWOjI0s5wyH2gKTIw
+ceJ7nVvmnqLNWbEaoLp2tVH1+cI4guvgqzV60BoxNU5G5YMGqzMS4VQ47N4BENmn
+0sDEAWcBT1+PGFlXAeotmryF5ErMXesEVJJHru/KHZxsP3wwRp3qZjrwIWVgVlVo
+SpJJ+hwIRqCTbdsw0ejKY0+BJwQ/z+GPmYkWE+ARxkzwMzfA47PW+iNudrQNEJuu
+ALnsbyVX0/JA6E8jhvIA2OHb6G2yqFA499I6kWiSkT3m2SikuyKGybwWbaUDZVJW
+Y+JN2L8snDUHJKfl5JMhAgu2+fx/joE9PxM6fdx8rJJbpmTrPqrYf6vYyuq5BGYV
+snGJozwiN/cqR+PruMT7i5/Dhs/EDl3Z9D8iH7FEDtIzpSJNvGFIHetXC4JXgO7d
+C48g+0uuUL3SSuZviz+OgLJbyDu1Pc3tCrtBUs0zYJGio5ghJljU5tUnCNhbBwd7
+oWgYxOmSzbZrs73E5Lpvnq+juZpXSGNoYzaZHacp2FTpo0LlZ2k6o8kx6eYfOlHs
+JN+43CnzTnR6eZYkfjIaBbjYKi0rMH2DRJjMXyeYRLjEi9ET2fFX3WWn487snIjw
+om8EPhsOxQ==
+=wKPm
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/encrypted-signed.eml b/test/corpora/protected-headers/encrypted-signed.eml
new file mode 100644 (file)
index 0000000..f64b99b
--- /dev/null
@@ -0,0 +1,33 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Subject Unavailable
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <encrypted-signed@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdAliiUK1XTMeUUmjJ6n4yEEBGVPmQuVTD4wdJZx4r2J3Mw
+uSVIq8qBJ7NrZfvDa63807usyKpk0Io3tdH83hn3wIKKmUMTroaRZMP1BdJ3aVdh
+0sDEAf18AhE5POknHzzolNbTXmQkPNspdNpu0ANdKkukvUZjfWvssZ1WN3jB08Fc
+Wr2Byez+UyLggj1bm+HuzbcZGPfyMOpoajFG0+yK4Ccqpf3XapNJn7v1ic2QhR/B
+pmWpJR0cXDigNLYQ/RoNEiSur2+0hzeVkDiYQtDYD02Cv4rnhyJdCalyriRaLOZr
+4Tau4xfK9PdOydahZZHEFllDU+yfEb9xtBZ+DEGh5AyR1/sG8ORnJc+5m/z54zmJ
+W3MmZ7M+4O1q5l5MbDwHq1TrQF5R9JSXyeGTW2XscsIPdvY5bExMwyMoW/XY4E8+
+GJ12UVpC/EWt2Nisfx2LhIoxtwaZm2r0Xkt6hY5DSMkpHLZtGPOOe1QxtXF1qXH2
+owTZynRxlmaWslOXdhtFRHyfGiFK4pNoV6fVB0e2khKKfXynN9Zx99GJ+DoJsOFT
+AYOadP1WlbIbSaimv/xSpaijE2XReKTLfN6aK1vSe90r96nCWr31fcd6x9ax0nVx
+jDReMRJrHA==
+=8mRD
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/misplaced-protected-header.eml b/test/corpora/protected-headers/misplaced-protected-header.eml
new file mode 100644 (file)
index 0000000..c93ce0f
--- /dev/null
@@ -0,0 +1,43 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Subject Unavailable
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <misplaced-protected-header@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdAlqPAqdaaB751ZhN8drQheW77zbyPrbofTZgTa/32UFow
+ShGDg7SQxQD+LssHmGT0g4pUPo8JGor8BIvnvcZa4qRiZJI1i/VWUVoLc+TBT442
+0ukBxtUeKC0V35/zRgS1ZXrbkiBh8TGy0lee/eM9yvfT5FmFhotzoQkexDSEPxjb
+g8Qr/IcH0v2XOEmHux3hQpngY/LRYoCv1c8Niu2Zfan1p7zefOZjAxRraeihHi6I
+KyyA8WIdiIac97vO1CRyXXATJabjeijH/BaUDlPvQ39c9Mdie+HeaVYcaC3e9oW3
+sjn8fN+KjkjLXwPWhgMDpvP9f3ZD8oCHkN7Utq4OTwz8gb0IbUIkElF4G6O3AckV
+UXwrASodKBL58IWYz6VWhyjYxoV2R2hLNtnQWWvKgYzru2Mdb/ONZBM/JMXMjFUt
+oxDJs2pb2sun2q6n1sWKTrS9MXbxeFEcp2v3hMXrmP8nLeNiPJ+HlerFCSZasDOm
+c/3X97zIneJUosm3ltqLunSSE4vp+FAGIRaibMjyE2u2niDReINxX58E7o9cfJL0
+9Af6olZA1gQ/8t5qMei5qTN3wi4a1ieqTN4yD4v7r8yZE5PmlQfyY0joXVepm8Tx
+9/rDLM/gU/zogoo17enlU3ipvLZKsVDkLIlv2SdT1AF0ROnjLBGJ2iWzKst9Rg7/
+mXzEqv9CSVM2lXd3qUqra7reeaVD3vd00zmLq3yM/2sHeuLAc40R0MkIOSa89eRP
+iXu4k9+/m4WU4bNPugaH8Szm7OuLQNoB7mAB6t9GJGq7Y8DKxMn5FSUwLtKkGkUQ
+YdGCw2/salvqoQIDJVMteKpM4pWi9sFoVbxAU+djinJejSjSmY+iKHcMYeQwOpja
+33+W2RM40JOUZ7yc+LzgWy/ahyD27cdS1TPaghlQxPeSjcEfekrjgGW+2wIMmCxy
+xyMPWeN+Mf7e/fNh6YDbld9BKk6zY4fNVWlC+gR/aMVapomNgW1/FS7F7vk5qY0Q
+pAf1GS1YR4pKpo3sSEaLHw5rhpI0pT5ZvB09+GYRhsBQzP3LvnBCsxcVvi7yERY2
+QVlHejpTWwuRcTQCXP9UXa7z9UhEOieHwy6xNRTi4HVWHbXMxVunvlUnBCnQdFIr
+s4bcOjY94Q29FeRjaweYIZBUSabXDLJhLZNH1u6Z9Z8dBZ6Th1+ou77m0rEN6B3d
+mf83pKPrOR8ASw88rRGq8O9yVpFR4mD7HiE7YNofsPBtk0Cz178WLCi/D8b9A80h
+R/jVyxLmrCPHQUFfu46JgytBta+VBYwnYFHDynnYczuB0gydvuPJ4cJvfn3vPA==
+=DmA/
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/nested-rfc822-message.eml b/test/corpora/protected-headers/nested-rfc822-message.eml
new file mode 100644 (file)
index 0000000..4d5073b
--- /dev/null
@@ -0,0 +1,33 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Subject Unavailable
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <nested-rfc822-message@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdAc+vNxgvGe1T23qYt8zZ2dtmU4+DzCiMlKBFrI64sICMw
+foE2ym+RPLXzL2SbgDaUqgYbkiKmo6HrFSURtlDae0lFHrmLYZHToVWXF14DGsyu
+0sDgAcsIt7i8j6XWpAI9slRqjDAEPBp+4EFJKL+BdIuEYa6z1ULrv5BUimUi6D1o
+UE+k49xE6iFOpgSEghF1+ZneE3bj8rqJJwA+sAjth/Kp8vNccA42mCyn3Avxvk9q
+aMw7GWvTRJ5oc+RGo6BZukQtApQTbLzOeI92w68XNmQaSq4+LKUA4+CTZpasR/WA
+CR2/MWfwW3vmZilPbW2249Nj8CAawaDxTsIY9i5bHE0HjbfJhBBNffDPoNCh/+Pb
+6wIZ9/6xLHAzxFtY+qvDhVO/nWOLrdd9ACZudoD/x4qITc9IFo4F8bWF5iKPs67q
+wtw1qFTT8ODc2WWhOizDByOkk/D+Z3mrlsOC/x7ioho0IIeWldkfaET2ucc9FI9S
+SPX+huu6vnPAGO21T4EMqevwDLNGWMQBolHSlU3SRnNyU1bqUNDF+/9RiOk/NFj9
+FTwMj8FAI6/q0kZLQUF34h4BPF8/v1TmVZKniaVXqQIE78MvFWXV4FXYvP4w+IdK
+q634Ah+cC9NVEB+U6H4aSB9IojUurd+RvD+4x7rd+EtLFdc=
+=HmgM
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/no-protected-header-attribute.eml b/test/corpora/protected-headers/no-protected-header-attribute.eml
new file mode 100644 (file)
index 0000000..cf786c0
--- /dev/null
@@ -0,0 +1,29 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Subject Unavailable
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <no-protected-header-attribute@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdAQd0btUvfUMmCgmDv3mG8d2tfmr3MYsOdE4TeSqk6sFsw
+8Wsp5J4s2t9ua6ScvqCVtlUVUL4Z6BsACArwy0/XwGQ8JQ6BUuYH7JSWb6O6tzed
+0r8BYDEY7/8+QCgXneo6k3wvPHrzTsvg9fxmCSTbvA+8JCrEzbvM5l/wQmSDf18x
+difPsrCT75x3eYgOy7NNYIAg97teybVWZ4raPQ/SdJ9J0TmPTjQF07HPndrsYGQC
+rEKvu6oq2/HL4NWOWMNNixs0u6ALRpPuUUXKgwSaZUG8+juOZ3yFe56bd1IvwkWK
+ZkKwa9gBn7WA+eNpcJauux/ta6LuXiNvNHUGUnd9puVepi9GO3XAxYcOzqF6ly1V
+iw==
+=c3OU
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/phony-protected-header-bad-encryption.eml b/test/corpora/protected-headers/phony-protected-header-bad-encryption.eml
new file mode 100644 (file)
index 0000000..d0e11a1
--- /dev/null
@@ -0,0 +1,27 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Subject Unavailable
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <phony-protected-header-bad-encryption@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+Subject: this should not show up as a protected header
+
+-----BEGIN PGP MESSAGE-----
+
+BADBADBAD49rSK8SAQdAeybb8KrIaEFV5+Ks5loaz651PudVdzS8ombK8EW7Mnsw
+kEppTiE4jo6ZocHvhjSzPdEK4MPh0qmKvu1RrHa23dc1n7Cutg1FjOb6ZRloTisz
+0jEB3YxBhFDBZyWSGeAeZx93JaNcV6CBjOfZ6GJhzkqmSs73VdwFUWQxcoV4q5sL
+GYCW
+=BADC
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/protected-header.eml b/test/corpora/protected-headers/protected-header.eml
new file mode 100644 (file)
index 0000000..a10612f
--- /dev/null
@@ -0,0 +1,29 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Subject Unavailable
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <protected-header@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+Subject: this should not show up as a protected header
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdA3xy46BwN9R4tHbyAqwTMeOSfrrNzqsvCT9hqcVfRIzQw
+ThEINU6n9x5QgU2L/mtSaAMkw7ikOmzrJkvjEEE913dvUsw80+Q3QDYODETYzXBN
+0qIBrEpD0pYlXiQECDAYqox9JBkOPi3K6c4TLdACG2q7oOtHzbApzHC636oOFAXB
+uhARzvr/TN5/Fr4KDM2j4LqdsxSwE2mOJn4lM8EfPAm0jxbDmHKOpjpk/QnKR7ry
+BbctXwYIwlkp6voRsAJ/zG3XcLpbO/w+a5U14P6qCp2RM+bNWKtZTT4Gl0yCBk3A
+CYGvWb797ZA+5BhraXCWAQcscec=
+=WKBy
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/protected-with-legacy-display.eml b/test/corpora/protected-headers/protected-with-legacy-display.eml
new file mode 100644 (file)
index 0000000..9a3c764
--- /dev/null
@@ -0,0 +1,42 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: Subject Unavailable
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-Id: <protected-with-legacy-display@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdAGiLZx0/sI1uoQ27OPpwunlzzlY7ba4E2YNU+AErRA1gw
+YYKlhgfzSgO0LpbxHJfithVAkrGYvj5HNWGxriXEsOyfy+Ax0FtAJuzYr6DGKGx1
+0ukBxLoRfWuoo2C9gXwojieSI9lTh7V+FaVUoVmTXhd/WFjWLtHbkIVmvXI1WVyi
+wM5NfDi3Ho995P+DMZxTKkdqtbdeYgUK7oCw3FHsYsNSTf3XFazjEvHg/TvOWd5u
+m9SbZAKfOBrEGGCzNHfgyluaBKdzVX04GPfZ2GblHlnU9AnGSWys1i8kxmuQYAKp
+UrXDlMcVB4V5bcGd1KMvGyhnKzICXWhXiJhEiNOc+uhl97jxXRujflfT6S1+8thv
+o86332XixGu8o5svVAEWobN8LUXpHZZlnK9a0Zftf4v8ATHEzQLAa5vdx+BYN8ua
+e4dmCtxA4XCfRD58FJ6EwjDqhv45KYnJP2W5eZujQ7Pl1m3HJXGwFQmtnOSB/9dw
+M+y1Aif07VBYE3LmUUqmS0HLZoqmOEoh6rKldzyxFmtfZyn73n/zcUoQblEWTE7z
+lxIqpCmo8jHPcs1tm9QD3sUmqQ/YXwmqZbD3pOn0PIXZKVY8/DaeggMWKQ/UhCWa
+7Z8g2GVq17AjHsS9n3ShDhf6B/8qI+jjaZQqH0W6KLmDQixjf1BoPnTrXNjcloJk
+uf0YAuol05fXNAiyPbFNO9zoFPxm8ZVEZG9nbcnNOz7ac/Aea6hqhxHnzNFPU09K
+J92FZ08XXDlrt0jw11Z/i606U/7kX6Zy4vCtZjGB4h04msBiLQwI0POIcY28SJ0U
+W1AqcReye6lQTz47AkOKAfVQl9hQP++G7nZXlxUQ+z0VRqBEqd/QJdHgoe6X4ctd
+r8093odiz6/DXJNwDTHPkaV5IseghzSLYyjmbLR5DUjnfuxKw5zpG+mK3X2PDx1B
+LtUNfBGmnLN3jBa8Q/i2WYxYpAuMZzJcCcocxW0H+yBf8+rZNpIvi/RsTklKkaap
+EOgP9sZXlgJePUbBmdd4Wwx7WTsjna6ckNp/9WE8CuDy6x9Zkc97Rkd+Oxc/KKtF
+1mQ/VdRZj3trlABnHmF0H/H4Qlrt//P/PCl3qRZpE5v34OHDlTT6UjLh5ahWZ3hf
+pj3cSKy9uajnWPFf9tnI0/9cWYbllaCMhIMbDZXRM3F4H03bi2k=
+=VKCz
+-----END PGP MESSAGE-----
+
+--=-=-=--
diff --git a/test/corpora/protected-headers/signed-protected-header.eml b/test/corpora/protected-headers/signed-protected-header.eml
new file mode 100644 (file)
index 0000000..f5efaff
--- /dev/null
@@ -0,0 +1,28 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: This is a signed message
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <signed-protected-header@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-=";
+ protocol="application/pgp-signature";
+ micalg=pgp-sha256
+
+--=-=-=
+Content-Type: text/plain; protected-headers="v1"
+Subject: This is a signed message
+
+Here is the signed message body.
+
+--=-=-=
+Content-Disposition: attachment; filename=signature.asc
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+
+iHUEARYIAB0WIQSaOv5sYAZaFI/UtYp+ar6SRkXMYAUCYxiQlwAKCRB+ar6SRkXM
+YIm6AP0UlyfUbhd7bG4Azs0rby3qPUXOC1DtbSpQegSuR7nGgAEAub3WeYgEVVOS
+fsnuNE9Q/LnPTS5m85eMa1s1bS8fcAE=
+=O+fm
+-----END PGP SIGNATURE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/simple-signed-mail.eml b/test/corpora/protected-headers/simple-signed-mail.eml
new file mode 100644 (file)
index 0000000..cd1e9fc
--- /dev/null
@@ -0,0 +1,27 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: This is a signed message
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <simple-signed-mail@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-=";
+ protocol="application/pgp-signature";
+ micalg=pgp-sha256
+
+--=-=-=
+Content-Type: text/plain
+
+Here is the signed message body.
+
+--=-=-=
+Content-Disposition: attachment; filename=signature.asc
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+
+iHUEARYIAB0WIQSaOv5sYAZaFI/UtYp+ar6SRkXMYAUCYxiQYgAKCRB+ar6SRkXM
+YJkmAP9TEGDYF4GZcHaxWDZYf6EKHmNqu1RPYuwEN8QdVbUIxAEA7IiFYPQtKXgr
+wyEYNcJ8aD1CYCGhR8pTA9oT/Vp16Qk=
+=a+TS
+-----END PGP SIGNATURE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/smime-enc+legacy-disp.eml b/test/corpora/protected-headers/smime-enc+legacy-disp.eml
new file mode 100644 (file)
index 0000000..6f5c941
--- /dev/null
@@ -0,0 +1,50 @@
+Received: from localhost (localhost [127.0.0.1]); Wed, 27 Nov 2019
+ 01:27:28 -0700 (UTC-07:00)
+MIME-Version: 1.0
+Content-Transfer-Encoding: base64
+Content-Type: application/pkcs7-mime; name="smime.p7m";
+ smime-type="enveloped-data"
+From: Alice Lovelace <alice@smime.example>
+To: Bob Babbage <bob@smime.example>
+Date: Wed, 27 Nov 2019 01:27:00 -0700
+Message-ID: <smime-enc+legacy-disp@protected-headers.example>
+Subject: ...
+
+MIIG5QYJKoZIhvcNAQcDoIIG1jCCBtICAQAxggLCMIIBXQIBADBFMC0xKzApBgNV
+BAMTIlNhbXBsZSBMQU1QUyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkCFCJT7jBtAgsf
+As31ycE+Ot95phvCMA0GCSqGSIb3DQEBAQUABIIBADEhlzhFzYj6tUAdsRCrSiLl
+d9cgKtlAesJ4cDY4szFWAbnwrCmEcFxjFDUOjbfQCYCG80Sxd+xntni73I7PI2rR
+QLjk3w9VhLwFRyzy7qyJi2CavjKTxysX9f36+FXA+THfVQRM5ypiyYJg91X51PNX
+hJj3DHrnxqKeSl/z1hdt9r+s6XAUCBSvL99BGnODWhNIZtPDzt8fMNcgarfw+D5F
+IZJb6+wX30tkztHkpHHKrrDPveyfnlS/p06Gi3ekrrhBtMQMRb9PA/E+ivDPktsm
+aKg0Oauw4oZSKW3f4ukYhbnndbbagNsnTfs/QFy/p+hhKTrfCd0h1N8mTzedVX0w
+ggFdAgEAMEUwLTErMCkGA1UEAxMiU2FtcGxlIExBTVBTIENlcnRpZmljYXRlIEF1
+dGhvcml0eQIUZ4K0WXNSS8H0cUcZavD9EYqqTAswDQYJKoZIhvcNAQEBBQAEggEA
+FaK5QaPXJ133D2uybQt//oeDm6PkCAFW9YVOgjnLLz6FD54Dt2i1KCQu1Xlg9W3P
+1zJdYXOftDgilylNfmt/muEsvbRfFtMWUq0VGirHz//BWmY2cW/ocinFO514iviL
+MLE1umsXRNwVIVIk/uh7AmqXjPkRZgRgIMUbSbtmW4DDja+ZM0vmqFQ1iUIlApth
+FpjFfPDHHD8isLTbGi2iK6dEN3DIJFGbg5o3nK6yAhVZ7x3LfFNSNVDDSY5mPFG9
+Vm6uRgEE3Y5P6DbXXo6MHTgg0XY2f4y6MEWhOg37NT9aFAfzBBxJ1oSBWpOOfZnV
+K1DvAwPaemSRz9oWDcBM8DCCBAUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIsFkN
+8DEx8muAggPgWGF2WsPq3/a9jUa5GA0YFPiINuETCGTNaEXiVxnT0h0CF+EhZ0T2
+HFCiZEM0dzO05zt9WdVvAREaCSh7ZWG9D9wJF9x+tqQbzMuJ2AdKuoOH73kClvkx
+pHxANLhkY7hzIqRb/eLG5D7Xh8iCDiFecXDh7EHqD/R+sfLN9aHKOcKyY36kesBQ
+R8aHZbbFnnD+oXSDNIPcntGG3BSGMxsWuOp+rpTKeIHWFIungDNKsLIy3kWleENw
+FVIcjUF6QhI1HYW6BeXuVq40GV2OOkmB24rYEW1Jg0hAtY+5rn2mRoyxvUC87bjQ
+hLu6xgPmhun9J324eM5aYVwkmVBnRW9hyxClZ7Sv0zlL7lGQ0VQG+zWHeJ+h/M2j
+mQpLgAUEGxxNCm5ASHuXPIN6pSvrOVplrT8kKLPpmMYEwmTX2/rBO4P8I8uNrqYD
+AyX8p0/l2ArczkWzGTz2luBahrD+cTZPApe5SeyXOxWBl1Lmb0G8o4twBeeBLiHP
+XwYvttx0JYG/hc/lmMpEemJqwj9uZ3wGD03dIhhDX2Oj4ek/7jT6yqJh8C1H+PqA
++HNfNXsFQDrRORoqJS8YVEiYRDQNyePy2ugzLTh88nPtJp92hY7bk9zl3AYaiVFH
++szlLoyzfM9D+geZemR8XfI2ijGnrWMlnyPah/zA6J6RwemhuiMklZGYG85hMU9H
+K4CFVM+m7xYxKpwFVnmkVZjzWInirJhehElhtCXpx/IFGxH9CPbCyEZV1WVStrl/
+0fWTGicMXez6hVQCadWCXy96/eLIXOrC54gSoIJX2TD6jdVEu1YptutyGI6KdQ2p
+yXwhs98Uj7DM3nmFeAcjjN3e8pPoX7aG8eP+MfmHlWN6jA44jMaJmIdp9J20g74J
+MdjvnHa/cGibW/RamPiFObN0F94A83vcpUfU/zZ8cFHi/3/lN6Rm9+3/giGRZa9E
+Y6e2/CEq1cUbPQ09fPwRJmjZCfDce71DKe+ZFGdYtFR7JwDEeZ6BB4Ff4rXctcWD
+PgUJqUGv/SXBcFn4cNUK9MYYqVu1ovd/T7FMf+i3c5MH6BRCvft/i5aeBR+A26Gk
+2awtBPYdHW6+AslrFjncBbtPDlU6vX9AWuC0k0MQYnNkTWS8gTvsriXJZ6Zu5iFE
+ExNuFz7YcnMKnguOn2ph5azzeMm83AYzWXzZPu3mdr5Siuu/Ke38oADKP+BZ08Za
+XVvKvvfnRPXO9kG9hgvEMRU9KOcxn82XoGPNZib+9SPa2zYx5P6HX1Bqe/cmKAen
+FKEiJLSTP2/pc6AWAICqJl978HaUHfMFiN7jEUppAifpAWqNcIGSW5w=
+
diff --git a/test/corpora/protected-headers/smime-multipart-signed.eml b/test/corpora/protected-headers/smime-multipart-signed.eml
new file mode 100644 (file)
index 0000000..f05d2d9
--- /dev/null
@@ -0,0 +1,68 @@
+Received: from localhost (localhost [127.0.0.1]); Tue, 26 Nov 2019
+ 20:03:17 -0400 (UTC-04:00)
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="179";
+ protocol="application/pkcs7-signature"; micalg="sha-256"
+From: Alice Lovelace <alice@smime.example>
+To: Bob Babbage <bob@smime.example>
+Date: Tue, 26 Nov 2019 20:03:00 -0400
+Subject: The FooCorp contract
+Message-ID: <smime-multipart-signed@protected-headers.example>
+
+--179
+Content-Type: text/plain; charset="us-ascii"; protected-headers="v1"
+From: Alice Lovelace <alice@smime.example>
+To: Bob Babbage <bob@smime.example>
+Date: Tue, 26 Nov 2019 20:03:00 -0400
+Subject: The FooCorp contract
+Message-ID: <smime-multipart-signed@protected-headers.example>
+
+Bob, we need to cancel this contract.
+
+Please start the necessary processes to make that happen today.
+
+(this is the 'smime-multipart-signed' message)
+
+Thanks, Alice
+-- 
+Alice Lovelace
+President
+Example Corp
+
+--179
+Content-Transfer-Encoding: base64
+Content-Type: application/pkcs7-signature; name="smime.p7s"
+
+MIIFhQYJKoZIhvcNAQcCoIIFdjCCBXICAQExDTALBglghkgBZQMEAgEwCwYJKoZI
+hvcNAQcBoIIDcjCCA24wggJWoAMCAQICFGeCtFlzUkvB9HFHGWrw/RGKqkwLMA0G
+CSqGSIb3DQEBDQUAMC0xKzApBgNVBAMTIlNhbXBsZSBMQU1QUyBDZXJ0aWZpY2F0
+ZSBBdXRob3JpdHkwIBcNMTkxMTIwMDY1NDE4WhgPMjA1MjA5MjcwNjU0MThaMBkx
+FzAVBgNVBAMTDkFsaWNlIExvdmVsYWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAw+6t+WXRtiQM8yRjWQ2fbFewCodIZUX6BY02TeZuEXoEAGEsmoON
+6LlotcUTdGr39FE2K8IytOKkXVexswgAqBCqv8YjVDrI3yV82wrm5Td32TDlw7IS
+igak4ZSu+UowPQs8YO3oxqImP4onZNHvdZ3it9EggmgUyZX0dmQ6z5O9yDzHpLMa
+E2rXxfYcPXQwPvx4tcqbTf2htEP7PYnBa8a+sts0F7I7kD5ozGYI9dGg/XGs1lYE
+WAoH5YZgNFdbkJdcKG2FPAwFcVZ/hoGm6soxkDKMrYSCtBp+fqH8MV11DP821PoO
+vtSEnaF8UURbaths2yKpAB2WUJvgW5xa4QIDAQABo4GXMIGUMAwGA1UdEwEB/wQC
+MAAwHgYDVR0RBBcwFYETYWxpY2VAc21pbWUuZXhhbXBsZTATBgNVHSUEDDAKBggr
+BgEFBQcDBDAPBgNVHQ8BAf8EBQMDB6AAMB0GA1UdDgQWBBSsLlRapP1VGK8u6GZE
+ONEl0dcAeTAfBgNVHSMEGDAWgBS3Uk1zwIg9ssN6WgzzlPf3gKJ32zANBgkqhkiG
+9w0BAQ0FAAOCAQEAe+qOGM+8q1UhXKV6i63BrXSOKvd2iglxAggszUC6eMnrIem6
+6mmRzSbcGHCeU6m1MpvYSe9IiROIxjTfsgGUdZbbXtBxSmCASjOBCbphvvtoam1G
+i8+LZdOgR2kDwr//TYjWO6vUfXPwerNWMx4cKpFobdmvgLYCeAZKRvoPjJmTEFfw
+KO0cCxSifTpTFiwZhFxXKSCTdB6T2rE9JxJfzJqLUrvvEZwpQIt8hX8kym/vKw+1
+cbsl3rag2enVP/f4qg/0mUuzkCI8sLXd+N5gAs9wdUZRcTB0gOnUAH9m7RrpqkdC
+ogKdypGEQHj6GiamJAe2WndOp4BZdBtBRzjfuzGCAdkwggHVAgEBMEUwLTErMCkG
+A1UEAxMiU2FtcGxlIExBTVBTIENlcnRpZmljYXRlIEF1dGhvcml0eQIUZ4K0WXNS
+S8H0cUcZavD9EYqqTAswCwYJYIZIAWUDBAIBoGkwGAYJKoZIhvcNAQkDMQsGCSqG
+SIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMTkxMTI3MDAwMzAwWjAvBgkqhkiG9w0B
+CQQxIgQgGeoQw8WDmjB606EKGR5n1oMuV7Te1VjfA2oB2ebW390wDQYJKoZIhvcN
+AQEBBQAEggEABblYEWSnYyzL3jTS3AoPr93YKksIZr5q/b8Y5/1rMxdYxPm+iReO
+RHRgpbFQeiqZXzRXtMohfoIkh7RmdQoSV4OpwiUmNU+f0ZEAu8cMVJM6gdyUD+1D
+JwDNr+YNLV/1UUGhqx0FExOa/4O92KYBD4eRQw4KDWrkfh9dlSj0Bsl4thrZYGLz
+e7ut3FN5TBruZfmqMy50xZ9yUW91YyQUBLiIcuF185y5ZW/aQCxBKBbrNNGXLJbo
+8yKFJqSPiWZvwUmVQvfgL182hg823OJTtP4VImcUakTF0+k+BM//qqKXYrlX/tZn
+QzG+4ZH/XM1vgHl7ShjHS6TSOHz2ODqD6Q==
+
+--179--
+
diff --git a/test/corpora/protected-headers/smime-onepart-signed.eml b/test/corpora/protected-headers/smime-onepart-signed.eml
new file mode 100644 (file)
index 0000000..028e02c
--- /dev/null
@@ -0,0 +1,54 @@
+Received: from localhost (localhost [127.0.0.1]); Tue, 26 Nov 2019
+ 20:06:17 -0400 (UTC-04:00)
+Content-Transfer-Encoding: base64
+Content-Type: application/pkcs7-mime; name="smime.p7m";
+ smime-type="signed-data"
+MIME-Version: 1.0
+From: Alice Lovelace <alice@smime.example>
+To: Bob Babbage <bob@smime.example>
+Date: Tue, 26 Nov 2019 20:06:00 -0400
+Subject: The FooCorp contract
+Message-ID: <smime-onepart-signed@protected-headers.example>
+
+MIIHhQYJKoZIhvcNAQcCoIIHdjCCB3ICAQExDTALBglghkgBZQMEAgEwggIJBgkq
+hkiG9w0BBwGgggH6BIIB9kNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNl
+dD0idXMtYXNjaWkiOyBwcm90ZWN0ZWQtaGVhZGVycz0idjEiDQpGcm9tOiBBbGlj
+ZSBMb3ZlbGFjZSA8YWxpY2VAc21pbWUuZXhhbXBsZT4NClRvOiBCb2IgQmFiYmFn
+ZSA8Ym9iQHNtaW1lLmV4YW1wbGU+DQpEYXRlOiBUdWUsIDI2IE5vdiAyMDE5IDIw
+OjA2OjAwIC0wNDAwDQpTdWJqZWN0OiBUaGUgRm9vQ29ycCBjb250cmFjdA0KTWVz
+c2FnZS1JRDogPHNtaW1lLW9uZXBhcnQtc2lnbmVkQHByb3RlY3RlZC1oZWFkZXJz
+LmV4YW1wbGU+DQoNCkJvYiwgd2UgbmVlZCB0byBjYW5jZWwgdGhpcyBjb250cmFj
+dC4NCg0KUGxlYXNlIHN0YXJ0IHRoZSBuZWNlc3NhcnkgcHJvY2Vzc2VzIHRvIG1h
+a2UgdGhhdCBoYXBwZW4gdG9kYXkuDQoNCih0aGlzIGlzIHRoZSAnc21pbWUtb25l
+cGFydC1zaWduZWQnIG1lc3NhZ2UpDQoNClRoYW5rcywgQWxpY2UNCi0tIA0KQWxp
+Y2UgTG92ZWxhY2UNClByZXNpZGVudA0KRXhhbXBsZSBDb3JwDQqgggNyMIIDbjCC
+AlagAwIBAgIUZ4K0WXNSS8H0cUcZavD9EYqqTAswDQYJKoZIhvcNAQENBQAwLTEr
+MCkGA1UEAxMiU2FtcGxlIExBTVBTIENlcnRpZmljYXRlIEF1dGhvcml0eTAgFw0x
+OTExMjAwNjU0MThaGA8yMDUyMDkyNzA2NTQxOFowGTEXMBUGA1UEAxMOQWxpY2Ug
+TG92ZWxhY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDD7q35ZdG2
+JAzzJGNZDZ9sV7AKh0hlRfoFjTZN5m4RegQAYSyag43ouWi1xRN0avf0UTYrwjK0
+4qRdV7GzCACoEKq/xiNUOsjfJXzbCublN3fZMOXDshKKBqThlK75SjA9Czxg7ejG
+oiY/iidk0e91neK30SCCaBTJlfR2ZDrPk73IPMeksxoTatfF9hw9dDA+/Hi1yptN
+/aG0Q/s9icFrxr6y2zQXsjuQPmjMZgj10aD9cazWVgRYCgflhmA0V1uQl1wobYU8
+DAVxVn+GgabqyjGQMoythIK0Gn5+ofwxXXUM/zbU+g6+1ISdoXxRRFtq2GzbIqkA
+HZZQm+BbnFrhAgMBAAGjgZcwgZQwDAYDVR0TAQH/BAIwADAeBgNVHREEFzAVgRNh
+bGljZUBzbWltZS5leGFtcGxlMBMGA1UdJQQMMAoGCCsGAQUFBwMEMA8GA1UdDwEB
+/wQFAwMHoAAwHQYDVR0OBBYEFKwuVFqk/VUYry7oZkQ40SXR1wB5MB8GA1UdIwQY
+MBaAFLdSTXPAiD2yw3paDPOU9/eAonfbMA0GCSqGSIb3DQEBDQUAA4IBAQB76o4Y
+z7yrVSFcpXqLrcGtdI4q93aKCXECCCzNQLp4yesh6brqaZHNJtwYcJ5TqbUym9hJ
+70iJE4jGNN+yAZR1ltte0HFKYIBKM4EJumG++2hqbUaLz4tl06BHaQPCv/9NiNY7
+q9R9c/B6s1YzHhwqkWht2a+AtgJ4BkpG+g+MmZMQV/Ao7RwLFKJ9OlMWLBmEXFcp
+IJN0HpPasT0nEl/MmotSu+8RnClAi3yFfyTKb+8rD7VxuyXetqDZ6dU/9/iqD/SZ
+S7OQIjywtd343mACz3B1RlFxMHSA6dQAf2btGumqR0KiAp3KkYRAePoaJqYkB7Za
+d06ngFl0G0FHON+7MYIB2TCCAdUCAQEwRTAtMSswKQYDVQQDEyJTYW1wbGUgTEFN
+UFMgQ2VydGlmaWNhdGUgQXV0aG9yaXR5AhRngrRZc1JLwfRxRxlq8P0RiqpMCzAL
+BglghkgBZQMEAgGgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3
+DQEJBTEPFw0xOTExMjcwMDA2MDBaMC8GCSqGSIb3DQEJBDEiBCAKDM98nuDl98sK
+i4SDvP2xlxr2SdV/xNVYs6SeGCBRuTANBgkqhkiG9w0BAQEFAASCAQAcryWkSIbG
+rrc/aDF1Z4KRnoRpr+fOutQSLV7k0Tgezt+X/kJCIiuLvjUxLrTux1yUWCKUPb6T
+KLYASPJpwDXrNzqmGs1pJmWHTZwUhbFVXt16FaQZkDSATtvhQU39Rsot2j1pP/UV
+J7+5FPQwNc4dt7MFW7jU4TBHo2VrzjZ2K8ioELPxsixOCAp3ytkhf1Umw6bC5M/u
+oWjsa6xzAl4fw5+pxZw0JdbrYn5kmPiekSsYy2/+yOwzrtIYtHW5dY7DoWWXDXtD
+cmCGHkO8qry+MnMy3PwvXiX0warQo1fnhXB5tlk2K9YdiDcOtnAshEBXAudnxlPK
+JGzeJVUfbfM0
+
diff --git a/test/corpora/protected-headers/smime-sign+enc+legacy-disp.eml b/test/corpora/protected-headers/smime-sign+enc+legacy-disp.eml
new file mode 100644 (file)
index 0000000..7ec3396
--- /dev/null
@@ -0,0 +1,102 @@
+Received: from localhost (localhost [127.0.0.1]); Wed, 27 Nov 2019
+ 01:24:28 -0700 (UTC-07:00)
+MIME-Version: 1.0
+Content-Transfer-Encoding: base64
+Content-Type: application/pkcs7-mime; name="smime.p7m";
+ smime-type="enveloped-data"
+From: Alice Lovelace <alice@smime.example>
+To: Bob Babbage <bob@smime.example>
+Date: Wed, 27 Nov 2019 01:24:00 -0700
+Message-ID: <smime-sign+enc+legacy-disp@protected-headers.example>
+Subject: ...
+
+MIIQjQYJKoZIhvcNAQcDoIIQfjCCEHoCAQAxggLCMIIBXQIBADBFMC0xKzApBgNV
+BAMTIlNhbXBsZSBMQU1QUyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkCFCJT7jBtAgsf
+As31ycE+Ot95phvCMA0GCSqGSIb3DQEBAQUABIIBAFbDR6j4ZB/Mo9BQygYItwFc
+P+4rO4d1ak51hc1DpSqyhiMcGahA3yxDRbZ4W1rbmC/s3d5+OWXKYgs1nNMQJ48F
+f45BtNTNslPZ1+NZVbkoVJO8Bxv1rjB8/qWuSUsroqzn9enS8DUBxxPL5aSWKQQN
+G2IaH9BUkMXLPUYA46GATly94IS4fZqwBtNNBP5eiIIPc9Ogjy+7At5GG7rVMN0M
+G5FL0oq52SYUe1167jp378JI+2dkA1q5+Cru/ZE2Rdw3DrMDAFO5GwC7fWKg4zPm
+IHZj92caVj1IyfTmGogT2o5tLMqn61BkptqxZwHDr3FI/aYo4vcHgmlKR/TdbHww
+ggFdAgEAMEUwLTErMCkGA1UEAxMiU2FtcGxlIExBTVBTIENlcnRpZmljYXRlIEF1
+dGhvcml0eQIUZ4K0WXNSS8H0cUcZavD9EYqqTAswDQYJKoZIhvcNAQEBBQAEggEA
+hXeYVSUsT1EBZ/+AjwyEcnlM0kuFMaNvGlBMhAZzAsy012rrZTWbqWkcA3abgm/M
+CuZX7mQL0I79KZdmClGpLx6gQFjLemHaClQV0ZNdX4DxakWuME/kCMqbo4MZXStT
+a0MHlKUdoMt72Rz4YBzNQCL7ePaii5w6Nd2KD7yJAirLYUMJEjVweVaMI9y9LmbO
+vb0g0iuoUe0vp9B20LRcIX37nN5D1GG4tHLPjBD43gC8iqxZQf0uah2cWD1mAG5R
+oBgIDKXPy2eVbcMdSaOirDKYZ49WFe9Lad9q3mHHbFs6K6/yuBm/thMEdCJKZTHo
+jiPvYdYF8IJfEd368I+DujCCDa0GCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIsb1a
+JX/RU9aAgg2I0VXWfs5fc/Yad2qvawUVNX+LObjA6/+t9WxuV2emOeBYzQGjo7q+
+xaIXQwbbF1ej27efGhxUYDwBNS56c0uI0Ta7jxv5OFZhzQGLRzoFp0bbZ+uVC4eP
+bFHarRQiPzlg900XASO0RW+UOtqN5raZ3Ry2lKwXxuStZ0pX666Rz4c8PrmMb4/B
+aQYn6iKcT6fDU2TpSbWY9iph6kZczSeewK+pIj9nXfjDKXScs8D2Raezev2ciq/V
+ZRpRH8JxieimI2yeBmEzTCq11TDYycDfMHB6reGaiCGX//8kAWtskzRyNlV61unY
+ZKSNhVKLwKmCQh1V1Nd3oLApT41EeM2oWedUqNBYqB+XGCD4DUYdm1e+4h73d4dn
+JTkCdadxEn+9RRvZ4YMlw3mvT997Dy3rTXT29dj14TstZZf2O63pY0TpYy0HZy6Z
+Jug1qoe/vdcJ9SPOSfJE6VWCeVjxB+eGgheFLKqzK8Hs/Bm0/wDKpSFgEpOPnkJ4
+HJ2Uzgn1Emo6gBDJt+qn3s2UnowcMsTgellhKvgzVq59LTyRyWL5U8XMBsXT4qjm
+0LkRvDkOIjMQH7kqvWbpPlnWpLKo/VVoxifldEegWAqFVrP7f5Y+nNQttAYV79uk
+MXvR+5YFkvmQAerfllPqXBJdbB65ovikSVsy/kAboGpRG1oAZ4ODdwdGyiGIzyyc
+lE0x/8+gY8BqWzRtWX4GySKyZ50/+xkJe5ss0IXPCgq/09bdihsRn57v4V4SpdDO
+k3g/Dce+LzCRL8uTbUhrhZnjKSjRc3fFaD/BpLYjEDbnGF0ICslN3vb2xWUK1u4M
+uUH9r7lH/DCb0+TxIBtxOnP7W02bz8gGJAxEVEqk6pjxxOYqfS9/uBrrAY8P21Y9
+PFLdeHzEdYemq3il+4S7OU3uNUuAYijxmCRs7JQxZ9puA0iaTME9gK1yikzsLtVZ
+f+9osk2nYgfXvlL0AiYabd5cU2GNW33TkdDMNBsB7lx77J9erVLZpPKNo4vgHA7b
+owrDaYe0AgcZm79fvmR0RdtIZI91MouEhkdhaPiXmypmszjR/M0Ot3Y+oU/ks+yV
+Sle0S0h4V8wJRJYG/9VVurm8012ke2U3EGFlVnSv/IYtpssC+U4McRCmakKCrGU7
+OhL5JKBQN/DFTu4pV39IQlLLhg3wzA2FSkyIL5gEbS6sP9GTPo5LlNm2nYfJQX9A
+sHKSrfh68dvjSNExxi/8hdmFnnRwbAnUCI/WObGOkKdheOfdQ1AAHtLO7G65X1Cx
+RctbAJWa93M+iRUN6qnB+vIbPPnI1Mc7i6mPYzgtPrM9bYqEZz69pQtHcGTfxOrU
+tm+/h36CRzJBfXodBZbwQ9mZAzfkKdlArlZYIeBUw3ORQnQ7UlJgG8KsZpUhTxCc
+gvMoExtlvkXcYLRUBFfZWyOi6FePzQjuCK1w58OdweJgXprEAWSvyxhmVdg4jUpX
+MYKE0tZI9xwujyWjACO0myYqTdmsqyds+BgfBn96XiA9OFUH2C0/GAomhNs8uPSO
+T3Gt7Ld/FByxEVrtl9A37X6bAwZO01j5tHmdXFPmMVep0R8zsWtPn3RyGAjcgcq6
+50wJRwhvofdI7wilZ0KUBsAaPj3MK52cRyD19VXKNNwt2bLDV6gcWQ8+QEMusxfp
+1Dc9N9DSs+w3lGsFfpoeQ53/fXcVNJm6Bv89bH9anLGYdCdRGvZsvw+xRuglykqb
+xLtL2lB6wzlRFREJoWTzCVsdpIZ8znPmk1cB0wDlbMeu6sddHmv+6fpyuvQfQmdj
+D8WLRTuyxax94TmBlhJCFYxmO/y4Ivlx5C60GIRTkHpBYL/M0RjrbIszXEqcogzU
+bdwjLIhdEnpJ5vy0uXwhltce8BDpenmHE7y1kHvPBiUG3vB7AIXqhohFsJU3AYUj
+d1TvFKS2AsizUTLuq0Ydbnz3AxMfmnZe8qYkNu2zRygL2xTa58f/MwsHKakk3OmS
+9JFZLrkkVWZKXoARctuahYtWBAsykaWVNnB6zGcdX1MGVccl930Z6QWHyydtZpQc
+ivNdEGdGv9B0K7/ngNdVgD5Wd29AMMFnS8+55mLfRZDCjUmshSySaf6Ein4HD9Hr
+vk6dJvBPjnI5UjeUPjmH+wcZKIjLHW/aV/6/zoxzBh61rWFlr/daec+CFZE/+epr
+LRRYSmv8oY47fF4duDDhoexcvP/CH+A2Hr40OfciL4vKy3nuUDCNa59xO9JWv4NL
+n3MQypC9bcaVPkXa7TK3ECq1Jgv8gwfdh5/ovG5OdZA4uIcO+aqcskt/PD252c63
+0Znww3RXXf46KT4GdKO5A377ixkUMkznnCMvottmkPxjnhQjAsQg3bJeQk8EoX8f
+Pq0If4i7SRBSDtb2OH1pPmk0RVPtxlRDTVj3vS3Lci4xADFgC09n9nIvPO/55aau
+O6StbJtLmpubS5giuDH3uftwuyRiLqm3gtbSKPdoTk+dJhHXbbpBknL4XYTPxSsR
+IIaRds6w30vf7/IscyunMcquJlsO929SSa93UevKEIZbqbV9oGIqwkiUMdVZK09g
+rW0F//Ts4a5nYdEQth/fq3JnwqeHvvUfKdasK4TtrTnUBX7qZk/K3Y1fZwjKdd/8
+t9t1z7Kb2d9hWwtY7xP8liDluVFTsq8NM54ZC2218X5ViWz1yFmF2LXvRixsmYJv
+Tz8lUUnC2B/Etm1kkU4zrYK0/L77EikKVl+B7BXfEqx6ow41j7e1YZYaqmZ9mph+
+UieSdzqVYxhPwT25DrkU3r74iS28gKsbFhUaNklaFOO5iDWsKgBXT+wdZqlYQ6Fo
+oPe66025iJMwK8t+d53jEduHezHO2sTMAuf2hpdaZo7+rP/hRTReAR6CmI7nkWhP
+z5Kno9S+XhiSP+WTSpsoA4ubx0T94mL8NOVvSZA76TZ3ObVAP5VI/bwv6Grighor
+Kpsjt7dhSJRv+RHv95sAWBeW1Fgv8XOPSAZOmpJV2qc3x3Qmj0MXIR+7+3GlUr8+
+Dit3CE1hwtxgOW0tc8kuBTfQD+wNSa9r0eUyFscEBBljpEVbLjgjVdNv4Hc+fsbT
+g1JzZuUIDQZoEO2xLjxD+I7vLZKQa0J1JeZ7O+NqmSxsvSnwCWtJEWNMMxYNfwsP
+rdj1zPLqn3rzSBqhroNbaDGn86BTwIqfhr+AKbvevxS6bI8IbyKm9u3BFr9cuawx
+Sp1QM3NtqNStV67qR4A6U/ZyPUJdO1bxo8F3oRmJqOt7Jc93rFgkhBJ2+eMtrA75
+Om5tB9LBVSl5U5yLP0COO1QE5pqk5yuhJLT9Dyss8bWDRbSWKj83e4YXhPnq71Bm
+001czylLVNUlDc69Tf7FXjtIxh2yjvOT3zeLBPXOjU0it+gAma4vgrh8/mMXnNiq
+OLsVow8aKqm+Ofd6m13K5riDFgXgNI9lbvPKUSWlEqDMEqXk1oAqD4Nb5NTGSFpQ
+Q4G+cHAxJCu7vcXBaZnP8uMP5IAkdg5jIPvvMRwg/aqkl/KbL98oYZ5+1xrOMuKA
+LT1uCJ4MMB0lWsa1He4jPe8LneSupw7vAXlbo2VzcOI6oCSY5hV+cGQRY+LjW81q
+Cu5nLq8bwgnZMSlPmwr0YrKmvh8YKyGOrmTadxykC5IC+XbrLDsw2Jd9mLIjUQ/V
+4ibjeb+e0QGob22WOplCLnHGW/SnYei8KG1dxs/ahS+8vQdrI880ZJx2QJnrz0Ej
+ux6tKv4mvUkqYA5hlTFeT3PTr54yA+YLcCLMfBDx4ykPQnYUBj7ONHuNSUYt1CJy
+faZ7cWAbhgH+wlTFdVBVeW5D4FRbM8dMTPXyfC5ygwTJOiDu3vQKyyDkmiX7sEaC
+P1JN2V55uacyR8ZAG5+Mlc4ZMx83kAIZZXTCdqa1EX8yda31FI2rDHmvW/82bmjL
+pvI4Nnn9+zzJtDVCJ0B2VAZ3Edov5GzPikm3un4+mvyhUZpH4sbT0+VhPCsr1+zn
+bDJyNw4AswxaaJKh2+7wBiU6h+9TP/lI8SAJHtZL7zHBH8tD10ptksLRWDs9vYqp
+/3T86S2vxJL5DvLFJSAZrYOE3InS+keGmTMCdAl9I8zIworC/8uQp0N8ESebEVjA
+aHotBk59lj/OW4JZ3tQkcdQWkpnUfW/x9xE2wthacHlRzYDDsFByjEqkQr0MU8VF
+EGij9RCC97zyFrhv0xJm1C6wX0pcuEcuPTNBf38WyBTIfmVHHz/I5YKk5cdWG7Hq
+fmccV5GKrs2BseR683HM+/u50sq0km9UrqjgFR1DjfDoRKp0guP9PqkJAnwG2nv1
+hmNtXumzkF0otP5LDKLJ84MGP8Wnb006iEdD48Lra+clRAIIuLX4A0wRQjViDp7n
+OByI6ZcQd4DTMHnFPRvMkNMLYn13LghD6P9TTjQZ0KCOCwmc2TMCIhJlvzOYX6Cc
+wJZYLO1ltgfnHEuh8ijv0u3d/BUpsknYKBSJGUyMEZ9iUtbFPVfXBGSTi3gcWHtl
+IrM7wjswJwHWSvZKWUs+YWWJTwj0apG6ViGllwOAqR9C48uLKgFWPbMoTpolnp69
+eiij5ZHxB0i7SI80D+r65b+fqaFzVIJXVEI0zu/mIilbYBnGkhLI/Naw1m2e1qVJ
+mi1JBjXLAT3pEJDh8b3Lpgw=
+
diff --git a/test/corpora/protected-headers/smime-sign+enc.eml b/test/corpora/protected-headers/smime-sign+enc.eml
new file mode 100644 (file)
index 0000000..aa09d19
--- /dev/null
@@ -0,0 +1,95 @@
+Received: from localhost (localhost [127.0.0.1]); Wed, 27 Nov 2019
+ 01:15:28 -0700 (UTC-07:00)
+MIME-Version: 1.0
+Content-Transfer-Encoding: base64
+Content-Type: application/pkcs7-mime; name="smime.p7m";
+ smime-type="enveloped-data"
+From: Alice Lovelace <alice@smime.example>
+To: Bob Babbage <bob@smime.example>
+Date: Wed, 27 Nov 2019 01:15:00 -0700
+Message-ID: <smime-sign+enc@protected-headers.example>
+Subject: ...
+
+MIIPVQYJKoZIhvcNAQcDoIIPRjCCD0ICAQAxggLCMIIBXQIBADBFMC0xKzApBgNV
+BAMTIlNhbXBsZSBMQU1QUyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkCFCJT7jBtAgsf
+As31ycE+Ot95phvCMA0GCSqGSIb3DQEBAQUABIIBAKswTlBs+STeesZIYAf7Gqsj
+Za0rdUeDTSxt8RCa010EHb2lqKzHRwwPJkClLm6Glb09nYnQiFrEl6jbWTG3hMRD
+OSt9kyqeg+MxXr2g4LoXAT+8hg/qBoF//tX+bzxhx0gx8wjxBc3bvp4esCJro7Aq
+tx56BtVsIO6TA0NT0CaOcnMhIo09raR6JQX+DoPynKeXihny6TFDP7eopCgorCfR
+o59O3ZMvaui6Q9KixZy3Yae8fa0ZdJu3FahIZTPdBHzbmirLxcYgp+cbTpW+Yno2
+X5GJ8eq8Y0qcc/8r6Xd3REarUxO2YbO2D6cgDj+aNnnsoG1/9psaYl8W1MSc2/Qw
+ggFdAgEAMEUwLTErMCkGA1UEAxMiU2FtcGxlIExBTVBTIENlcnRpZmljYXRlIEF1
+dGhvcml0eQIUZ4K0WXNSS8H0cUcZavD9EYqqTAswDQYJKoZIhvcNAQEBBQAEggEA
+RHhTarDqNLzXSaBokp2L3EwDv11KiGtMSMUQuPelNoC2nNYU1yzAF4jd+1UUo4Uu
+quiHg5Hn44a9MejrVmQRLd5IEJiZGD8m5JguuOjn0ooyA6EEWUpMn6hOAKlaCiXd
+kwTivKfhQFJe9Eb6TKqtvT2IEu3kXFfJKi+VyQw49+RXBmajDKJoHtumMJs8k4Ll
+kJah+wD+snwHg2LCiJeSVHmpf4RvSiIJSvk206IeTxN3JecNbBpKLtIoy/CjWEZv
+G3Pj/zkBbb+XhHbXo+Zk/e3aLToVG/cldx6Ti8zArOYNAzgt1G7dmJ3mnNPitEwN
+O4qIozhT2Qn8P95AEV5PsDCCDHUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIUzdf
+vwulBs+AggxQMK121v6lO7W1r96RW0rsOHzsIvGyfyRTT1UuZRxVL09BQZstI5ss
+5Zv8BogoKA0mLaNBKM755joUbzF5f/jMYhkW3q0Het9/HRH0mOnCSnoT4i2yzNdi
+0tj8ixPT4sgPe9FOTkke9CzoJ967kj9D8u7Ik2goojttt3ViJkv3a1qrWDMiJRIJ
+gOTTA6ZaQep5L92vtCobhD+i7iaktEpmbYucXs8jjMmwyxCFxHXGD/fwDk3UDgeu
+8a5f66YepZdbLKB61A3rBwJMvQubuXEIEb04tG0Fgwx3Ao2NshN+XRk/y+uhQKdC
+5ZduTxk5sokA+H4nzVv0IUkAAI+8FwY5ZWFGlncKUM/wvrGHQq3R/utChFauOHxD
+7vZQLM91TcQzVWdHfJGPtp+ekjRlu9UqatQgc1ogObw3PGYlJc90Gl7AZHAsYncU
+jsMbdsweuFuYNHJ8lR5VMo6L4bCNMy+tQBOfYTF1el+i9S3r3SWdBP+uLiKgDQ52
+/o4shxoi+YOf9k8wRR0iDKqwzcJuABplpgA9qjsQNqBKF5t5p3l3ihH1mfh8FaPL
+ab0aDC7uunY5g44qXcG9YS+j5wUFuxgYyGkVcJq3xIit9YbEy8uPxJFz4g0vNC+r
+uUSsztbLyHkhv7vnCTAlmjgG9eDpW/tEC/85pLOV1HUooD05eRfkjU+1XsccX8DG
+iCax2C6W3cc1SC/d3a1+27OcgvPdDcb7zuL3v6qgqbN+7GDrcQHQRFMd2vd6+xGk
+NWZQMBZVHmdCcKGl9YaH0RgkGH5beTRKEV1wBafuVOwTEwl/FuZzD4oHrOaP3GLO
+cLxi44her/hNxtxDc2Lw0VQcxD8A55OkCt9+u9M5/YPj41FWyH6kdh86p958gzF5
+EpwCnQDe+s7OrwFVV00DEJhqtEcxRCSSW8dS4hVEhVxQJ56liJP+VZ+LTUJBelt4
+mfSpSqxeJnmyY0nmhEbZKVbK95a1WYMJCEpk2n1g/bQGqJKRryGwbEF9WqqHuvPo
+Bv/BfinoUL3Kd3g+hgSCR4mCg5EhEsCx21jEqEggzb2XMcA+knGUYxSWj322pZfW
+LDh50gkL3GQSmm9fOvjdK40GwZv8HUdLXuAQ/J19PafMaDkd4jzRi37VBqdDgLY3
+u6K+oFKhG4oqQYa/er+ZGAqqldTmu8HGCsjm6kGZvSAocJg0UnLPBNI0/iB0BYGf
+KJk302jy8kfAXGSiWrYDNbTuDzFMD0zsbHbM07AOOROGwKv5TxAF1EHHTxGb3IKI
+jRkVBL7QdRtDH03zlxv0lnFwiuCrzLrQdUuEG/0wt8RaNr+p8hAo0YEGbB9jmbax
+CSLLWeNbMOo8eIi3Mft4qmDXp3TEuHHru8kbvA36vQ8+dunSf2BcecyM6UAYBqaw
+SCcxQmEcyMuyjSLVerVfMl5lwlmM+qabxHq0hpJHnCR3Vl2qX3CiRWpVlNaBVyTf
+793bAm7DU7G+Tzt5gdgE4s41aZt8fFXyclhH1QLPNSnctxJjuW1gJJ0h51iCQJp2
+TgzDw35oqvBxbN3yqCFjScsQXPXYErGWkLrAkUurff4x/ZAizFkmjjdpyaIK9JBw
+QRyrYYQ8pJhXJe9BrP3OS6evFlsWZW1MaoQcOUMWsuVucE0e4AQRGlPixDjJWW7L
+I6AQ3KUW6ggzDJksaYHDiuEoBa7vcYoTar+/AhNjYMjkQX/3kptQryqy+xke0t8O
+EPQER0Wur2IpvM6YsvI/SoeFwxMb4Zm5AFvvibiCCmmoJc4A9E1tZ/sMstHyZ5iu
+tJqu1M5B0DIoFdB5pzbZYCkgN2n7EY23JS7E/ozOrzYuOIVUJVtB5awqmuSLmI+N
+R91g4FMEfLYC1HYKYlaknX2zmrx8+Z8MEJNM2K0q8wPBnm86OpGeJmlZhFwT2x0R
+eJpKcfLGroXYh2Gb6BxwIfKjOOTXCoIFP02JbTJ7clc/2ei0BN6JxywPkH4renaP
+SkuNBgbexfZGBhMTlR+CtKLEUmw5bxBTDwjjcvzWDPhy/VurLQxhOqYnbhZW21SV
+4qMrJ4uGXEhylnP0FD+HR4mB2epYcW3dFj4cGN3B2Y5NnOTw0Z7fi4S0BPdvYjP9
+LL5WZ6p90mII9wcunGCRnLUUYumRnIbhVHIBTTIRI5PUSVFfEuotrDZ9oZcwYkO7
+fQX21gJCzvJyp8ft01HX4Kc4mN/FMPgGcmq70N335yQ4mQ/eSvTNn7E+35ZGn9f8
+PI7QPJRhdUkBZCnwyv+OwK2VzySxnqNfPaZk168foGRd9eFCw80L4U+SuLDQH6ZT
+o++VKk4Ce2jx1khoig16wic0dVFwt4bmybNz4u/qdobYr5fs7dKPHHO02SBvAl60
+16foheiBtV2VA8mEBA1BhcNmKYegu+RGhmGfNDuZB8XdbPQ6M+N+ilEj/6rr+wgD
+gcmEyAGNwJkmWpbyrm9M4lDtzemv5N5V32ppGizEt6c0xlkiULllwGdWey3+YRez
+7b+Kl/uIpDuRbp5Tf43dyPsy/cx4DNm5kAB4CcyyVlXPaqXm0llEPYBmaMW3O+D2
+5v4Wj1qwIRO5qgI8FyVnX6sm/oucfg5l172edaCG8f42gIMNfQBgWVMsSG7Nt00x
+dJo/OGtACwnY47ohMFG0BejWueAksdnqVWCIto989iBHgegNx5jUCycB/YOm0xh0
+pfeNjA9PwZMUpjlqrjDFIan/UFYAZH5ISSV7G30oRKJ3TTEshShXP2K3cn7Fa9W+
+H/jyTEQGfCiTq7Xx5FrOIJBmKjylkF7oGlIBxJgKKRm0iD/sGNTaSJ6Pl8/K6dEz
+zsMwEFTawnWVq32Xn3d6/+FADZ9lGhC5WwVgaQHRb/9Ejt1mBdptmXjEj5w0YOib
+xFer54LrQgvBWEYRqDneh3bI53BudbTl7YitqULVGETe+k1T0NbcyElrr2Y/NKHk
+rPMarAfByookkJrDtVh3VrAm2ows7OwvKGyoNybjlyczjt7xosatZ1xkgb9mtR5i
+E2l9ajSR4SzQjHoboRyOCwl5ZgLV/+yp3jTkNcUkFDRtkVbGfascBIMe0ifUGfvP
+mJ9AQHZxdfm99KlQjCZzR8CBUvR+zsT43jr91CQKSSEvPMl6vVRV2thiWw3VGgP+
+c8i5zj6+zCnlEdSWiIeFwOJ9/ewKSdU9pGrA0OQtXbYQlDCKuGK1Vgy6jJCeglDH
+T6gVNy5ip593wWWfOVxVEWUygi6JCdS27b5+P/wlNjTrzpZ4yWDCpyogyrT1gf1/
+GgvdGuWWinKSLOyh1fJ1p9WoDWcqH98QhJXLV+X3OC+tmMofytmHgXN8jjVsWSRa
+VWrFUarMs2hZDWf6e6ncwvMC8QliiszrKXQNckxvBuh5hug9WKurVj4CIWnoqXFh
+OqlO+VbqZSj+TT5pCN//370vsIZIn5UbrpDmUP0rUvdTGz9iWQRUl6R2g2h286s6
+pAGHv9luXCoPJ5uPTwcbBSl/js6J+K5McyqRl4fucacfVFnMuDpET/tT1eAROP3F
+DOBKqV5YOO0rWMexzMLJUEQ/eGSwfp7wv8on7jeGxAexMqyWCrhRk9G2ZwiT4L7Q
+rX4NIDj6oujCCkeFUATs0pGKwEFGmpbEUfDOsioWoVYJZPsO9kAGq6bhbKACOkeZ
+v95ha/3CleYXGUUNtzLsCx+c9Zp/Wl+0PcT3ZSWhmRbXiIvz+ntHVe47PHxbvH6a
+ZG7YGc/9u3jTvJJyYtQO54uGET/eFWSxCUo5/VfsheOuLdXN7JnVi6ooF+c7WUZd
+61FwfDwNf8z0GWs3EotozrWyBgKS5VFP99vZM64nSqu9v5PSzmb0AY/Zc5KhVXVY
+zQqmO3keXq92Fejtgyd/O9ITZf5GkMQVU7+IT52JxFRQplkbTHJj4HRGtGHtIyPW
+Rmf9qSZz8QgVyAUKK1k+kLBJTHN3CWIB6S9hO42HWEFvLVl8wPWW5aLYTsVMGnMU
+aZ35M35odjrvY9B0INMpL53Hm7qH1w/h9QCv+xsFmanYsoylwbuKW2TcSnWB74C7
+Wy0NmCkaM+JweOgygffWicLGJ3jKWccykTUZtodz1ectNHh24puZICnvfzwjte+n
+eSQqJfHMsra6V8BcshpwmvPylHnkU+2KyhQ8430OR/qaXAYJ7EWRBEFe4EIpxzfL
+zQF0LwbhpAstpcjOlJfEHmQiWx8ASzE1LMSfZo148sXYEWsJL7t5tWs=
+
diff --git a/test/corpora/protected-headers/subjectless-protected-header.eml b/test/corpora/protected-headers/subjectless-protected-header.eml
new file mode 100644 (file)
index 0000000..8c7fb3e
--- /dev/null
@@ -0,0 +1,28 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <subjectless-protected-header@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+Subject: this should not show up as a protected header
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DHXHP849rSK8SAQdAxAZy4nBUDdm2u4sgr1inLki0LMCVcsVlax6Pd0AiZAow
+iYz940UtZwQNRRb640w1bB2pAvg5Nn8hJK5ye3qtyUWNW1VEvAa+GXndI/Qt0+7x
+0qIBOXRBCrkOxB10iCvSDVoOMZPj8GgvQwpsnslATJbsp9jV74fU7eFCKE5VWKUw
+FTos+VX1YFZyf2RsznHXdi0CrL2rkUNoLby4SEUa/urd6GKb3xuOjJlIYN4Fh9xz
+e33+Dl3NHohNooytxoJuTNiXbNWe7kidfOwMGzvdYsegk/WMqMLg8DmZPvl0BcYI
+o+4ZU3kuhbk5Pup5nOV6OLrs7A0=
+=BF8X
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/test/corpora/protected-headers/wrapped-protected-header.eml b/test/corpora/protected-headers/wrapped-protected-header.eml
new file mode 100644 (file)
index 0000000..87f3f8d
--- /dev/null
@@ -0,0 +1,38 @@
+From: test_suite@notmuchmail.org
+To: test_suite@notmuchmail.org
+Subject: [mailing-list] Subject Unavailable
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+Message-ID: <wrapped-protected-header@crypto.notmuchmail.org>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="zzzz"
+
+--zzzz
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+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-----
+--=-=-=--
+
+--zzzz
+Content-Type: text/plain
+
+This message body was re-wrapped by a mailing list
+which is why the protected headers no longer work.
+--zzzz--
diff --git a/test/corpora/threading/ghost-root/1529425589.M615261P21663.len:2,S b/test/corpora/threading/ghost-root/1529425589.M615261P21663.len:2,S
new file mode 100644 (file)
index 0000000..62bf98d
--- /dev/null
@@ -0,0 +1,9 @@
+From: Gregor Zattler <g.zattler@xxxxxxx-xxxxxxxxx.de>
+To: xxx request tracker <rt-xxx@xxxxxxx-xxxxxxxxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de>
+Subject: FYI: xxxx  xxxxxxx  xxxxxxxxxxxx xxx
+Date: Tue, 19 Jun 2016 18:26:26 +0200
+Message-ID: <87bmc6lp3h.fsf@len.workgroup>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: quoted-printable
+
diff --git a/test/corpora/threading/ghost-root/1532672447.R3166642290392477575.len:2,S b/test/corpora/threading/ghost-root/1532672447.R3166642290392477575.len:2,S
new file mode 100644 (file)
index 0000000..b79eaf7
--- /dev/null
@@ -0,0 +1,17 @@
+Return-Path: <prvs=701fd58e1=www-data@support.xxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de>
+Subject: [support.xxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de #33575] AutoReply: FYI: xxxx  xxxxxxx  xxxxxxxxxxxx xxx
+From: " via RT" <rt-xxx@xxxxxxx-xxxxxxxxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de>
+Reply-To: rt-xxx@xxxxxxx-xxxxxxxxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de
+In-Reply-To: <87bmc6lp3h.fsf@len.workgroup>
+References: <RT-Ticket-33575@xxxxxxx-xxxxxxxxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de>
+ <87bmc6lp3h.fsf@len.workgroup>
+Message-ID: <rt-4.2.8-22046-1529425595-591.33575-211-0@xxxxxxx-xxxxxxxxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de>
+To: g.zattler@xxxxxxx-xxxxxxxxx.de
+Content-Type: text/plain; charset="utf-8"
+Date: Tue, 19 Jun 2016 18:26:36 +0200
+MIME-Version: 1.0
+Content-Transfer-Encoding: 8bit
+
+
+
+
diff --git a/test/corpora/threading/ghost-root/1532672447.R6968667928580738175.len:2,S b/test/corpora/threading/ghost-root/1532672447.R6968667928580738175.len:2,S
new file mode 100644 (file)
index 0000000..343a855
--- /dev/null
@@ -0,0 +1,18 @@
+Return-Path: <prvs=708ebe06b=www-data@support.xxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de>
+Subject: [support.xxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de #33575] Resolved: FYI: xxxx  xxxxxxx  xxxxxxxxxxxx xxx
+From: " via RT" <rt-xxx@xxxxxxx-xxxxxxxxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de>
+Reply-To: rt-xxx@xxxxxxx-xxxxxxxxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de
+References: <RT-Ticket-33575@xxxxxxx-xxxxxxxxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de>
+Message-ID: <rt-4.2.8-6644-1530017064-1465.33575-215-0@xxxxxxx-xxxxxxxxxxxxxxxxx-xxxxxxxxx-xxxxxxxxx.de>
+To: g.zattler@xxxxxxx-xxxxxxxxx.de
+Content-Type: text/plain; charset="utf-8"
+Date: Tue, 26 Jun 2016 14:44:24 +0200
+MIME-Version: 1.0
+Content-Transfer-Encoding: 8bit
+
+
+
+
+According to our records, your request has been resolved. If you have any
+further questions or concerns, please respond to this message.
+
diff --git a/test/corpora/threading/ghost-root/child b/test/corpora/threading/ghost-root/child
new file mode 100644 (file)
index 0000000..4c36af9
--- /dev/null
@@ -0,0 +1,9 @@
+From: Alice <alice@example.org>
+To: Daniel <daniel@example.org>
+Subject: child message
+Message-ID: <001-child@example.org>
+In-Reply-To: <000-real-root@example.org>
+References:  <000-real-root@example.org>
+Date: Fri, 17 Jun 2016 22:14:41 -0400
+
+
diff --git a/test/corpora/threading/ghost-root/fake-root b/test/corpora/threading/ghost-root/fake-root
new file mode 100644 (file)
index 0000000..a698185
--- /dev/null
@@ -0,0 +1,9 @@
+From: Mallory <mallory@example.org>
+To: Daniel <daniel@example.org>
+Subject: fake root message
+Message-ID: <001-fake-message-root@example.org>
+In-Reply-to: <nonexistent-message@example.org>
+References: <000-real-root@example.org> <001-child@example.org> <nonexistent-message@example.org>
+Date: Thu, 16 Jun 2016 22:14:41 -0400
+
+This message has an in-reply-to pointing to a non-existent message
diff --git a/test/corpora/threading/ghost-root/grand-child b/test/corpora/threading/ghost-root/grand-child
new file mode 100644 (file)
index 0000000..5f77ac3
--- /dev/null
@@ -0,0 +1,9 @@
+From: Alice <alice@example.org>
+To: Daniel <daniel@example.org>
+Subject: grand-child message
+Message-ID: <001-grand-child@example.org>
+In-Reply-To: <001-child@example.org>
+References:  <000-real-root@example.org> <001-child@example.org>
+Date: Fri, 17 Jun 2016 22:24:41 -0400
+
+
diff --git a/test/corpora/threading/ghost-root/grand-child2 b/test/corpora/threading/ghost-root/grand-child2
new file mode 100644 (file)
index 0000000..59682a9
--- /dev/null
@@ -0,0 +1,9 @@
+From: Daniel <daniel@example.org>
+To: Alice <alice@example.org>
+Subject: grand-child message 2
+Message-ID: <001-grand-child2@example.org>
+In-Reply-To: <001-child@example.org>
+References:  <000-real-root@example.org> <001-child@example.org>
+Date: Fri, 17 Jun 2016 22:34:41 -0400
+
+
diff --git a/test/corpora/threading/ghost-root/great-grand-child b/test/corpora/threading/ghost-root/great-grand-child
new file mode 100644 (file)
index 0000000..287a895
--- /dev/null
@@ -0,0 +1,9 @@
+From: Alice <alice@example.org>
+To: Daniel <daniel@example.org>
+Subject: great grand-child message
+Message-ID: <001-great-grand-child@example.org>
+In-Reply-To: <001-grand-child@example.org>
+References:  <000-real-root@example.org> <001-grand-child@example.org>
+Date: Fri, 17 Jun 2016 22:44:41 -0400
+
+
diff --git a/test/corpora/threading/ghost-root/real-root b/test/corpora/threading/ghost-root/real-root
new file mode 100644 (file)
index 0000000..f1b16a0
--- /dev/null
@@ -0,0 +1,7 @@
+From: Alice <alice@example.org>
+To: Daniel <daniel@example.org>
+Subject: root message
+Message-ID: <000-real-root@example.org>
+Date: Thu, 16 Jun 2016 22:14:41 -0400
+
+This message has no in-reply-to
diff --git a/test/corpora/threading/parent-priority/cur/child b/test/corpora/threading/parent-priority/cur/child
new file mode 100644 (file)
index 0000000..23ee649
--- /dev/null
@@ -0,0 +1,11 @@
+From: Alice <alice@example.org>
+To: Daniel <daniel@example.org>
+Subject: child message
+Message-ID: <B01-child@example.org>
+In-Reply-To: <B00-root@example.org>
+References:  <B00--root@example.org>
+Date: Fri, 17 Jun 2016 22:14:41 -0400
+
+This is a normal-ish reply, and has both a references header and an
+in-reply-to header.
+
diff --git a/test/corpora/threading/parent-priority/cur/grand-child b/test/corpora/threading/parent-priority/cur/grand-child
new file mode 100644 (file)
index 0000000..028371d
--- /dev/null
@@ -0,0 +1,10 @@
+From: Alice <alice@example.org>
+To: Daniel <daniel@example.org>
+Subject: grand-child message
+Message-ID: <B01-grand-child@example.org>
+In-Reply-To: <B01-child@example.org>
+References:  <B01-child@example.org> <B00-root@example.org>
+Date: Fri, 17 Jun 2016 22:24:41 -0400
+
+This has the references headers in the wrong order, with oldest first.
+Debbugs does this.
diff --git a/test/corpora/threading/parent-priority/cur/root b/test/corpora/threading/parent-priority/cur/root
new file mode 100644 (file)
index 0000000..3990843
--- /dev/null
@@ -0,0 +1,7 @@
+From: Alice <alice@example.org>
+To: Daniel <daniel@example.org>
+Subject: root message
+Message-ID: <B00-root@example.org>
+Date: Thu, 16 Jun 2016 22:14:41 -0400
+
+This message has no reply-to
diff --git a/test/database-test.c b/test/database-test.c
new file mode 100644 (file)
index 0000000..42f6655
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Database routines intended only for testing, not exported from
+ * library.
+ *
+ * Copyright (c) 2012 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 "notmuch-private.h"
+#include "database-test.h"
+
+notmuch_status_t
+notmuch_database_add_stub_message (notmuch_database_t *notmuch,
+                                  const char *message_id,
+                                  const char **tags)
+{
+    const char **tag;
+    notmuch_status_t ret;
+    notmuch_private_status_t private_status;
+    notmuch_message_t *message;
+
+    ret = _notmuch_database_ensure_writable (notmuch);
+    if (ret)
+       return ret;
+
+    message = _notmuch_message_create_for_message_id (notmuch,
+                                                     message_id,
+                                                     &private_status);
+    if (message == NULL) {
+       return COERCE_STATUS (private_status,
+                             "Unexpected status value from _notmuch_message_create_for_message_id");
+
+    }
+
+    if (private_status != NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
+       return NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+
+    _notmuch_message_add_term (message, "type", "mail");
+
+    if (tags) {
+       ret = notmuch_message_freeze (message);
+       if (ret)
+           return ret;
+
+       for (tag = tags; *tag; tag++) {
+           ret = notmuch_message_add_tag (message, *tag);
+           if (ret)
+               return ret;
+       }
+
+       ret = notmuch_message_thaw (message);
+       if (ret)
+           return ret;
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
diff --git a/test/database-test.h b/test/database-test.h
new file mode 100644 (file)
index 0000000..84f7988
--- /dev/null
@@ -0,0 +1,21 @@
+#ifndef _DATABASE_TEST_H
+#define _DATABASE_TEST_H
+/* Add a new stub message to the given notmuch database.
+ *
+ * At least the following return values are possible:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully added to database.
+ *
+ * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: Message has the same message
+ *     ID as another message already in the database.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ *     mode so no message can be added.
+ */
+
+notmuch_status_t
+notmuch_database_add_stub_message (notmuch_database_t *database,
+                                  const char *message_id,
+                                  const char **tag_list);
+
+#endif
diff --git a/test/emacs-address-cleaning.el b/test/emacs-address-cleaning.el
new file mode 100644 (file)
index 0000000..6eda0eb
--- /dev/null
@@ -0,0 +1,39 @@
+(defun notmuch-test-address-cleaning-1 ()
+  (notmuch-test-expect-equal (notmuch-show-clean-address "dme@dme.org")
+                            "dme@dme.org"))
+
+(defun notmuch-test-address-cleaning-2 ()
+  (let* ((input '("foo@bar.com"
+                 "<foo@bar.com>"
+                 "Foo Bar <foo@bar.com>"
+                 "foo@bar.com <foo@bar.com>"
+                 "\"Foo Bar\" <foo@bar.com>"))
+        (expected '("foo@bar.com"
+                    "foo@bar.com"
+                    "Foo Bar <foo@bar.com>"
+                    "foo@bar.com"
+                    "Foo Bar <foo@bar.com>"))
+        (output (mapcar #'notmuch-show-clean-address input)))
+    (notmuch-test-expect-equal output expected)))
+
+(defun notmuch-test-address-cleaning-3 ()
+  (let* ((input '("ДБ <db-uknot@stop.me.uk>"
+                 "foo (at home) <foo@bar.com>"
+                 "foo [at home] <foo@bar.com>"
+                 "Foo Bar"
+                 "'Foo Bar' <foo@bar.com>"
+                 "\"'Foo Bar'\" <foo@bar.com>"
+                 "'\"Foo Bar\"' <foo@bar.com>"
+                 "'\"'Foo Bar'\"' <foo@bar.com>"
+                 "Fred Dibna \\[extraordinaire\\] <fred@dibna.com>"))
+        (expected '("ДБ <db-uknot@stop.me.uk>"
+                    "foo (at home) <foo@bar.com>"
+                    "foo [at home] <foo@bar.com>"
+                    "Foo Bar"
+                    "Foo Bar <foo@bar.com>"
+                    "Foo Bar <foo@bar.com>"
+                    "Foo Bar <foo@bar.com>"
+                    "Foo Bar <foo@bar.com>"
+                    "Fred Dibna [extraordinaire] <fred@dibna.com>"))
+        (output (mapcar #'notmuch-show-clean-address input)))
+    (notmuch-test-expect-equal output expected)))
diff --git a/test/emacs-attachment-warnings.el b/test/emacs-attachment-warnings.el
new file mode 100644 (file)
index 0000000..8f4918e
--- /dev/null
@@ -0,0 +1,81 @@
+(require 'cl-lib)
+(require 'notmuch-mua)
+
+(defun attachment-check-test (&optional fn)
+  "Test `notmuch-mua-attachment-check' using a message where optional FN is evaluated.
+
+Return `t' if the message would be sent, otherwise `nil'"
+  (notmuch-mua-mail)
+  (message-goto-body)
+  (when fn
+    (funcall fn))
+  (prog1
+      (condition-case nil
+         ;; Force `y-or-n-p' to always return `nil', as if the user
+         ;; pressed "n".
+         (cl-letf (((symbol-function 'y-or-n-p)
+                    (lambda (&rest args) nil)))
+           (notmuch-mua-attachment-check)
+           t)
+       ('error nil))
+    (set-buffer-modified-p nil)
+    (kill-buffer (current-buffer))))
+
+(defvar attachment-check-tests
+  '(
+    ;; These are all okay:
+    (t)
+    (t . (lambda () (insert "Nothing is a-tt-a-ch-ed!\n")))
+    (t . (lambda ()
+          (insert "Here is an attachment:\n")
+          (insert "<#part filename=\"foo\" />\n")))
+    (t . (lambda () (insert "<#part filename=\"foo\" />\n")))
+    (t . (lambda ()
+          ;; "attachment" is only mentioned in a quoted section.
+          (insert "> I sent you an attachment!\n")
+          ;; Code in `notmuch-mua-attachment-check' avoids matching on
+          ;; "attachment" in a quoted section of the message by looking at
+          ;; fontification properties. For fontification to happen we need to
+          ;; allow some time for redisplay.
+          (sit-for 0.01)))
+    (t . (lambda ()
+          ;; "attach" is only mentioned in a forwarded message.
+          (insert "Hello\n")
+          (insert "<#mml type=message/rfc822 disposition=inline>\n")
+          (insert "X-Has-Attach:\n")
+          (insert "<#/mml>\n")))
+
+    ;; These should not be okay:
+    (nil . (lambda () (insert "Here is an attachment:\n")))
+    (nil . (lambda ()
+            ;; "attachment" is mentioned in both a quoted section and
+            ;; outside of it.
+            (insert "> I sent you an attachment!\n")
+            (insert "The attachment was missing!\n")
+            ;; Code in `notmuch-mua-attachment-check' avoids matching
+            ;; on "attachment" in a quoted section of the message by
+            ;; looking at fontification properties. For fontification
+            ;; to happen we need to allow some time for redisplay.
+            (sit-for 0.01)))
+    (nil . (lambda ()
+          ;; "attachment" is mentioned before a forwarded message.
+          (insert "I also attach something.\n")
+          (insert "<#mml type=message/rfc822 disposition=inline>\n")
+          (insert "X-Has-Attach:\n")
+          (insert "<#/mml>\n")))
+    ))
+
+(defun notmuch-test-attachment-warning-1 ()
+  (let (output expected)
+    (dolist (test attachment-check-tests)
+      (let* ((expect (car test))
+            (body (cdr test))
+            (result (attachment-check-test body)))
+       (push expect expected)
+       (push (if (eq result expect)
+                 result
+               ;; In the case of a failure, include the test
+               ;; details to make it simpler to debug.
+               (format "%S <-- %S" result body))
+             output)))
+    (notmuch-test-expect-equal output expected)))
diff --git a/test/emacs-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-decrypted-message b/test/emacs-show.expected-output/notmuch-show-decrypted-message
new file mode 100644 (file)
index 0000000..c4ec4b3
--- /dev/null
@@ -0,0 +1,10 @@
+test_suite@notmuchmail.org (2000-01-01) (encrypted inbox)
+Subject: Here is the password
+To: test_suite@notmuchmail.org
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+
+[ multipart/encrypted ]
+[ Decryption successful ]
+[ application/pgp-encrypted ]
+[ text/plain ]
+The password is "abcd1234!", please do not tell anyone.
diff --git a/test/emacs-show.expected-output/notmuch-show-decrypted-message-no-crypto b/test/emacs-show.expected-output/notmuch-show-decrypted-message-no-crypto
new file mode 100644 (file)
index 0000000..49d8533
--- /dev/null
@@ -0,0 +1,9 @@
+test_suite@notmuchmail.org (2000-01-01) (encrypted inbox)
+Subject: Here is the password
+To: test_suite@notmuchmail.org
+Date: Sat, 01 Jan 2000 12:00:00 +0000
+
+[ multipart/encrypted ]
+[ Unknown encryption status ]
+[ application/pgp-encrypted ]
+[ application/octet-stream ]
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-elide-non-matching-messages-off b/test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-off
new file mode 100644 (file)
index 0000000..e0bd2c7
--- /dev/null
@@ -0,0 +1,82 @@
+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)
+  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 ]
+  > See the patch just posted here.
+
+  Is the list archived anywhere?  The obvious archives
+  (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+  think I subscribed too late to get the patch (I only just saw the
+  discussion about it).
+
+  It doesn't look like the patch is in git yet.
+
+  -- Lars
+
+  [ 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 unread)
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+    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)
diff --git a/test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-on b/test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-on
new file mode 100644 (file)
index 0000000..d76d095
--- /dev/null
@@ -0,0 +1,78 @@
+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. ]
+  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 ]
+  > See the patch just posted here.
+
+  Is the list archived anywhere?  The obvious archives
+  (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+  think I subscribed too late to get the patch (I only just saw the
+  discussion about it).
+
+  It doesn't look like the patch is in git yet.
+
+  -- Lars
+
+  [ 4-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-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. ]
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-indent-thread-content-off b/test/emacs-show.expected-output/notmuch-show-indent-thread-content-off
new file mode 100644 (file)
index 0000000..0bb5833
--- /dev/null
@@ -0,0 +1,82 @@
+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)
+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 ]
+> See the patch just posted here.
+
+Is the list archived anywhere?  The obvious archives
+(http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+think I subscribed too late to get the patch (I only just saw the
+discussion about it).
+
+It doesn't look like the patch is in git yet.
+
+-- Lars
+
+[ 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 unread)
+Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+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)
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-process-crypto-mime-parts-off b/test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-off
new file mode 100644 (file)
index 0000000..3282c7b
--- /dev/null
@@ -0,0 +1,31 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed unread)
+Subject: [notmuch] Working with Maildir storage?
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+  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 ]
+  [ text/plain ]
+  > See the patch just posted here.
+
+  Is the list archived anywhere?  The obvious archives
+  (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+  think I subscribed too late to get the patch (I only just saw the
+  discussion about it).
+
+  It doesn't look like the patch is in git yet.
+
+  -- Lars
+
+  [ 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 unread)
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-on b/test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-on
new file mode 100644 (file)
index 0000000..eaa557a
--- /dev/null
@@ -0,0 +1,32 @@
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed unread)
+Subject: [notmuch] Working with Maildir storage?
+ Mikhail Gusarov <dottedmag@dottedmag.net> (2009-11-17) (inbox signed unread)
+  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 ]
+  > See the patch just posted here.
+
+  Is the list archived anywhere?  The obvious archives
+  (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+  think I subscribed too late to get the patch (I only just saw the
+  discussion about it).
+
+  It doesn't look like the patch is in git yet.
+
+  -- Lars
+
+  [ 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 unread)
+   Keith Packard <keithp@keithp.com> (2009-11-17) (inbox unread)
+    Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-18) (inbox signed unread)
+ Carl Worth <cworth@cworth.org> (2009-11-18) (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-show.expected-output/notmuch-show-undecryptable-message b/test/emacs-show.expected-output/notmuch-show-undecryptable-message
new file mode 100644 (file)
index 0000000..95c1646
--- /dev/null
@@ -0,0 +1,9 @@
+Daniel Kahn Gillmor <dkg@fifthhorseman.net> (2016-12-22) (encrypted inbox)
+Subject: encrypted message
+To: dkg@fifthhorseman.net
+Date: Thu, 22 Dec 2016 08:34:56 -0400
+
+[ multipart/encrypted ]
+[ Decryption error ]
+[ application/pgp-encrypted ]
+[ application/octet-stream ]
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-show-window b/test/emacs-tree.expected-output/notmuch-tree-show-window
new file mode 100644 (file)
index 0000000..7d860c6
--- /dev/null
@@ -0,0 +1,41 @@
+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. ]
+-- 
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
diff --git a/test/emacs-tree.expected-output/notmuch-tree-single-thread b/test/emacs-tree.expected-output/notmuch-tree-single-thread
new file mode 100644 (file)
index 0000000..2285d10
--- /dev/null
@@ -0,0 +1,6 @@
+  2009-11-17  Mikhail Gusarov       ┬►[notmuch] [PATCH 1/2] Close message file after parsing message       headers (inbox)
+  2009-11-17  Mikhail Gusarov       ├─►[notmuch] [PATCH 2/2] Include <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)
+End of search results.
diff --git a/test/emacs-tree.expected-output/notmuch-tree-tag-inbox b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox
new file mode 100644 (file)
index 0000000..f28d485
--- /dev/null
@@ -0,0 +1,53 @@
+  2010-12-29  François Boulogne     ─►[aur-general] Guidelines: cp, mkdir vs install      (inbox unread)
+  2010-12-16  Olivier Berger        ─►Essai accentué                                      (inbox unread)
+  2009-11-18  Chris Wilson          ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread)
+  2009-11-18  Alex Botero-Lowry     ┬►[notmuch] [PATCH] Error out if no query is supplied to search        instead of going into an infinite loop (attachment inbox unread)
+  2009-11-18  Carl Worth            ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox unread)
+  2009-11-17  Ingmar Vanhassel      ┬►[notmuch] [PATCH] Typsos                            (inbox unread)
+  2009-11-18  Carl Worth            ╰─► ...                                               (inbox unread)
+  2009-11-17  Adrian Perez de Cast  ┬►[notmuch] Introducing myself                        (inbox signed unread)
+  2009-11-18  Keith Packard         ├─► ...                                               (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  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  Jan Janak             ┬►[notmuch] [PATCH] Older versions of install do not support -C. (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  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  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  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-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  Alexander Botero-Low  ─►[notmuch] request for pull                          (inbox unread)
+  2009-11-18  Jjgod Jiang           ┬►[notmuch] Mac OS X/Darwin compatibility issues      (inbox unread)
+  2009-11-18  Alexander Botero-Low  ╰┬► ...                                               (inbox unread)
+  2009-11-18  Jjgod Jiang            ╰┬► ...                                              (inbox unread)
+  2009-11-18  Alexander Botero-Low    ╰─► ...                                             (inbox unread)
+  2009-11-18  Rolland Santimano     ─►[notmuch] Link to mailing list archives ?           (inbox unread)
+  2009-11-18  Jan Janak             ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool       subdirectories into tags (inbox unread)
+  2009-11-18  Stewart Smith         ─►[notmuch] [PATCH] count_files: sort directory in inode order before  statting (inbox unread)
+  2009-11-18  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-18  Lars Kellogg-Stedman  ╰─► ...                                               (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)
+  2009-11-17  Carl Worth            ╰─► ...                                               (inbox unread)
+End of search results.
diff --git a/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-tagged b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-tagged
new file mode 100644 (file)
index 0000000..428c0ae
--- /dev/null
@@ -0,0 +1,53 @@
+  2010-12-29  François Boulogne     ─►[aur-general] Guidelines: cp, mkdir vs install      (inbox unread)
+  2010-12-16  Olivier Berger        ─►Essai accentué                                      (inbox test_tag unread)
+  2009-11-18  Chris Wilson          ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread)
+  2009-11-18  Alex Botero-Lowry     ┬►[notmuch] [PATCH] Error out if no query is supplied to search        instead of going into an infinite loop (attachment inbox unread)
+  2009-11-18  Carl Worth            ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox unread)
+  2009-11-17  Ingmar Vanhassel      ┬►[notmuch] [PATCH] Typsos                            (inbox unread)
+  2009-11-18  Carl Worth            ╰─► ...                                               (inbox unread)
+  2009-11-17  Adrian Perez de Cast  ┬►[notmuch] Introducing myself                        (inbox signed unread)
+  2009-11-18  Keith Packard         ├─► ...                                               (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  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  Jan Janak             ┬►[notmuch] [PATCH] Older versions of install do not support -C. (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  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  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  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-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  Alexander Botero-Low  ─►[notmuch] request for pull                          (inbox unread)
+  2009-11-18  Jjgod Jiang           ┬►[notmuch] Mac OS X/Darwin compatibility issues      (inbox unread)
+  2009-11-18  Alexander Botero-Low  ╰┬► ...                                               (inbox unread)
+  2009-11-18  Jjgod Jiang            ╰┬► ...                                              (inbox unread)
+  2009-11-18  Alexander Botero-Low    ╰─► ...                                             (inbox unread)
+  2009-11-18  Rolland Santimano     ─►[notmuch] Link to mailing list archives ?           (inbox unread)
+  2009-11-18  Jan Janak             ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool       subdirectories into tags (inbox unread)
+  2009-11-18  Stewart Smith         ─►[notmuch] [PATCH] count_files: sort directory in inode order before  statting (inbox unread)
+  2009-11-18  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-18  Lars Kellogg-Stedman  ╰─► ...                                               (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)
+  2009-11-17  Carl Worth            ╰─► ...                                               (inbox unread)
+End of search results.
diff --git a/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-thread-tagged b/test/emacs-tree.expected-output/notmuch-tree-tag-inbox-thread-tagged
new file mode 100644 (file)
index 0000000..828c525
--- /dev/null
@@ -0,0 +1,53 @@
+  2010-12-29  François Boulogne     ─►[aur-general] Guidelines: cp, mkdir vs install      (inbox unread)
+  2010-12-16  Olivier Berger        ─►Essai accentué                                      (inbox unread)
+  2009-11-18  Chris Wilson          ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread)
+  2009-11-18  Alex Botero-Lowry     ┬►[notmuch] [PATCH] Error out if no query is supplied to search        instead of going into an infinite loop (attachment inbox unread)
+  2009-11-18  Carl Worth            ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (inbox unread)
+  2009-11-17  Ingmar Vanhassel      ┬►[notmuch] [PATCH] Typsos                            (inbox unread)
+  2009-11-18  Carl Worth            ╰─► ...                                               (inbox unread)
+  2009-11-17  Adrian Perez de Cast  ┬►[notmuch] Introducing myself                        (inbox signed unread)
+  2009-11-18  Keith Packard         ├─► ...                                               (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  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  Jan Janak             ┬►[notmuch] [PATCH] Older versions of install do not support -C. (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  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  Lars Kellogg-Stedman  ┬►[notmuch] Working with Maildir storage?             (inbox signed test_thread_tag unread)
+  2009-11-17  Mikhail Gusarov       ├┬► ...                                               (inbox signed test_thread_tag unread)
+  2009-11-17  Lars Kellogg-Stedman  │╰┬► ...                                              (inbox signed test_thread_tag unread)
+  2009-11-17  Mikhail Gusarov       │ ├─► ...                                             (inbox test_thread_tag unread)
+  2009-11-17  Keith Packard         │ ╰┬► ...                                             (inbox test_thread_tag unread)
+  2009-11-18  Lars Kellogg-Stedman  │  ╰─► ...                                            (inbox signed test_thread_tag unread)
+  2009-11-18  Carl Worth            ╰─► ...                                               (inbox test_thread_tag unread)
+  2009-11-17  Mikhail Gusarov       ┬►[notmuch] [PATCH 1/2] Close message file after parsing message       headers (inbox unread)
+  2009-11-17  Mikhail Gusarov       ├─►[notmuch] [PATCH 2/2] Include <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-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  Alexander Botero-Low  ─►[notmuch] request for pull                          (inbox unread)
+  2009-11-18  Jjgod Jiang           ┬►[notmuch] Mac OS X/Darwin compatibility issues      (inbox unread)
+  2009-11-18  Alexander Botero-Low  ╰┬► ...                                               (inbox unread)
+  2009-11-18  Jjgod Jiang            ╰┬► ...                                              (inbox unread)
+  2009-11-18  Alexander Botero-Low    ╰─► ...                                             (inbox unread)
+  2009-11-18  Rolland Santimano     ─►[notmuch] Link to mailing list archives ?           (inbox unread)
+  2009-11-18  Jan Janak             ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool       subdirectories into tags (inbox unread)
+  2009-11-18  Stewart Smith         ─►[notmuch] [PATCH] count_files: sort directory in inode order before  statting (inbox unread)
+  2009-11-18  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-18  Lars Kellogg-Stedman  ╰─► ...                                               (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)
+  2009-11-17  Carl Worth            ╰─► ...                                               (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/attachment b/test/emacs.expected-output/attachment
new file mode 100644 (file)
index 0000000..1e22d3a
--- /dev/null
@@ -0,0 +1,32 @@
+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
+
diff --git a/test/emacs.expected-output/notmuch-hello b/test/emacs.expected-output/notmuch-hello
new file mode 100644 (file)
index 0000000..8918608
--- /dev/null
@@ -0,0 +1,12 @@
+   Welcome to notmuch. You have 52 messages.
+
+Saved searches: [edit]
+
+         52 inbox           52 unread          52 all mail
+
+Search:                                                                     .
+
+All tags: [show]
+
+        Hit `?' for context-sensitive help in any Notmuch screen.
+                     Customize Notmuch or this page.
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.
diff --git a/test/emacs.expected-output/notmuch-hello-empty-custom-queries-section b/test/emacs.expected-output/notmuch-hello-empty-custom-queries-section
new file mode 100644 (file)
index 0000000..cd0fdf0
--- /dev/null
@@ -0,0 +1,3 @@
+: [hide]
+
+
diff --git a/test/emacs.expected-output/notmuch-hello-empty-custom-tags-section b/test/emacs.expected-output/notmuch-hello-empty-custom-tags-section
new file mode 100644 (file)
index 0000000..b56fd67
--- /dev/null
@@ -0,0 +1,5 @@
+: [hide]
+
+          4 attachment             7 signed
+         52 inbox                 52 unread
+
diff --git a/test/emacs.expected-output/notmuch-hello-long-names b/test/emacs.expected-output/notmuch-hello-long-names
new file mode 100644 (file)
index 0000000..da0f352
--- /dev/null
@@ -0,0 +1,15 @@
+   Welcome to notmuch. You have 52 messages.
+
+Saved searches: [edit]
+
+         52 inbox           52 unread          52 all mail
+
+Search:                                                                     .
+
+All tags: [hide]
+
+         52 a-very-long-tag       52 inbox                 52 unread
+          4 attachment             7 signed
+
+        Hit `?' for context-sensitive help in any Notmuch screen.
+                     Customize Notmuch or this page.
diff --git a/test/emacs.expected-output/notmuch-hello-new-section b/test/emacs.expected-output/notmuch-hello-new-section
new file mode 100644 (file)
index 0000000..67fdef2
--- /dev/null
@@ -0,0 +1,4 @@
+Test: [hide]
+
+         52 inbox
+
diff --git a/test/emacs.expected-output/notmuch-hello-no-saved-searches b/test/emacs.expected-output/notmuch-hello-no-saved-searches
new file mode 100644 (file)
index 0000000..939965f
--- /dev/null
@@ -0,0 +1,8 @@
+   Welcome to notmuch. You have 52 messages.
+
+Search:                                                                     .
+
+All tags: [show]
+
+        Hit `?' for context-sensitive help in any Notmuch screen.
+                     Customize Notmuch or this page.
diff --git a/test/emacs.expected-output/notmuch-hello-section-counts b/test/emacs.expected-output/notmuch-hello-section-counts
new file mode 100644 (file)
index 0000000..7a9827c
--- /dev/null
@@ -0,0 +1,5 @@
+Test-with-counts: [hide]
+
+          2 attachment             7 signed
+          7 inbox                  7 unread
+
diff --git a/test/emacs.expected-output/notmuch-hello-section-hidden-tag b/test/emacs.expected-output/notmuch-hello-section-hidden-tag
new file mode 100644 (file)
index 0000000..809a114
--- /dev/null
@@ -0,0 +1,4 @@
+Test-with-filtered: [hide]
+
+          4 attachment            52 inbox                  7 signed
+
diff --git a/test/emacs.expected-output/notmuch-hello-section-with-empty b/test/emacs.expected-output/notmuch-hello-section-with-empty
new file mode 100644 (file)
index 0000000..5c67317
--- /dev/null
@@ -0,0 +1,4 @@
+Test-with-empty: [hide]
+
+         52 inbox
+
diff --git a/test/emacs.expected-output/notmuch-hello-view-inbox b/test/emacs.expected-output/notmuch-hello-view-inbox
new file mode 100644 (file)
index 0000000..1688d67
--- /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. (inbox unread)
+  2009-11-18 [1/1]   Stewart Smith        [notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox unread)
+  2009-11-18 [1/1]   Stewart Smith        [notmuch] [PATCH] count_files: sort directory in inode order before statting (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 (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.expected-output/notmuch-hello-with-empty b/test/emacs.expected-output/notmuch-hello-with-empty
new file mode 100644 (file)
index 0000000..97d7db2
--- /dev/null
@@ -0,0 +1,12 @@
+   Welcome to notmuch. You have 52 messages.
+
+Saved searches: [edit]
+
+         52 inbox           52 unread           0 empty
+
+Search:                                                                     .
+
+All tags: [show]
+
+        Hit `?' for context-sensitive help in any Notmuch screen.
+                     Customize Notmuch or this page.
diff --git a/test/emacs.expected-output/notmuch-search-tag-inbox b/test/emacs.expected-output/notmuch-search-tag-inbox
new file mode 100644 (file)
index 0000000..8a53555
--- /dev/null
@@ -0,0 +1,25 @@
+  2010-12-29 [1/1]   François Boulogne    [aur-general] Guidelines: cp, mkdir vs install (inbox unread)
+  2010-12-16 [1/1]   Olivier Berger       Essai accentué (inbox unread)
+  2009-11-18 [1/1]   Chris Wilson         [notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (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]   Ingmar Vanhassel, Carl Worth  [notmuch] [PATCH] Typsos (inbox unread)
+  2009-11-18 [3/3]   Adrian Perez de Castro, Keith Packard, Carl Worth  [notmuch] Introducing myself (inbox signed unread)
+  2009-11-18 [3/3]   Israel Herraiz, Keith Packard, Carl Worth   [notmuch] New to the list (inbox unread)
+  2009-11-18 [3/3]   Jan Janak, Carl Worth        [notmuch] What a great idea! (inbox unread)
+  2009-11-18 [2/2]   Jan Janak, Carl Worth        [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+  2009-11-18 [3/3]   Aron Griffis, Keith Packard, Carl Worth     [notmuch] archive (inbox unread)
+  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)
+  2009-11-18 [7/7]   Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth  [notmuch] Working with Maildir storage? (inbox signed unread)
+  2009-11-18 [5/5]   Mikhail Gusarov, Carl Worth, Keith Packard  [notmuch] [PATCH 1/2] Close message file after parsing message headers (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]   Alexander Botero-Lowry  [notmuch] request for pull (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]   Rolland Santimano    [notmuch] Link to mailing list archives ? (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]   Stewart Smith        [notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread)
+  2009-11-18 [1/1]   Stewart Smith        [notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox unread)
+  2009-11-18 [1/1]   Stewart Smith        [notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread)
+  2009-11-18 [2/2]   Lars Kellogg-Stedman [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread)
+  2009-11-17 [1/1]   Mikhail Gusarov      [notmuch] [PATCH] Handle rename of message file (inbox unread)
+  2009-11-17 [2/2]   Alex Botero-Lowry, Carl Worth  [notmuch] preliminary FreeBSD support (attachment inbox unread)
+End of search results.
diff --git a/test/emacs.expected-output/notmuch-show-message-with-headers-hidden b/test/emacs.expected-output/notmuch-show-message-with-headers-hidden
new file mode 100644 (file)
index 0000000..9d7f91b
--- /dev/null
@@ -0,0 +1,22 @@
+Jan Janak <jan@ryngle.com> (2009-11-17) (inbox unread)
+Subject: [notmuch] What a great idea!
+ Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+
+ On Tue, Nov 17, 2009 at 11:35 PM, Jan Janak <jan at ryngle.com> wrote:
+ > Hello,
+ >
+ > First of all, notmuch is a wonderful idea, both the cmdline tool and
+ [ 2 more citation lines. Click/Enter to show. ]
+ >
+ > Have you considered sending an announcement to the org-mode mailing list?
+ > http://org-mode.org
+
+ Sorry, wrong URL, the correct one is: http://orgmode.org
+
+ > Various ways of searching/referencing emails from emacs were discussed
+ > there several times and none of them were as elegant as notmuch (not
+ > even close). Maybe notmuch would attract some of the developers
+ > there..
+
+   -- Jan
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs.expected-output/notmuch-show-message-with-headers-visible b/test/emacs.expected-output/notmuch-show-message-with-headers-visible
new file mode 100644 (file)
index 0000000..8efbd60
--- /dev/null
@@ -0,0 +1,25 @@
+Jan Janak <jan@ryngle.com> (2009-11-17) (inbox unread)
+Subject: [notmuch] What a great idea!
+ Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+ Subject: [notmuch] What a great idea!
+ To: notmuch@notmuchmail.org
+ Date: Tue, 17 Nov 2009 23:38:47 +0100
+
+ On Tue, Nov 17, 2009 at 11:35 PM, Jan Janak <jan at ryngle.com> wrote:
+ > Hello,
+ >
+ > First of all, notmuch is a wonderful idea, both the cmdline tool and
+ [ 2 more citation lines. Click/Enter to show. ]
+ >
+ > Have you considered sending an announcement to the org-mode mailing list?
+ > http://org-mode.org
+
+ Sorry, wrong URL, the correct one is: http://orgmode.org
+
+ > Various ways of searching/referencing emails from emacs were discussed
+ > there several times and none of them were as elegant as notmuch (not
+ > even close). Maybe notmuch would attract some of the developers
+ > there..
+
+   -- Jan
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs.expected-output/notmuch-show-thread-maildir-storage b/test/emacs.expected-output/notmuch-show-thread-maildir-storage
new file mode 100644 (file)
index 0000000..1f89dbe
--- /dev/null
@@ -0,0 +1,218 @@
+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. ]
+-- 
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+ 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. ]
+ -- 
+ http://fossarchy.blogspot.com/
+ [ application/pgp-signature ]
+ [ text/plain ]
+ [ 4-line signature. Click/Enter to show. ]
+ _______________________________________________
+ notmuch mailing list
+ notmuch@notmuchmail.org
+ http://notmuchmail.org/mailman/listinfo/notmuch
+  Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed unread)
+  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 ]
+  > See the patch just posted here.
+
+  Is the list archived anywhere?  The obvious archives
+  (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+  think I subscribed too late to get the patch (I only just saw the
+  discussion about it).
+
+  It doesn't look like the patch is in git yet.
+
+  -- Lars
+
+  [ 4-line signature. Click/Enter to show. ]
+  -- 
+  Lars Kellogg-Stedman <lars@seas.harvard.edu>
+  Senior Technologist, Computing and Information Technology
+  Harvard University School of Engineering and Applied Sciences
+  [ application/pgp-signature ]
+  [ text/plain ]
+  [ 4-line signature. Click/Enter to show. ]
+  _______________________________________________
+  notmuch mailing list
+  notmuch@notmuchmail.org
+  http://notmuchmail.org/mailman/listinfo/notmuch
+   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. ]
+   -- 
+   http://fossarchy.blogspot.com/
+   -------------- next part --------------
+   A non-text attachment was scrubbed...
+   Name: not available
+   Type: application/pgp-signature
+   Size: 834 bytes
+   Desc: not available
+   URL:
+   <http://notmuchmail.org/pipermail/notmuch/attachments/20091118/0e33d964/attachment.pgp>
+   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
+
+   On Tue, 17 Nov 2009 15:33:01 -0500, Lars Kellogg-Stedman <lars at
+   seas.harvard.edu> wrote:
+   > > See the patch just posted here.
+
+   I've also pushed a slightly more complicated (and complete) fix to my
+   private notmuch repository
+
+   git://keithp.com/git/notmuch
+
+   > Is the list archived anywhere?
+
+   Oops. Looks like Carl's mail server is broken. He's traveling to
+   Barcelona today and so it won't get fixed for a while.
+
+   Thanks to everyone for trying out notmuch!
+
+   -keith
+    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. ]
+    -- 
+    Lars Kellogg-Stedman <lars@seas.harvard.edu>
+    Senior Technologist, Computing and Information Technology
+    Harvard University School of Engineering and Applied Sciences
+    [ application/pgp-signature ]
+    [ text/plain ]
+    [ 4-line signature. Click/Enter to show. ]
+    _______________________________________________
+    notmuch mailing list
+    notmuch@notmuchmail.org
+    http://notmuchmail.org/mailman/listinfo/notmuch
+ 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.expected-output/notmuch-show-thread-maildir-storage-with-fourfold-indentation b/test/emacs.expected-output/notmuch-show-thread-maildir-storage-with-fourfold-indentation
new file mode 100644 (file)
index 0000000..5c4ec97
--- /dev/null
@@ -0,0 +1,223 @@
+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. ]
+-- 
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+    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. ]
+    -- 
+    http://fossarchy.blogspot.com/
+    [ application/pgp-signature ]
+    [ text/plain ]
+    [ 4-line signature. Click/Enter to show. ]
+    _______________________________________________
+    notmuch mailing list
+    notmuch@notmuchmail.org
+    http://notmuchmail.org/mailman/listinfo/notmuch
+        Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed unread)
+       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 ]
+       > See the patch just posted here.
+
+       Is the list archived anywhere?  The obvious archives
+       (http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+       think I subscribed too late to get the patch (I only just saw the
+       discussion about it).
+
+       It doesn't look like the patch is in git yet.
+
+       -- Lars
+
+       [ 4-line signature. Click/Enter to show. ]
+       -- 
+       Lars Kellogg-Stedman <lars@seas.harvard.edu>
+       Senior Technologist, Computing and Information Technology
+       Harvard University School of Engineering and Applied Sciences
+       [ application/pgp-signature ]
+       [ text/plain ]
+       [ 4-line signature. Click/Enter to show. ]
+       _______________________________________________
+       notmuch mailing list
+       notmuch@notmuchmail.org
+       http://notmuchmail.org/mailman/listinfo/notmuch
+            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. ]
+           -- 
+           http://fossarchy.blogspot.com/
+           -------------- next part --------------
+           A non-text attachment was scrubbed...
+           Name: not available
+           Type: application/pgp-signature
+           Size: 834 bytes
+           Desc: not available
+           URL:
+           <http://notmuchmail.org/pipermail/notmuch/attachments/20091118/0e33d964/attachment.pgp>
+            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
+
+           On Tue, 17 Nov 2009 15:33:01 -0500, Lars Kellogg-Stedman <lars at
+           seas.harvard.edu> wrote:
+           > > See the patch just posted here.
+
+           I've also pushed a slightly more complicated (and complete) fix to
+           my
+           private notmuch repository
+
+           git://keithp.com/git/notmuch
+
+           > Is the list archived anywhere?
+
+           Oops. Looks like Carl's mail server is broken. He's traveling to
+           Barcelona today and so it won't get fixed for a while.
+
+           Thanks to everyone for trying out notmuch!
+
+           -keith
+                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. ]
+               -- 
+               Lars Kellogg-Stedman <lars@seas.harvard.edu>
+               Senior Technologist, Computing and Information Technology
+               Harvard University School of Engineering and Applied Sciences
+               [ application/pgp-signature ]
+               [ text/plain ]
+               [ 4-line signature. Click/Enter to show. ]
+               _______________________________________________
+               notmuch mailing list
+               notmuch@notmuchmail.org
+               http://notmuchmail.org/mailman/listinfo/notmuch
+    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.expected-output/notmuch-show-thread-maildir-storage-without-indentation b/test/emacs.expected-output/notmuch-show-thread-maildir-storage-without-indentation
new file mode 100644 (file)
index 0000000..24cdd56
--- /dev/null
@@ -0,0 +1,218 @@
+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. ]
+-- 
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+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. ]
+-- 
+http://fossarchy.blogspot.com/
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+Lars Kellogg-Stedman <lars@seas.harvard.edu> (2009-11-17) (inbox signed unread)
+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 ]
+> See the patch just posted here.
+
+Is the list archived anywhere?  The obvious archives
+(http://notmuchmail.org/pipermail/notmuch/) aren't available, and I
+think I subscribed too late to get the patch (I only just saw the
+discussion about it).
+
+It doesn't look like the patch is in git yet.
+
+-- Lars
+
+[ 4-line signature. Click/Enter to show. ]
+-- 
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+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. ]
+-- 
+http://fossarchy.blogspot.com/
+-------------- next part --------------
+A non-text attachment was scrubbed...
+Name: not available
+Type: application/pgp-signature
+Size: 834 bytes
+Desc: not available
+URL:
+<http://notmuchmail.org/pipermail/notmuch/attachments/20091118/0e33d964/attachment.pgp>
+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
+
+On Tue, 17 Nov 2009 15:33:01 -0500, Lars Kellogg-Stedman <lars at
+seas.harvard.edu> wrote:
+> > See the patch just posted here.
+
+I've also pushed a slightly more complicated (and complete) fix to my
+private notmuch repository
+
+git://keithp.com/git/notmuch
+
+> Is the list archived anywhere?
+
+Oops. Looks like Carl's mail server is broken. He's traveling to
+Barcelona today and so it won't get fixed for a while.
+
+Thanks to everyone for trying out notmuch!
+
+-keith
+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. ]
+-- 
+Lars Kellogg-Stedman <lars@seas.harvard.edu>
+Senior Technologist, Computing and Information Technology
+Harvard University School of Engineering and Applied Sciences
+[ application/pgp-signature ]
+[ text/plain ]
+[ 4-line signature. Click/Enter to show. ]
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+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.expected-output/notmuch-show-thread-with-all-messages-collapsed b/test/emacs.expected-output/notmuch-show-thread-with-all-messages-collapsed
new file mode 100644 (file)
index 0000000..73b0e60
--- /dev/null
@@ -0,0 +1,4 @@
+Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+Subject: [notmuch] What a great idea!
+ Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs.expected-output/notmuch-show-thread-with-all-messages-uncollapsed b/test/emacs.expected-output/notmuch-show-thread-with-all-messages-uncollapsed
new file mode 100644 (file)
index 0000000..bd5598e
--- /dev/null
@@ -0,0 +1,79 @@
+Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+Subject: [notmuch] What a great idea!
+To: notmuch@notmuchmail.org
+Date: Tue, 17 Nov 2009 23:35:30 +0100
+
+Hello,
+
+First of all, notmuch is a wonderful idea, both the cmdline tool and
+the emacs interface! Thanks a lot for writing it, I was really excited
+when I read the announcement today.
+
+Have you considered sending an announcement to the org-mode mailing list?
+http://org-mode.org
+
+Various ways of searching/referencing emails from emacs were discussed
+there several times and none of them were as elegant as notmuch (not
+even close). Maybe notmuch would attract some of the developers
+there..
+
+   -- Jan
+ Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+ Subject: [notmuch] What a great idea!
+ To: notmuch@notmuchmail.org
+ Date: Tue, 17 Nov 2009 23:38:47 +0100
+
+ On Tue, Nov 17, 2009 at 11:35 PM, Jan Janak <jan at ryngle.com> wrote:
+ > Hello,
+ >
+ > First of all, notmuch is a wonderful idea, both the cmdline tool and
+ [ 2 more citation lines. Click/Enter to show. ]
+ >
+ > Have you considered sending an announcement to the org-mode mailing list?
+ > http://org-mode.org
+
+ Sorry, wrong URL, the correct one is: http://orgmode.org
+
+ > Various ways of searching/referencing emails from emacs were discussed
+ > there several times and none of them were as elegant as notmuch (not
+ > even close). Maybe notmuch would attract some of the developers
+ > there..
+
+   -- Jan
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
+ Subject: [notmuch] What a great idea!
+ To: notmuch@notmuchmail.org
+ Date: Wed, 18 Nov 2009 02:49:52 -0800
+
+ On Tue, 17 Nov 2009 23:35:30 +0100, Jan Janak <jan at ryngle.com> wrote:
+ > First of all, notmuch is a wonderful idea, both the cmdline tool and
+ > the emacs interface! Thanks a lot for writing it, I was really excited
+ > when I read the announcement today.
+
+ Ah, here's where I planned a nice welcome. So welcome (again), Jan! :-)
+
+ I've been having a lot of fun with notmuch already, (though there have
+ been some days of pain before it was functional enough and my
+ email-reply latency went way up). But regardless---I got through that,
+ and I'm able to work more efficiently with notmuch now than I could with
+ sup before. So I'm happy.
+
+ And I'm delighted when other people find this interesting as well.
+
+ > Have you considered sending an announcement to the org-mode mailing list?
+ > http://orgmode.org
+
+ Thanks for the idea. I think I may have looked into org-mode years ago,
+ (when I was investigating planner-mode and various emacs "personal wiki"
+ systems for keeping random notes and what-not).
+
+ > Various ways of searching/referencing emails from emacs were discussed
+ > there several times and none of them were as elegant as notmuch (not
+ > even close). Maybe notmuch would attract some of the developers
+ > there..
+
+ Yeah. I'll drop them a mail. Having a real emacs wizard on board would
+ be nice. (I'm afraid the elisp I've written so far for this project is
+ fairly grim.)
+
+ -Carl
diff --git a/test/emacs.expected-output/notmuch-show-thread-with-hidden-messages b/test/emacs.expected-output/notmuch-show-thread-with-hidden-messages
new file mode 100644 (file)
index 0000000..8a0660f
--- /dev/null
@@ -0,0 +1,4 @@
+Jan Janak <jan@ryngle.com> (2009-11-17) (inbox unread)
+Subject: [notmuch] What a great idea!
+ Jan Janak <jan@ryngle.com> (2009-11-17) (inbox)
+ Carl Worth <cworth@cworth.org> (2009-11-18) (inbox unread)
diff --git a/test/emacs.expected-output/raw-message-cf0c4d-52ad0a b/test/emacs.expected-output/raw-message-cf0c4d-52ad0a
new file mode 100644 (file)
index 0000000..ce174b5
--- /dev/null
@@ -0,0 +1,104 @@
+MIME-Version: 1.0
+Date: Tue, 17 Nov 2009 11:36:14 -0800
+Message-ID: <cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com>
+From: Alex Botero-Lowry <alex.boterolowry@gmail.com>
+To: notmuch@notmuchmail.org
+Content-Type: multipart/mixed; boundary=0016e687869333b1570478963d35
+Subject: [notmuch] preliminary FreeBSD support
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.12
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+       <notmuch.notmuchmail.org>
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>
+List-Post: <mailto:notmuch@notmuchmail.org>
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
+Sender: notmuch-bounces@notmuchmail.org
+Errors-To: notmuch-bounces@notmuchmail.org
+
+--0016e687869333b1570478963d35
+Content-Type: multipart/alternative; boundary=0016e687869333b14e0478963d33
+
+--0016e687869333b14e0478963d33
+Content-Type: text/plain; charset=ISO-8859-1
+
+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.
+
+--0016e687869333b14e0478963d33
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: quoted-printable
+
+I saw the announcement this morning, and was very excited, as I had been ho=
+ping sup would be turned into a library,<br>since I like the concept more t=
+han the UI (I&#39;d rather an emacs interface).<br><br>I did a preliminary =
+compile which worked out fine, but sysconf(_SC_SC_GETPW_R_SIZE_MAX) returns=
+ -1 on<br>
+FreeBSD, so notmuch_config_open segfaulted.<br><br>Attached is a patch that=
+ supplies a default buffer size of 64 in cases where -1 is returned.<br><br=
+><a href=3D"http://www.opengroup.org/austin/docs/austin_328.txt">http://www=
+.opengroup.org/austin/docs/austin_328.txt</a> - seems to indicate this is a=
+cceptable behavior,<br>
+and <a href=3D"http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg01680=
+8.html">http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg016808.html<=
+/a> specifically uses 64 as the<br>buffer size.<br><br><br>
+
+--0016e687869333b14e0478963d33--
+--0016e687869333b1570478963d35
+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"
+Content-Transfer-Encoding: base64
+X-Attachment-Id: f_g252e6gs0
+
+RnJvbSBlM2JjNGJiZDdiOWQwZDA4NjgxNmFiNWY4ZjJkNmZmZWExZGQzZWE0IE1vbiBTZXAgMTcg
+MDA6MDA6MDAgMjAwMQpGcm9tOiBBbGV4YW5kZXIgQm90ZXJvLUxvd3J5IDxhbGV4LmJvdGVyb2xv
+d3J5QGdtYWlsLmNvbT4KRGF0ZTogVHVlLCAxNyBOb3YgMjAwOSAxMTozMDozOSAtMDgwMApTdWJq
+ZWN0OiBbUEFUQ0hdIERlYWwgd2l0aCBzaXR1YXRpb24gd2hlcmUgc3lzY29uZihfU0NfR0VUUFdf
+Ul9TSVpFX01BWCkgcmV0dXJucyAtMQoKLS0tCiBub3RtdWNoLWNvbmZpZy5jIHwgICAgMiArKwog
+MSBmaWxlcyBjaGFuZ2VkLCAyIGluc2VydGlvbnMoKyksIDAgZGVsZXRpb25zKC0pCgpkaWZmIC0t
+Z2l0IGEvbm90bXVjaC1jb25maWcuYyBiL25vdG11Y2gtY29uZmlnLmMKaW5kZXggMjQ4MTQ5Yy4u
+ZTcyMjBkOCAxMDA2NDQKLS0tIGEvbm90bXVjaC1jb25maWcuYworKysgYi9ub3RtdWNoLWNvbmZp
+Zy5jCkBAIC03Nyw2ICs3Nyw3IEBAIHN0YXRpYyBjaGFyICoKIGdldF9uYW1lX2Zyb21fcGFzc3dk
+X2ZpbGUgKHZvaWQgKmN0eCkKIHsKICAgICBsb25nIHB3X2J1Zl9zaXplID0gc3lzY29uZihfU0Nf
+R0VUUFdfUl9TSVpFX01BWCk7CisgICAgaWYgKHB3X2J1Zl9zaXplID09IC0xKSBwd19idWZfc2l6
+ZSA9IDY0OwogICAgIGNoYXIgKnB3X2J1ZiA9IHRhbGxvY196ZXJvX3NpemUgKGN0eCwgcHdfYnVm
+X3NpemUpOwogICAgIHN0cnVjdCBwYXNzd2QgcGFzc3dkLCAqaWdub3JlZDsKICAgICBjaGFyICpu
+YW1lOwpAQCAtMTAxLDYgKzEwMiw3IEBAIHN0YXRpYyBjaGFyICoKIGdldF91c2VybmFtZV9mcm9t
+X3Bhc3N3ZF9maWxlICh2b2lkICpjdHgpCiB7CiAgICAgbG9uZyBwd19idWZfc2l6ZSA9IHN5c2Nv
+bmYoX1NDX0dFVFBXX1JfU0laRV9NQVgpOworICAgIGlmIChwd19idWZfc2l6ZSA9PSAtMSkgcHdf
+YnVmX3NpemUgPSA2NDsKICAgICBjaGFyICpwd19idWYgPSB0YWxsb2NfemVyb19zaXplIChjdHgs
+IHB3X2J1Zl9zaXplKTsKICAgICBzdHJ1Y3QgcGFzc3dkIHBhc3N3ZCwgKmlnbm9yZWQ7CiAgICAg
+Y2hhciAqbmFtZTsKLS0gCjEuNi41LjIKCg==
+--0016e687869333b1570478963d35
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+notmuch mailing list
+notmuch@notmuchmail.org
+http://notmuchmail.org/mailman/listinfo/notmuch
+
+--0016e687869333b1570478963d35--
+
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.
diff --git a/test/export-dirs.sh b/test/export-dirs.sh
new file mode 100644 (file)
index 0000000..9bc99a2
--- /dev/null
@@ -0,0 +1,31 @@
+# Source this script to set and export NOTMUCH_SRCDIR and
+# NOTMUCH_BUILDDIR.
+#
+# For this to work, always have current directory somewhere within the
+# build directory hierarchy, and run the script sourcing this script
+# using a path (relative or absolute) to the source directory.
+
+if [[ -z "${NOTMUCH_SRCDIR}" ]]; then
+       export NOTMUCH_SRCDIR="$(cd "$(dirname "$0")"/.. && pwd)"
+fi
+
+find_builddir () {
+       local dir="$1"
+
+       while [[ -n "$dir" ]] && [[ "$dir" != "/" ]]; do
+               if [[ -x "$dir/notmuch" ]] && [[ ! -d "$dir/notmuch" ]]; then
+                       echo "$dir"
+                       break
+               fi
+               dir="$(dirname "$dir")"
+       done
+}
+
+if [[ -z "${NOTMUCH_BUILDDIR}" ]]; then
+       export NOTMUCH_BUILDDIR="$(find_builddir "$(pwd)")"
+
+       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
+fi
diff --git a/test/gen-threads.py b/test/gen-threads.py
new file mode 100644 (file)
index 0000000..70fb1f6
--- /dev/null
@@ -0,0 +1,33 @@
+# Generate all possible single-root message thread structures of size
+# argv[1].  Each output line is a thread structure, where the n'th
+# field is either a number giving the parent of message n or "None"
+# for the root.
+import sys
+from itertools import chain, combinations
+
+def subsets(s):
+    return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
+
+nodes = set(range(int(sys.argv[1])))
+
+# Queue of (tree, free, to_expand) where tree is a {node: parent}
+# dictionary, free is a set of unattached nodes, and to_expand is
+# itself a queue of nodes in the tree that need to be expanded.
+# The queue starts with all single-node trees.
+queue = [({root: None}, nodes - {root}, (root,)) for root in nodes]
+
+# Process queue
+while queue:
+    tree, free, to_expand = queue.pop()
+
+    if len(to_expand) == 0:
+        # Only print full-sized trees
+        if len(free) == 0:
+            print(" ".join(map(str, [msg[1] for msg in sorted(tree.items())])))
+    else:
+        # Expand node to_expand[0] with each possible set of children
+        for children in subsets(free):
+            ntree = {child: to_expand[0] for child in children}
+            ntree.update(tree)
+            nfree = free.difference(children)
+            queue.append((ntree, nfree, to_expand[1:] + tuple(children)))
diff --git a/test/ghost-report.cc b/test/ghost-report.cc
new file mode 100644 (file)
index 0000000..9d9e7a7
--- /dev/null
@@ -0,0 +1,17 @@
+#include <iostream>
+#include <cstdlib>
+#include <xapian.h>
+
+int
+main (int argc, char **argv)
+{
+
+    if (argc < 2) {
+       std::cerr << "usage: ghost-report xapian-dir" << std::endl;
+       exit (1);
+    }
+
+    Xapian::Database db (argv[1]);
+
+    std::cout << db.get_termfreq ("Tghost") << std::endl;
+}
diff --git a/test/hex-xcode.c b/test/hex-xcode.c
new file mode 100644 (file)
index 0000000..2e70f73
--- /dev/null
@@ -0,0 +1,109 @@
+/* No, nothing to to with IDE from Apple Inc.
+ * testbed for ../util/hex-escape.c.
+ *
+ * usage:
+ * hex-xcode [--direction=(encode|decode)] [--omit-newline] < file
+ * hex-xcode [--direction=(encode|decode)] [--omit-newline] [--in-place] arg1 arg2 arg3 ...
+ *
+ */
+
+#include "notmuch-client.h"
+#include "hex-escape.h"
+#include <assert.h>
+
+enum direction {
+    ENCODE,
+    DECODE
+};
+
+static bool inplace = false;
+
+static int
+xcode (void *ctx, enum direction dir, char *in, char **buf_p, size_t *size_p)
+{
+    hex_status_t status;
+
+    if (dir == ENCODE)
+       status = hex_encode (ctx, in, buf_p, size_p);
+    else
+    if (inplace) {
+       status = hex_decode_inplace (in);
+       *buf_p = in;
+       *size_p = strlen (in);
+    } else {
+       status = hex_decode (ctx, in, buf_p, size_p);
+    }
+
+    if (status == HEX_SUCCESS)
+       fputs (*buf_p, stdout);
+
+    return status;
+}
+
+int
+main (int argc, char **argv)
+{
+
+    int dir = DECODE;
+    bool omit_newline = false;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_keyword = &dir, .name = "direction", .keywords =
+             (notmuch_keyword_t []){ { "encode", ENCODE },
+                                     { "decode", DECODE },
+                                     { 0, 0 } } },
+       { .opt_bool = &omit_newline, .name = "omit-newline" },
+       { .opt_bool = &inplace, .name = "in-place" },
+       { }
+    };
+
+    int opt_index = parse_arguments (argc, argv, options, 1);
+
+    if (opt_index < 0)
+       exit (1);
+
+    void *ctx = talloc_new (NULL);
+
+    char *line = NULL;
+    size_t line_size;
+    ssize_t line_len;
+
+    char *buffer = NULL;
+    size_t buf_size = 0;
+
+    bool read_stdin = true;
+
+    for (; opt_index < argc; opt_index++) {
+
+       if (xcode (ctx, dir, argv[opt_index],
+                  &buffer, &buf_size) != HEX_SUCCESS)
+           return 1;
+
+       if (! omit_newline)
+           putchar ('\n');
+
+       read_stdin = false;
+    }
+
+    if (! read_stdin)
+       return 0;
+
+    while ((line_len = getline (&line, &line_size, stdin)) != -1) {
+
+       chomp_newline (line);
+
+       if (xcode (ctx, dir, line, &buffer, &buf_size) != HEX_SUCCESS)
+           return 1;
+
+       if (! omit_newline)
+           putchar ('\n');
+
+    }
+
+    if (line)
+       free (line);
+
+    talloc_free (ctx);
+
+    return 0;
+}
diff --git a/test/json_check_nodes.py b/test/json_check_nodes.py
new file mode 100755 (executable)
index 0000000..fd8f160
--- /dev/null
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+import re
+import sys
+import json
+
+
+EXPR_RE = re.compile('(?P<label>[a-zA-Z0-9_-]+):(?P<address>[^=!]+)(?:(?P<type>[=!])(?P<val>.*))?', re.DOTALL|re.MULTILINE)
+
+
+if len(sys.argv) < 2:
+    sys.exit('usage: '+ sys.argv[0] + """ EXPR [EXPR]
+
+Takes json data on stdin and evaluates test expressions specified in
+arguments.  Each test is evaluated, and output is printed only if the
+test fails.  If any test fails the return value of execution will be
+non-zero.
+
+EXPR can be one of following types:
+
+Value test: test that object in json data found at address is equal to
+specified value:
+
+  label:address=value
+
+Existence test: test that dict or list in json data found at address
+does *not* contain the specified key:
+
+  label:address!key
+
+Extract: extract object from json data found at address and print
+
+  label:address
+
+Results are printed to stdout prefixed by expression label.  In all
+cases the test will fail if object does not exist in data.
+
+Example:
+
+0 $ echo '["a", "b", {"c": 1}]' | python3 json_check_nodes.py 'second_d:[1]="d"' 'no_c:[2]!"c"'
+second_d: value not equal: data[1] = 'b' != 'd'
+no_c: dict contains key: data[2]["c"] = 1
+1 $
+
+""")
+
+
+# parse expressions from arguments
+exprs = []
+for expr in sys.argv[1:]:
+    m = re.match(EXPR_RE, expr)
+    if not m:
+        sys.exit("Invalid expression: {}".format(expr))
+    exprs.append(m)
+
+data = json.load(sys.stdin)
+
+fail = False
+
+for expr in exprs:
+    # print(expr.groups(),fail)
+
+    e = 'data{}'.format(expr.group('address'))
+    try:
+        val = eval(e)
+    except SyntaxError:
+        fail = True
+        print("{}: syntax error on evaluation of object: {}".format(
+            expr.group('label'), e))
+        continue
+    except:
+        fail = True
+        print("{}: object not found: data{}".format(
+            expr.group('label'), expr.group('address')))
+        continue
+
+    if expr.group('type') == '=':
+        try:
+            obj_val = json.loads(expr.group('val'))
+        except:
+            fail = True
+            print("{}: error evaluating value: {}".format(
+                expr.group('label'), expr.group('address')))
+            continue
+        if val != obj_val:
+            fail = True
+            print("{}: value not equal: data{} = {} != {}".format(
+                expr.group('label'), expr.group('address'), repr(val), repr(obj_val)))
+
+    elif expr.group('type') == '!':
+        if not isinstance(val, (dict, list)):
+            fail = True
+            print("{}: not a dict or a list: data{}".format(
+                expr.group('label'), expr.group('address')))
+            continue
+        try:
+            idx = json.loads(expr.group('val'))
+            if idx in val:
+                fail = True
+                print("{}: {} contains key: {}[{}] = {}".format(
+                    expr.group('label'), type(val).__name__, e, expr.group('val'), val[idx]))
+        except SyntaxError:
+            fail = True
+            print("{}: syntax error on evaluation of value: {}".format(
+                expr.group('label'), expr.group('val')))
+            continue
+
+
+    elif expr.group('type') is None:
+        print("{}: {}".format(expr.group('label'), val))
+
+
+if fail:
+    sys.exit(1)
+sys.exit(0)
diff --git a/test/make-db-version.cc b/test/make-db-version.cc
new file mode 100644 (file)
index 0000000..238584e
--- /dev/null
@@ -0,0 +1,37 @@
+/* Create an empty notmuch database with a specific version and
+ * features. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <xapian.h>
+
+int
+main (int argc, char **argv)
+{
+    if (argc != 4) {
+       fprintf (stderr, "Usage: %s mailpath version features\n", argv[0]);
+       exit (2);
+    }
+
+    std::string nmpath (argv[1]);
+
+    nmpath += "/.notmuch";
+    if (mkdir (nmpath.c_str (), 0777) < 0) {
+       perror (("failed to create " + nmpath).c_str ());
+       exit (1);
+    }
+
+    try {
+       Xapian::WritableDatabase db (
+           nmpath + "/xapian", Xapian::DB_CREATE_OR_OPEN);
+       db.set_metadata ("version", argv[2]);
+       db.set_metadata ("features", argv[3]);
+       db.commit ();
+    } catch (const Xapian::Error &e) {
+       fprintf (stderr, "%s\n", e.get_description ().c_str ());
+       exit (1);
+    }
+}
diff --git a/test/message-id-parse.c b/test/message-id-parse.c
new file mode 100644 (file)
index 0000000..752eb1f
--- /dev/null
@@ -0,0 +1,26 @@
+#include <stdio.h>
+#include <talloc.h>
+#include "notmuch-private.h"
+
+int
+main (unused (int argc), unused (char **argv))
+{
+    char *line = NULL;
+    size_t len = 0;
+    ssize_t nread;
+    void *local = talloc_new (NULL);
+
+    while ((nread = getline (&line, &len, stdin)) != -1) {
+       int last = strlen (line) - 1;
+       if (line[last] == '\n')
+           line[last] = '\0';
+
+       char *mid = _notmuch_message_id_parse_strict (local, line);
+       if (mid)
+           printf ("GOOD: %s\n", mid);
+       else
+           printf ("BAD: %s\n", line);
+    }
+
+    talloc_free (local);
+}
diff --git a/test/notmuch-test b/test/notmuch-test
new file mode 100755 (executable)
index 0000000..5d27e4d
--- /dev/null
@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+
+# Run tests
+#
+# Copyright (c) 2005 Junio C Hamano
+# Copyright (c) 2010 Notmuch Developers
+#
+# Adapted from a Makefile to a shell script by Carl Worth (2010)
+
+if [ ${BASH_VERSINFO[0]} -lt 4 ]; then
+    echo "Error: The notmuch test suite requires a bash version >= 4.0"
+    echo "due to use of associative arrays within the test suite."
+    echo "Please try again with a newer bash (or help us fix the"
+    echo "test suite to be more portable). Thanks."
+    exit 1
+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
+    TESTS="$TESTS $NOTMUCH_SRCDIR/test/$test"
+done
+
+if [ -z "$TESTS" ]; then
+    TESTS="$NOTMUCH_SRCDIR/test/T[0-9][0-9][0-9]-*.sh"
+fi
+
+# Clean up any results from a previous run
+rm -rf $NOTMUCH_BUILDDIR/test/test-results
+
+# Test for timeout utility
+if command -v timeout >/dev/null; then
+    TEST_TIMEOUT=${NOTMUCH_TEST_TIMEOUT:-2m}
+    if [ "$TEST_TIMEOUT" = 0 ]; then
+        TEST_TIMEOUT_CMD=""
+        echo "INFO: timeout disabled"
+    else
+        TEST_TIMEOUT_CMD="timeout $TEST_TIMEOUT"
+        echo "INFO: using $TEST_TIMEOUT timeout for tests"
+    fi
+else
+    TEST_TIMEOUT_CMD=""
+fi
+
+META_FAILURE=
+RES=0
+# Run the tests
+if test -z "${NOTMUCH_TEST_SERIALIZE-}" && command -v parallel >/dev/null ; then
+    test -t 1 && export COLORS_WITHOUT_TTY=t || :
+    if parallel --minversion 0 >/dev/null 2>&1 ; then
+        echo "INFO: running tests with GNU parallel"
+        printf '%s\n' $TESTS | $TEST_TIMEOUT_CMD parallel || RES=$?
+    else
+        echo "INFO: running tests with moreutils parallel"
+        $TEST_TIMEOUT_CMD parallel -- $TESTS || RES=$?
+    fi
+    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 $! && 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
+
+# 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 $RESULT_FILES && ev=0 || ev=$?
+
+if [ -n "$META_FAILURE" ]; then
+    printf 'ERROR: %s\n' "$META_FAILURE"
+    if [ $ev = 0 ]; then
+        ev=$RES
+    fi
+fi
+
+# Clean up
+rm -rf $TEST_DIRECTORY/test-results
+
+exit $ev
diff --git a/test/notmuch-test.h b/test/notmuch-test.h
new file mode 100644 (file)
index 0000000..ed71309
--- /dev/null
@@ -0,0 +1,50 @@
+#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
+expect0 (int line, notmuch_status_t ret)
+{
+    if (ret) {
+       fprintf (stderr, "line %d: %d\n", line, ret);
+       exit (1);
+    }
+}
+
+#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
diff --git a/test/parse-time.c b/test/parse-time.c
new file mode 100644 (file)
index 0000000..6aac89b
--- /dev/null
@@ -0,0 +1,314 @@
+/*
+ * parse time string - user friendly date and time parser
+ * Copyright © 2012 Jani Nikula
+ *
+ * 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/>.
+ *
+ * Author: Jani Nikula <jani@nikula.org>
+ */
+
+#include <assert.h>
+#include <ctype.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "parse-time-string.h"
+
+#define ARRAY_SIZE(a) (sizeof (a) / sizeof (a[0]))
+
+static const char *parse_time_error_strings[] = {
+    [PARSE_TIME_OK] = "OK",
+    [PARSE_TIME_ERR] = "ERR",
+    [PARSE_TIME_ERR_LIB] = "LIB",
+    [PARSE_TIME_ERR_ALREADYSET] = "ALREADYSET",
+    [PARSE_TIME_ERR_FORMAT] = "FORMAT",
+    [PARSE_TIME_ERR_DATEFORMAT] = "DATEFORMAT",
+    [PARSE_TIME_ERR_TIMEFORMAT] = "TIMEFORMAT",
+    [PARSE_TIME_ERR_INVALIDDATE] = "INVALIDDATE",
+    [PARSE_TIME_ERR_INVALIDTIME] = "INVALIDTIME",
+    [PARSE_TIME_ERR_KEYWORD] = "KEYWORD",
+};
+
+static const char *
+parse_time_strerror (unsigned int errnum)
+{
+    if (errnum < ARRAY_SIZE (parse_time_error_strings))
+       return parse_time_error_strings[errnum];
+    else
+       return NULL;
+}
+
+/*
+ * concat argv[start]...argv[end - 1], separating them by a single
+ * space, to a malloced string
+ */
+static char *
+concat_args (int start, int end, char *argv[])
+{
+    int i;
+    size_t len = 1;
+    char *p;
+
+    for (i = start; i < end; i++)
+       len += strlen (argv[i]) + 1;
+
+    p = malloc (len);
+    if (! p)
+       return NULL;
+
+    *p = 0;
+
+    for (i = start; i < end; i++) {
+       if (i != start)
+           strcat (p, " ");
+       strcat (p, argv[i]);
+    }
+
+    return p;
+}
+
+#define DEFAULT_FORMAT "%a %b %d %T %z %Y"
+
+static void
+usage (const char *name)
+{
+    printf ("Usage: %s [options ...] [<date/time>]\n\n", name);
+    printf (
+       "Parse <date/time> and display it in given format. If <date/time> is\n"
+       "not given, parse each line in stdin according to:\n\n"
+       "  <date/time> [(==>|==_>|==^>|==^^>)<ignored>] [#<comment>]\n\n"
+       "and produce output:\n\n"
+       "  <date/time> (==>|==_>|==^>|==^^>) <time in --format=FMT> [#<comment>]\n\n"
+       "preserving whitespace and comment in input. The operators ==>, ==_>,\n"
+       "==^>, and ==^^> define rounding as no rounding, round down, round up\n"
+       "inclusive, and round up, respectively.\n\n"
+
+       "  -f, --format=FMT output format, FMT according to strftime(3)\n"
+       "                   (default: \"%s\")\n"
+       "  -r, --ref=N      use N seconds since epoch as reference time\n"
+       "                   (default: now)\n"
+       "  -u, --^          round result up inclusive (default: no rounding)\n"
+       "  -U, --^^         round result up (default: no rounding)\n"
+       "  -d, --_          round result down (default: no rounding)\n"
+       "  -h, --help       print this help\n",
+       DEFAULT_FORMAT);
+}
+
+struct {
+    const char *operator;
+    int round;
+} operators[] = {
+    { "==>",    PARSE_TIME_NO_ROUND },
+    { "==_>",   PARSE_TIME_ROUND_DOWN },
+    { "==^>",   PARSE_TIME_ROUND_UP_INCLUSIVE },
+    { "==^^>",  PARSE_TIME_ROUND_UP },
+};
+
+static const char *
+find_operator_in_string (char *str, char **ptr, int *round)
+{
+    const char *oper = NULL;
+    unsigned int i;
+
+    for (i = 0; i < ARRAY_SIZE (operators); i++) {
+       char *p = strstr (str, operators[i].operator);
+       if (p) {
+           if (round)
+               *round = operators[i].round;
+           if (ptr)
+               *ptr = p;
+
+           oper = operators[i].operator;
+           break;
+       }
+    }
+
+    return oper;
+}
+
+static const char *
+get_operator (int round)
+{
+    const char *oper = NULL;
+    unsigned int i;
+
+    for (i = 0; i < ARRAY_SIZE (operators); i++) {
+       if (round == operators[i].round) {
+           oper = operators[i].operator;
+           break;
+       }
+    }
+
+    return oper;
+}
+
+static int
+parse_stdin (FILE *infile, time_t *ref, int round, const char *format)
+{
+    char *input = NULL;
+    char result[1024];
+    size_t inputsize;
+    ssize_t len;
+    struct tm tm;
+    time_t t;
+    int r;
+
+    while ((len = getline (&input, &inputsize, infile)) != -1) {
+       const char *oper;
+       char *trail, *tmp;
+
+       /* trail is trailing whitespace and (optional) comment */
+       trail = strchr (input, '#');
+       if (! trail)
+           trail = input + len;
+
+       while (trail > input && isspace ((unsigned char) *(trail - 1)))
+           trail--;
+
+       if (trail == input) {
+           printf ("%s", input);
+           continue;
+       }
+
+       tmp = strdup (trail);
+       if (! tmp) {
+           fprintf (stderr, "strdup() failed\n");
+           continue;
+       }
+       *trail = '\0';
+       trail = tmp;
+
+       /* operator */
+       oper = find_operator_in_string (input, &tmp, &round);
+       if (oper) {
+           *tmp = '\0';
+       } else {
+           oper = get_operator (round);
+           assert (oper);
+       }
+
+       r = parse_time_string (input, &t, ref, round);
+       if (! r) {
+           if (! localtime_r (&t, &tm)) {
+               fprintf (stderr, "localtime_r() failed\n");
+               free (trail);
+               continue;
+           }
+
+           strftime (result, sizeof (result), format, &tm);
+       } else {
+           const char *errstr = parse_time_strerror (r);
+           if (errstr)
+               snprintf (result, sizeof (result), "ERROR: %s", errstr);
+           else
+               snprintf (result, sizeof (result), "ERROR: %d", r);
+       }
+
+       printf ("%s%s %s%s", input, oper, result, trail);
+       free (trail);
+    }
+
+    free (input);
+
+    return 0;
+}
+
+int
+main (int argc, char *argv[])
+{
+    int r;
+    struct tm tm;
+    time_t result;
+    time_t now;
+    time_t *nowp = NULL;
+    char *argstr;
+    int round = PARSE_TIME_NO_ROUND;
+    char buf[1024];
+    const char *format = DEFAULT_FORMAT;
+    struct option options[] = {
+       { "help",       no_argument,            NULL,   'h' },
+       { "^",          no_argument,            NULL,   'u' },
+       { "^^",         no_argument,            NULL,   'U' },
+       { "_",          no_argument,            NULL,   'd' },
+       { "format",     required_argument,      NULL,   'f' },
+       { "ref",        required_argument,      NULL,   'r' },
+       { NULL, 0, NULL, 0 },
+    };
+
+    for (;;) {
+       int c;
+
+       c = getopt_long (argc, argv, "huUdf:r:", options, NULL);
+       if (c == -1)
+           break;
+
+       switch (c) {
+       case 'f':
+           /* output format */
+           format = optarg;
+           break;
+       case 'u':
+           round = PARSE_TIME_ROUND_UP_INCLUSIVE;
+           break;
+       case 'U':
+           round = PARSE_TIME_ROUND_UP;
+           break;
+       case 'd':
+           round = PARSE_TIME_ROUND_DOWN;
+           break;
+       case 'r':
+           /* specify now in seconds since epoch */
+           now = (time_t) strtol (optarg, NULL, 10);
+           if (now >= (time_t) 0)
+               nowp = &now;
+           break;
+       case 'h':
+       case '?':
+       default:
+           usage (argv[0]);
+           return 1;
+       }
+    }
+
+    if (optind == argc)
+       return parse_stdin (stdin, nowp, round, format);
+
+    argstr = concat_args (optind, argc, argv);
+    if (! argstr)
+       return 1;
+
+    r = parse_time_string (argstr, &result, nowp, round);
+
+    free (argstr);
+
+    if (r) {
+       const char *errstr = parse_time_strerror (r);
+       if (errstr)
+           fprintf (stderr, "ERROR: %s\n", errstr);
+       else
+           fprintf (stderr, "ERROR: %d\n", r);
+
+       return r;
+    }
+
+    if (! localtime_r (&result, &tm))
+       return 1;
+
+    strftime (buf, sizeof (buf), format, &tm);
+    printf ("%s\n", buf);
+
+    return 0;
+}
diff --git a/test/random-corpus.c b/test/random-corpus.c
new file mode 100644 (file)
index 0000000..8ae0897
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+ * Generate a random corpus of stub messages.
+ *
+ * Initial use case is testing dump and restore, so we only have
+ * message-ids and tags.
+ *
+ * Generated message-id's and tags are intentionally nasty.
+ *
+ * Copyright (c) 2012 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 <stdlib.h>
+#include <assert.h>
+#include <talloc.h>
+#include <string.h>
+#include <glib.h>
+#include <math.h>
+
+#include "notmuch-client.h"
+#include "command-line-arguments.h"
+#include "database-test.h"
+
+/* Current largest Unicode value defined. Note that most of these will
+ * be printed as boxes in most fonts.
+ */
+
+#define GLYPH_MAX 0x10FFFE
+
+
+typedef struct {
+    int weight;
+    int start;
+    int stop;
+} char_class_t;
+
+/*
+ *  Choose about half ascii as test characters, as ascii
+ *  punctuation and whitespace is the main cause of problems for
+ *  the (old) restore parser.
+ *
+ *  We then favour code points with 2 byte encodings. Note that
+ *  code points 0xD800-0xDFFF are forbidden in UTF-8.
+ */
+
+static const
+char_class_t char_class[] = { { 0.50 * GLYPH_MAX, 0x0001, 0x007f },
+                             { 0.75 * GLYPH_MAX, 0x0080, 0x07ff },
+                             { 0.88 * GLYPH_MAX, 0x0800, 0xd7ff },
+                             { 0.90 * GLYPH_MAX, 0xE000, 0xffff },
+                             {        GLYPH_MAX, 0x10000, GLYPH_MAX } };
+
+static gunichar
+random_unichar ()
+{
+    int i;
+    int class = random () % GLYPH_MAX;
+    int size;
+
+    for (i = 0; char_class[i].weight < class; i++) /* nothing */;
+
+    size = char_class[i].stop - char_class[i].start + 1;
+
+    return char_class[i].start + (random () % size);
+}
+
+static char *
+random_utf8_string (void *ctx, size_t char_count)
+{
+    size_t offset = 0;
+    size_t i;
+    gchar *buf = NULL;
+    size_t buf_size = 0;
+
+    for (i = 0; i < char_count; i++) {
+       gunichar randomchar;
+       size_t written;
+
+       /* 6 for one glyph, one for null, one for luck */
+       while (buf_size <= offset + 8) {
+           buf_size = 2 * buf_size + 8;
+           buf = talloc_realloc (ctx, buf, gchar, buf_size);
+       }
+
+       do {
+           randomchar = random_unichar ();
+       } while (randomchar == '\n');
+
+       written = g_unichar_to_utf8 (randomchar, buf + offset);
+
+       if (written <= 0) {
+           fprintf (stderr, "error converting to utf8\n");
+           exit (1);
+       }
+
+       offset += written;
+
+    }
+    buf[offset] = 0;
+    return buf;
+}
+
+/* stubs since we cannot link with notmuch.o */
+const notmuch_opt_desc_t notmuch_shared_options[] = {
+    { }
+};
+
+const char *notmuch_requested_db_uuid = NULL;
+
+void
+notmuch_process_shared_options (unused (notmuch_database_t *notmuch),
+                               unused (const char *dummy))
+{
+}
+
+int
+notmuch_minimal_options (unused (const char *subcommand),
+                        unused (int argc),
+                        unused (char **argv))
+{
+    return 0;
+}
+
+int
+main (int argc, char **argv)
+{
+
+    void *ctx = talloc_new (NULL);
+
+    const char *config_path = NULL;
+    notmuch_database_t *notmuch;
+
+    int num_messages = 500;
+    int max_tags = 10;
+    // leave room for UTF-8 encoding.
+    int tag_len = NOTMUCH_TAG_MAX / 6;
+    // NOTMUCH_MESSAGE_ID_MAX is not exported, so we make a
+    // conservative guess.
+    int message_id_len = (NOTMUCH_TAG_MAX - 20) / 6;
+
+    int seed = 734569;
+
+    notmuch_opt_desc_t options[] = {
+       { .opt_string = &config_path, .name = "config-path" },
+       { .opt_int = &num_messages, .name = "num-messages" },
+       { .opt_int = &max_tags, .name = "max-tags" },
+       { .opt_int = &message_id_len, .name = "message-id-len" },
+       { .opt_int = &tag_len, .name = "tag-len" },
+       { .opt_int = &seed, .name = "seed" },
+       { }
+    };
+
+    int opt_index = parse_arguments (argc, argv, options, 1);
+
+    if (opt_index < 0)
+       exit (1);
+
+    if (message_id_len < 1) {
+       fprintf (stderr, "message id's must be least length 1\n");
+       exit (1);
+    }
+
+    if (config_path == NULL) {
+       fprintf (stderr, "configuration path must be specified");
+       exit (1);
+    }
+
+    if (notmuch_database_open_with_config (NULL,
+                                          NOTMUCH_DATABASE_MODE_READ_WRITE,
+                                          config_path,
+                                          NULL,
+                                          &notmuch,
+                                          NULL))
+       return 1;
+
+    srandom (seed);
+
+    int count;
+
+    for (count = 0; count < num_messages; count++) {
+       int j;
+       /* explicitly allow zero tags */
+       int num_tags = random () % (max_tags + 1);
+       /* message ids should be non-empty */
+       int this_mid_len = (random () % message_id_len) + 1;
+       const char **tag_list;
+       char *mid;
+       notmuch_status_t status;
+
+       do {
+           mid = random_utf8_string (ctx, this_mid_len);
+
+           tag_list = talloc_realloc (ctx, NULL, const char *, num_tags + 1);
+
+           for (j = 0; j < num_tags; j++) {
+               int this_tag_len = random () % tag_len + 1;
+
+               tag_list[j] = random_utf8_string (ctx, this_tag_len);
+           }
+
+           tag_list[j] = NULL;
+
+           status = notmuch_database_add_stub_message (notmuch, mid, tag_list);
+       } while (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID);
+
+       if (status != NOTMUCH_STATUS_SUCCESS) {
+           fprintf (stderr, "error %d adding message", status);
+           exit (status);
+       }
+    }
+
+    notmuch_database_destroy (notmuch);
+
+    talloc_free (ctx);
+
+    return 0;
+}
diff --git a/test/setup.expected-output/config-with-comments b/test/setup.expected-output/config-with-comments
new file mode 100644 (file)
index 0000000..d925ace
--- /dev/null
@@ -0,0 +1,81 @@
+# .notmuch-config - Configuration file for the notmuch mail system
+#
+# For more information about notmuch, see https://notmuchmail.org
+# Database configuration
+#
+# The only value supported here is 'path' which should be the top-level
+# directory where your mail currently exists and to where mail will be
+# delivered in the future. Files should be individual email messages.
+# Notmuch will store its database within a sub-directory of the path
+# configured here named ".notmuch".
+#
+[database]
+path=/path/to/maildir
+# User configuration
+#
+# Here is where you can let notmuch know how you would like to be
+# addressed. Valid settings are
+#
+#      name            Your full name.
+#      primary_email   Your primary email address.
+#      other_email     A list (separated by ';') of other email addresses
+#                      at which you receive email.
+#
+# Notmuch will use the various email addresses configured here when
+# formatting replies. It will avoid including your own addresses in the
+# recipient list of replies, and will set the From address based on the
+# address to which the original email was addressed.
+#
+[user]
+name=Test Suite
+primary_email=test.suite@example.com
+other_email=another.suite@example.com
+# Configuration for "notmuch new"
+#
+# The following options are supported here:
+#
+#      tags    A list (separated by ';') of the tags that will be
+#              added to all messages incorporated by "notmuch new".
+#
+#      ignore  A list (separated by ';') of file and directory names
+#              that will not be searched for messages by "notmuch new".
+#
+#              NOTE: *Every* file/directory that goes by one of those
+#              names will be ignored, independent of its depth/location
+#              in the mail store.
+#
+[new]
+tags=foo;bar;
+# Search configuration
+#
+# The following option is supported here:
+#
+#      exclude_tags
+#              A ;-separated list of tags that will be excluded from
+#              search results by default.  Using an excluded tag in a
+#              query will override that exclusion.
+#
+[search]
+exclude_tags=baz
+# Maildir compatibility configuration
+#
+# The following option is supported here:
+#
+#      synchronize_flags      Valid values are true and false.
+#
+#      If true, then the following maildir flags (in message filenames)
+#      will be synchronized with the corresponding notmuch tags:
+#
+#              Flag    Tag
+#              ----    -------
+#              D       draft
+#              F       flagged
+#              P       passed
+#              R       replied
+#              S       unread (added when 'S' flag is not present)
+#
+#      The "notmuch new" command will notice flag changes in filenames
+#      and update tags, while the "notmuch tag" and "notmuch restore"
+#      commands will notice tag changes and update flags in filenames
+#
+[maildir]
diff --git a/test/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-----
diff --git a/test/smime/README b/test/smime/README
new file mode 100644 (file)
index 0000000..6f27639
--- /dev/null
@@ -0,0 +1,9 @@
+test.crt: self signed certificated
+    % gpgsm --gen-key # needs gpgsm 2.1
+
+key+cert.pem: cert + unencryped private
+    % gpgsm --import test.crt
+    % gpgsm --export-private-key-p12 -out foo.p12  (no passphrase)
+    % openssl pkcs12 -in ns.p12 -clcerts -nodes > key+cert.pem
+
+ca.crt: from https://tools.ietf.org/id/draft-dkg-lamps-samples-01.html#name-certificate-authority-certi
diff --git a/test/smime/bob.p12 b/test/smime/bob.p12
new file mode 100644 (file)
index 0000000..774c77d
--- /dev/null
@@ -0,0 +1,58 @@
+-----BEGIN PKCS12-----
+MIIKWAIBAzCCCh4GCSqGSIb3DQEHAaCCCg8EggoLMIIKBzCCBGcGCSqGSIb3DQEH
+BqCCBFgwggRUAgEAMIIETQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQICE8J
+3kMad9UCAggAgIIEIPvHjK0eRQrnowMUsz1z1x/IxslNvG6DjPZjNHCkNYYmiRsg
+Leu5nqKf4emWVvYpnlh+4Gql7pyJm3G3zSNhobPkW+P1Eh80tTBoUk7TIvvvmtrE
+YEc/nRR1p1MgjISq4Q/CM6ccCCw6YEiQcj/0mSS7gmHUegD5glcWbVuqAT8M/p1z
+98OP3z37G8ARRLNj1yyp0SVlt59Sx3WNbmYBqkQ96iukjMJvmjV7o6BFYUx46Llb
+tphhdRgKXbK2r1R0TUlvE659TUwlrpGlaFpaGj1kLdzVAnjh1ZWnWO2a2BSj0LzG
+qRyiLwqDFPLJLQEckfV+RPWiRrSewME8URNKdk6eewtHdhrehMo4ZJnOIum8qcSz
+giW61SSyZJsFvILpmMYghIxWmPd/8cNIHBrdFEa7z3QKh5jcJNTCxz6yO9f8F830
+d+WDK7DbGkUW4mVTGg/lEYnCFZDF6S1mr0hx+cew1FbKjLpxfQllIIrLf5d2BF8H
+0STpuylQDVVBFdTRHyeS6td5nulANgOProrRzy3aAKQmZ6iullKl+i2t/2TwfVP/
+gG+yszpOEf8U9txuvbiZ7j4XV158zdaaGiduDqMKLOvbdctwHAsR9ecx5C3NTRDl
+ZlttNoXN9zhT4CkWk1w4sFk2KUurjVraIcjWVT7yOreaaK+6N09M0tnLPDJDTrow
+8WwP/rZhA+t+CMrhqkFBxXsyo5VTM0jWJGO/NLpYXPhDPBsRq8rs1OCrUoVr34aR
+cpUTNhyXkvJUarWDHs88lg0ps0G9/1dXI1AbEsQQg8u+QT2ztGYrg2OQxQyi1Mo4
+u/FkAcEbtlYYLmJjj/S2qVRPJgBALVjw9k5hnYRdAXWVDCJ96PMn1SKORvlMxnZ7
+djlhaztOhTLsiDzywVDYWLvQElunWcAGeDZykWNytwcEagc0VjWKHMibc0JOZQ1T
+crGyOzTlt09xHj1NrItYefIwdtKuJfkAh03B5xI6rJ9ZbK9xidcVxyeRX0lEqdo9
+WHQrhHefAmeyo0TlfsN67kFDp5FLpwEtNaN0lyzpkl30aWZdtP5vkvtfmy5ugYIO
+bXoVa+tO6k5V/VfUFUKdaY7xAX7XRzUUg4jB0D0CuaX+YS+GL+5wuQwIY1y2ihBb
+CuCxlcP1lVEU4CVQba60VTudJtWyE7QpPhf+y81f1wRjwIihFvwzpUFWf8JVEppe
+v3Yot3OWGBmhEqLkC9LELth8o5gLfyYHaXTYNd9aRTiI+0ZC5U3O4wUwYLTG3exM
+rIDTzEMk/p4DYIHkNKVUiRJfGYdAwuRxf3IMcYWARTXlSzl1C3hWmZfvTPlKs1bB
+OHTHP/P+qdOFjxOh+fbyqXPJauBAhHvHgrp3iI6t834wJou26oWNihM7OnWuyQRt
+9DVxG4l+1VjtbQZfTDCCBZgGCSqGSIb3DQEHAaCCBYkEggWFMIIFgTCCBX0GCyqG
+SIb3DQEMCgECoIIE7jCCBOowHAYKKoZIhvcNAQwBAzAOBAjqo0x2p5SqLAICCAAE
+ggTIe6Ws+lu0CoNlCXGM2BEPV09wuRHTJe+KnesrmRbXPF9linG3d6G++tTkBHz/
+yr77/DV5aDYciV1pGAbLuX2lMwuqdxzJ4OBPBAjuX5H+IPRaTbxfHYYIwhG8oZzy
+aHyVhHr9j0h7lzW7xSTYJuBNEJ58L42dfzpNRw9dyRPmcuhZqW14Z3xyDm8yjHfB
+2p99y9/A4qSyJJSUM3O3nLdtIar3ktSTRAijgqq+s9wnsfozQRzWpYaqiRrdzwfO
+HqXk54l3/lMSyLpfPl9LW7er6JbGI4jEyQ3x8WijATM5h/lkZKejh/mOaWCvs6G6
+fGzV4P35EsToYbOk9GX4jl4SyDBt3iEHYm5teDUhJmTcR39lAQuAfxN6rOn/TkoO
+YLxtdD5DLiTfYZPCFyavLEsamr8A4p93torF6Rs7GsaHE6PmCcprzqx71KV0DZKv
+tMY86RoiWPKLFxZcYt1yz9/95c1SO1s4i1GvLpJTEgQxLM2OhfEwDNKd2rMJoq1I
+YIRPSP204dIVwwNdXN1vB2slhN2+/QMOqsEkWtTOpW2QoTGSze49hfmJGdu+91jd
+XZBBMJQfY4q066/eE4IOW7ZZId5uMYxDRnGdEQjJsxyW8YHWLRGQvBC8gMkdbj8e
+0wkXbe+jML7vG7t3hDhLEbj5sTquIMTWrTirPw4SxLCuGZAyJHFN3/nCaOSMFlCG
+wEZHrAozgQXPBYU7p+uIkJ4lDc2ZtW8NM8U15gKZLDFfAE6Vg0jAtfFMqvNnX630
+xfo1z4jBd7VXbBFrPzrmvlTnb1XxNFcPycowzW9tgtN4YnNroCq98VpMC914tdpJ
++C/PI0eJ7M2ir3ajN0RabSm02JO9Hdwoa5OgqLwPYDwiFyQvKFGKqAF8Ph6pSEiZ
+10OnH+DVgEY70A+Le+ZSDosMdrhZfHbCcIFitZJ3sYV/7Q118QckW3szcjmLHS5g
+M6Whl2HhjLsAfsmCnoRlIwjx4g0TiuZcb4hGysq8QjD3Z8qqFK28m6OMHbASQfWg
+U+Qg3vmEvVsnBxStFEIImS3QYQoaT0pk6zKUYsI/fOBnEgxsY0XwTfXzVw7hZDct
+yhNIQVWmfgVZwUw0wLoNu3A5hupjUwQzQr4TPnKkFPI8qHmRrJgP8EA0U0019y3W
+MlK0h/LAJEaUBS0goLJCJ8+1EWr6femjnyuU5hMizOm+3j0JexjWz5TQttioS7Q/
+vcxt5pA9yAWQdH9j72saKEoKmDi+kIPr4mimKJz99LhKp9A6Hj0f1P2V3As8JWyW
+ZKmJKW7qMMCFADlALolobqzA60j6Zeo5jiEj/j2lVlUPPz47WO+uKeb+rx+hgTUc
+Xrhq0+an5tvEXt/8wy3PJFqP+qqHGhOIuPLuhqPyzNowuXirIXsiWnI44/X48W91
+HPEoL3xaebQ6oyTP8dI4CCkkHgiLWL5mskjHMEXvcdR6k0ygmu8DGQCPfUweUZqZ
+wfkhD/jwbVpLR5Y3chpatW0cJ2bsAWdxwtuxF05+fVEePUsR0x+2/v/8eDEHKYwt
+aYlAhI48nyrKKVMmqvqcXnzmJlUaq05GnEcglFbv4MUExL7CxClls6QnVNiZFPrV
+ffVsYT2A300xrm4pan89n3nuavjJn7L1JJdmMXwwVQYJKoZIhvcNAQkUMUgeRgBH
+AG4AdQBQAEcAIABlAHgAcABvAHIAdABlAGQAIABjAGUAcgB0AGkAZgBpAGMAYQB0
+AGUAIAA0ADIAYgBiADIANAAwADYwIwYJKoZIhvcNAQkVMRYEFGaI9k+ZdE9/rxBZ
+4rSdH1BCuyQGMDEwITAJBgUrDgMCGgUABBRJfL4XyIHpXmjbziCGCbSAOK9jKgQI
+drOMeIgXcCYCAggA
+-----END PKCS12-----
diff --git a/test/smime/ca.crt b/test/smime/ca.crt
new file mode 100644 (file)
index 0000000..b33d087
--- /dev/null
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDLTCCAhWgAwIBAgIULXcNXGI2bZp38sV7cF6VcQfnKDwwDQYJKoZIhvcNAQEN
+BQAwLTErMCkGA1UEAxMiU2FtcGxlIExBTVBTIENlcnRpZmljYXRlIEF1dGhvcml0
+eTAgFw0xOTExMjAwNjU0MThaGA8yMDUyMDkyNzA2NTQxOFowLTErMCkGA1UEAxMi
+U2FtcGxlIExBTVBTIENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAMUfZ8+NYSh6h36zQcXBo5B6ficAcBJ1f3aLxyN8
+QXB83XuP8aDRWQ9uJvJpQkWVH4zx96/E/zI0t0lDMYtZNqra16h+gxbHJgoq2pRw
+RCOiyYu/p2vzvvZ1dtFTMc/mIigjA/73kokui62j1EFy//fNVIihkVS3rAweq+fI
+8qJHSMhdc2aYa9wOP0eGe/HTiDYgT4L4f2HTGMGGwQgj1vub0gpR4YHmNqr0GyEA
+63mHUQUZpnmN1FEl+nVFA5Ntu4uF++qf/tkTji89/eXYBdKX2yUdTeTIKoCI65IL
+EXxezjTc8aFjf/8E0aWGVZR/DtCsjWOh/s/mV7n/YPyb4+ECAwEAAaNDMEEwDwYD
+VR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBS3Uk1zwIg9
+ssN6WgzzlPf3gKJ32zANBgkqhkiG9w0BAQ0FAAOCAQEALsU91Bmhc6EgCNr7inY2
+2gYPnosJ+kZ1eC0hvHIK9e0Tx74RmhTOe8M2C9YXQKehHpRaX+DLcjup6scoH/bT
+u0THbmzeOy29TTiFcyV9BK+SEKQWW4s98Fwdk9fPWcflHtYvqxjooAV3vHbt6Xmp
+KrKDz/jdg7t0ptI4zSqAf3wNppiJoswlOHBUnH2W1MIYkWQ4jYj5socblVlklHOr
+ykKUiEZAbjU+C1+0FhT4HgLjBB9R4H1H0JRKsggWiZBBJ6UpN0dTN4iD0mDVa0jy
+sJqqWnIViy/xaSDcNaWJmU3o2KmkMkdpinoJ5uLkAHQqXjFaujdU1PkufeA7v3uG
+Rw==
+-----END CERTIFICATE-----
diff --git a/test/smime/key+cert.pem b/test/smime/key+cert.pem
new file mode 100644 (file)
index 0000000..6ee30cf
--- /dev/null
@@ -0,0 +1,56 @@
+Bag Attributes
+    friendlyName: GnuPG exported certificate e0972a47
+    localKeyID: 61 6F 46 CD 73 83 4C 63 84 77 56 AF 0D FB 64 A6 E0 97 2A 47 
+subject=/CN=Notmuch Test Suite
+issuer=/CN=Notmuch Test Suite
+-----BEGIN CERTIFICATE-----
+MIIDCzCCAfOgAwIBAgIIb3SMlL0MZ6kwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UE
+AxMSTm90bXVjaCBUZXN0IFN1aXRlMCAXDTE1MTIxNDAyMDgxMFoYDzIwNjMwNDA1
+MTcwMDAwWjAdMRswGQYDVQQDExJOb3RtdWNoIFRlc3QgU3VpdGUwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7vH1/lkENTAJRbyq2036K7Pw+imSIhB5T
+U0WnAgVGWOemY1Eppi9Dk6rjDxuuUKOCQ5el2wmFZN57Fi/4leBH7x217BnnqWNU
+QV88DxEfV+sk8dSb4a5FOOyfhFJmZso/0lK8x0fBcCNjmRFIjB1afSSXWnCvRpAR
+v+O9trLJuIjbbmXg1gltjuB5yDw8/OLEI7G7YSIop9FxopWJL5rW/o2WEfRPGpYe
+HNRLObCRIvbyDd6XjaCrKBuIrhN7R7mmIa9PUyl8TiY+pCMWs9dHmOsiC73/+P6E
+AhsTOY1bfbGQXBAGZ/FL+SgC5wEcPr2u3+y8y5gw2bpaVhQnu6YLAgMBAAGjTTBL
+MCUGA1UdEQQeMByBGnRlc3Rfc3VpdGVAbm90bXVjaG1haWwub3JnMBEGCisGAQQB
+2kcCAgEEAwEB/zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBb
+XP5OnRVplrEdlnivx3CbCLWO13fcMWXfvKxLGsKFwKuxtpvINFUKM+jDr0kVdQ3d
+u3DJe2hNFQMILK/KrGyN5qEz2YBdHNvdkkvWA+3WHr/tiNr6Rly6QuxBzouxzmRu
+MmnUhsOzZaHT3GmLSVJlwie8KqSfKVGwyBmCyHbUQkMrSEV6QDESN6KyWt85gokB
+56Bc/wVq073xS1nFbfF1M3Z5q5BlLZK4IOerKTQx/oSfR4EX6B7rW2pttWsUCyEj
+LljaA8ehxR9B29m08IGGl43pHEpC1WnOHvsEGs99mPpjWbUgVv5KY7OuS/8iVw6v
+/Yy5Z+JBwlMzTBaUXXl3
+-----END CERTIFICATE-----
+Bag Attributes
+    friendlyName: GnuPG exported certificate e0972a47
+    localKeyID: 61 6F 46 CD 73 83 4C 63 84 77 56 AF 0D FB 64 A6 E0 97 2A 47 
+Key Attributes: <No Attributes>
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7vH1/lkENTAJR
+byq2036K7Pw+imSIhB5TU0WnAgVGWOemY1Eppi9Dk6rjDxuuUKOCQ5el2wmFZN57
+Fi/4leBH7x217BnnqWNUQV88DxEfV+sk8dSb4a5FOOyfhFJmZso/0lK8x0fBcCNj
+mRFIjB1afSSXWnCvRpARv+O9trLJuIjbbmXg1gltjuB5yDw8/OLEI7G7YSIop9Fx
+opWJL5rW/o2WEfRPGpYeHNRLObCRIvbyDd6XjaCrKBuIrhN7R7mmIa9PUyl8TiY+
+pCMWs9dHmOsiC73/+P6EAhsTOY1bfbGQXBAGZ/FL+SgC5wEcPr2u3+y8y5gw2bpa
+VhQnu6YLAgMBAAECggEAVhtHCHz3C01Ahu9RDRgGI1w8+cZqA/9tFVTNTqNrne9r
+GHLXKB4z8W/KYmhsjtAnnri31neXb1prfNMZX5AGlZfD7cwDubCEgYGWV6qldNXT
+YVeV54VkdBV+2k9Lp/Ifc5RZJILWk4+Ge8kaF0dEs1tQrCbsJkhcDfgQUdR5PnGe
+6cKv/8HJo0ep6u5cJloIluit8yF3z4+aHixMQBvQKm/8tug+EsrQZ3IVXbh1hONO
+AZ68z9CrU2pJ/0w/jwwcM5feRfTMC7bZ3vkQb1mQKYFJrvN77TGroUtAZFWqJw7M
+r0f2MShdVjfEdJ1ySnCyKF24cSSPSQsLZUe4UlFyQQKBgQDlqr9ajaUzc6Lyma2e
+Q1IJapbX2OZQtf5tlKVCVtZOlu5r97YMOK96XsQFKtdxhAhrGvvTJwPmwhj+fqfR
+XltNrmUBpHCMsm9nloADvBS83KTP5tw9TMT0VZpt+m5XmvutdyQbSKwy+KMy+GZz
+/XBQCfTEoiDS4grGFftvZuRB4QKBgQDRQvsVFMh2NOnVGqczHJNGjvbDueUJmPUN
+3VxZc/FpBGLRSoN7uxQ4dGNnwyvXHs+pLAAC6xZpFCos9c3R8EPvoMyUehoDSAKW
+CMD4C+K8z7n4ducE5a0NrGIgQvnXtteKr3ZwK8V7cscyTCyjXdrQmQ5XHeue8asR
+758g+dG9awKBgEWuZJho2XKe5xWMIu0dp8pLmLCsklRyo1tD+lACYMs/Z99CLO3Q
+VQ1fq0GWGf/K+3LjoPwTnk9pHIQ6kVgotLMA8oxpA+zsRni7ZOO9MN2MZETf2nqO
+zEMFpfEwRkI2N54Nw9qzVeuxHHLegtc2Udk27BisyCCzjGlFSiAmq6KBAoGAFGfE
+RXjcvT65HX8Gaya+wtugFB8BRx0JX7dI6OLk5ZKLmq0ykH2bQepgnWermmU4we77
+0Dvtfa3u0YjZ/24XXg2YbSpWiWps0Y2/C7AyAAzq12/1OGcX5qk4Tbd0f+QkIset
+qxzmt4XcAKw50J+Vf3DmbYQ1M/BftCZcTm0ShHcCgYEAxp8mjE8iIHxFrm7nHMS0
+2/iWxO8DYaAZ0OLfjaZELHchVvTwa+DynbkwvOc3l4cbNTVaf9O6nmHTkLyBLBNr
+2htPKm1vi9TzNdvGqobFO3ijfvdGvq1rjQl86ns0cf395REmEaVX3zcw2v+GyC5n
+qE6Aa5bvdZ9Yykg6aoFo1mY=
+-----END PRIVATE KEY-----
diff --git a/test/smime/test.crt b/test/smime/test.crt
new file mode 100644 (file)
index 0000000..e5d1e82
--- /dev/null
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDCzCCAfOgAwIBAgIIb3SMlL0MZ6kwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UE
+AxMSTm90bXVjaCBUZXN0IFN1aXRlMCAXDTE1MTIxNDAyMDgxMFoYDzIwNjMwNDA1
+MTcwMDAwWjAdMRswGQYDVQQDExJOb3RtdWNoIFRlc3QgU3VpdGUwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7vH1/lkENTAJRbyq2036K7Pw+imSIhB5T
+U0WnAgVGWOemY1Eppi9Dk6rjDxuuUKOCQ5el2wmFZN57Fi/4leBH7x217BnnqWNU
+QV88DxEfV+sk8dSb4a5FOOyfhFJmZso/0lK8x0fBcCNjmRFIjB1afSSXWnCvRpAR
+v+O9trLJuIjbbmXg1gltjuB5yDw8/OLEI7G7YSIop9FxopWJL5rW/o2WEfRPGpYe
+HNRLObCRIvbyDd6XjaCrKBuIrhN7R7mmIa9PUyl8TiY+pCMWs9dHmOsiC73/+P6E
+AhsTOY1bfbGQXBAGZ/FL+SgC5wEcPr2u3+y8y5gw2bpaVhQnu6YLAgMBAAGjTTBL
+MCUGA1UdEQQeMByBGnRlc3Rfc3VpdGVAbm90bXVjaG1haWwub3JnMBEGCisGAQQB
+2kcCAgEEAwEB/zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBb
+XP5OnRVplrEdlnivx3CbCLWO13fcMWXfvKxLGsKFwKuxtpvINFUKM+jDr0kVdQ3d
+u3DJe2hNFQMILK/KrGyN5qEz2YBdHNvdkkvWA+3WHr/tiNr6Rly6QuxBzouxzmRu
+MmnUhsOzZaHT3GmLSVJlwie8KqSfKVGwyBmCyHbUQkMrSEV6QDESN6KyWt85gokB
+56Bc/wVq073xS1nFbfF1M3Z5q5BlLZK4IOerKTQx/oSfR4EX6B7rW2pttWsUCyEj
+LljaA8ehxR9B29m08IGGl43pHEpC1WnOHvsEGs99mPpjWbUgVv5KY7OuS/8iVw6v
+/Yy5Z+JBwlMzTBaUXXl3
+-----END CERTIFICATE-----
diff --git a/test/smtp-dummy.c b/test/smtp-dummy.c
new file mode 100644 (file)
index 0000000..1c89e9a
--- /dev/null
@@ -0,0 +1,287 @@
+/* smtp-dummy - Dummy SMTP server that delivers mail to the given file
+ *
+ * Copyright © 2010 Carl Worth
+ *
+ * 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/ .
+ *
+ * Authors: Carl Worth <cworth@cworth.org>
+ */
+
+/* This (non-compliant) SMTP server listens on localhost, port 25025
+ * and delivers a mail received to the given filename, (specified as a
+ * command-line argument). It exists after the first client connection
+ * completes.
+ *
+ * It implements very little of the SMTP protocol, even less than
+ * specified as the minimum implementation in the SMTP RFC, (not
+ * implementing RSET, NOOP, nor VRFY). And it doesn't do any
+ * error-checking on the input.
+ *
+ * That is to say, if you use this program, you will very likely find
+ * cases where it doesn't do everything your SMTP client expects. You
+ * have been warned.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netdb.h>
+#include <unistd.h>
+
+#define STRNCMP_LITERAL(var, literal) \
+    strncmp ((var), (literal), sizeof (literal) - 1)
+
+static void
+receive_data_to_file (FILE *peer, FILE *output)
+{
+    char *line = NULL;
+    size_t line_size;
+    ssize_t line_len;
+
+    while ((line_len = getline (&line, &line_size, peer)) != -1) {
+       if (STRNCMP_LITERAL (line, ".\r\n") == 0)
+           break;
+       if (line_len < 2)
+           continue;
+       if (line[line_len - 1] == '\n' && line[line_len - 2] == '\r') {
+           line[line_len - 2] = '\n';
+           line[line_len - 1] = '\0';
+       }
+       fprintf (output, "%s",
+                line[0] == '.' ? line + 1 : line);
+    }
+
+    free (line);
+}
+
+static int
+process_command (FILE *peer, FILE *output, const char *command)
+{
+    if (STRNCMP_LITERAL (command, "EHLO ") == 0) {
+       fprintf (peer, "502 not implemented\r\n");
+       fflush (peer);
+    } else if (STRNCMP_LITERAL (command, "HELO ") == 0) {
+       fprintf (peer, "250 localhost\r\n");
+       fflush (peer);
+    } else if (STRNCMP_LITERAL (command, "MAIL FROM:") == 0 ||
+              STRNCMP_LITERAL (command, "RCPT TO:") == 0) {
+       fprintf (peer, "250 OK\r\n");
+       fflush (peer);
+    } else if (STRNCMP_LITERAL (command, "DATA") == 0) {
+       fprintf (peer, "354 End data with <CR><LF>.<CR><LF>\r\n");
+       fflush (peer);
+       receive_data_to_file (peer, output);
+       fprintf (peer, "250 OK\r\n");
+       fflush (peer);
+    } else if (STRNCMP_LITERAL (command, "QUIT") == 0) {
+       fprintf (peer, "221 BYE\r\n");
+       fflush (peer);
+       return 1;
+    } else {
+       fprintf (stderr, "Unknown command: %s\n", command);
+    }
+    return 0;
+}
+
+static void
+do_smtp_to_file (FILE *peer, FILE *output)
+{
+    char *line = NULL;
+    size_t line_size;
+    ssize_t line_len;
+
+    fprintf (peer, "220 localhost smtp-dummy\r\n");
+    fflush (peer);
+
+    while ((line_len = getline (&line, &line_size, peer)) != -1) {
+       if (process_command (peer, output, line))
+           break;
+    }
+
+    free (line);
+}
+
+int
+main (int argc, char *argv[])
+{
+    const char *progname;
+    char *output_filename;
+    FILE *peer_file = NULL, *output = NULL;
+    int sock = -1, peer, err;
+    struct sockaddr_in addr, peer_addr;
+    struct hostent *hostinfo;
+    socklen_t peer_addr_len;
+    int reuse;
+    int background;
+    int ret = 0;
+    socklen_t addrlen;
+
+    progname = argv[0];
+
+    background = 0;
+    for (; argc >= 2; argc--, argv++) {
+       if (argv[1][0] != '-')
+           break;
+       if (strcmp (argv[1], "--") == 0) {
+           argc--;
+           argv++;
+           break;
+       }
+       if (strcmp (argv[1], "--background") == 0) {
+           background = 1;
+           continue;
+       }
+       fprintf (stderr, "%s: unregognized option '%s'\n",
+                progname, argv[1]);
+       return 1;
+    }
+
+    if (argc != 2) {
+       fprintf (stderr,
+                "Usage: %s [--background] <output-file>\n", progname);
+       return 1;
+    }
+
+    output_filename = argv[1];
+    output = fopen (output_filename, "w");
+    if (output == NULL) {
+       fprintf (stderr, "Failed to open %s for writing: %s\n",
+                output_filename, strerror (errno));
+       ret = 1;
+       goto DONE;
+    }
+
+    sock = socket (AF_INET, SOCK_STREAM, 0);
+    if (sock == -1) {
+       fprintf (stderr, "Error: socket() failed: %s\n",
+                strerror (errno));
+       ret = 1;
+       goto DONE;
+    }
+
+    reuse = 1;
+    err = setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof (reuse));
+    if (err) {
+       fprintf (stderr, "Error: setsockopt() failed: %s\n",
+                strerror (errno));
+       ret = 1;
+       goto DONE;
+    }
+
+    hostinfo = gethostbyname ("localhost");
+    if (hostinfo == NULL) {
+       fprintf (stderr, "Unknown host: localhost\n");
+       ret = 1;
+       goto DONE;
+    }
+
+    memset (&addr, 0, sizeof (addr));
+    addr.sin_family = AF_INET;
+    addr.sin_port = 0;
+    addr.sin_addr = *(struct in_addr *) hostinfo->h_addr;
+    err = bind (sock, (struct sockaddr *) &addr, sizeof (addr));
+    if (err) {
+       fprintf (stderr, "Error: bind() failed: %s\n",
+                strerror (errno));
+       close (sock);
+       ret = 1;
+       goto DONE;
+    }
+
+    addrlen = sizeof (addr);
+    err = getsockname (sock, (struct sockaddr *) &addr, &addrlen);
+    if (err) {
+       fprintf (stderr, "Error: getsockname() failed: %s\n",
+                strerror (errno));
+       close (sock);
+       ret = 1;
+       goto DONE;
+    }
+
+    printf ("smtp_dummy_port='%d'\n", ntohs (addr.sin_port));
+
+    err = listen (sock, 1);
+    if (err) {
+       fprintf (stderr, "Error: listen() failed: %s\n",
+                strerror (errno));
+       close (sock);
+       ret = 1;
+       goto DONE;
+    }
+
+    if (background) {
+       int pid = fork ();
+       if (pid > 0) {
+           printf ("smtp_dummy_pid='%d'\n", pid);
+           fflush (stdout);
+           close (sock);
+           ret = 0;
+           goto DONE;
+       }
+       if (pid < 0) {
+           fprintf (stderr, "Error: fork() failed: %s\n",
+                    strerror (errno));
+           close (sock);
+           ret = 1;
+           goto DONE;
+       }
+       /* Reached if pid == 0 (the child process). */
+       /* Close stdout so that the one interested in pid value will
+        * also get EOF. */
+       close (STDOUT_FILENO);
+       /* dup2() will re-reserve fd of stdout (1) (opportunistically),
+        * in case fd of stderr (2) is open. If that was not open we
+        * don't care fd of stdout (1) either. */
+       dup2 (STDERR_FILENO, STDOUT_FILENO);
+
+       /* This process is now out of reach of shell's job control.
+        * To resolve the rare but possible condition where this
+        * "daemon" is started but never connected this process will
+        * (only) have 30 seconds to exist. */
+       alarm (30);
+    }
+
+    peer_addr_len = sizeof (peer_addr);
+    peer = accept (sock, (struct sockaddr *) &peer_addr, &peer_addr_len);
+    if (peer == -1) {
+       fprintf (stderr, "Error: accept() failed: %s\n",
+                strerror (errno));
+       ret = 1;
+       goto DONE;
+    }
+
+    peer_file = fdopen (peer, "w+");
+    if (peer_file == NULL) {
+       fprintf (stderr, "Error: fdopen() failed: %s\n",
+                strerror (errno));
+       ret = 1;
+       goto DONE;
+    }
+
+    do_smtp_to_file (peer_file, output);
+
+  DONE:
+    if (output)
+       fclose (output);
+    if (peer_file)
+       fclose (peer_file);
+    if (sock >= 0)
+       close (sock);
+
+    return ret;
+}
diff --git a/test/symbol-test.cc b/test/symbol-test.cc
new file mode 100644 (file)
index 0000000..9e956dd
--- /dev/null
@@ -0,0 +1,33 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <xapian.h>
+#include <notmuch.h>
+
+int
+main (int argc, char **argv)
+{
+    notmuch_database_t *notmuch;
+    char *message = NULL;
+
+    if (argc != 3)
+       return 1;
+
+    if (notmuch_database_open_with_config (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY,
+                                          "",
+                                          NULL,
+                                          &notmuch, &message)) {
+       if (message) {
+           fputs (message, stderr);
+           free (message);
+       }
+    }
+
+    try {
+       (void) new Xapian::WritableDatabase (argv[2], Xapian::DB_OPEN);
+    } catch (const Xapian::Error &error) {
+       printf ("caught %s\n", error.get_msg ().c_str ());
+       return 0;
+    }
+
+    return 1;
+}
diff --git a/test/test-lib-FREEBSD.sh b/test/test-lib-FREEBSD.sh
new file mode 100644 (file)
index 0000000..d1840b5
--- /dev/null
@@ -0,0 +1,9 @@
+# If present, use GNU Coreutils instead of a native BSD utils
+if command -v gdate >/dev/null
+   then
+       date () { gdate "$@"; }
+       base64 () { gbase64 "$@"; }
+       wc () { gwc "$@"; }
+       sed () { gsed "$@"; }
+       sha256sum () { gsha256sum "$@"; }
+   fi
diff --git a/test/test-lib-common.sh b/test/test-lib-common.sh
new file mode 100644 (file)
index 0000000..f5d72e1
--- /dev/null
@@ -0,0 +1,357 @@
+#
+# Copyright (c) 2005 Junio C Hamano
+# Copyright (c) 2010 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/ .
+
+# This file contains common code to be used by both the regular
+# (correctness) tests and the performance tests.
+
+# test-lib.sh defines die() which echoes to nonstandard fd where
+# output was redirected earlier in that file. If test-lib.sh is not
+# loaded, neither this redirection nor die() function were defined.
+#
+type die >/dev/null 2>&1 || die () { echo "$@" >&2; exit 1; }
+
+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"
+    cp -pR ${MAIL_DIR}/.notmuch $TMP_DIRECTORY/notmuch-dir-backup."${test_name}"
+}
+
+restore_database () {
+    test_name=$(basename $0 .sh)
+    rm -rf ${MAIL_DIR}/.notmuch
+    cp -pR $TMP_DIRECTORY/notmuch-dir-backup."${test_name}" ${MAIL_DIR}/.notmuch
+}
+
+# Prepend $TEST_DIRECTORY/../lib to LD_LIBRARY_PATH, to make tests work
+# on systems where ../notmuch depends on LD_LIBRARY_PATH.
+LD_LIBRARY_PATH=${TEST_DIRECTORY%/*}/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
+export LD_LIBRARY_PATH
+
+# configure output
+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
+    . "$NOTMUCH_SRCDIR/test/test-lib-$PLATFORM.sh" || exit 1
+fi
+
+# Generate a new message in the mail directory, with a unique message
+# ID and subject. The message is not added to the index.
+#
+# After this function returns, the filename of the generated message
+# is available as $gen_msg_filename and the message ID is available as
+# $gen_msg_id .
+#
+# This function supports named parameters with the bash syntax for
+# assigning a value to an associative array ([name]=value). The
+# supported parameters are:
+#
+#  [dir]=directory/of/choice
+#
+#      Generate the message in directory 'directory/of/choice' within
+#      the mail store. The directory will be created if necessary.
+#
+#  [filename]=name
+#
+#      Store the message in file 'name'. The default is to store it
+#      in 'msg-<count>', where <count> is three-digit number of the
+#      message.
+#
+#  [body]=text
+#
+#      Text to use as the body of the email message
+#
+#  '[from]="Some User <user@example.com>"'
+#  '[to]="Some User <user@example.com>"'
+#  '[subject]="Subject of email message"'
+#  '[date]="RFC 822 Date"'
+#
+#      Values for email headers. If not provided, default values will
+#      be generated instead.
+#
+#  '[cc]="Some User <user@example.com>"'
+#  [reply-to]=some-address
+#  [in-reply-to]=<message-id>
+#  [references]=<message-id>
+#  [content-type]=content-type-specification
+#  '[header]=full header line, including keyword'
+#
+#      Additional values for email headers. If these are not provided
+#      then the relevant headers will simply not appear in the
+#      message.
+#
+#  '[id]=message-id'
+#
+#      Controls the message-id of the created message.
+gen_msg_cnt=0
+gen_msg_filename=""
+gen_msg_id=""
+generate_message () {
+    # This is our (bash-specific) magic for doing named parameters
+    local -A template="($@)"
+    local additional_headers
+
+    gen_msg_cnt=$((gen_msg_cnt + 1))
+    if [ -z "${template[filename]}" ]; then
+       gen_msg_name="msg-$(printf "%03d" $gen_msg_cnt)"
+    else
+       gen_msg_name=${template[filename]}
+    fi
+
+    if [ -z "${template[id]}" ]; then
+       gen_msg_id="${gen_msg_name%:2,*}@notmuch-test-suite"
+    else
+       gen_msg_id="${template[id]}"
+    fi
+
+    if [ -z "${template[dir]}" ]; then
+       gen_msg_filename="${MAIL_DIR}/$gen_msg_name"
+    else
+       gen_msg_filename="${MAIL_DIR}/${template[dir]}/$gen_msg_name"
+       mkdir -p "$(dirname "$gen_msg_filename")"
+    fi
+
+    if [ -z "${template[body]}" ]; then
+       template[body]="This is just a test message (#${gen_msg_cnt})"
+    fi
+
+    if [ -z "${template[from]}" ]; then
+       template[from]="Notmuch Test Suite <test_suite@notmuchmail.org>"
+    fi
+
+    if [ -z "${template[to]}" ]; then
+       template[to]="Notmuch Test Suite <test_suite@notmuchmail.org>"
+    fi
+
+    if [ -z "${template[subject]}" ]; then
+       if [ -n "$test_subtest_name" ]; then
+           template[subject]="$test_subtest_name"
+       else
+           template[subject]="Test message #${gen_msg_cnt}"
+       fi
+    elif [ "${template[subject]}" = "@FORCE_EMPTY" ]; then
+       template[subject]=""
+    fi
+
+    if [ -z "${template[date]}" ]; then
+       # we use decreasing timestamps here for historical reasons;
+       # the existing test suite when we converted to unique timestamps just
+       # happened to have signicantly fewer failures with that choice.
+       local date_secs=$((978709437 - gen_msg_cnt))
+       # printf %(..)T is bash 4.2+ feature. use perl fallback if needed...
+       TZ=UTC printf -v template[date] "%(%a, %d %b %Y %T %z)T" $date_secs 2>/dev/null ||
+           template[date]=`perl -le 'use POSIX "strftime";
+                               @time = gmtime '"$date_secs"';
+                               print strftime "%a, %d %b %Y %T +0000", @time'`
+    fi
+
+    additional_headers=""
+    if [ ! -z "${template[header]}" ]; then
+       additional_headers="${template[header]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[reply-to]}" ]; then
+       additional_headers="Reply-To: ${template[reply-to]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[in-reply-to]}" ]; then
+       additional_headers="In-Reply-To: ${template[in-reply-to]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[cc]}" ]; then
+       additional_headers="Cc: ${template[cc]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[bcc]}" ]; then
+       additional_headers="Bcc: ${template[bcc]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[references]}" ]; then
+       additional_headers="References: ${template[references]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[content-type]}" ]; then
+       additional_headers="Content-Type: ${template[content-type]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[content-transfer-encoding]}" ]; then
+       additional_headers="Content-Transfer-Encoding: ${template[content-transfer-encoding]}
+${additional_headers}"
+    fi
+
+    # Note that in the way we're setting it above and using it below,
+    # `additional_headers' will also serve as the header / body separator
+    # (empty line in between).
+
+    cat <<EOF >"$gen_msg_filename"
+From: ${template[from]}
+To: ${template[to]}
+Message-Id: <${gen_msg_id}>
+Subject: ${template[subject]}
+Date: ${template[date]}
+${additional_headers}
+${template[body]}
+EOF
+}
+
+# Generate a new message and add it to the database.
+#
+# All of the arguments and return values supported by generate_message
+# are also supported here, so see that function for details.
+add_message () {
+    generate_message "$@" &&
+    notmuch new > /dev/null
+}
+
+if test -n "$valgrind"
+then
+       make_symlink () {
+               test -h "$2" &&
+               test "$1" = "$(readlink "$2")" || {
+                       # be super paranoid
+                       if mkdir "$2".lock
+                       then
+                               rm -f "$2" &&
+                               ln -s "$1" "$2" &&
+                               rm -r "$2".lock
+                       else
+                               while test -d "$2".lock
+                               do
+                                       say "Waiting for lock on $2."
+                                       sleep 1
+                               done
+                       fi
+               }
+       }
+
+       make_valgrind_symlink () {
+               # handle only executables
+               test -x "$1" || return
+
+               base=$(basename "$1")
+               symlink_target=$TEST_DIRECTORY/../$base
+               # do not override scripts
+               if test -x "$symlink_target" &&
+                   test ! -d "$symlink_target" &&
+                   test "#!" != "$(head -c 2 < "$symlink_target")"
+               then
+                       symlink_target=$TEST_DIRECTORY/valgrind.sh
+               fi
+               case "$base" in
+               *.sh|*.perl)
+                       symlink_target=$TEST_DIRECTORY/unprocessed-script
+               esac
+               # create the link, or replace it if it is out of date
+               make_symlink "$symlink_target" "$GIT_VALGRIND/bin/$base" || exit
+       }
+
+       # override notmuch executable in TEST_DIRECTORY/..
+       GIT_VALGRIND=$TEST_DIRECTORY/valgrind
+       mkdir -p "$GIT_VALGRIND"/bin
+       make_valgrind_symlink $TEST_DIRECTORY/../notmuch
+       OLDIFS=$IFS
+       IFS=:
+       for path in $PATH
+       do
+               ls "$path"/notmuch 2> /dev/null |
+               while read file
+               do
+                       make_valgrind_symlink "$file"
+               done
+       done
+       IFS=$OLDIFS
+       PATH=$GIT_VALGRIND/bin:$PATH
+       GIT_EXEC_PATH=$GIT_VALGRIND/bin
+       export GIT_VALGRIND
+       test -n "$NOTMUCH_BUILDDIR" && MANPATH="$NOTMUCH_BUILDDIR/doc/_build/man"
+else # normal case
+       if test -n "$NOTMUCH_BUILDDIR"
+               then
+                       PATH="$NOTMUCH_BUILDDIR:$PATH"
+                       MANPATH="$NOTMUCH_BUILDDIR/doc/_build/man"
+               fi
+fi
+export PATH MANPATH
+
+# Test repository
+test="tmp.$(basename "$0" .sh)"
+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
+       echo >&6 "FATAL: Cannot prepare test area"
+       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"
+# - crypto/emacs_deliver_message
+export HOME="${TMP_DIRECTORY}/home"
+mkdir -p "${HOME}"
+
+MAIL_DIR="${TMP_DIRECTORY}/mail"
+export NOTMUCH_CONFIG="${TMP_DIRECTORY}/notmuch-config"
+
+mkdir -p "${MAIL_DIR}"
+
+cat <<EOF >"${NOTMUCH_CONFIG}"
+[database]
+path=${MAIL_DIR}
+
+[user]
+name=Notmuch Test Suite
+primary_email=test_suite@notmuchmail.org
+other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org
+EOF
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
diff --git a/test/test-lib.el b/test/test-lib.el
new file mode 100644 (file)
index 0000000..4cfb8ef
--- /dev/null
@@ -0,0 +1,221 @@
+;;; test-lib.el --- auxiliary stuff for Notmuch Emacs tests
+;;
+;; Copyright © Carl Worth
+;; Copyright © David Edmondson
+;;
+;; This file is part of Notmuch test suit.
+;;
+;; Notmuch is free software: you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; Notmuch is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
+;;
+;; Authors: Dmitry Kurochkin <dmitry.kurochkin@gmail.com>
+
+;;; 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
+;; are defined by the time that we let-bind them.  This is needed
+;; because starting with Emacs 27 undeclared variables in evaluated
+;; interactive code (such as our tests) use lexical scope.
+(require 'smtpmail)
+
+;; `read-file-name' by default uses `completing-read' function to read
+;; user input.  It does not respect `standard-input' variable which we
+;; use in tests to provide user input.  So replace it with a plain
+;; `read' call.
+(setq read-file-name-function (lambda (&rest _) (read)))
+
+(defun notmuch-test-wait ()
+  "Wait for process completion."
+  (while (get-buffer-process (current-buffer))
+    (accept-process-output nil 0.1)))
+
+(defun test-output (&optional filename)
+  "Save current buffer to file FILENAME.  Default FILENAME is OUTPUT."
+  (notmuch-post-command)
+  (write-region (point-min) (point-max) (or filename "OUTPUT")))
+
+(defun test-visible-output (&optional filename)
+  "Save visible text in current buffer to file FILENAME.  Default
+FILENAME is OUTPUT."
+  (notmuch-post-command)
+  (let ((text (visible-buffer-string))
+       ;; Tests expect output in UTF-8 encoding
+       (coding-system-for-write 'utf-8))
+    (with-temp-file (or filename "OUTPUT") (insert text))))
+
+(defun visible-buffer-string ()
+  "Same as `buffer-string', but excludes invisible text and
+removes any text properties."
+  (visible-buffer-substring (point-min) (point-max)))
+
+(defun visible-buffer-substring (start end)
+  "Same as `buffer-substring-no-properties', but excludes
+invisible text."
+  (let (str)
+    (while (< start end)
+      (let ((next-pos (next-char-property-change start end)))
+       (unless (invisible-p start)
+         (setq str (concat str (buffer-substring-no-properties
+                                start next-pos))))
+       (setq start next-pos)))
+    str))
+
+;; process-attributes is not defined everywhere, so define an
+;; alternate way to test if a process still exists.
+
+(defun test-process-running (pid)
+  (= 0
+   (signal-process pid 0)))
+
+(defun orphan-watchdog-check (pid)
+  "Periodically check that the process with id PID is still
+running, quit if it terminated."
+  (unless (test-process-running pid)
+    (kill-emacs)))
+
+(defun orphan-watchdog (pid)
+  "Initiate orphan watchdog check."
+  (run-at-time 60 60 'orphan-watchdog-check pid))
+
+(defvar notmuch-hello-mode-hook-counter -100
+  "Tests that care about this counter must let-bind it to 0.")
+(add-hook 'notmuch-hello-mode-hook
+         (lambda () (cl-incf notmuch-hello-mode-hook-counter)))
+
+(defvar notmuch-hello-refresh-hook-counter -100
+  "Tests that care about this counter must let-bind it to 0.")
+(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
+  (jit-lock-fontify-now)
+  (save-excursion
+    (let ((inhibit-read-only t))
+      (goto-char (point-min))
+      (let ((button))
+       (while (setq button (next-button (point)))
+         (goto-char (button-start button))
+         (insert "<<")
+         (goto-char (button-end button))
+         (insert ">>"))))))
+
+(defmacro notmuch-test-run (&rest body)
+  "Evaluate a BODY of test expressions and output the result."
+  `(with-temp-buffer
+     (let ((buffer (current-buffer))
+          (result (progn ,@body)))
+       (switch-to-buffer buffer)
+       (insert (if (stringp result)
+                  result
+                (prin1-to-string result)))
+       (test-output))))
+
+(defun notmuch-test-report-unexpected (output expected)
+  "Report that the OUTPUT does not match the EXPECTED result."
+  (concat "Expect:\t" (prin1-to-string expected) "\n"
+         "Output:\t" (prin1-to-string output) "\n"))
+
+(defun notmuch-test-expect-equal (output expected)
+  "Compare OUTPUT with EXPECTED. Report any discrepancies."
+  (cond
+   ((equal output expected)
+    t)
+   ((and (listp output)
+        (listp expected))
+    ;; Reporting the difference between two lists is done by
+    ;; reporting differing elements of OUTPUT and EXPECTED
+    ;; pairwise. This is expected to make analysis of failures
+    ;; simpler.
+    (apply #'concat (cl-loop for o in output
+                            for e in expected
+                            if (not (equal o e))
+                            collect (notmuch-test-report-unexpected o e))))
+   (t
+    (notmuch-test-report-unexpected output expected))))
+
+(defun notmuch-post-command ()
+  (run-hooks 'post-command-hook))
+
+(defmacro notmuch-test-progn (&rest body)
+  (cons 'progn
+       (mapcar
+        (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
+      '((".*" nil)))
+
+;; Also for historical reasons, we set the fcc handler to file not
+;; insert.
+
+(setq notmuch-maildir-use-notmuch-insert nil)
+
+;; force a common html renderer, to avoid test variations between
+;; environments
+
+(setq mm-text-html-renderer 'html2text)
+
+;; Set our own default for message-hidden-headers, to avoid tests
+;; breaking when the Emacs default changes.
+(setq message-hidden-headers
+      '("^References:" "^Face:" "^X-Face:" "^X-Draft-From:"))
diff --git a/test/test-lib.sh b/test/test-lib.sh
new file mode 100644 (file)
index 0000000..059e110
--- /dev/null
@@ -0,0 +1,1102 @@
+#
+# Copyright (c) 2005 Junio C Hamano
+# Copyright (c) 2010 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/ .
+
+if [ ${BASH_VERSINFO[0]} -lt 4 ]; then
+    echo "Error: The notmuch test suite requires a bash version >= 4.0"
+    echo "due to use of associative arrays within the test suite."
+    echo "Please try again with a newer bash (or help us fix the"
+    echo "test suite to be more portable). Thanks."
+    exit 1
+fi
+
+# Make sure echo builtin does not expand backslash-escape sequences by default.
+shopt -u xpg_echo
+
+# Ensure NOTMUCH_SRCDIR and NOTMUCH_BUILDDIR are set.
+. $(dirname "$0")/export-dirs.sh || exit 1
+
+# 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
+
+this_test=${0##*/}
+this_test=${this_test%.sh}
+this_test_bare=${this_test#T[0-9][0-9][0-9]-}
+
+# if --tee was passed, write the output not only to the terminal, but
+# additionally to the file test-results/$BASENAME.out, too.
+case "$GIT_TEST_TEE_STARTED, $* " in
+done,*)
+       # do not redirect again
+       ;;
+*' --tee '*|*' --va'*)
+       mkdir -p test-results
+       BASE=test-results/$this_test
+       (GIT_TEST_TEE_STARTED=done "$BASH" "$0" "$@" 2>&1;
+        echo $? > $BASE.exit) | tee $BASE.out
+       test "$(cat $BASE.exit)" = 0
+       exit
+       ;;
+esac
+
+# STDIN from /dev/null. EOF for readers (and ENOTTY for tty related ioctls).
+exec </dev/null
+
+# Save STDOUT to fd 6 and STDERR to fd 7.
+exec 6>&1 7>&2
+# Make xtrace debugging (when used) use redirected STDERR, with verbose lead:
+BASH_XTRACEFD=7
+export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
+
+. "$NOTMUCH_SRCDIR/test/test-vars.sh" || exit 1
+
+add_gnupg_home () {
+    [ -e "${GNUPGHOME}/gpg.conf" ] && return
+    _gnupg_exit () { gpgconf --kill all 2>/dev/null || true; }
+    at_exit_function _gnupg_exit
+    mkdir -p -m 0700 "$GNUPGHOME"
+    gpg --no-tty --import <$NOTMUCH_SRCDIR/test/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
+    elif (gpg --debug-quick-random --version >/dev/null 2>&1) ; then
+       echo debug-quick-random >> "$GNUPGHOME"/gpg.conf
+    fi
+    echo no-emit-version >> "$GNUPGHOME"/gpg.conf
+
+    # Change this if we ship a new test key
+    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 () {
+    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"
+    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"
+    printf '%s::1\n' include-certs disable-crl-checks | gpgconf --output /dev/null --change-options gpgsm
+    gpgsm --batch --no-tty --no-common-certs-import --pinentry-mode=loopback --passphrase-fd 3 \
+             --disable-dirmngr --import "$NOTMUCH_SRCDIR/test/smime/bob.p12" >>"$GNUPGHOME"/import.log 2>&1 3<<<''
+    test_debug "cat $GNUPGHOME/import.log"
+}
+
+# Each test should start with something like this, after copyright notices:
+#
+# test_description='Description of this test...
+# This test checks if command xyzzy does the right thing...
+# '
+# . ./test-lib.sh || exit 1
+
+color=maybe
+
+while test "$#" -ne 0
+do
+       case "$1" in
+       -d|--debug)
+               debug=t; shift ;;
+       -i|--immediate)
+               immediate=t; shift ;;
+       -h|--help)
+               help=t; shift ;;
+       -v|--verbose)
+               verbose=t; shift ;;
+       -q|--quiet)
+               quiet=t; shift ;;
+       --with-dashes)
+               with_dashes=t; shift ;;
+       --no-color)
+               color=; shift ;;
+       --no-python)
+               # noop now...
+               shift ;;
+       --valgrind)
+               valgrind=t; verbose=t; shift ;;
+       --tee)
+               shift ;; # was handled already
+       *)
+               echo "error: unknown test option '$1'" >&2; exit 1 ;;
+       esac
+done
+
+if test -n "$debug"; then
+       fmt_subtest () {
+               printf -v $1 " %-4s" "[$((test_count - 1))]"
+       }
+else
+       fmt_subtest () {
+               printf -v $1 ''
+       }
+fi
+
+test -n "$COLORS_WITHOUT_TTY" || [ -t 1 ] || color=
+
+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
+       # _tput run in subshell (``) only
+       _tput () { exec tput -T "$ORIGINAL_TERM" "$@"; }
+       unset BOLD RED GREEN BROWN SGR0
+       say_color () {
+               case "$1" in
+                       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
+               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
+               f=$2
+               shift 2
+               fmt_subtest st
+               printf " ${f}${st}" "$@"
+       }
+fi
+
+error () {
+       say_color error "error: $*\n"
+       GIT_EXIT_OK=t
+       exit 1
+}
+
+say () {
+       say_color info "$*"
+}
+
+test "${test_description}" != "" ||
+error "Test script did not set test_description."
+
+if test "$help" = "t"
+then
+       echo "Tests ${test_description}"
+       exit 0
+fi
+
+test_description_printed=
+print_test_description () {
+       test -z "$test_description_printed" || return 0
+       echo
+       echo $this_test: "Testing ${test_description}"
+       test_description_printed=1
+}
+if [ -z "$NOTMUCH_TEST_QUIET" ]
+then
+       print_test_description
+fi
+
+test_failure=0
+test_count=0
+test_fixed=0
+test_broken=0
+test_success=0
+
+declare -a _exit_functions=()
+
+at_exit_function () {
+       _exit_functions=($1 ${_exit_functions[@]/$1})
+}
+
+rm_exit_function () {
+       _exit_functions=(${_exit_functions[@]/$1})
+}
+
+_exit_common () {
+       code=$?
+       trap - EXIT
+       set +ex
+       for _fn in ${_exit_functions[@]}; do $_fn; done
+       rm -rf "$TEST_TMPDIR"
+}
+
+trap_exit () {
+       _exit_common
+       if test -n "$GIT_EXIT_OK"
+       then
+               exit $code
+       else
+               exec >&6
+               say_color error '%-6s' FATAL
+               echo " $test_subtest_name"
+               echo
+               echo "Unexpected exit while executing $0. Exit code $code."
+               exit 1
+       fi
+}
+
+trap_signal () {
+       _exit_common
+       echo >&6 "FATAL: $0: interrupted by signal" $((code - 128))
+       exit $code
+}
+
+die () {
+       _exit_common
+       exec >&6
+       say_color error '%-6s' FATAL
+       echo " $*"
+       echo
+       echo "Unexpected exit while executing $0."
+       exit 1
+}
+
+trap 'trap_exit' EXIT
+trap 'trap_signal' HUP INT TERM
+
+# Add an existing, fixed corpus of email to the database.
+#
+# $1 is the corpus dir under corpora to add, using "default" if unset.
+#
+# The default corpus is based on about 50 messages from early in the
+# 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 () {
+    local corpus
+    corpus=${1:-default}
+
+    rm -rf ${MAIL_DIR}
+    cp -a $NOTMUCH_SRCDIR/test/corpora/$corpus ${MAIL_DIR}
+    notmuch new >/dev/null || die "'notmuch new' failed while adding email corpus"
+}
+
+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]}"
+    fi
+    test_subtest_name="$1"
+    test_reset_state_
+    # Redirect test output to the previously prepared file descriptors
+    # 3 and 4 (see below)
+    if test "$verbose" != "t"; then exec 4>test.output 3>&4; fi
+    exec >&3 2>&4
+    inside_subtest=t
+}
+
+# Pass test if two arguments match
+#
+# Note: Unlike all other test_expect_* functions, this function does
+# 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 () {
+       local output expected testname
+       exec 1>&6 2>&7          # Restore stdout and stderr
+       if [ -z "$inside_subtest" ]; then
+               error "bug in the test script: test_expect_equal without test_begin_subtest"
+       fi
+       inside_subtest=
+       test "$#" = 2 ||
+       error "bug in the test script: not 2 parameters to test_expect_equal"
+
+       output="$1"
+       expected="$2"
+       if ! test_skip "$test_subtest_name"
+       then
+               if [ "$output" = "$expected" ]; then
+                       test_ok_
+               else
+                       testname=$this_test.$test_count
+                       echo "$expected" > $testname.expected
+                       echo "$output" > $testname.output
+                       test_failure_ "$(diff -u $testname.expected $testname.output)"
+               fi
+    fi
+}
+
+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
+    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"
+
+    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
+# canonicalized before diff'ing.  If an argument cannot be parsed, it
+# is used unchanged so that there's something to diff against.
+test_expect_equal_json () {
+    local script output expected
+    # The test suite forces LC_ALL=C, but this causes Python 3 to
+    # decode stdin as ASCII.  We need to read JSON in UTF-8, so
+    # override Python's stdio encoding defaults.
+    script='import json, sys; json.dump(json.load(sys.stdin), sys.stdout, sort_keys=True, indent=4)'
+    output=$(echo "$1" | PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -c "$script" \
+       || echo "$1")
+    expected=$(echo "$2" | PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -c "$script" \
+       || echo "$2")
+    shift 2
+    test_expect_equal "$output" "$expected" "$@"
+}
+
+# Ensure that the argument is valid JSON data.
+test_valid_json () {
+    PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -c "import sys, json; json.load(sys.stdin)" <<<"$1"
+    test_expect_equal "$?" 0
+}
+
+# Sort the top-level list of JSON data from stdin.
+test_sort_json () {
+    PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -c \
+       "import sys, json; json.dump(sorted(json.load(sys.stdin)),sys.stdout)"
+}
+
+# test for json objects:
+# read the source of test/json_check_nodes.py (or the output when
+# invoking it without arguments) for an explanation of the syntax.
+test_json_nodes () {
+       local output
+       exec 1>&6 2>&7          # Restore stdout and stderr
+       if [ -z "$inside_subtest" ]; then
+               error "bug in the test script: test_json_eval without test_begin_subtest"
+       fi
+       inside_subtest=
+       test "$#" > 0 ||
+           error "bug in the test script: test_json_nodes needs at least 1 parameter"
+
+       if ! test_skip "$test_subtest_name"
+       then
+           output=$(PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -B "$NOTMUCH_SRCDIR"/test/json_check_nodes.py "$@")
+               if [ "$?" = 0 ]
+               then
+                       test_ok_
+               else
+                       test_failure_ "$output"
+               fi
+       fi
+}
+
+NOTMUCH_NEW () {
+    notmuch new "${@}" | grep -v -E -e '^Processed [0-9]*( total)? file|Found [0-9]* total file'
+}
+
+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_PYTHON -c '
+import email, sys
+msg = email.message_from_file(sys.stdin)
+for hdr in sys.argv[1:]: del msg[hdr]
+print(msg.as_string(False))
+' "$@"
+}
+
+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 () {
+    notmuch_debug_sanitize | perl -pe 's/("?thread"?: ?)("?)................("?)/\1\2XXX\3/'
+}
+
+notmuch_search_files_sanitize () {
+    notmuch_dir_sanitize |  sed 's/msg-[0-9][0-9][0-9]/msg-XXX/'
+}
+
+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 () {
+    sed -e "$NOTMUCH_SHOW_FILENAME_SQUELCH"
+}
+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 () {
+    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_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 ==="
+       notmuch_debug_sanitize < "$file"
+    done | sed \
+       -e '/^$/d' \
+       -e '/^\[.*\]$/d' \
+       -e "s|^\(command: \)\{0,1\}/.*/$command|\1YYY/$command|"
+}
+
+notmuch_date_sanitize () {
+    sed \
+       -e 's/^Date: Fri, 05 Jan 2001 .*0000/Date: GENERATED_DATE/'
+}
+
+# remove redundant parts of notmuch-git internal paths
+notmuch_git_sanitize () {
+    sed  -e 's,tags/\([0-9a-f]\{2\}/\)\{2\},,' -e '/FORMAT/d'
+}
+notmuch_uuid_sanitize () {
+    sed 's/[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}/UUID/g'
+}
+
+notmuch_built_with_sanitize () {
+    sed 's/^built_with[.]\(.*\)=.*$/built_with.\1=something/'
+}
+
+notmuch_config_sanitize () {
+    notmuch_dir_sanitize | notmuch_built_with_sanitize
+}
+
+notmuch_show_part () {
+    awk '/^\014part}/{ f=0 }; { if (f) { print $0 } } /^\014part{ ID: '"$1"'/{ f=1 }'
+}
+
+# End of notmuch helper functions
+
+# Use test_set_prereq to tell that a particular prerequisite is available.
+#
+# The prerequisite can later be checked for by using test_have_prereq.
+#
+# The single parameter is the prerequisite tag (a simple word, in all
+# capital letters by convention).
+
+test_set_prereq () {
+       satisfied="$satisfied$1 "
+}
+satisfied=" "
+
+test_have_prereq () {
+       case $satisfied in
+       *" $1 "*)
+               : yes, have it ;;
+       *)
+               ! : nope ;;
+       esac
+}
+
+declare -A test_missing_external_prereq_
+declare -A test_subtest_missing_external_prereq_
+
+# declare prerequisite for the given external binary
+test_declare_external_prereq () {
+       local binary
+       binary="$1"
+       test "$#" = 2 && name=$2 || name="$binary(1)"
+
+       if ! hash $binary 2>/dev/null; then
+               test_missing_external_prereq_["${binary}"]=t
+               eval "
+$binary () {
+       test_subtest_missing_external_prereq_[\"${name}\"]=t
+       false
+}"
+       fi
+}
+
+# You are not expected to call test_ok_ and test_failure_ directly, use
+# the text_expect_* functions instead.
+
+test_ok_ () {
+       if test "$test_subtest_known_broken_" = "t"; then
+               test_known_broken_ok_
+               return
+       fi
+       test_success=$(($test_success + 1))
+       if test -n "$NOTMUCH_TEST_QUIET"; then
+               return 0
+       fi
+       say_color pass "%-6s" "PASS"
+       echo " $test_subtest_name"
+}
+
+test_failure_ () {
+       print_test_description
+       if test "$test_subtest_known_broken_" = "t"; then
+               test_known_broken_failure_ "$@"
+               return
+       fi
+       test_failure=$(($test_failure + 1))
+       test_failure_message_ "FAIL" "$test_subtest_name" "$@"
+       test "$immediate" = "" || { GIT_EXIT_OK=t; exit 1; }
+       return 1
+}
+
+test_failure_message_ () {
+       say_color error "%-6s" "$1"
+       echo " $2"
+       shift 2
+       if [ "$#" != "0" ]; then
+               echo "$@" | sed -e 's/^/        /'
+       fi
+       if test "$verbose" != "t"; then cat test.output; fi
+}
+
+test_known_broken_ok_ () {
+       test_reset_state_
+       test_fixed=$(($test_fixed+1))
+       say_color pass "%-6s" "FIXED"
+       echo " $test_subtest_name"
+}
+
+test_known_broken_failure_ () {
+       test_reset_state_
+       test_broken=$(($test_broken+1))
+       if [ -z "$NOTMUCH_TEST_QUIET" ]; then
+               test_failure_message_ "BROKEN" "$test_subtest_name" "$@"
+       else
+               test_failure_message_ "BROKEN" "$test_subtest_name"
+       fi
+       return 1
+}
+
+test_debug () {
+       test "$debug" = "" || eval "$1"
+}
+
+test_run_ () {
+       test_cleanup=:
+       if test "$verbose" != "t"; then exec 4>test.output 3>&4; fi
+       eval >&3 2>&4 "$1"
+       eval_ret=$?
+       eval >&3 2>&4 "$test_cleanup"
+       return 0
+}
+
+test_skip () {
+       test_count=$(($test_count+1))
+       to_skip=
+       for skp in $NOTMUCH_SKIP_TESTS
+       do
+               case $this_test.$test_count in
+               $skp)
+                       to_skip=t
+                       break
+               esac
+               case $this_test_bare.$test_count in
+               $skp)
+                       to_skip=t
+                       break
+               esac
+       done
+       case "$to_skip" in
+       t)
+               test_report_skip_ "$@"
+               ;;
+       *)
+               test_check_missing_external_prereqs_ "$@"
+               ;;
+       esac
+}
+
+test_check_missing_external_prereqs_ () {
+       if [[ ${#test_subtest_missing_external_prereq_[@]} != 0 ]]; then
+               say_color skip >&1 "missing prerequisites: "
+               echo ${!test_subtest_missing_external_prereq_[@]} >&1
+               test_report_skip_ "$@"
+       else
+               false
+       fi
+}
+
+test_report_skip_ () {
+       test_reset_state_
+       say_color skip >&3 "skipping test:"
+       echo " $@" >&3
+       say_color skip "%-6s" "SKIP"
+       echo " $1"
+}
+
+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
+               error "bug in the test script: test_expect_success without test_begin_subtest"
+       fi
+       inside_subtest=
+       test "$#" = 1 ||
+       error "bug in the test script: not 1 parameters to test_expect_success"
+
+       if ! test_skip "$test_subtest_name"
+       then
+               test_run_ "$1"
+               run_ret="$?"
+               # test_run_ may update missing external prerequisites
+               test_check_missing_external_prereqs_ "$test_subtest_name" ||
+               if [ "$run_ret" = 0 -a "$eval_ret" = 0 ]
+               then
+                       test_ok_
+               else
+                       test_failure_ "$1"
+               fi
+       fi
+}
+
+test_expect_code () {
+       exec 1>&6 2>&7          # Restore stdout and stderr
+       if [ -z "$inside_subtest" ]; then
+               error "bug in the test script: test_expect_code without test_begin_subtest"
+       fi
+       inside_subtest=
+       test "$#" = 2 ||
+       error "bug in the test script: not 2 parameters to test_expect_code"
+
+       if ! test_skip "$test_subtest_name"
+       then
+               test_run_ "$2"
+               run_ret="$?"
+               # test_run_ may update missing external prerequisites,
+               test_check_missing_external_prereqs_ "$test_subtest_name" ||
+               if [ "$run_ret" = 0 -a "$eval_ret" = "$1" ]
+               then
+                       test_ok_
+               else
+                       test_failure_ "exit code $eval_ret, expected $1" "$2"
+               fi
+       fi
+}
+
+# This is not among top-level (test_expect_success)
+# but is a prefix that can be used in the test script, like:
+#
+#      test_expect_success 'complain and die' '
+#          do something &&
+#          do something else &&
+#          test_must_fail git checkout ../outerspace
+#      '
+#
+# Writing this as "! git checkout ../outerspace" is wrong, because
+# the failure could be due to a segv.  We want a controlled failure.
+
+test_must_fail () {
+       "$@"
+       test $? -gt 0 -a $? -le 129 -o $? -gt 192
+}
+
+# test_cmp is a helper function to compare actual and expected output.
+# You can use it like:
+#
+#      test_expect_success 'foo works' '
+#              echo expected >expected &&
+#              foo >actual &&
+#              test_cmp expected actual
+#      '
+#
+# This could be written as either "cmp" or "diff -u", but:
+# - cmp's output is not nearly as easy to read as diff -u
+# - not all diff versions understand "-u"
+
+test_cmp () {
+       $GIT_TEST_CMP "$@"
+}
+
+# This function can be used to schedule some commands to be run
+# unconditionally at the end of the test to restore sanity:
+#
+#      test_expect_success 'test core.capslock' '
+#              git config core.capslock true &&
+#              test_when_finished "git config --unset core.capslock" &&
+#              hello world
+#      '
+#
+# That would be roughly equivalent to
+#
+#      test_expect_success 'test core.capslock' '
+#              git config core.capslock true &&
+#              hello world
+#              git config --unset core.capslock
+#      '
+#
+# except that the greeting and config --unset must both succeed for
+# the test to pass.
+
+test_when_finished () {
+       test_cleanup="{ $*
+               } && (exit \"\$eval_ret\"); eval_ret=\$?; $test_cleanup"
+}
+
+test_done () {
+       GIT_EXIT_OK=t
+       test_results_dir="$TEST_DIRECTORY/test-results"
+       mkdir -p "$test_results_dir"
+       test_results_path="$test_results_dir/$this_test"
+
+       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)'
+
+       if [ "$test_failure" = "0" ]; then
+           if [ "$test_broken" = "0" ]; then
+               rm -rf "$remove_tmp"
+           fi
+           exit 0
+       else
+           exit 1
+       fi
+}
+
+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_BUILDDIR/bindings/python-cffi/build/stage:$NOTMUCH_SRCDIR/bindings/python${PYTHONPATH:+:$PYTHONPATH}" \
+       $NOTMUCH_PYTHON -B - > OUTPUT
+}
+
+test_C () {
+    local exec_file test_file
+    exec_file="test${test_count}"
+    test_file="${exec_file}.c"
+    cat > ${test_file}
+    ${TEST_CC} ${TEST_CFLAGS} -I${NOTMUCH_SRCDIR}/test -I${NOTMUCH_SRCDIR}/lib -o ${exec_file} ${test_file} -L${NOTMUCH_BUILDDIR}/lib/ -lnotmuch -ltalloc
+    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
+}
+
+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 () {
+    local base_name test_file shim_file
+    base_name="$1"
+    test_file="${base_name}.c"
+    shim_file="${base_name}.so"
+    cat > ${test_file}
+    ${TEST_CC} ${TEST_CFLAGS} ${TEST_SHIM_CFLAGS} -I${NOTMUCH_SRCDIR}/test -I${NOTMUCH_SRCDIR}/lib -o ${shim_file} ${test_file} ${TEST_SHIM_LDFLAGS}
+}
+
+notmuch_with_shim () {
+    local base_name shim_file notmuch_cmd
+    if [ -n "${NOTMUCH_TEST_INSTALLED-}" ]; then
+       notmuch_cmd="notmuch"
+    else
+       notmuch_cmd="notmuch-shared"
+    fi
+    base_name=$1
+    shift
+    shim_file="${base_name}.so"
+    LD_PRELOAD=${LD_PRELOAD:+:$LD_PRELOAD}:./${shim_file} $notmuch_cmd "$@"
+}
+
+# Creates a script that counts how much time it is executed and calls
+# notmuch.  $notmuch_counter_command is set to the path to the
+# generated script.  Use notmuch_counter_value() function to get the
+# current counter value.
+notmuch_counter_reset () {
+       notmuch_counter_command="$TMP_DIRECTORY/notmuch_counter"
+       if [ ! -x "$notmuch_counter_command" ]; then
+               notmuch_counter_state_path="$TMP_DIRECTORY/notmuch_counter.state"
+               cat >"$notmuch_counter_command" <<EOF || return
+#!/bin/sh
+
+read count < "$notmuch_counter_state_path"
+echo \$((count + 1)) > "$notmuch_counter_state_path"
+
+exec notmuch "\$@"
+EOF
+               chmod +x "$notmuch_counter_command" || return
+       fi
+
+       echo 0 > "$notmuch_counter_state_path"
+}
+
+# Returns the current notmuch counter value.
+notmuch_counter_value () {
+       if [ -r "$notmuch_counter_state_path" ]; then
+               read count < "$notmuch_counter_state_path"
+       else
+               count=0
+       fi
+       echo $count
+}
+
+test_reset_state_ () {
+       test -z "$test_init_done_" && test_init_
+
+       test_subtest_known_broken_=
+       test_subtest_missing_external_prereq_=()
+}
+
+# called once before the first subtest
+test_init_ () {
+       test_init_done_=t
+
+       # skip all tests if there were external prerequisites missing during init
+       test_check_missing_external_prereqs_ "all tests in $this_test" && test_done
+}
+
+
+# Where to run the tests
+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
+
+# 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"
+
+if test "$verbose" = "t"
+then
+       exec 4>&2 3>&1
+else
+       exec 4>test.output 3>&4
+fi
+
+for skp in $NOTMUCH_SKIP_TESTS
+do
+       to_skip=
+       for skp in $NOTMUCH_SKIP_TESTS
+       do
+               case "$this_test" in
+               $skp)
+                       to_skip=t
+                       break
+               esac
+               case "$this_test_bare" in
+               $skp)
+                       to_skip=t
+                       break
+               esac
+       done
+       case "$to_skip" in
+       t)
+               say_color skip >&3 "skipping test $this_test altogether"
+               say_color skip "skip all tests in $this_test"
+               test_done
+       esac
+done
+
+# Provide an implementation of the 'yes' utility
+yes () {
+       if test $# = 0
+       then
+               y=y
+       else
+               y="$*"
+       fi
+
+       while echo "$y"
+       do
+               :
+       done
+}
+
+# Fix some commands on Windows
+case $(uname -s) in
+*MINGW*)
+       # Windows has its own (incompatible) sort and find
+       sort () {
+               /usr/bin/sort "$@"
+       }
+       find () {
+               /usr/bin/find "$@"
+       }
+       sum () {
+               md5sum "$@"
+       }
+       # git sees Windows-style pwd
+       pwd () {
+               builtin pwd -W
+       }
+       # no POSIX permissions
+       # backslashes in pathspec are converted to '/'
+       # exec does not inherit the PID
+       ;;
+*)
+       test_set_prereq POSIXPERM
+       test_set_prereq BSLASHPSPEC
+       test_set_prereq EXECKEEPSPID
+       ;;
+esac
+
+test -z "$NO_PERL" && test_set_prereq PERL
+test -z "$NO_PYTHON" && test_set_prereq PYTHON
+
+# test whether the filesystem supports symbolic links
+ln -s x y 2>/dev/null && test -h y 2>/dev/null && test_set_prereq SYMLINKS
+rm -f y
+
+# declare prerequisites for external binaries used in tests
+test_declare_external_prereq dtach
+test_declare_external_prereq emacs
+test_declare_external_prereq ${TEST_EMACSCLIENT}
+test_declare_external_prereq ${TEST_GDB}
+test_declare_external_prereq gpg
+test_declare_external_prereq openssl
+test_declare_external_prereq gpgsm
+test_declare_external_prereq ${NOTMUCH_PYTHON}
+test_declare_external_prereq xapian-metadata
+test_declare_external_prereq xapian-delve
diff --git a/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"
diff --git a/test/test-verbose b/test/test-verbose
new file mode 100755 (executable)
index 0000000..8af6d9a
--- /dev/null
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+test_description='the verbosity options of the test framework itself.'
+
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_begin_subtest 'print something in test_expect_success and pass'
+test_expect_success '
+  echo "hello stdout" &&
+  echo "hello stderr" >&2 &&
+  true
+'
+test_begin_subtest 'print something in test_expect_success and fail'
+test_expect_success '
+  echo "hello stdout" &&
+  echo "hello stderr" >&2 &&
+  false
+'
+test_begin_subtest 'print something between test_begin_subtest and test_expect_equal and pass'
+echo "hello stdout"
+echo "hello stderr" >&2
+test_expect_equal "a" "a"
+
+test_begin_subtest 'print something test_begin_subtest and test_expect_equal and fail'
+echo "hello stdout"
+echo "hello stderr" >&2
+test_expect_equal "a" "b"
+
+test_done
diff --git a/test/test.expected-output/test-verbose-no b/test/test.expected-output/test-verbose-no
new file mode 100644 (file)
index 0000000..1a2ff61
--- /dev/null
@@ -0,0 +1,21 @@
+
+test-verbose: Testing the verbosity options of the test framework itself.
+ PASS   print something in test_expect_success and pass
+ FAIL   print something in test_expect_success and fail
+       
+         echo "hello stdout" &&
+         echo "hello stderr" >&2 &&
+         false
+       
+hello stdout
+hello stderr
+ PASS   print something between test_begin_subtest and test_expect_equal and pass
+ FAIL   print something test_begin_subtest and test_expect_equal and fail
+       --- test-verbose.4.expected     2010-11-14 21:41:12.738189710 +0000
+       +++ test-verbose.4.output       2010-11-14 21:41:12.738189710 +0000
+       @@ -1 +1 @@
+       -b
+       +a
+hello stdout
+hello stderr
+
diff --git a/test/test.expected-output/test-verbose-yes b/test/test.expected-output/test-verbose-yes
new file mode 100644 (file)
index 0000000..d25466e
--- /dev/null
@@ -0,0 +1,25 @@
+
+test-verbose: Testing the verbosity options of the test framework itself.
+hello stdout
+hello stderr
+ PASS   print something in test_expect_success and pass
+hello stdout
+hello stderr
+ FAIL   print something in test_expect_success and fail
+       
+         echo "hello stdout" &&
+         echo "hello stderr" >&2 &&
+         false
+       
+hello stdout
+hello stderr
+ PASS   print something between test_begin_subtest and test_expect_equal and pass
+hello stdout
+hello stderr
+ FAIL   print something test_begin_subtest and test_expect_equal and fail
+       --- test-verbose.4.expected     2010-11-14 21:41:06.650023289 +0000
+       +++ test-verbose.4.output       2010-11-14 21:41:06.650023289 +0000
+       @@ -1 +1 @@
+       -b
+       +a
+
diff --git a/test/valgrind/suppressions b/test/valgrind/suppressions
new file mode 100644 (file)
index 0000000..6abf8b2
--- /dev/null
@@ -0,0 +1,6 @@
+{
+   zlib inflation uses uninitialize values
+   Memcheck:Cond
+   fun:inflateReset2
+   fun:inflateInit2_
+}
\ No newline at end of file
diff --git a/test/valgrind/valgrind.sh b/test/valgrind/valgrind.sh
new file mode 100755 (executable)
index 0000000..78700c0
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+base=$(basename "$0")
+
+TRACK_ORIGINS=
+
+VALGRIND_VERSION=$(valgrind --version)
+VALGRIND_MAJOR=$(expr "$VALGRIND_VERSION" : '[^0-9]*\([0-9]*\)')
+VALGRIND_MINOR=$(expr "$VALGRIND_VERSION" : '[^0-9]*[0-9]*\.\([0-9]*\)')
+test 3 -gt "$VALGRIND_MAJOR" ||
+test 3 -eq "$VALGRIND_MAJOR" -a 4 -gt "$VALGRIND_MINOR" ||
+TRACK_ORIGINS=--track-origins=yes
+
+exec valgrind -q --error-exitcode=126 \
+       --leak-check=no \
+       --suppressions="$GIT_VALGRIND/suppressions" \
+       --gen-suppressions=all \
+       $TRACK_ORIGINS \
+       --log-fd=4 \
+       --input-fd=4 \
+       $GIT_VALGRIND_OPTIONS \
+       "$GIT_VALGRIND"/../../"$base" "$@"
diff --git a/util/Makefile b/util/Makefile
new file mode 100644 (file)
index 0000000..fa25832
--- /dev/null
@@ -0,0 +1,5 @@
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/util/Makefile.local b/util/Makefile.local
new file mode 100644 (file)
index 0000000..8a0b9bc
--- /dev/null
@@ -0,0 +1,18 @@
+# -*- makefile-gmake -*-
+
+dir := util
+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)/path-util.c \
+               $(dir)/unicode-util.c
+
+libnotmuch_util_modules := $(libnotmuch_util_c_srcs:.c=.o)
+
+$(dir)/libnotmuch_util.a: $(libnotmuch_util_modules)
+       $(call quiet,AR) rcs $@ $^
+
+SRCS := $(SRCS) $(libnotmuch_util_c_srcs)
+CLEAN := $(CLEAN) $(libnotmuch_util_modules) $(dir)/libnotmuch_util.a
diff --git a/util/crypto.c b/util/crypto.c
new file mode 100644 (file)
index 0000000..156a655
--- /dev/null
@@ -0,0 +1,245 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2012 Jameson Rollins
+ *
+ * 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/ .
+ *
+ * Authors: Jameson Rollins <jrollins@finestructure.net>
+ */
+
+#include "crypto.h"
+#include <strings.h>
+#include "error_util.h"
+#define unused(x) x __attribute__ ((unused))
+
+#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
+
+void
+_notmuch_crypto_cleanup (unused(_notmuch_crypto_t *crypto))
+{
+}
+
+GMimeObject *
+_notmuch_crypto_decrypt (bool *attempted,
+                        notmuch_decryption_policy_t decrypt,
+                        notmuch_message_t *message,
+                        GMimeObject *part,
+                        GMimeDecryptResult **decrypt_result,
+                        GError **err)
+{
+    GMimeObject *ret = NULL;
+
+    if (decrypt == NOTMUCH_DECRYPT_FALSE)
+       return NULL;
+
+    /* try decryption with session key if one is stashed */
+    if (message) {
+       notmuch_message_properties_t *list = NULL;
+
+       for (list = notmuch_message_get_properties (message, "session-key", TRUE);
+            notmuch_message_properties_valid (list); notmuch_message_properties_move_to_next (
+                list)) {
+           if (err && *err) {
+               g_error_free (*err);
+               *err = NULL;
+           }
+           if (attempted)
+               *attempted = true;
+           if (GMIME_IS_MULTIPART_ENCRYPTED (part)) {
+               ret = g_mime_multipart_encrypted_decrypt (GMIME_MULTIPART_ENCRYPTED (part),
+                                                         GMIME_DECRYPT_NONE,
+                                                         notmuch_message_properties_value (list),
+                                                         decrypt_result, err);
+           } else if (GMIME_IS_APPLICATION_PKCS7_MIME (part)) {
+               GMimeApplicationPkcs7Mime *pkcs7 = GMIME_APPLICATION_PKCS7_MIME (part);
+               GMimeSecureMimeType type = g_mime_application_pkcs7_mime_get_smime_type (pkcs7);
+               if (type == GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA) {
+                   ret = g_mime_application_pkcs7_mime_decrypt (pkcs7,
+                                                                GMIME_DECRYPT_NONE,
+                                                                notmuch_message_properties_value (
+                                                                    list),
+                                                                decrypt_result, err);
+               }
+           }
+           if (ret)
+               break;
+       }
+       if (list)
+           notmuch_message_properties_destroy (list);
+       if (ret)
+           return ret;
+    }
+
+    if (err && *err) {
+       g_error_free (*err);
+       *err = NULL;
+    }
+
+    if (decrypt == NOTMUCH_DECRYPT_AUTO)
+       return ret;
+
+    if (attempted)
+       *attempted = true;
+    GMimeDecryptFlags flags = GMIME_DECRYPT_NONE;
+
+    if (decrypt == NOTMUCH_DECRYPT_TRUE && decrypt_result)
+       flags |= GMIME_DECRYPT_EXPORT_SESSION_KEY;
+    if (GMIME_IS_MULTIPART_ENCRYPTED (part)) {
+       ret = g_mime_multipart_encrypted_decrypt (GMIME_MULTIPART_ENCRYPTED (part), flags, NULL,
+                                                 decrypt_result, err);
+    } else if (GMIME_IS_APPLICATION_PKCS7_MIME (part)) {
+       GMimeApplicationPkcs7Mime *pkcs7 = GMIME_APPLICATION_PKCS7_MIME (part);
+       GMimeSecureMimeType p7type = g_mime_application_pkcs7_mime_get_smime_type (pkcs7);
+       if (p7type == GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA) {
+           ret = g_mime_application_pkcs7_mime_decrypt (pkcs7, flags, NULL,
+                                                        decrypt_result, err);
+       }
+    }
+    return ret;
+}
+
+static int
+_notmuch_message_crypto_destructor (_notmuch_message_crypto_t *msg_crypto)
+{
+    if (! msg_crypto)
+       return 0;
+    if (msg_crypto->sig_list)
+       g_object_unref (msg_crypto->sig_list);
+    if (msg_crypto->payload_subject)
+       talloc_free (msg_crypto->payload_subject);
+    return 0;
+}
+
+_notmuch_message_crypto_t *
+_notmuch_message_crypto_new (void *ctx)
+{
+    _notmuch_message_crypto_t *ret = talloc_zero (ctx, _notmuch_message_crypto_t);
+
+    talloc_set_destructor (ret, _notmuch_message_crypto_destructor);
+    return ret;
+}
+
+notmuch_status_t
+_notmuch_message_crypto_potential_sig_list (_notmuch_message_crypto_t *msg_crypto,
+                                           GMimeSignatureList *sigs)
+{
+    if (! msg_crypto)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    /* Signatures that arrive after a payload part during DFS are not
+     * part of the cryptographic envelope: */
+    if (msg_crypto->payload_encountered)
+       return NOTMUCH_STATUS_SUCCESS;
+
+    if (msg_crypto->sig_list)
+       g_object_unref (msg_crypto->sig_list);
+
+    /* This signature list needs to persist as long as the _n_m_crypto
+     * object survives. Increasing its reference counter prevents
+     * garbage-collection until after _n_m_crypto_destroy is
+     * called. */
+    msg_crypto->sig_list = sigs;
+    if (sigs)
+       g_object_ref (sigs);
+
+    if (msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL)
+       msg_crypto->signature_encrypted = true;
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+
+bool
+_notmuch_message_crypto_potential_payload (_notmuch_message_crypto_t *msg_crypto, GMimeObject *part,
+                                          GMimeObject *parent, int childnum)
+{
+    const char *protected_headers = NULL;
+    const char *forwarded = NULL;
+    const char *subject = NULL;
+
+    if ((! msg_crypto) || (! part))
+       INTERNAL_ERROR ("_notmuch_message_crypto_potential_payload() got NULL for %s\n",
+                       msg_crypto? "part" : "msg_crypto");
+
+    /* only fire on the first payload part encountered */
+    if (msg_crypto->payload_encountered)
+       return false;
+
+    /* the first child of multipart/encrypted that matches the
+     * encryption protocol should be "control information" metadata,
+     * not payload.  So we skip it. (see
+     * https://tools.ietf.org/html/rfc1847#page-8) */
+    if (parent && GMIME_IS_MULTIPART_ENCRYPTED (parent) && childnum ==
+       GMIME_MULTIPART_ENCRYPTED_VERSION) {
+       const char *enc_type = g_mime_object_get_content_type_parameter (parent, "protocol");
+       GMimeContentType *ct = g_mime_object_get_content_type (part);
+       if (ct && enc_type) {
+           const char *part_type = g_mime_content_type_get_mime_type (ct);
+           if (part_type && strcmp (part_type, enc_type) == 0)
+               return false;
+       }
+    }
+
+    msg_crypto->payload_encountered = true;
+
+    /* don't bother recording anything if there is no cryptographic
+     * envelope: */
+    if ((msg_crypto->decryption_status != NOTMUCH_MESSAGE_DECRYPTED_FULL) &&
+       (msg_crypto->sig_list == NULL))
+       return false;
+
+    /* Verify that this payload has headers that are intended to be
+     * exported to the larger message: */
+
+    /* Consider a payload that uses Alexei Melinkov's forwarded="no" for
+     * message/global or message/rfc822:
+     * https://tools.ietf.org/html/draft-melnikov-smime-header-signing-05#section-4 */
+    forwarded = g_mime_object_get_content_type_parameter (part, "forwarded");
+    if (GMIME_IS_MESSAGE_PART (part) && forwarded && strcmp (forwarded, "no") == 0) {
+       GMimeMessage *message = g_mime_message_part_get_message (GMIME_MESSAGE_PART (part));
+       subject = g_mime_message_get_subject (message);
+       /* FIXME: handle more than just Subject: at some point */
+    } else {
+       /* Consider "memoryhole"-style protected headers as practiced by Enigmail and K-9 */
+       protected_headers = g_mime_object_get_content_type_parameter (part, "protected-headers");
+       if (protected_headers && strcasecmp ("v1", protected_headers) == 0)
+           subject = g_mime_object_get_header (part, "Subject");
+       /* FIXME: handle more than just Subject: at some point */
+    }
+
+    if (subject) {
+       if (msg_crypto->payload_subject)
+           talloc_free (msg_crypto->payload_subject);
+       msg_crypto->payload_subject = talloc_strdup (msg_crypto, subject);
+    }
+
+    return true;
+}
+
+
+notmuch_status_t
+_notmuch_message_crypto_successful_decryption (_notmuch_message_crypto_t *msg_crypto)
+{
+    if (! msg_crypto)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    /* see the rationale for different values of
+     * _notmuch_message_decryption_status_t in util/crypto.h */
+    if (! msg_crypto->payload_encountered)
+       msg_crypto->decryption_status = NOTMUCH_MESSAGE_DECRYPTED_FULL;
+    else if (msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_NONE)
+       msg_crypto->decryption_status = NOTMUCH_MESSAGE_DECRYPTED_PARTIAL;
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
diff --git a/util/crypto.h b/util/crypto.h
new file mode 100644 (file)
index 0000000..3c5d384
--- /dev/null
@@ -0,0 +1,106 @@
+#ifndef _CRYPTO_H
+#define _CRYPTO_H
+
+#include <stdbool.h>
+#include "gmime-extra.h"
+#include "notmuch.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct _notmuch_crypto {
+    bool verify;
+    notmuch_decryption_policy_t decrypt;
+} _notmuch_crypto_t;
+
+GMimeObject *
+_notmuch_crypto_decrypt (bool *attempted,
+                        notmuch_decryption_policy_t decrypt,
+                        notmuch_message_t *message,
+                        GMimeObject *part,
+                        GMimeDecryptResult **decrypt_result,
+                        GError **err);
+
+void
+_notmuch_crypto_cleanup (_notmuch_crypto_t *crypto);
+
+/* The user probably wants to know if the entire message was in the
+ * clear.  When replying, the MUA probably wants to know whether there
+ * was any part decrypted in the message.  And when displaying to the
+ * user, we probably only want to display "encrypted message" if the
+ * entire message was covered by encryption. */
+typedef enum {
+    NOTMUCH_MESSAGE_DECRYPTED_NONE = 0,
+    NOTMUCH_MESSAGE_DECRYPTED_PARTIAL,
+    NOTMUCH_MESSAGE_DECRYPTED_FULL,
+} _notmuch_message_decryption_status_t;
+
+/* description of the cryptographic state of a given message overall;
+ * for use by simple user agents.
+ */
+typedef struct _notmuch_message_crypto {
+    /* encryption status: partial, full, none */
+    _notmuch_message_decryption_status_t decryption_status;
+    /* FIXME: can we show what key(s) a fully-encrypted message was
+     * encrypted to? This data is not necessarily cryptographically
+     * reliable; even when we decrypt, we might not know which public
+     * key was used (e.g. if we're using a session key). */
+
+    /* signature status of the whole message (either the whole message
+     * is signed, or it is not) -- this means that partially-signed
+     * messages will get no signature status. */
+    GMimeSignatureList *sig_list;
+    /* if part of the message was signed, and the MUA is clever, it
+     * can determine on its own exactly which part and try to make
+     * more sense of it. */
+
+    /* mark this flag once we encounter a payload (i.e. something that
+     * is not part of the cryptographic envelope) */
+    bool payload_encountered;
+
+    /* the value of any "Subject:" header in the cryptographic payload
+     * (the top level part within the crypto envelope), converted to
+     * UTF-8 */
+    char *payload_subject;
+
+    /* if both signed and encrypted, was the signature encrypted? */
+    bool signature_encrypted;
+} _notmuch_message_crypto_t;
+
+
+/* _notmuch_message_crypto_t objects should be released with
+ * talloc_free (), or they will be released along with their parent
+ * context.
+ */
+_notmuch_message_crypto_t *
+_notmuch_message_crypto_new (void *ctx);
+
+/* call potential_sig_list during a depth-first-search on a message to
+ * consider a particular signature as relevant for the message.
+ */
+notmuch_status_t
+_notmuch_message_crypto_potential_sig_list (_notmuch_message_crypto_t *msg_crypto,
+                                           GMimeSignatureList *sigs);
+
+/* call successful_decryption during a depth-first-search on a message
+ * to indicate that a part was successfully decrypted.
+ */
+notmuch_status_t
+_notmuch_message_crypto_successful_decryption (_notmuch_message_crypto_t *msg_crypto);
+
+/* call potential_payload during a depth-first-search on a message
+ * when encountering a message part that is not part of the envelope.
+ *
+ * Returns true if part is the root of the cryptographic payload of
+ * this message.
+ */
+bool
+_notmuch_message_crypto_potential_payload (_notmuch_message_crypto_t *msg_crypto, GMimeObject *part,
+                                          GMimeObject *parent, int childnum);
+
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/util/error_util.c b/util/error_util.c
new file mode 100644 (file)
index 0000000..e64162c
--- /dev/null
@@ -0,0 +1,40 @@
+/* error_util.c - internal error utilities for notmuch.
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdio.h>
+
+#include "error_util.h"
+
+void
+_internal_error (const char *format, ...)
+{
+    va_list va_args;
+
+    va_start (va_args, format);
+
+    fprintf (stderr, "Internal error: ");
+    vfprintf (stderr, format, va_args);
+
+    va_end (va_args);
+    exit (1);
+}
+
diff --git a/util/error_util.h b/util/error_util.h
new file mode 100644 (file)
index 0000000..a51f001
--- /dev/null
@@ -0,0 +1,54 @@
+/* error_util.h - Provide the INTERNAL_ERROR macro
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#ifndef ERROR_UTIL_H
+#define ERROR_UTIL_H
+
+#include <talloc.h>
+
+#include "function-attributes.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* There's no point in continuing when we've detected that we've done
+ * something wrong internally (as opposed to the user passing in a
+ * bogus value).
+ *
+ * Note that PRINTF_ATTRIBUTE comes from talloc.h
+ */
+void
+_internal_error (const char *format, ...) PRINTF_ATTRIBUTE (1, 2) NORETURN_ATTRIBUTE;
+
+/* There's no point in continuing when we've detected that we've done
+ * something wrong internally (as opposed to the user passing in a
+ * bogus value).
+ *
+ * Note that __location__ comes from talloc.h.
+ */
+#define INTERNAL_ERROR(format, ...)                     \
+    _internal_error (format " (%s).\n",                 \
+                    ##__VA_ARGS__, __location__)
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/util/gmime-extra.c b/util/gmime-extra.c
new file mode 100644 (file)
index 0000000..192cb07
--- /dev/null
@@ -0,0 +1,221 @@
+#include "gmime-extra.h"
+#include <string.h>
+
+static
+GMimeStream *
+_gzfile_maybe_filter (GMimeStream *file_stream)
+{
+    char buf[4];
+    int bytes_read;
+
+    if ((bytes_read = g_mime_stream_read (file_stream, buf, sizeof (buf))) < 0)
+       return NULL;
+
+    if (g_mime_stream_reset (file_stream))
+       return NULL;
+
+    /* check for gzipped input */
+    if (bytes_read >= 2 && buf[0] == 0x1f && (unsigned char) buf[1] == 0x8b) {
+       GMimeStream *gzstream;
+       GMimeFilter *gzfilter;
+
+       gzfilter = g_mime_filter_gzip_new (GMIME_FILTER_GZIP_MODE_UNZIP, 0);
+       if (! gzfilter)
+           return NULL;
+
+       gzstream = g_mime_stream_filter_new (file_stream);
+       if (! gzstream)
+           return NULL;
+
+       /* ignore filter id */
+       (void) g_mime_stream_filter_add ((GMimeStreamFilter *) gzstream, gzfilter);
+       g_object_unref (gzfilter);
+       g_object_unref (file_stream);
+       return gzstream;
+    } else {
+       return file_stream;
+    }
+}
+
+GMimeStream *
+g_mime_stream_gzfile_new (int fd)
+{
+    GMimeStream *file_stream;
+
+    file_stream = g_mime_stream_fs_new (fd);
+    if (! file_stream)
+       return NULL;
+
+    return _gzfile_maybe_filter (file_stream);
+}
+
+GMimeStream *
+g_mime_stream_gzfile_open (const char *filename)
+{
+    GMimeStream *file_stream;
+
+    file_stream = g_mime_stream_fs_open (filename, 0, 0, NULL);
+    if (! file_stream)
+       return NULL;
+
+    return _gzfile_maybe_filter (file_stream);
+}
+
+GMimeStream *
+g_mime_stream_stdout_new ()
+{
+    GMimeStream *stream_stdout = NULL;
+    GMimeStream *stream_buffered = NULL;
+
+    stream_stdout = g_mime_stream_pipe_new (STDOUT_FILENO);
+    if (! stream_stdout)
+       return NULL;
+
+    g_mime_stream_pipe_set_owner (GMIME_STREAM_PIPE (stream_stdout), FALSE);
+
+    stream_buffered = g_mime_stream_buffer_new (stream_stdout, GMIME_STREAM_BUFFER_BLOCK_WRITE);
+
+    g_object_unref (stream_stdout);
+
+    return stream_buffered;
+}
+
+/**
+ * copy a glib string into a talloc context, and free it.
+ */
+static char *
+g_string_talloc_strdup (void *ctx, char *g_string)
+{
+    char *new_str = talloc_strdup (ctx, g_string);
+
+    g_free (g_string);
+    return new_str;
+}
+
+const char *
+g_mime_certificate_get_valid_userid (GMimeCertificate *cert)
+{
+    /* output user id only if validity is FULL or ULTIMATE. */
+    const char *uid = g_mime_certificate_get_user_id (cert);
+
+    if (uid == NULL)
+       return uid;
+    GMimeValidity validity = g_mime_certificate_get_id_validity (cert);
+
+    if (validity == GMIME_VALIDITY_FULL || validity == GMIME_VALIDITY_ULTIMATE)
+       return uid;
+    return NULL;
+}
+
+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)
+{
+    const char *fpr = g_mime_certificate_get_fingerprint (cert);
+
+    if (! fpr || strlen (fpr) < 16)
+       return fpr;
+
+    return fpr + (strlen (fpr) - 16);
+}
+
+char *
+g_mime_message_get_address_string (GMimeMessage *message, GMimeAddressType type)
+{
+    InternetAddressList *list = g_mime_message_get_addresses (message, type);
+
+    return internet_address_list_to_string (list, NULL, 0);
+}
+
+char *
+g_mime_message_get_date_string (void *ctx, GMimeMessage *message)
+{
+    GDateTime *parsed_date = g_mime_message_get_date (message);
+
+    if (parsed_date) {
+       char *date = g_mime_utils_header_format_date (parsed_date);
+       return g_string_talloc_strdup (ctx, date);
+    } else {
+       return talloc_strdup (ctx, "Thu, 01 Jan 1970 00:00:00 +0000");
+    }
+}
+
+InternetAddressList *
+g_mime_message_get_reply_to_list (GMimeMessage *message)
+{
+    return g_mime_message_get_reply_to (message);
+}
+
+const char *
+g_mime_message_get_from_string (GMimeMessage *message)
+{
+    return g_mime_object_get_header (GMIME_OBJECT (message), "From");
+}
+
+char *
+g_mime_message_get_reply_to_string (void *ctx, GMimeMessage *message)
+{
+    InternetAddressList *list = g_mime_message_get_reply_to (message);
+
+    return g_string_talloc_strdup (ctx, internet_address_list_to_string (list, NULL, 0));
+}
+
+void
+g_mime_parser_set_scan_from (GMimeParser *parser, gboolean flag)
+{
+    g_mime_parser_set_format (parser, flag ? GMIME_FORMAT_MBOX : GMIME_FORMAT_MESSAGE);
+}
+
+/* In GMime 3.0, status GOOD and VALID both imply something about the
+ * validity of the UIDs attached to the signing key. This forces us to
+ * use following somewhat relaxed definition of a "good" signature to
+ * preserve current notmuch semantics.
+ */
+
+gboolean
+g_mime_signature_status_good (GMimeSignatureStatus status)
+{
+    return ((status & (GMIME_SIGNATURE_STATUS_RED | GMIME_SIGNATURE_STATUS_ERROR_MASK)) == 0);
+}
+
+gboolean
+g_mime_signature_status_bad (GMimeSignatureStatus status)
+{
+    return (status & GMIME_SIGNATURE_STATUS_RED);
+}
+
+gboolean
+g_mime_signature_status_error (GMimeSignatureStatus status)
+{
+    return (status & GMIME_SIGNATURE_STATUS_ERROR_MASK);
+}
+
+gint64
+g_mime_utils_header_decode_date_unix (const char *date)
+{
+    GDateTime *parsed_date = g_mime_utils_header_decode_date (date);
+    time_t ret;
+
+    if (parsed_date) {
+       ret = g_date_time_to_unix (parsed_date);
+       g_date_time_unref (parsed_date);
+    } else {
+       ret = 0;
+    }
+
+    return ret;
+}
diff --git a/util/gmime-extra.h b/util/gmime-extra.h
new file mode 100644 (file)
index 0000000..889e91f
--- /dev/null
@@ -0,0 +1,81 @@
+#ifndef _GMIME_EXTRA_H
+#define _GMIME_EXTRA_H
+#include <gmime/gmime.h>
+#include <talloc.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+GMimeStream *g_mime_stream_stdout_new (void);
+
+/* Return a GMime stream for this open file descriptor, un-gzipping if
+ * necessary */
+GMimeStream *g_mime_stream_gzfile_new (int fd);
+
+/* Return a GMime stream for this path, un-gzipping if
+ * necessary */
+GMimeStream *g_mime_stream_gzfile_open (const char *filename);
+
+/**
+ * Get last 16 hex digits of fingerprint ("keyid")
+ */
+const char *g_mime_certificate_get_fpr16 (GMimeCertificate *cert);
+/**
+ * Return the contents of the appropriate address header as a string
+ * Should be freed using g_free
+ */
+char *g_mime_message_get_address_string (GMimeMessage *message, GMimeAddressType type);
+
+InternetAddressList *g_mime_message_get_addresses (GMimeMessage *message, GMimeAddressType type);
+
+/**
+ * return talloc allocated date string
+ */
+
+char *g_mime_message_get_date_string (void *ctx, GMimeMessage *message);
+
+/**
+ * glib allocated list of From: addresses
+ */
+
+InternetAddressList *g_mime_message_get_from (GMimeMessage *message);
+
+
+/**
+ * return string for From: address
+ * (owned by gmime)
+ */
+const char *g_mime_message_get_from_string (GMimeMessage *message);
+
+InternetAddressList *g_mime_message_get_reply_to_list (GMimeMessage *message);
+
+/**
+ * return talloc allocated reply-to string
+ */
+char *g_mime_message_get_reply_to_string (void *ctx, GMimeMessage *message);
+
+void g_mime_parser_set_scan_from (GMimeParser *parser, gboolean flag);
+
+gboolean g_mime_signature_status_good (GMimeSignatureStatus status);
+
+gboolean g_mime_signature_status_bad (GMimeSignatureStatus status);
+
+gboolean g_mime_signature_status_error (GMimeSignatureStatus status);
+
+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
+}
+#endif
+
+#endif
diff --git a/util/hex-escape.c b/util/hex-escape.c
new file mode 100644 (file)
index 0000000..81534a8
--- /dev/null
@@ -0,0 +1,159 @@
+/* hex-escape.c -  Manage encoding and decoding of byte strings into path names
+ *
+ * Copyright (c) 2011 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 <assert.h>
+#include <string.h>
+#include <talloc.h>
+#include <ctype.h>
+#include "error_util.h"
+#include "hex-escape.h"
+
+static const char *output_charset =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-_@=.,";
+
+static const char escape_char = '%';
+
+static int
+is_output (char c)
+{
+    return (strchr (output_charset, c) != NULL);
+}
+
+static int
+maybe_realloc (void *ctx, size_t needed, char **out, size_t *out_size)
+{
+    if (*out_size < needed) {
+
+       if (*out == NULL)
+           *out = talloc_size (ctx, needed);
+       else
+           *out = talloc_realloc (ctx, *out, char, needed);
+
+       if (*out == NULL)
+           return 0;
+
+       *out_size = needed;
+    }
+    return 1;
+}
+
+hex_status_t
+hex_encode (void *ctx, const char *in, char **out, size_t *out_size)
+{
+
+    const char *p;
+    char *q;
+
+    size_t needed = 1;  /* for the NUL */
+
+    assert (ctx); assert (in); assert (out); assert (out_size);
+
+    for (p = in; *p; p++) {
+       needed += is_output (*p) ? 1 : 3;
+    }
+
+    if (*out == NULL)
+       *out_size = 0;
+
+    if (! maybe_realloc (ctx, needed, out, out_size))
+       return HEX_OUT_OF_MEMORY;
+
+    q = *out;
+    p = in;
+
+    while (*p) {
+       if (is_output (*p)) {
+           *q++ = *p++;
+       } else {
+           sprintf (q, "%%%02x", (unsigned char) *p++);
+           q += 3;
+       }
+    }
+
+    *q = '\0';
+    return HEX_SUCCESS;
+}
+
+/* Hex decode 'in' to 'out'.
+ *
+ * This must succeed for in == out to support hex_decode_inplace().
+ */
+static hex_status_t
+hex_decode_internal (const char *in, unsigned char *out)
+{
+    char buf[3];
+
+    while (*in) {
+       if (*in == escape_char) {
+           char *endp;
+
+           /* This also handles unexpected end-of-string. */
+           if (! isxdigit ((unsigned char) in[1]) ||
+               ! isxdigit ((unsigned char) in[2]))
+               return HEX_SYNTAX_ERROR;
+
+           buf[0] = in[1];
+           buf[1] = in[2];
+           buf[2] = '\0';
+
+           *out = strtoul (buf, &endp, 16);
+
+           if (endp != buf + 2)
+               return HEX_SYNTAX_ERROR;
+
+           in += 3;
+           out++;
+       } else {
+           *out++ = *in++;
+       }
+    }
+
+    *out = '\0';
+
+    return HEX_SUCCESS;
+}
+
+hex_status_t
+hex_decode_inplace (char *s)
+{
+    /* A decoded string is never longer than the encoded one, so it is
+     * safe to decode a string onto itself. */
+    return hex_decode_internal (s, (unsigned char *) s);
+}
+
+hex_status_t
+hex_decode (void *ctx, const char *in, char **out, size_t *out_size)
+{
+    const char *p;
+    size_t needed = 1;  /* for the NUL */
+
+    assert (ctx); assert (in); assert (out); assert (out_size);
+
+    for (p = in; *p; p++)
+       if ((p[0] == escape_char) && isxdigit (p[1]) && isxdigit (p[2]))
+           needed -= 1;
+       else
+           needed += 1;
+
+    if (! maybe_realloc (ctx, needed, out, out_size))
+       return HEX_OUT_OF_MEMORY;
+
+    return hex_decode_internal (in, (unsigned char *) *out);
+}
diff --git a/util/hex-escape.h b/util/hex-escape.h
new file mode 100644 (file)
index 0000000..83a4c6f
--- /dev/null
@@ -0,0 +1,50 @@
+#ifndef _HEX_ESCAPE_H
+#define _HEX_ESCAPE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+    HEX_SUCCESS = 0,
+    HEX_SYNTAX_ERROR,
+    HEX_OUT_OF_MEMORY
+} hex_status_t;
+
+/*
+ * The API for hex_encode() and hex_decode() is modelled on that for
+ * getline.
+ *
+ * If 'out' points to a NULL pointer a char array of the appropriate
+ * size is allocated using talloc, and out_size is updated.
+ *
+ * If 'out' points to a non-NULL pointer, it assumed to describe an
+ * existing char array, with the size given in *out_size.  This array
+ * may be resized by talloc_realloc if needed; in this case *out_size
+ * will also be updated.
+ *
+ * Note that it is an error to pass a NULL pointer for any parameter
+ * of these routines.
+ */
+
+hex_status_t
+hex_encode (void *talloc_ctx, const char *in, char **out,
+           size_t *out_size);
+
+hex_status_t
+hex_decode (void *talloc_ctx, const char *in, char **out,
+           size_t *out_size);
+
+/*
+ * Non-allocating hex decode to decode 's' in-place. The length of the
+ * result is always equal to or shorter than the length of the
+ * original.
+ */
+hex_status_t
+hex_decode_inplace (char *s);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
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_ */
diff --git a/util/repair.c b/util/repair.c
new file mode 100644 (file)
index 0000000..5b0dfdf
--- /dev/null
@@ -0,0 +1,158 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2019 Daniel Kahn Gillmor
+ *
+ * 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/ .
+ *
+ * Authors: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+ */
+
+#include <stdbool.h>
+#include "repair.h"
+
+
+static bool
+_notmuch_crypto_payload_has_legacy_display (GMimeObject *payload)
+{
+    GMimeMultipart *mpayload;
+    const char *protected_header_parameter;
+    GMimeObject *first;
+
+    if (! g_mime_content_type_is_type (g_mime_object_get_content_type (payload),
+                                      "multipart", "mixed"))
+       return false;
+    protected_header_parameter = g_mime_object_get_content_type_parameter (payload,
+                                                                          "protected-headers");
+    if ((! protected_header_parameter) || strcmp (protected_header_parameter, "v1"))
+       return false;
+    if (! GMIME_IS_MULTIPART (payload))
+       return false;
+    mpayload = GMIME_MULTIPART (payload);
+    if (mpayload == NULL)
+       return false;
+    if (g_mime_multipart_get_count (mpayload) != 2)
+       return false;
+    first = g_mime_multipart_get_part (mpayload, 0);
+    /* Early implementations that generated "Legacy Display" parts used
+     * Content-Type: text/rfc822-headers, but text/plain is more widely
+     * rendered, so it is now the standard choice.  We accept either as a
+     * Legacy Display part. */
+    if (! (g_mime_content_type_is_type (g_mime_object_get_content_type (first),
+                                       "text", "plain") ||
+          g_mime_content_type_is_type (g_mime_object_get_content_type (first),
+                                       "text", "rfc822-headers")))
+       return false;
+    protected_header_parameter = g_mime_object_get_content_type_parameter (first,
+                                                                          "protected-headers");
+    if ((! protected_header_parameter) || strcmp (protected_header_parameter, "v1"))
+       return false;
+    if (! GMIME_IS_TEXT_PART (first))
+       return false;
+
+    return true;
+}
+
+GMimeObject *
+_notmuch_repair_crypto_payload_skip_legacy_display (GMimeObject *payload)
+{
+    if (_notmuch_crypto_payload_has_legacy_display (payload)) {
+       return g_mime_multipart_get_part (GMIME_MULTIPART (payload), 1);
+    } else {
+       return payload;
+    }
+}
+
+/* see
+ * https://tools.ietf.org/html/draft-dkg-openpgp-pgpmime-message-mangling-00#section-4.1.1 */
+static bool
+_notmuch_is_mixed_up_mangled (GMimeObject *part)
+{
+    GMimeMultipart *mpart = NULL;
+    GMimeObject *parts[3] = { NULL, NULL, NULL };
+    GMimeContentType *type = NULL;
+    char *prelude_string = NULL;
+    bool prelude_is_empty;
+
+    if (part == NULL)
+       return false;
+    type = g_mime_object_get_content_type (part);
+    if (type == NULL)
+       return false;
+    if (! g_mime_content_type_is_type (type, "multipart", "mixed"))
+       return false;
+    if (! GMIME_IS_MULTIPART (part)) /* probably impossible */
+       return false;
+    mpart = GMIME_MULTIPART (part);
+    if (mpart == NULL)
+       return false;
+    if (g_mime_multipart_get_count (mpart) != 3)
+       return false;
+    parts[0] = g_mime_multipart_get_part (mpart, 0);
+    if (! g_mime_content_type_is_type (g_mime_object_get_content_type (parts[0]),
+                                      "text", "plain"))
+       return false;
+    if (! GMIME_IS_TEXT_PART (parts[0]))
+       return false;
+    parts[1] = g_mime_multipart_get_part (mpart, 1);
+    if (! g_mime_content_type_is_type (g_mime_object_get_content_type (parts[1]),
+                                      "application", "pgp-encrypted"))
+       return false;
+    parts[2] = g_mime_multipart_get_part (mpart, 2);
+    if (! g_mime_content_type_is_type (g_mime_object_get_content_type (parts[2]),
+                                      "application", "octet-stream"))
+       return false;
+
+    /* Is parts[0] length 0? */
+    prelude_string = g_mime_text_part_get_text (GMIME_TEXT_PART (parts[0]));
+    prelude_is_empty = (prelude_string[0] == '\0');
+    g_free (prelude_string);
+    if (! prelude_is_empty)
+       return false;
+
+    /* FIXME: after decoding and stripping whitespace, is parts[1]
+     * subpart just "Version: 1" ? */
+
+    /* FIXME: can we determine that parts[2] subpart is *only* PGP
+     * encrypted data?  I tried g_mime_part_get_openpgp_data () but
+     * found https://github.com/jstedfast/gmime/issues/60 */
+
+    return true;
+}
+
+
+/* see
+ * https://tools.ietf.org/html/draft-dkg-openpgp-pgpmime-message-mangling-00#section-4.1.2 */
+GMimeObject *
+_notmuch_repair_mixed_up_mangled (GMimeObject *part)
+{
+    GMimeMultipart *mpart = NULL, *mpart_ret = NULL;
+    GMimeObject *ret = NULL;
+
+    if (! _notmuch_is_mixed_up_mangled (part))
+       return NULL;
+    mpart = GMIME_MULTIPART (part);
+    ret = GMIME_OBJECT (g_mime_multipart_encrypted_new ());
+    if (ret == NULL)
+       return NULL;
+    mpart_ret = GMIME_MULTIPART (ret);
+    if (mpart_ret == NULL) {
+       g_object_unref (ret);
+       return NULL;
+    }
+    g_mime_object_set_content_type_parameter (ret, "protocol", "application/pgp-encrypted");
+
+    g_mime_multipart_insert (mpart_ret, 0, g_mime_multipart_get_part (mpart, 1));
+    g_mime_multipart_insert (mpart_ret, 1, g_mime_multipart_get_part (mpart, 2));
+    return ret;
+}
diff --git a/util/repair.h b/util/repair.h
new file mode 100644 (file)
index 0000000..492f5a2
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef _REPAIR_H
+#define _REPAIR_H
+
+#include "gmime-extra.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* This is a collection of message structure and message format repair
+ * techniques that are designed to improve the user experience of
+ * notmuch */
+
+/* If payload is a cryptographic payload within an encrypted message, and
+ * it has a "legacy display" part, then we can skip over it and jump
+ * to the actual content, because notmuch already handles protected
+ * headers appropriately.
+ *
+ * This function either returns payload directly (if it does not have
+ * a "legacy display" part), or it returns a pointer to its
+ * content-bearing subpart, with the "legacy display" part and the
+ * surrounding multipart/mixed object bypassed.
+ *
+ * No new objects are created by calling this function, and the
+ * returned object will only be released when the original part is
+ * disposed of.
+ */
+
+GMimeObject *
+_notmuch_repair_crypto_payload_skip_legacy_display (GMimeObject *payload);
+
+/* Detecting and repairing "Mixed-Up MIME mangling". see
+ * https://tools.ietf.org/html/draft-dkg-openpgp-pgpmime-message-mangling-00#section-4.1
+ * If this returns NULL, the message was probably not "Mixed up".  If
+ * it returns non-NULL, then there is a newly-allocated MIME part that
+ * represents the repaired version.  The caller is responsible for
+ * ensuring that any returned object is freed with g_object_unref. */
+GMimeObject *
+_notmuch_repair_mixed_up_mangled (GMimeObject *part);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/util/string-util.c b/util/string-util.c
new file mode 100644 (file)
index 0000000..03d7648
--- /dev/null
@@ -0,0 +1,298 @@
+/* string-util.c -  Extra or enhanced routines for null terminated strings.
+ *
+ * Copyright (c) 2012 Jani Nikula
+ *
+ * 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: Jani Nikula <jani@nikula.org>
+ */
+
+
+#include "string-util.h"
+#include "talloc.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdbool.h>
+
+char *
+strtok_len (char *s, const char *delim, size_t *len)
+{
+    /* skip initial delims */
+    s += strspn (s, delim);
+
+    /* length of token */
+    *len = strcspn (s, delim);
+
+    return *len ? s : NULL;
+}
+
+const char *
+strsplit_len (const char *s, char delim, size_t *len)
+{
+    bool escaping = false;
+    size_t count = 0, last_nonspace = 0;
+
+    /* 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++;
+    }
+
+    if (count == 0)
+       return NULL;
+
+    *len = last_nonspace + 1;
+    return s;
+}
+
+const char *
+strtok_len_c (const char *s, const char *delim, size_t *len)
+{
+    /* strtok_len is already const-safe, but we can't express both
+     * versions in the C type system. */
+    return strtok_len ((char *) s, delim, len);
+}
+
+char *
+sanitize_string (const void *ctx, const char *str)
+{
+    char *out, *loop;
+
+    if (! str)
+       return NULL;
+
+    out = talloc_strdup (ctx, str);
+    if (! out)
+       return NULL;
+
+    for (loop = out; *loop; loop++) {
+       if (*loop == '\t' || *loop == '\n')
+           *loop = ' ';
+       else if ((unsigned char) (*loop) < 32)
+           *loop = '?';
+    }
+
+    return out;
+}
+
+static int
+is_unquoted_terminator (unsigned char c)
+{
+    return c == 0 || c <= ' ' || c == ')';
+}
+
+int
+make_boolean_term (void *ctx, const char *prefix, const char *term,
+                  char **buf, size_t *len)
+{
+    const char *in;
+    char *out;
+    size_t needed = 3;
+    int need_quoting = 0;
+
+    /* Do we need quoting?  To be paranoid, we quote anything
+     * containing a quote or '(', even though these only matter at the
+     * beginning, and anything containing non-ASCII text. */
+    if (! term[0])
+       need_quoting = 1;
+    for (in = term; *in && ! need_quoting; in++)
+       if (is_unquoted_terminator (*in) || *in == '"' || *in == '('
+           || (unsigned char) *in > 127)
+           need_quoting = 1;
+
+    if (need_quoting)
+       for (in = term; *in; in++)
+           needed += (*in == '"') ? 2 : 1;
+    else
+       needed = strlen (term) + 1;
+
+    /* Reserve space for the prefix */
+    if (prefix)
+       needed += strlen (prefix) + 1;
+
+    if ((*buf == NULL) || (needed > *len)) {
+       *len = 2 * needed;
+       *buf = talloc_realloc (ctx, *buf, char, *len);
+    }
+
+    if (! *buf) {
+       errno = ENOMEM;
+       return -1;
+    }
+
+    out = *buf;
+
+    /* Copy in the prefix */
+    if (prefix) {
+       strcpy (out, prefix);
+       out += strlen (prefix);
+       *out++ = ':';
+    }
+
+    if (! need_quoting) {
+       strcpy (out, term);
+       return 0;
+    }
+
+    /* Quote term by enclosing it in double quotes and doubling any
+     * internal double quotes. */
+    *out++ = '"';
+    in = term;
+    while (*in) {
+       if (*in == '"')
+           *out++ = '"';
+       *out++ = *in++;
+    }
+    *out++ = '"';
+    *out = '\0';
+
+    return 0;
+}
+
+const char *
+skip_space (const char *str)
+{
+    while (*str && isspace ((unsigned char) *str))
+       ++str;
+    return str;
+}
+
+int
+parse_boolean_term (void *ctx, const char *str,
+                   char **prefix_out, char **term_out)
+{
+    int err = EINVAL;
+
+    *prefix_out = *term_out = NULL;
+
+    /* Parse prefix */
+    str = skip_space (str);
+    const char *pos = strchr (str, ':');
+
+    if (! pos || pos == str)
+       goto FAIL;
+    *prefix_out = talloc_strndup (ctx, str, pos - str);
+    if (! *prefix_out) {
+       err = ENOMEM;
+       goto FAIL;
+    }
+    ++pos;
+
+    /* Implement de-quoting compatible with make_boolean_term. */
+    if (*pos == '"') {
+       char *out = talloc_array (ctx, char, strlen (pos));
+       int closed = 0;
+       if (! out) {
+           err = ENOMEM;
+           goto FAIL;
+       }
+       *term_out = out;
+       /* Skip the opening quote, find the closing quote, and
+        * un-double doubled internal quotes. */
+       for (++pos; *pos; ) {
+           if (*pos == '"') {
+               ++pos;
+               if (*pos != '"') {
+                   /* Found the closing quote. */
+                   closed = 1;
+                   pos = skip_space (pos);
+                   break;
+               }
+           }
+           *out++ = *pos++;
+       }
+       /* Did the term terminate without a closing quote or is there
+        * trailing text after the closing quote? */
+       if (! closed || *pos)
+           goto FAIL;
+       *out = '\0';
+    } else {
+       const char *start = pos;
+       /* Check for text after the boolean term. */
+       while (! is_unquoted_terminator (*pos))
+           ++pos;
+       if (*skip_space (pos)) {
+           err = EINVAL;
+           goto FAIL;
+       }
+       /* No trailing text; dup the string so the caller can free
+        * it. */
+       *term_out = talloc_strndup (ctx, start, pos - start);
+       if (! *term_out) {
+           err = ENOMEM;
+           goto FAIL;
+       }
+    }
+    return 0;
+
+  FAIL:
+    talloc_free (*prefix_out);
+    talloc_free (*term_out);
+    errno = err;
+    return -1;
+}
+
+int
+strcmp_null (const char *s1, const char *s2)
+{
+    if (s1 && s2)
+       return strcmp (s1, s2);
+    else if (! s1 && ! s2)
+       return 0;
+    else if (s1)
+       return 1;       /* s1 (non-NULL) is greater than s2 (NULL) */
+    else
+       return -1;      /* s1 (NULL) is less than s2 (non-NULL) */
+}
+
+int
+strcase_equal (const void *a, const void *b)
+{
+    return strcasecmp (a, b) == 0;
+}
+
+unsigned int
+strcase_hash (const void *ptr)
+{
+    const char *s = ptr;
+
+    /* This is the djb2 hash. */
+    unsigned int hash = 5381;
+
+    while (s && *s) {
+       hash = ((hash << 5) + hash) + tolower (*s);
+       s++;
+    }
+
+    return hash;
+}
+
+void
+strip_trailing (char *str, char ch)
+{
+    int i;
+
+    for (i = strlen (str) - 1; i >= 0; i--) {
+       if (str[i] == ch)
+           str[i] = '\0';
+       else
+           break;
+    }
+}
diff --git a/util/string-util.h b/util/string-util.h
new file mode 100644 (file)
index 0000000..80647c5
--- /dev/null
@@ -0,0 +1,100 @@
+#ifndef _STRING_UTIL_H
+#define _STRING_UTIL_H
+
+#include <string.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* like strtok(3), but without state, and doesn't modify s.  Return
+ * value is indicated by pointer and length, not null terminator.
+ *
+ * Usage pattern:
+ *
+ * const char *tok = input;
+ * const char *delim = " \t";
+ * size_t tok_len = 0;
+ *
+ * while ((tok = strtok_len (tok + tok_len, delim, &tok_len)) != NULL) {
+ *     // do stuff with string tok of length tok_len
+ * }
+ */
+
+char *strtok_len (char *s, const char *delim, size_t *len);
+
+/* Const version of strtok_len. */
+const char *strtok_len_c (const char *s, const char *delim, size_t *len);
+
+/* Simplified version of strtok_len, with a single delimiter.
+ * Handles escaping delimiters with \
+ * Usage pattern:
+ *
+ * const char *tok = input;
+ * const char *delim = ';';
+ * size_t tok_len = 0;
+ *
+ * while ((tok = strsplit_len (tok + tok_len, delim, &tok_len)) != NULL) {
+ *     // do stuff with string tok of length tok_len
+ * }
+ */
+const char *strsplit_len (const char *s, char delim, size_t *len);
+
+/* Return a talloced string with str sanitized.
+ *
+ * Whitespace characters (tabs and newlines) are replaced with spaces,
+ * non-printable characters with question marks.
+ */
+char *sanitize_string (const void *ctx, const char *str);
+
+/* Construct a boolean term query with the specified prefix (e.g.,
+ * "id") and search term, quoting term as necessary.  Specifically, if
+ * term contains any non-printable ASCII characters, non-ASCII
+ * characters, close parenthesis or double quotes, it will be enclosed
+ * in double quotes and any internal double quotes will be doubled
+ * (e.g. a"b -> "a""b").  The result will be a valid notmuch query and
+ * can be parsed by parse_boolean_term.
+ *
+ * Output is into buf; it may be talloc_realloced.
+ * Return: 0 on success, -1 on error.  errno will be set to ENOMEM if
+ * there is an allocation failure.
+ */
+int make_boolean_term (void *talloc_ctx, const char *prefix, const char *term,
+                      char **buf, size_t *len);
+
+/* Parse a boolean term query consisting of a prefix, a colon, and a
+ * term that may be quoted as described for make_boolean_term.  If the
+ * term is not quoted, then it ends at the first whitespace or close
+ * parenthesis.  str may containing leading or trailing whitespace,
+ * but anything else is considered a parse error.  This is compatible
+ * with anything produced by make_boolean_term, and supports a subset
+ * of the quoting styles supported by Xapian (and hence notmuch).
+ * *prefix_out and *term_out will be talloc'd with context ctx.
+ *
+ * Return: 0 on success, -1 on error.  errno will be set to EINVAL if
+ * there is a parse error or ENOMEM if there is an allocation failure.
+ */
+int
+parse_boolean_term (void *ctx, const char *str,
+                   char **prefix_out, char **term_out);
+
+/* strcmp that handles NULL strings; in strcmp terms a NULL string is
+ * considered to be less than a non-NULL string.
+ */
+int strcmp_null (const char *s1, const char *s2);
+
+/* GLib GEqualFunc compatible strcasecmp wrapper */
+int strcase_equal (const void *a, const void *b);
+
+/* GLib GHashFunc compatible case insensitive hash function */
+unsigned int strcase_hash (const void *ptr);
+
+void strip_trailing (char *str, char ch);
+
+const char *skip_space (const char *str);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/util/talloc-extra.c b/util/talloc-extra.c
new file mode 100644 (file)
index 0000000..9626247
--- /dev/null
@@ -0,0 +1,14 @@
+#include <string.h>
+#include "talloc-extra.h"
+
+char *
+talloc_strndup_named_const (void *ctx, const char *str,
+                           size_t len, const char *name)
+{
+    char *ptr = talloc_strndup (ctx, str, len);
+
+    if (ptr)
+       talloc_set_name_const (ptr, name);
+
+    return ptr;
+}
diff --git a/util/talloc-extra.h b/util/talloc-extra.h
new file mode 100644 (file)
index 0000000..e2e6173
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef _TALLOC_EXTRA_H
+#define _TALLOC_EXTRA_H
+
+#include <talloc.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Like talloc_strndup, but take an extra parameter for the internal talloc
+ * name (for debugging) */
+
+char *
+talloc_strndup_named_const (void *ctx, const char *str,
+                           size_t len, const char *name);
+
+/* use the __location__ macro from talloc.h to name a string according to its
+ * source location */
+
+#define talloc_strndup_debug(ctx, str, len) talloc_strndup_named_const (ctx, str, len, __location__)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/util/unicode-util.c b/util/unicode-util.c
new file mode 100644 (file)
index 0000000..ccb787e
--- /dev/null
@@ -0,0 +1,43 @@
+#include "unicode-util.h"
+
+/* Based on Xapian::Unicode::is_wordchar, to avoid forcing clients to
+ * link directly to libxapian.
+ */
+
+static bool
+unicode_is_wordchar (notmuch_unichar ch)
+{
+    switch (g_unichar_type (ch)) {
+    case G_UNICODE_UPPERCASE_LETTER:
+    case G_UNICODE_LOWERCASE_LETTER:
+    case G_UNICODE_TITLECASE_LETTER:
+    case G_UNICODE_MODIFIER_LETTER:
+    case G_UNICODE_OTHER_LETTER:
+    case G_UNICODE_NON_SPACING_MARK:
+    case G_UNICODE_ENCLOSING_MARK:
+    case G_UNICODE_SPACING_MARK:
+    case G_UNICODE_DECIMAL_NUMBER:
+    case G_UNICODE_LETTER_NUMBER:
+    case G_UNICODE_OTHER_NUMBER:
+    case G_UNICODE_CONNECT_PUNCTUATION:
+       return true;
+    default:
+       return false;
+    }
+}
+
+bool
+unicode_word_utf8 (const char *utf8_str)
+{
+    gunichar *decoded = g_utf8_to_ucs4_fast (utf8_str, -1, NULL);
+    const gunichar *p = decoded;
+    bool ret;
+
+    while (*p && unicode_is_wordchar (*p))
+       p++;
+
+    ret =  (*p == '\0');
+
+    g_free (decoded);
+    return ret;
+}
diff --git a/util/unicode-util.h b/util/unicode-util.h
new file mode 100644 (file)
index 0000000..1bb9336
--- /dev/null
@@ -0,0 +1,19 @@
+#ifndef UNICODE_UTIL_H
+#define UNICODE_UTIL_H
+
+#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/util.c b/util/util.c
new file mode 100644 (file)
index 0000000..6abe221
--- /dev/null
@@ -0,0 +1,24 @@
+#include "util.h"
+#include "error_util.h"
+#include <string.h>
+#include <errno.h>
+
+const char *
+util_error_string (util_status_t errnum)
+{
+    switch (errnum) {
+    case UTIL_SUCCESS:
+       return "success";
+    case UTIL_OUT_OF_MEMORY:
+       return "out of memory";
+    case UTIL_EOF:
+       return "end of file";
+    case UTIL_ERRNO:
+       return strerror (errno);
+    case UTIL_GZERROR:
+       /* we lack context to be more informative here */
+       return "zlib error";
+    default:
+       INTERNAL_ERROR ("unexpected error status %d", errnum);
+    }
+}
diff --git a/util/util.h b/util/util.h
new file mode 100644 (file)
index 0000000..b24860a
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef _UTIL_H
+#define _UTIL_H
+
+typedef enum util_status {
+    /**
+     * No error occurred.
+     */
+    UTIL_SUCCESS = 0,
+    /**
+     * Out of memory.
+     */
+    UTIL_OUT_OF_MEMORY,
+    /**
+     * End of stream reached while attempting to read.
+     */
+    UTIL_EOF,
+    /**
+     * Low level error occurred, consult errno.
+     */
+    UTIL_ERRNO,
+    /**
+     * Zlib error occurred, call gzerror for details.
+     */
+    UTIL_GZERROR
+} util_status_t;
+
+const char *
+util_error_string (util_status_t status);
+#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
diff --git a/util/xutil.c b/util/xutil.c
new file mode 100644 (file)
index 0000000..07a0034
--- /dev/null
@@ -0,0 +1,139 @@
+/* xutil.c - Various wrapper functions to abort on error.
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "xutil.h"
+#include "error_util.h"
+
+void *
+xcalloc (size_t nmemb, size_t size)
+{
+    void *ret;
+
+    ret = calloc (nmemb, size);
+    if (ret == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       exit (1);
+    }
+
+    return ret;
+}
+
+void *
+xmalloc (size_t size)
+{
+    void *ret;
+
+    ret = malloc (size);
+    if (ret == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       exit (1);
+    }
+
+    return ret;
+}
+
+void *
+xrealloc (void *ptr, size_t size)
+{
+    void *ret;
+
+    ret = realloc (ptr, size);
+    if (ret == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       exit (1);
+    }
+
+    return ret;
+}
+
+char *
+xstrdup (const char *s)
+{
+    char *ret;
+
+    ret = strdup (s);
+    if (ret == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       exit (1);
+    }
+
+    return ret;
+}
+
+char *
+xstrndup (const char *s, size_t n)
+{
+    char *ret;
+
+    if (strlen (s) <= n)
+       n = strlen (s);
+
+    ret = malloc (n + 1);
+    if (ret == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       exit (1);
+    }
+    memcpy (ret, s, n);
+    ret[n] = '\0';
+
+    return ret;
+}
+
+int
+xregcomp (regex_t *preg, const char *regex, int cflags)
+{
+    int rerr;
+
+    rerr = regcomp (preg, regex, cflags);
+    if (rerr) {
+       size_t error_size = regerror (rerr, preg, NULL, 0);
+       char *error = xmalloc (error_size);
+
+       regerror (rerr, preg, error, error_size);
+       fprintf (stderr, "compiling regex %s: %s\n",
+                regex, error);
+       free (error);
+       return 1;
+    }
+    return 0;
+}
+
+int
+xregexec (const regex_t *preg, const char *string,
+         size_t nmatch, regmatch_t pmatch[], int eflags)
+{
+    unsigned int i;
+    int rerr;
+
+    rerr = regexec (preg, string, nmatch, pmatch, eflags);
+    if (rerr)
+       return rerr;
+
+    for (i = 0; i < nmatch; i++) {
+       if (pmatch[i].rm_so == -1)
+           INTERNAL_ERROR ("matching regex against %s: Sub-match %d not found\n",
+                           string, i);
+    }
+
+    return 0;
+}
diff --git a/util/xutil.h b/util/xutil.h
new file mode 100644 (file)
index 0000000..e270700
--- /dev/null
@@ -0,0 +1,60 @@
+/* xutil.h - Various wrapper functions to abort on error.
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ */
+
+#ifndef NOTMUCH_XUTIL_H
+#define NOTMUCH_XUTIL_H
+
+#include <stdlib.h>
+#include <sys/types.h>
+#include <regex.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* xutil.c */
+void *
+xcalloc (size_t nmemb, size_t size);
+
+void *
+xmalloc (size_t size);
+
+void *
+xrealloc (void *ptrr, size_t size);
+
+char *
+xstrdup (const char *s);
+
+char *
+xstrndup (const char *s, size_t n);
+
+/* Returns 0 for successful compilation, 1 otherwise */
+int
+xregcomp (regex_t *preg, const char *regex, int cflags);
+
+int
+xregexec (const regex_t *preg, const char *string,
+         size_t nmatch, regmatch_t pmatch[], int eflags);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/util/zlib-extra.c b/util/zlib-extra.c
new file mode 100644 (file)
index 0000000..1f5f9db
--- /dev/null
@@ -0,0 +1,95 @@
+/* zlib-extra.c -  Extra or enhanced routines for compressed I/O.
+ *
+ * Copyright (c) 2014 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 "zlib-extra.h"
+#include <talloc.h>
+#include <stdio.h>
+#include <string.h>
+
+/* mimic POSIX/glibc getline, but on a zlib gzFile stream, and using talloc */
+util_status_t
+gz_getline (void *talloc_ctx, char **bufptr, ssize_t *bytes_read, gzFile stream)
+{
+    char *buf = *bufptr;
+    unsigned int len;
+    size_t offset = 0;
+
+    if (buf) {
+       len = talloc_array_length (buf);
+    } else {
+       /* same as getdelim from gnulib */
+       len = 120;
+       buf = talloc_array (talloc_ctx, char, len);
+       if (buf == NULL)
+           return UTIL_OUT_OF_MEMORY;
+    }
+
+    while (1) {
+       if (! gzgets (stream, buf + offset, len - offset)) {
+           /* Null indicates EOF or error */
+           int zlib_status = 0;
+           (void) gzerror (stream, &zlib_status);
+           switch (zlib_status) {
+           case Z_STREAM_END:
+           case Z_OK:
+               /* no data read before EOF */
+               if (offset == 0)
+                   return UTIL_EOF;
+               else
+                   goto SUCCESS;
+           case Z_ERRNO:
+               return UTIL_ERRNO;
+           default:
+               return UTIL_GZERROR;
+           }
+       }
+
+       offset += strlen (buf + offset);
+
+       if (buf[offset - 1] == '\n')
+           goto SUCCESS;
+
+       len *= 2;
+       buf = talloc_realloc (talloc_ctx, buf, char, len);
+       if (buf == NULL)
+           return UTIL_OUT_OF_MEMORY;
+    }
+  SUCCESS:
+    *bufptr = buf;
+    *bytes_read = offset;
+    return UTIL_SUCCESS;
+}
+
+const char *
+gz_error_string (util_status_t status, gzFile file)
+{
+    if (status == UTIL_GZERROR)
+       return gzerror_str (file);
+    else
+       return util_error_string (status);
+}
+
+const char *
+gzerror_str (gzFile file)
+{
+    int dummy;
+
+    return gzerror (file, &dummy);
+}
diff --git a/util/zlib-extra.h b/util/zlib-extra.h
new file mode 100644 (file)
index 0000000..7532339
--- /dev/null
@@ -0,0 +1,39 @@
+#ifndef _ZLIB_EXTRA_H
+#define _ZLIB_EXTRA_H
+
+#include "util.h"
+#include <zlib.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Like getline, but read from a gzFile. Allocation is with talloc.
+ * Returns:
+ *
+ *   UTIL_SUCCESS, UTIL_OUT_OF_MEMORY, UTIL_ERRNO, UTIL_GZERROR
+ *                     Consult util.h for description
+ *
+ *   UTIL_EOF          End of file encountered before
+ *                     any characters read
+ */
+util_status_t
+gz_getline (void *ctx, char **lineptr, ssize_t *bytes_read, gzFile stream);
+
+/* return a suitable error string based on the return status
+ *  from gz_readline
+ */
+
+const char *
+gz_error_string (util_status_t status, gzFile stream);
+
+/* Call gzerror with a dummy errno argument, the docs don't promise to
+ * support the NULL case */
+const char *
+gzerror_str (gzFile file);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/version.txt b/version.txt
new file mode 100644 (file)
index 0000000..f2e78d4
--- /dev/null
@@ -0,0 +1 @@
+0.38.2
diff --git a/vim/Makefile b/vim/Makefile
new file mode 100644 (file)
index 0000000..b6f9db7
--- /dev/null
@@ -0,0 +1,15 @@
+prefix = $(HOME)/.vim
+
+INSTALL = install -v -D -m644
+D = $(DESTDIR)
+
+all:
+       @echo "Nothing to build"
+
+install:
+       $(INSTALL) $(CURDIR)/notmuch.vim $(D)$(prefix)/plugin/notmuch.vim
+       $(INSTALL) $(CURDIR)/notmuch.txt $(D)$(prefix)/doc/notmuch.txt
+       @$(foreach file,$(wildcard syntax/*), \
+               $(INSTALL) $(CURDIR)/$(file) $(D)$(prefix)/$(file);)
+
+.PHONY: all install
diff --git a/vim/README b/vim/README
new file mode 100644 (file)
index 0000000..777c20c
--- /dev/null
@@ -0,0 +1,62 @@
+== notmuch vim ruby ==
+
+This is a vim plug-in that provides a fully usable mail client interface,
+utilizing the notmuch framework, through it's ruby bindings.
+
+== install ==
+
+Simply run 'make install'. However, check that you have the dependencies below.
+
+=== vim +ruby ===
+
+Make sure your vim version has ruby support: check for +ruby in 'vim --version'
+features.
+
+=== ruby bindings ===
+
+Check if you are able to run the following command cleanly:
+
+ % ruby -e "require 'notmuch'"
+
+If you don't see any errors, it means it's working and you can go to the next
+section.
+
+If it's not, you would need to compile them. Go to the 'bindings/ruby'
+directory in the notmuch source tree.
+
+=== mail gem ===
+
+Since libnotmuch library concentrates on things other than handling mail, we
+need a library to do that, and for Ruby the best library for that is called
+'mail'. The easiest way to install it is with ruby's gem. In most distro's the
+package is called 'rubygems'.
+
+Once you have gem, run:
+
+ % gem install mail
+
+In some systems gems are installed on a per-user basis by default, so make sure
+you are running as the same user as the one that installed them.
+
+This gem is not mandatory, but it's extremely recommended.
+
+== Running ==
+
+Simple:
+
+ % gvim -c ':NotMuch'
+
+Enjoy ;)
+
+== More stuff ==
+
+As an example to configure a key mapping to add the tag 'to-do' and archive,
+this is what I use:
+
+let g:notmuch_custom_search_maps = {
+       \ 't':          'search_tag("+to-do -inbox")',
+       \ }
+
+let g:notmuch_custom_show_maps = {
+       \ 't':          'show_tag("+to-do -inbox")',
+       \ }
diff --git a/vim/notmuch.txt b/vim/notmuch.txt
new file mode 100644 (file)
index 0000000..c98f2b5
--- /dev/null
@@ -0,0 +1,153 @@
+*notmuch.txt*  Plug-in to make vim a nice email client using notmuch
+
+Author: Felipe Contreras <felipe.contreras@gmail.com>
+
+Overview                                       |notmuch-intro|
+Usage                                          |notmuch-usage|
+Mappings                                       |notmuch-mappings|
+Configuration                                  |notmuch-config|
+
+==============================================================================
+OVERVIEW                                       *notmuch-intro*
+
+This is a vim plug-in that provides a fully usable mail client interface,
+utilizing the notmuch framework.
+
+It has three main views: folders, search, and thread. In the folder view you
+can find a summary of saved searches, In the search view you can see all the
+threads that comprise the selected search, and in the thread view you can read
+every mail in the thread.
+
+==============================================================================
+USAGE                                          *notmuch-usage*
+
+To use it, simply run the `:NotMuch` command.
+
+By default you start in the folder view which shows you default searches and
+the number of threads that match those:
+>
+       10 new                  (tag:inbox and tag:unread)
+       20 inbox                (tag:inbox)
+       30 unread               (tag:unread)
+<
+You can see the threads of each by clicking `enter`, which sends you to the
+search view. In both the search and folder views you can type `s` to type a
+new search, or `=` to refresh. To see a thread, type `enter` again.
+
+To exit a view, click `q`.
+
+Also, you can specify a search directly:
+>
+       :NotMuch is:inbox and date:yesterday..
+<
+==============================================================================
+MAPPINGS                                       *notmuch-mappings*
+
+------------------------------------------------------------------------------
+Folder view~
+
+<enter>        Show selected search
+s      Enter a new search
+=      Refresh
+c      Compose a new mail
+
+------------------------------------------------------------------------------
+Search view~
+
+q      Quit view
+<enter>        Show selected search
+<space>        Show selected search with filter
+A      Archive (-inbox -unread)
+I      Mark as read (-unread)
+t      Tag (prompted)
+s      Search
+=      Refresh
+?      Show search information
+c      Compose a new mail
+>
+------------------------------------------------------------------------------
+Thread view~
+
+q      Quit view
+A      Archive (-inbox -unread)
+I      Mark as read (-unread)
+t      Tag (prompted)
+s      Search
+p      Save patches
+r      Reply
+?      Show thread information
+<tab>  Show next message
+c      Compose a new mail
+
+------------------------------------------------------------------------------
+Compose view~
+
+q      Quit view
+s      Send
+
+==============================================================================
+CONFIGURATION                                  *notmuch-config*
+
+You can add the following configurations to your `.vimrc`, or
+`~/.vim/after/plugin/notmuch.vim`.
+
+                                               *g:notmuch_folders*
+
+The first thing you might want to do is set your custom searches.
+>
+       let g:notmuch_folders = [
+               \ [ 'new', 'tag:inbox and tag:unread' ],
+               \ [ 'inbox', 'tag:inbox' ],
+               \ [ 'unread', 'tag:unread' ],
+               \ [ 'to-do', 'tag:to-do' ],
+               \ [ 'to-me', 'to:john.doe and tag:new' ],
+               \ ]
+<
+
+                                               *g:notmuch_custom_search_maps*
+                                               *g:notmuch_custom_show_maps*
+
+You can also configure the keyboard mappings for the different views:
+>
+       let g:notmuch_custom_search_maps = {
+               \ 't':          'search_tag("+to-do -inbox")',
+               \ 'd':          'search_tag("+deleted -inbox -unread")',
+               \ }
+
+       let g:notmuch_custom_show_maps = {
+               \ 't':          'show_tag("+to-do -inbox")',
+               \ 'd':          'show_tag("+deleted -inbox -unread")',
+               \ }
+<
+
+                                               *g:notmuch_date_format*
+
+To configure the date format you want in the search view:
+>
+       let g:notmuch_date_format = '%d.%m.%y'
+<
+
+                                               *g:notmuch_datetime_format*
+
+You can do the same for the thread view:
+>
+       let g:notmuch_datetime_format = '%d.%m.%y %H:%M:%S'
+<
+
+                                               *g:notmuch_folders_count_threads*
+
+If you want to count the threads instead of the messages in the folder view:
+>
+       let g:notmuch_folders_count_threads = 1
+<
+
+                                               *g:notmuch_reader*
+                                               *g:notmuch_sendmail*
+
+You can also configure your external mail reader and sendmail program:
+>
+       let g:notmuch_reader = 'mutt -f %s'
+       let g:notmuch_sendmail = 'sendmail'
+<
+
+vim:tw=78:ts=8:noet:ft=help:
diff --git a/vim/notmuch.vim b/vim/notmuch.vim
new file mode 100644 (file)
index 0000000..c1c2f63
--- /dev/null
@@ -0,0 +1,973 @@
+if exists("g:loaded_notmuch")
+       finish
+endif
+
+if !has("ruby") || version < 700
+       finish
+endif
+
+let g:loaded_notmuch = "yep"
+
+let g:notmuch_folders_maps = {
+       \ '<Enter>':    'folders_show_search()',
+       \ 's':          'folders_search_prompt()',
+       \ '=':          'folders_refresh()',
+       \ 'c':          'compose()',
+       \ }
+
+let g:notmuch_search_maps = {
+       \ 'q':          'kill_this_buffer()',
+       \ '<Enter>':    'search_show_thread(1)',
+       \ '<Space>':    'search_show_thread(2)',
+       \ 'A':          'search_tag("-inbox -unread")',
+       \ 'I':          'search_tag("-unread")',
+       \ 't':          'search_tag("")',
+       \ 's':          'search_search_prompt()',
+       \ '=':          'search_refresh()',
+       \ '?':          'search_info()',
+       \ 'c':          'compose()',
+       \ }
+
+let g:notmuch_show_maps = {
+       \ 'q':          'kill_this_buffer()',
+       \ 'A':          'show_tag("-inbox -unread")',
+       \ 'I':          'show_tag("-unread")',
+       \ 't':          'show_tag("")',
+       \ 'o':          'show_open_msg()',
+       \ 'e':          'show_extract_msg()',
+       \ 's':          'show_save_msg()',
+       \ 'p':          'show_save_patches()',
+       \ 'r':          'show_reply()',
+       \ '?':          'show_info()',
+       \ '<Tab>':      'show_next_msg()',
+       \ 'c':          'compose()',
+       \ }
+
+let g:notmuch_compose_maps = {
+       \ ',s':         'compose_send()',
+       \ ',q':         'compose_quit()',
+       \ }
+
+let s:notmuch_folders_default = [
+       \ [ 'new', 'tag:inbox and tag:unread' ],
+       \ [ 'inbox', 'tag:inbox' ],
+       \ [ 'unread', 'tag:unread' ],
+       \ ]
+
+let s:notmuch_date_format_default = '%d.%m.%y'
+let s:notmuch_datetime_format_default = '%d.%m.%y %H:%M:%S'
+let s:notmuch_reader_default = 'mutt -f %s'
+let s:notmuch_sendmail_default = 'sendmail'
+let s:notmuch_folders_count_threads_default = 0
+let s:notmuch_compose_start_insert_default = 1
+
+function! s:new_file_buffer(type, fname)
+       exec printf('edit %s', a:fname)
+       execute printf('set filetype=notmuch-%s', a:type)
+       execute printf('set syntax=notmuch-%s', a:type)
+       ruby $curbuf.init(VIM::evaluate('a:type'))
+endfunction
+
+function! s:on_compose_delete()
+       if b:compose_done
+               return
+       endif
+       if input('[s]end/[q]uit? ') =~ '^s'
+               call s:compose_send()
+       endif
+endfunction
+
+"" actions
+
+function! s:compose_quit()
+       let b:compose_done = 1
+       call s:kill_this_buffer()
+endfunction
+
+function! s:compose_send()
+       let b:compose_done = 1
+       let fname = expand('%')
+       let lines = getline(5, '$')
+
+ruby << EOF
+       # Generate proper mail to send
+       text = VIM::evaluate('lines').join("\n")
+       fname = VIM::evaluate('fname')
+       transport = Mail.new(text)
+       transport.message_id = generate_message_id
+       transport.charset = 'utf-8'
+       File.write(fname, transport.to_s)
+EOF
+
+       let cmdtxt = g:notmuch_sendmail . ' -t -f ' . s:reply_from . ' < ' . fname
+       let out = system(cmdtxt)
+       let err = v:shell_error
+       if err
+               echohl Error
+               echo 'Eeek! unable to send mail'
+               echo out
+               echohl None
+               return
+       endif
+       call delete(fname)
+       echo 'Mail sent successfully.'
+       call s:kill_this_buffer()
+endfunction
+
+function! s:show_next_msg()
+ruby << EOF
+       r, c = $curwin.cursor
+       n = $curbuf.line_number
+       i = $messages.index { |m| n >= m.start && n <= m.end }
+       m = $messages[i + 1]
+       if m
+               r = m.body_start + 1
+               VIM::command("normal #{m.start}zt")
+               $curwin.cursor = r, c
+       end
+EOF
+endfunction
+
+function! s:show_reply()
+       ruby open_reply get_message.mail
+       let b:compose_done = 0
+       call s:set_map(g:notmuch_compose_maps)
+       autocmd BufDelete <buffer> call s:on_compose_delete()
+       if g:notmuch_compose_start_insert
+               startinsert!
+       end
+endfunction
+
+function! s:compose()
+       ruby open_compose
+       let b:compose_done = 0
+       call s:set_map(g:notmuch_compose_maps)
+       autocmd BufDelete <buffer> call s:on_compose_delete()
+       if g:notmuch_compose_start_insert
+               startinsert!
+       end
+endfunction
+
+function! s:show_info()
+       ruby vim_puts get_message.inspect
+endfunction
+
+function! s:show_extract_msg()
+ruby << EOF
+       m = get_message
+       m.mail.attachments.each do |a|
+               File.open(a.filename, 'w') do |f|
+                       f.write a.body.decoded
+                       print "Extracted '#{a.filename}'"
+               end
+       end
+EOF
+endfunction
+
+function! s:show_open_msg()
+ruby << EOF
+       m = get_message
+       mbox = File.expand_path('~/.notmuch/vim_mbox')
+       cmd = VIM::evaluate('g:notmuch_reader') % mbox
+       system "notmuch show --format=mbox id:#{m.message_id} > #{mbox} && #{cmd}"
+EOF
+endfunction
+
+function! s:show_save_msg()
+       let file = input('File name: ')
+ruby << EOF
+       file = VIM::evaluate('file')
+       m = get_message
+       system "notmuch show --format=mbox id:#{m.message_id} > #{file}"
+EOF
+endfunction
+
+function! s:show_save_patches()
+ruby << EOF
+       q = $curbuf.query($cur_thread)
+       t = q.search_threads.first
+       n = 0
+       t.toplevel_messages.first.replies.each do |m|
+               next if not m['subject'] =~ /^\[PATCH.*\]/
+               file = "%04d.patch" % [n += 1]
+               system "notmuch show --format=mbox id:#{m.message_id} > #{file}"
+       end
+       vim_puts "Saved #{n} patches"
+EOF
+endfunction
+
+function! s:show_tag(intags)
+       if empty(a:intags)
+               let tags = input('tags: ')
+       else
+               let tags = a:intags
+       endif
+       ruby do_tag(get_cur_view, VIM::evaluate('l:tags'))
+       call s:show_next_thread()
+endfunction
+
+function! s:search_search_prompt()
+       let text = input('Search: ')
+       if text == ""
+         return
+       endif
+       setlocal modifiable
+ruby << EOF
+       $cur_search = VIM::evaluate('text')
+       $curbuf.reopen
+       search_render($cur_search)
+EOF
+       setlocal nomodifiable
+endfunction
+
+function! s:search_info()
+       ruby vim_puts get_thread_id
+endfunction
+
+function! s:search_refresh()
+       setlocal modifiable
+       ruby $curbuf.reopen
+       ruby search_render($cur_search)
+       setlocal nomodifiable
+endfunction
+
+function! s:search_tag(intags)
+       if empty(a:intags)
+               let tags = input('tags: ')
+       else
+               let tags = a:intags
+       endif
+       ruby do_tag(get_thread_id, VIM::evaluate('l:tags'))
+       norm j
+endfunction
+
+function! s:folders_search_prompt()
+       let text = input('Search: ')
+       call s:search(text)
+endfunction
+
+function! s:folders_refresh()
+       setlocal modifiable
+       ruby $curbuf.reopen
+       ruby folders_render()
+       setlocal nomodifiable
+endfunction
+
+"" basic
+
+function! s:show_cursor_moved()
+ruby << EOF
+       if $render.is_ready?
+               VIM::command('setlocal modifiable')
+               $render.do_next
+               VIM::command('setlocal nomodifiable')
+       end
+EOF
+endfunction
+
+function! s:show_next_thread()
+       call s:kill_this_buffer()
+       if line('.') != line('$')
+               norm j
+               call s:search_show_thread(0)
+       else
+               echo 'No more messages.'
+       endif
+endfunction
+
+function! s:kill_this_buffer()
+ruby << EOF
+       $curbuf.close
+       VIM::command("bdelete!")
+EOF
+endfunction
+
+function! s:set_map(maps)
+       nmapclear <buffer>
+       for [key, code] in items(a:maps)
+               let cmd = printf(":call <SID>%s<CR>", code)
+               exec printf('nnoremap <buffer> %s %s', key, cmd)
+       endfor
+endfunction
+
+function! s:new_buffer(type)
+       enew
+       setlocal buftype=nofile bufhidden=hide
+       keepjumps 0d
+       execute printf('set filetype=notmuch-%s', a:type)
+       execute printf('set syntax=notmuch-%s', a:type)
+       ruby $curbuf.init(VIM::evaluate('a:type'))
+endfunction
+
+function! s:set_menu_buffer()
+       setlocal nomodifiable
+       setlocal cursorline
+       setlocal nowrap
+endfunction
+
+"" main
+
+function! s:show(thread_id)
+       call s:new_buffer('show')
+       setlocal modifiable
+ruby << EOF
+       thread_id = VIM::evaluate('a:thread_id')
+       $cur_thread = thread_id
+       $messages.clear
+       $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)
+                       part = m.find_first_text
+                       nm_m = Message.new(msg, m)
+                       $messages << nm_m
+                       date_fmt = VIM::evaluate('g:notmuch_datetime_format')
+                       date = Time.at(msg.date).strftime(date_fmt)
+                       nm_m.start = b.count
+                       b << "%s %s (%s)" % [msg['from'], date, msg.tags]
+                       b << "Subject: %s" % [msg['subject']]
+                       b << "To: %s" % msg['to']
+                       b << "Cc: %s" % msg['cc']
+                       b << "Date: %s" % msg['date']
+                       nm_m.body_start = b.count
+                       b << "--- %s ---" % part.mime_type
+                       part.convert.each_line do |l|
+                               b << l.chomp
+                       end
+                       b << ""
+                       nm_m.end = b.count
+               end
+               b.delete(b.count)
+       end
+       $messages.each_with_index do |msg, i|
+               VIM::command("syntax region nmShowMsg#{i}Desc start='\\%%%il' end='\\%%%il' contains=@nmShowMsgDesc" % [msg.start, msg.start + 1])
+               VIM::command("syntax region nmShowMsg#{i}Head start='\\%%%il' end='\\%%%il' contains=@nmShowMsgHead" % [msg.start + 1, msg.body_start])
+               VIM::command("syntax region nmShowMsg#{i}Body start='\\%%%il' end='\\%%%dl' contains=@nmShowMsgBody" % [msg.body_start, msg.end])
+       end
+EOF
+       setlocal nomodifiable
+       call s:set_map(g:notmuch_show_maps)
+endfunction
+
+function! s:search_show_thread(mode)
+ruby << EOF
+       mode = VIM::evaluate('a:mode')
+       id = get_thread_id
+       case mode
+       when 0;
+       when 1; $cur_filter = nil
+       when 2; $cur_filter = $cur_search
+       end
+       VIM::command("call s:show('#{id}')")
+EOF
+endfunction
+
+function! s:search(search)
+       call s:new_buffer('search')
+ruby << EOF
+       $cur_search = VIM::evaluate('a:search')
+       search_render($cur_search)
+EOF
+       call s:set_menu_buffer()
+       call s:set_map(g:notmuch_search_maps)
+       autocmd CursorMoved <buffer> call s:show_cursor_moved()
+endfunction
+
+function! s:folders_show_search()
+ruby << EOF
+       n = $curbuf.line_number
+       s = $searches[n - 1]
+       VIM::command("call s:search('#{s}')")
+EOF
+endfunction
+
+function! s:folders()
+       call s:new_buffer('folders')
+       ruby folders_render()
+       call s:set_menu_buffer()
+       call s:set_map(g:notmuch_folders_maps)
+endfunction
+
+"" root
+
+function! s:set_defaults()
+       if !exists('g:notmuch_date_format')
+               if exists('g:notmuch_rb_date_format')
+                       let g:notmuch_date_format = g:notmuch_rb_date_format
+               else
+                       let g:notmuch_date_format = s:notmuch_date_format_default
+               endif
+       endif
+
+       if !exists('g:notmuch_datetime_format')
+               if exists('g:notmuch_rb_datetime_format')
+                       let g:notmuch_datetime_format = g:notmuch_rb_datetime_format
+               else
+                       let g:notmuch_datetime_format = s:notmuch_datetime_format_default
+               endif
+       endif
+
+       if !exists('g:notmuch_reader')
+               if exists('g:notmuch_rb_reader')
+                       let g:notmuch_reader = g:notmuch_rb_reader
+               else
+                       let g:notmuch_reader = s:notmuch_reader_default
+               endif
+       endif
+
+       if !exists('g:notmuch_sendmail')
+               if exists('g:notmuch_rb_sendmail')
+                       let g:notmuch_sendmail = g:notmuch_rb_sendmail
+               else
+                       let g:notmuch_sendmail = s:notmuch_sendmail_default
+               endif
+       endif
+
+       if !exists('g:notmuch_folders_count_threads')
+               if exists('g:notmuch_rb_count_threads')
+                       let g:notmuch_count_threads = g:notmuch_rb_count_threads
+               else
+                       let g:notmuch_folders_count_threads = s:notmuch_folders_count_threads_default
+               endif
+       endif
+
+       if !exists('g:notmuch_compose_start_insert')
+               let g:notmuch_compose_start_insert = s:notmuch_compose_start_insert_default
+       endif
+
+       if !exists('g:notmuch_custom_search_maps') && exists('g:notmuch_rb_custom_search_maps')
+               let g:notmuch_custom_search_maps = g:notmuch_rb_custom_search_maps
+       endif
+
+       if !exists('g:notmuch_custom_show_maps') && exists('g:notmuch_rb_custom_show_maps')
+               let g:notmuch_custom_show_maps = g:notmuch_rb_custom_show_maps
+       endif
+
+       if exists('g:notmuch_custom_search_maps')
+               call extend(g:notmuch_search_maps, g:notmuch_custom_search_maps)
+       endif
+
+       if exists('g:notmuch_custom_show_maps')
+               call extend(g:notmuch_show_maps, g:notmuch_custom_show_maps)
+       endif
+
+       if !exists('g:notmuch_folders')
+               if exists('g:notmuch_rb_folders')
+                       let g:notmuch_folders = g:notmuch_rb_folders
+               else
+                       let g:notmuch_folders = s:notmuch_folders_default
+               endif
+       endif
+endfunction
+
+function! s:NotMuch(...)
+       call s:set_defaults()
+
+ruby << EOF
+       require 'notmuch'
+       require 'rubygems'
+       require 'tempfile'
+       require 'socket'
+       begin
+               require 'mail'
+       rescue LoadError
+       end
+
+       $db_name = nil
+       $email = $email_name = $email_address = nil
+       $exclude_tags = []
+       $searches = []
+       $threads = []
+       $messages = []
+       $mail_installed = defined?(Mail)
+
+       def get_config_item(item)
+               result = ''
+               IO.popen(['notmuch', 'config', 'get', item]) { |out|
+                       result = out.read
+               }
+               return result.rstrip
+       end
+
+       def get_config
+               $db_name = get_config_item('database.path')
+               $email_name = get_config_item('user.name')
+               $email_address = get_config_item('user.primary_email')
+               $email_name = get_config_item('user.name')
+               $email = "%s <%s>" % [$email_name, $email_address]
+               ignore_tags = get_config_item('search.exclude_tags')
+               $exclude_tags = ignore_tags.split("\n")
+       end
+
+       def vim_puts(s)
+               VIM::command("echo '#{s.to_s}'")
+       end
+
+       def vim_p(s)
+               VIM::command("echo '#{s.inspect}'")
+       end
+
+       def author_filter(a)
+               # TODO email format, aliases
+               a.strip!
+               a.gsub!(/[\.@].*/, '')
+               a.gsub!(/^ext /, '')
+               a.gsub!(/ \(.*\)/, '')
+               a
+       end
+
+       def get_thread_id
+               n = $curbuf.line_number - 1
+               return "thread:%s" % $threads[n]
+       end
+
+       def get_message
+               n = $curbuf.line_number
+               return $messages.find { |m| n >= m.start && n <= m.end }
+       end
+
+       def get_cur_view
+               if $cur_filter
+                       return "#{$cur_thread} and (#{$cur_filter})"
+               else
+                       return $cur_thread
+               end
+       end
+
+       def generate_message_id
+               t = Time.now
+               random_tag = sprintf('%x%x_%x%x%x',
+                       t.to_i, t.tv_usec,
+                       $$, Thread.current.object_id.abs, rand(255))
+               return "<#{random_tag}@#{Socket.gethostname}.notmuch>"
+       end
+
+       def open_compose_helper(lines, cur)
+               help_lines = [
+                       'Notmuch-Help: Type in your message here; to help you use these bindings:',
+                       'Notmuch-Help:   ,s    - send the message (Notmuch-Help lines will be removed)',
+                       'Notmuch-Help:   ,q    - abort the message',
+                       ]
+
+               dir = File.expand_path('~/.notmuch/compose')
+               FileUtils.mkdir_p(dir)
+               Tempfile.open(['nm-', '.mail'], dir) do |f|
+                       f.puts(help_lines)
+                       f.puts
+                       f.puts(lines)
+
+                       sig_file = File.expand_path('~/.signature')
+                       if File.exists?(sig_file)
+                               f.puts("-- ")
+                               f.write(File.read(sig_file))
+                       end
+
+                       f.flush
+
+                       cur += help_lines.size + 1
+
+                       VIM::command("let s:reply_from='%s'" % $email_address)
+                       VIM::command("call s:new_file_buffer('compose', '#{f.path}')")
+                       VIM::command("call cursor(#{cur}, 0)")
+               end
+       end
+
+       def open_reply(orig)
+               reply = orig.reply do |m|
+                       # fix headers
+                       if not m[:reply_to]
+                               m.to = [orig[:from].to_s, orig[:to].to_s]
+                       end
+                       m.cc = orig[:cc]
+                       m.from = $email
+                       m.charset = 'utf-8'
+               end
+
+               lines = []
+
+               body_lines = []
+               if $mail_installed
+                       addr = Mail::Address.new(orig[:from].value)
+                       name = addr.name
+                       name = addr.local + "@" if name.nil? && !addr.local.nil?
+               else
+                       name = orig[:from]
+               end
+               name = "somebody" if name.nil?
+
+               body_lines << "%s wrote:" % name
+               part = orig.find_first_text
+               part.convert.each_line do |l|
+                       body_lines << "> %s" % l.chomp
+               end
+               body_lines << ""
+               body_lines << ""
+               body_lines << ""
+
+               reply.body = body_lines.join("\n")
+
+               lines += reply.present.lines.map { |e| e.chomp }
+               lines << ""
+
+               cur = lines.count - 1
+
+               open_compose_helper(lines, cur)
+       end
+
+       def open_compose()
+               lines = []
+
+               lines << "From: #{$email}"
+               lines << "To: "
+               cur = lines.count
+
+               lines << "Cc: "
+               lines << "Bcc: "
+               lines << "Subject: "
+               lines << ""
+               lines << ""
+               lines << ""
+
+               open_compose_helper(lines, cur)
+       end
+
+       def folders_render()
+               $curbuf.render do |b|
+                       folders = VIM::evaluate('g:notmuch_folders')
+                       count_threads = VIM::evaluate('g:notmuch_folders_count_threads') == 1
+                       $searches.clear
+                       folders.each do |name, search|
+                               q = $curbuf.query(search)
+                               $exclude_tags.each { |t|
+                                       q.add_tag_exclude(t)
+                               }
+                               $searches << search
+                               count = count_threads ? q.count_threads : q.count_messages
+                               b << "%9d %-20s (%s)" % [count, name, search]
+                       end
+               end
+       end
+
+       def search_render(search)
+               date_fmt = VIM::evaluate('g:notmuch_date_format')
+               q = $curbuf.query(search)
+               q.sort = Notmuch::SORT_NEWEST_FIRST
+               $exclude_tags.each { |t|
+                       q.add_tag_exclude(t)
+               }
+               $threads.clear
+               t = q.search_threads
+
+               $render = $curbuf.render_staged(t) do |b, items|
+                       items.each do |e|
+                               authors = e.authors.to_utf8.split(/[,|]/).map { |a| author_filter(a) }.join(",")
+                               date = Time.at(e.newest_date).strftime(date_fmt)
+                               subject = e.messages.first['subject']
+                               if $mail_installed
+                                       subject = Mail::Field.parse("Subject: " + subject).to_s
+                               else
+                                       subject = subject.force_encoding('utf-8')
+                               end
+                               b << "%-12s %3s %-20.20s | %s (%s)" % [date, e.matched_messages, authors, subject, e.tags]
+                               $threads << e.thread_id
+                       end
+               end
+       end
+
+       def do_tag(filter, tags)
+               $curbuf.do_write do |db|
+                       q = db.query(filter)
+                       q.search_messages.each do |e|
+                               e.freeze
+                               tags.split.each do |t|
+                                       case t
+                                       when /^-(.*)/
+                                               e.remove_tag($1)
+                                       when /^\+(.*)/
+                                               e.add_tag($1)
+                                       when /^([^\+^-].*)/
+                                               e.add_tag($1)
+                                       end
+                               end
+                               e.thaw
+                               e.tags_to_maildir_flags
+                       end
+                       q.destroy!
+               end
+       end
+
+       module DbHelper
+               def init(name)
+                       @name = name
+                       @db = Notmuch::Database.new($db_name)
+                       @queries = []
+               end
+
+               def query(*args)
+                       q = @db.query(*args)
+                       @queries << q
+                       q
+               end
+
+               def close
+                       @queries.delete_if { |q| ! q.destroy! }
+                       @db.close
+               end
+
+               def reopen
+                       close if @db
+                       @db = Notmuch::Database.new($db_name)
+               end
+
+               def do_write
+                       db = Notmuch::Database.new($db_name, :mode => Notmuch::MODE_READ_WRITE)
+                       begin
+                               yield db
+                       ensure
+                               db.close
+                       end
+               end
+       end
+
+       class Message
+               attr_accessor :start, :body_start, :end
+               attr_reader :message_id, :filename, :mail
+
+               def initialize(msg, mail)
+                       @message_id = msg.message_id
+                       @filename = msg.filename
+                       @mail = mail
+                       @start = 0
+                       @end = 0
+                       mail.import_headers(msg) if not $mail_installed
+               end
+
+               def to_s
+                       "id:%s" % @message_id
+               end
+
+               def inspect
+                       "id:%s, file:%s" % [@message_id, @filename]
+               end
+       end
+
+       class StagedRender
+               def initialize(buffer, enumerable, block)
+                       @b = buffer
+                       @enumerable = enumerable
+                       @block = block
+                       @last_render = 0
+
+                       @b.render { do_next }
+               end
+
+               def is_ready?
+                       @last_render - @b.line_number <= $curwin.height
+               end
+
+               def do_next
+                       items = @enumerable.take($curwin.height * 2)
+                       return if items.empty?
+                       @block.call @b, items
+                       @last_render = @b.count
+               end
+       end
+
+       class VIM::Buffer
+               include DbHelper
+
+               def <<(a)
+                       append(count(), a)
+               end
+
+               def render_staged(enumerable, &block)
+                       StagedRender.new(self, enumerable, block)
+               end
+
+               def render
+                       old_count = count
+                       yield self
+                       (1..old_count).each do
+                               delete(1)
+                       end
+               end
+       end
+
+       class Notmuch::Tags
+               def to_s
+                       to_a.join(" ")
+               end
+       end
+
+       class Notmuch::Message
+               def to_s
+                       "id:%s" % message_id
+               end
+       end
+
+       # workaround for bug in vim's ruby
+       class Object
+               def flush
+               end
+       end
+
+       module SimpleMessage
+               class Header < Array
+                       def self.parse(string)
+                               return nil if string.empty?
+                               return Header.new(string.split(/,\s+/))
+                       end
+
+                       def to_s
+                               self.join(', ')
+                       end
+               end
+
+               def initialize(string = nil)
+                       @raw_source = string
+                       @body = nil
+                       @headers = {}
+
+                       return if not string
+
+                       if string =~ /(.*?(\r\n|\n))\2/m
+                               head, body = $1, $' || '', $2
+                       else
+                               head, body = string, ''
+                       end
+                       @body = body
+               end
+
+               def [](name)
+                       @headers[name.to_sym]
+               end
+
+               def []=(name, value)
+                       @headers[name.to_sym] = value
+               end
+
+               def format_header(value)
+                       value.to_s.tr('_', '-').gsub(/(\w+)/) { $1.capitalize }
+               end
+
+               def to_s
+                       buffer = ''
+                       @headers.each do |key, value|
+                               buffer << "%s: %s\r\n" %
+                                       [format_header(key), value]
+                       end
+                       buffer << "\r\n"
+                       buffer << @body
+                       buffer
+               end
+
+               def body=(value)
+                       @body = value
+               end
+
+               def from
+                       @headers[:from]
+               end
+
+               def decoded
+                       @body
+               end
+
+               def mime_type
+                       'text/plain'
+               end
+
+               def multipart?
+                       false
+               end
+
+               def reply
+                       r = Mail::Message.new
+                       r[:from] = self[:to]
+                       r[:to] = self[:from]
+                       r[:cc] = self[:cc]
+                       r[:in_reply_to] = self[:message_id]
+                       r[:references] = self[:references]
+                       r
+               end
+
+               HEADERS = [ :from, :to, :cc, :references, :in_reply_to, :reply_to, :message_id ]
+
+               def import_headers(m)
+                       HEADERS.each do |e|
+                               dashed = format_header(e)
+                               @headers[e] = Header.parse(m[dashed])
+                       end
+               end
+       end
+
+       module Mail
+
+               if not $mail_installed
+                       puts "WARNING: Install the 'mail' gem, without it support is limited"
+
+                       def self.read(filename)
+                               Message.new(File.open(filename, 'rb') { |f| f.read })
+                       end
+
+                       class Message
+                               include SimpleMessage
+                       end
+               end
+
+               class Message
+
+                       def find_first_text
+                               return self if not multipart?
+                               return text_part || html_part
+                       end
+
+                       def convert
+                               if mime_type != "text/html"
+                                       text = decoded
+                               else
+                                       IO.popen(VIM::evaluate('exists("g:notmuch_html_converter") ? ' +
+                                                       'g:notmuch_html_converter : "elinks --dump"'), "w+") do |pipe|
+                                               pipe.write(decode_body)
+                                               pipe.close_write
+                                               text = pipe.read
+                                       end
+                               end
+                               text
+                       end
+
+                       def present
+                               buffer = ''
+                               header.fields.each do |f|
+                                       buffer << "%s: %s\r\n" % [f.name, f.to_s]
+                               end
+                               buffer << "\r\n"
+                               buffer << body.to_s
+                               buffer
+                       end
+               end
+       end
+
+       class String
+               def to_utf8
+                       RUBY_VERSION >= "1.9" ? force_encoding('utf-8') : self
+               end
+       end
+
+       get_config
+EOF
+       if a:0
+         call s:search(join(a:000))
+       else
+         call s:folders()
+       endif
+endfunction
+
+command -nargs=* NotMuch call s:NotMuch(<f-args>)
+
+" vim: set noexpandtab:
diff --git a/vim/notmuch.yaml b/vim/notmuch.yaml
new file mode 100644 (file)
index 0000000..6f3b705
--- /dev/null
@@ -0,0 +1,10 @@
+addon: notmuch
+description: "notmuch mail user interface"
+files:
+  - plugin/notmuch.vim
+  - doc/notmuch.txt
+  - syntax/notmuch-compose.vim
+  - syntax/notmuch-folders.vim
+  - syntax/notmuch-git-diff.vim
+  - syntax/notmuch-search.vim
+  - syntax/notmuch-show.vim
diff --git a/vim/syntax/notmuch-compose.vim b/vim/syntax/notmuch-compose.vim
new file mode 100644 (file)
index 0000000..19adb75
--- /dev/null
@@ -0,0 +1,7 @@
+runtime! syntax/mail.vim
+
+syntax region nmComposeHelp          contains=nmComposeHelpLine start='^Notmuch-Help:\%1l' end='^\(Notmuch-Help:\)\@!'
+syntax match  nmComposeHelpLine      /Notmuch-Help:/ contained
+
+highlight link nmComposeHelp        Include
+highlight link nmComposeHelpLine    Error
diff --git a/vim/syntax/notmuch-folders.vim b/vim/syntax/notmuch-folders.vim
new file mode 100644 (file)
index 0000000..9477f86
--- /dev/null
@@ -0,0 +1,12 @@
+" notmuch folders mode syntax file
+
+syntax region nmFoldersCount     start='^' end='\%10v'
+syntax region nmFoldersName      start='\%11v' end='\%31v'
+syntax match  nmFoldersSearch    /([^()]\+)$/
+
+highlight link nmFoldersCount     Statement
+highlight link nmFoldersName      Type
+highlight link nmFoldersSearch    String
+
+highlight CursorLine term=reverse cterm=reverse gui=reverse
+
diff --git a/vim/syntax/notmuch-git-diff.vim b/vim/syntax/notmuch-git-diff.vim
new file mode 100644 (file)
index 0000000..6f15fdc
--- /dev/null
@@ -0,0 +1,26 @@
+syn match diffRemoved  "^-.*"
+syn match diffAdded    "^+.*"
+
+syn match diffSeparator        "^---$"
+syn match diffSubname  " @@..*"ms=s+3 contained
+syn match diffLine     "^@.*" contains=diffSubname
+
+syn match diffFile     "^diff .*"
+syn match diffNewFile  "^+++ .*"
+syn match diffOldFile  "^--- .*"
+
+hi def link diffOldFile                diffFile
+hi def link diffNewFile                diffFile
+
+hi def link diffFile           Type
+hi def link diffRemoved                Special
+hi def link diffAdded          Identifier
+hi def link diffLine           Statement
+hi def link diffSubname                PreProc
+
+syntax match gitDiffStatLine /^ .\{-}\zs[+-]\+$/ contains=gitDiffStatAdd,gitDiffStatDelete
+syntax match gitDiffStatAdd    /+/ contained
+syntax match gitDiffStatDelete /-/ contained
+
+hi def link gitDiffStatAdd diffAdded
+hi def link gitDiffStatDelete diffRemoved
diff --git a/vim/syntax/notmuch-search.vim b/vim/syntax/notmuch-search.vim
new file mode 100644 (file)
index 0000000..f458d77
--- /dev/null
@@ -0,0 +1,12 @@
+syntax region nmSearch         start=/^/ end=/$/               oneline contains=nmSearchDate
+syntax match nmSearchDate      /^.\{-13}/                      contained nextgroup=nmSearchNum
+syntax match nmSearchNum       /.\{-4}/                        contained nextgroup=nmSearchFrom
+syntax match nmSearchFrom      /.\{-21}/                       contained nextgroup=nmSearchSubject
+syntax match nmSearchSubject   /.\{0,}\(([^()]\+)$\)\@=/       contained nextgroup=nmSearchTags
+syntax match nmSearchTags      /.\+$/                          contained
+
+highlight link nmSearchDate    Statement
+highlight link nmSearchNum     Type
+highlight link nmSearchFrom    Include
+highlight link nmSearchSubject Normal
+highlight link nmSearchTags    String
diff --git a/vim/syntax/notmuch-show.vim b/vim/syntax/notmuch-show.vim
new file mode 100644 (file)
index 0000000..c3a98b7
--- /dev/null
@@ -0,0 +1,24 @@
+" notmuch show mode syntax file
+
+syntax cluster nmShowMsgDesc contains=nmShowMsgDescWho,nmShowMsgDescDate,nmShowMsgDescTags
+syntax match   nmShowMsgDescWho /[^)]\+)/ contained
+syntax match   nmShowMsgDescDate / ([^)]\+[0-9]) / contained
+syntax match   nmShowMsgDescTags /([^)]\+)$/ contained
+
+syntax cluster nmShowMsgHead contains=nmShowMsgHeadKey,nmShowMsgHeadVal
+syntax match   nmShowMsgHeadKey /^[^:]\+: / contained
+syntax match   nmShowMsgHeadVal /^\([^:]\+: \)\@<=.*/ contained
+
+syntax cluster nmShowMsgBody contains=@nmShowMsgBodyMail,@nmShowMsgBodyGit
+syntax include @nmShowMsgBodyMail syntax/mail.vim
+
+silent! syntax include @nmShowMsgBodyGit syntax/notmuch-git-diff.vim
+
+highlight nmShowMsgDescWho term=reverse cterm=reverse gui=reverse
+highlight link nmShowMsgDescDate Type
+highlight link nmShowMsgDescTags String
+
+highlight link nmShowMsgHeadKey  Macro
+"highlight link nmShowMsgHeadVal  NONE
+
+highlight Folded term=reverse ctermfg=LightGrey ctermbg=Black guifg=LightGray guibg=Black