From 126347b6942dd4b0291beb67b119431ebd750a2a Mon Sep 17 00:00:00 2001 From: David Bremner Date: Fri, 1 Dec 2023 07:51:09 -0400 Subject: [PATCH 1/9] Import notmuch_0.38.2.orig.tar.xz [dgit import orig notmuch_0.38.2.orig.tar.xz] --- .dir-locals.el | 25 + .gitignore | 23 + .mailmap | 8 + .travis.yml | 29 + AUTHORS | 129 + COPYING | 15 + COPYING-GPL-3 | 676 +++ INSTALL | 106 + Makefile | 71 + Makefile.global | 65 + Makefile.local | 324 ++ NEWS | 5120 +++++++++++++++++ README | 77 + README.rst | 11 + bindings/Makefile | 7 + bindings/Makefile.local | 40 + bindings/python-cffi/MANIFEST.in | 2 + bindings/python-cffi/notmuch2/__init__.py | 62 + bindings/python-cffi/notmuch2/_base.py | 238 + bindings/python-cffi/notmuch2/_build.py | 346 ++ bindings/python-cffi/notmuch2/_config.py | 101 + bindings/python-cffi/notmuch2/_database.py | 856 +++ bindings/python-cffi/notmuch2/_errors.py | 124 + bindings/python-cffi/notmuch2/_message.py | 724 +++ bindings/python-cffi/notmuch2/_query.py | 83 + bindings/python-cffi/notmuch2/_tags.py | 359 ++ bindings/python-cffi/notmuch2/_thread.py | 194 + bindings/python-cffi/setup.py | 25 + bindings/python-cffi/tests/conftest.py | 149 + bindings/python-cffi/tests/test_base.py | 116 + bindings/python-cffi/tests/test_config.py | 60 + bindings/python-cffi/tests/test_database.py | 342 ++ bindings/python-cffi/tests/test_errors.py | 8 + bindings/python-cffi/tests/test_message.py | 229 + bindings/python-cffi/tests/test_tags.py | 242 + bindings/python-cffi/tests/test_thread.py | 109 + bindings/python-cffi/tox.ini | 19 + bindings/python/.gitignore | 4 + bindings/python/MANIFEST.in | 2 + bindings/python/README | 17 + bindings/python/docs/COPYING | 674 +++ bindings/python/docs/Makefile | 88 + bindings/python/docs/source/conf.py | 209 + bindings/python/docs/source/database.rst | 54 + bindings/python/docs/source/filesystem.rst | 32 + bindings/python/docs/source/index.rst | 36 + bindings/python/docs/source/message.rst | 54 + bindings/python/docs/source/messages.rst | 15 + bindings/python/docs/source/notes.rst | 6 + bindings/python/docs/source/query.rst | 43 + bindings/python/docs/source/quickstart.rst | 19 + .../python/docs/source/status_and_errors.rst | 57 + bindings/python/docs/source/tags.rst | 17 + bindings/python/docs/source/thread.rst | 26 + bindings/python/docs/source/threads.rst | 14 + bindings/python/notmuch/__init__.py | 84 + bindings/python/notmuch/compat.py | 74 + bindings/python/notmuch/database.py | 789 +++ bindings/python/notmuch/directory.py | 185 + bindings/python/notmuch/errors.py | 204 + bindings/python/notmuch/filenames.py | 131 + bindings/python/notmuch/globals.py | 105 + bindings/python/notmuch/message.py | 721 +++ bindings/python/notmuch/messages.py | 199 + bindings/python/notmuch/query.py | 237 + bindings/python/notmuch/tag.py | 141 + bindings/python/notmuch/thread.py | 281 + bindings/python/notmuch/threads.py | 152 + bindings/python/notmuch/version.py | 3 + bindings/python/setup.py | 70 + bindings/ruby/.gitignore | 7 + bindings/ruby/README | 7 + bindings/ruby/database.c | 493 ++ bindings/ruby/defs.h | 369 ++ bindings/ruby/directory.c | 110 + bindings/ruby/extconf.rb | 26 + bindings/ruby/filenames.c | 11 + bindings/ruby/init.c | 377 ++ bindings/ruby/message.c | 366 ++ bindings/ruby/messages.c | 75 + bindings/ruby/query.c | 206 + bindings/ruby/rdoc.sh | 17 + bindings/ruby/status.c | 51 + bindings/ruby/tags.c | 13 + bindings/ruby/thread.c | 208 + bindings/ruby/threads.c | 55 + command-line-arguments.c | 320 ++ command-line-arguments.h | 82 + compat/.gitignore | 1 + compat/Makefile | 5 + compat/Makefile.local | 24 + compat/README | 21 + compat/check_asctime.c | 12 + compat/check_getpwuid.c | 12 + compat/compat.h | 77 + compat/function-attributes.h | 47 + compat/gen_zlib_pc.c | 19 + compat/getdelim.c | 125 + compat/getline.c | 29 + compat/have_canonicalize_file_name.c | 11 + compat/have_d_type.c | 11 + compat/have_getline.c | 14 + compat/have_strcasestr.c | 12 + compat/have_strsep.c | 12 + compat/have_timegm.c | 7 + compat/strcasestr.c | 42 + compat/strsep.c | 61 + compat/timegm.c | 56 + completion/Makefile | 7 + completion/Makefile.local | 22 + completion/README | 16 + completion/notmuch-completion.bash | 622 ++ completion/zsh/_email-notmuch | 9 + completion/zsh/_notmuch | 295 + configure | 1678 ++++++ contrib/go/.gitignore | 3 + contrib/go/LICENSE | 502 ++ contrib/go/Makefile | 40 + contrib/go/README | 16 + .../go/src/notmuch-addrlookup/addrlookup.go | 261 + contrib/go/src/notmuch/notmuch.go | 1404 +++++ contrib/notmuch-mutt/.gitignore | 2 + contrib/notmuch-mutt/Makefile | 25 + contrib/notmuch-mutt/README | 59 + contrib/notmuch-mutt/notmuch-mutt | 338 ++ contrib/notmuch-mutt/notmuch-mutt.rc | 19 + debian/.gitignore | 14 + debian/NEWS | 74 + debian/changelog | 1770 ++++++ debian/control | 247 + debian/copyright | 112 + debian/elpa-notmuch.elpa | 3 + debian/elpa-notmuch.info | 1 + debian/elpa-notmuch.lintian-overrides | 4 + debian/elpa-test | 1 + debian/gbp.conf | 14 + debian/libnotmuch-dev.install | 2 + debian/libnotmuch-dev.manpages | 1 + debian/libnotmuch5.install | 1 + debian/libnotmuch5.symbols | 166 + debian/not-installed | 3 + debian/notmuch-doc.install | 1 + debian/notmuch-emacs.README.Debian | 4 + debian/notmuch-emacs.maintscript | 1 + debian/notmuch-git.install | 2 + debian/notmuch-git.manpages | 2 + debian/notmuch-mutt.docs | 1 + debian/notmuch-mutt.install | 2 + debian/notmuch-mutt.manpages | 1 + debian/notmuch-vim.README.Debian | 8 + debian/notmuch-vim.dirs | 4 + debian/notmuch-vim.docs | 1 + debian/notmuch-vim.install | 4 + debian/notmuch.dirs | 1 + debian/notmuch.docs | 2 + debian/notmuch.install | 5 + debian/notmuch.maintscript | 1 + debian/notmuch.manpages | 20 + debian/ruby-notmuch.install | 1 + debian/rules | 40 + debian/source/format | 1 + debian/source/options | 3 + debian/tests/control | 18 + debian/upstream/metadata | 6 + debugger.c | 47 + devel/RELEASING | 113 + devel/STYLE | 118 + devel/TODO | 255 + devel/author-scan.sh | 11 + devel/check-notmuch-commit | 31 + devel/check-out-of-tree-build.sh | 15 + devel/emacs-keybindings.org | 60 + devel/man-to-mdwn.pl | 205 + devel/news2wiki.pl | 101 + devel/nmbug/doc/.gitignore | 2 + devel/nmbug/doc/Makefile | 38 + devel/nmbug/doc/conf.py | 67 + devel/nmbug/doc/index.rst | 17 + devel/nmbug/doc/man1/notmuch-report.1.rst | 54 + .../nmbug/doc/man5/notmuch-report.json.5.rst | 129 + devel/nmbug/notmuch-report | 441 ++ devel/nmbug/notmuch-report.json | 70 + devel/notmuch-web/nmgunicorn.py | 11 + devel/notmuch-web/nmweb.py | 366 ++ devel/notmuch-web/static/css/jquery-ui.css | 1 + devel/notmuch-web/static/css/notmuch-0.1.css | 15 + devel/notmuch-web/static/js/jquery-ui.js | 1 + devel/notmuch-web/static/js/jquery.js | 1 + devel/notmuch-web/static/js/notmuch-0.1.js | 35 + devel/notmuch-web/templates/base.html | 39 + devel/notmuch-web/templates/index.html | 9 + devel/notmuch-web/templates/search.html | 10 + devel/notmuch-web/templates/show.html | 15 + devel/notmuch-web/todo | 14 + devel/release-checks.sh | 208 + devel/schemata | 246 + devel/try-emacs-mua | 139 + devel/uncrustify.cfg | 127 + doc/.gitignore | 3 + doc/INSTALL | 11 + doc/Makefile | 5 + doc/Makefile.local | 163 + doc/command-line.rst | 36 + doc/conf.py | 226 + doc/doxygen.cfg | 298 + doc/elisp.py | 445 ++ doc/index.rst | 20 + doc/man1/notmuch-address.rst | 141 + doc/man1/notmuch-compact.rst | 56 + doc/man1/notmuch-config.rst | 385 ++ doc/man1/notmuch-count.rst | 81 + doc/man1/notmuch-dump.rst | 123 + doc/man1/notmuch-emacs-mua.rst | 102 + doc/man1/notmuch-git.rst | 340 ++ doc/man1/notmuch-insert.rst | 133 + doc/man1/notmuch-new.rst | 113 + doc/man1/notmuch-reindex.rst | 105 + doc/man1/notmuch-reply.rst | 143 + doc/man1/notmuch-restore.rst | 107 + doc/man1/notmuch-search.rst | 191 + doc/man1/notmuch-show.rst | 271 + doc/man1/notmuch-tag.rst | 121 + doc/man1/notmuch.rst | 237 + doc/man5/notmuch-hooks.rst | 64 + doc/man7/notmuch-properties.rst | 148 + doc/man7/notmuch-search-terms.rst | 481 ++ doc/man7/notmuch-sexp-queries.rst | 366 ++ doc/notmuch-emacs.rst | 785 +++ doc/python-bindings.rst | 5 + doc/queries.rst | 9 + emacs/.gitignore | 5 + emacs/Makefile | 7 + emacs/Makefile.local | 133 + emacs/coolj.el | 145 + emacs/make-deps.el | 69 + emacs/notmuch-address.el | 437 ++ emacs/notmuch-company.el | 106 + emacs/notmuch-compat.el | 58 + emacs/notmuch-crypto.el | 272 + emacs/notmuch-draft.el | 287 + emacs/notmuch-emacs-mua | 185 + emacs/notmuch-emacs-mua.desktop | 11 + emacs/notmuch-hello.el | 1010 ++++ emacs/notmuch-jump.el | 210 + emacs/notmuch-lib.el | 1075 ++++ emacs/notmuch-logo.svg | 27 + emacs/notmuch-maildir-fcc.el | 362 ++ emacs/notmuch-message.el | 76 + emacs/notmuch-mua.el | 651 +++ emacs/notmuch-parser.el | 194 + emacs/notmuch-pkg.el.tmpl | 6 + emacs/notmuch-print.el | 100 + emacs/notmuch-query.el | 74 + emacs/notmuch-show.el | 2732 +++++++++ emacs/notmuch-tag.el | 587 ++ emacs/notmuch-tree.el | 1464 +++++ emacs/notmuch-version.el.tmpl | 27 + emacs/notmuch-wash.el | 418 ++ emacs/notmuch.el | 1239 ++++ emacs/rstdoc.el | 90 + emacs/rstdoc.rsti | 21 + gmime-filter-reply.c | 212 + gmime-filter-reply.h | 74 + hooks.c | 104 + lib/Makefile | 7 + lib/Makefile.local | 99 + lib/add-message.cc | 605 ++ lib/built-with.c | 40 + lib/config.cc | 726 +++ lib/database-private.h | 395 ++ lib/database.cc | 1589 +++++ lib/directory.cc | 335 ++ lib/features.cc | 114 + lib/filenames.c | 76 + lib/index.cc | 770 +++ lib/indexopts.c | 81 + lib/init.cc | 21 + lib/lastmod-fp.cc | 83 + lib/lastmod-fp.h | 41 + lib/message-file.c | 402 ++ lib/message-id.c | 126 + lib/message-private.h | 16 + lib/message-property.cc | 214 + lib/message.cc | 2358 ++++++++ lib/messages.c | 194 + lib/notmuch-private.h | 768 +++ lib/notmuch.h | 2872 +++++++++ lib/notmuch.sym | 7 + lib/open.cc | 990 ++++ lib/parse-sexp.cc | 731 +++ lib/parse-time-vrp.cc | 107 + lib/parse-time-vrp.h | 46 + lib/prefix.cc | 215 + lib/query-fp.cc | 56 + lib/query-fp.h | 43 + lib/query.cc | 874 +++ lib/regexp-fields.cc | 252 + lib/regexp-fields.h | 89 + lib/sexp-fp.cc | 44 + lib/sexp-fp.h | 41 + lib/sha1.c | 94 + lib/string-list.c | 101 + lib/string-map.c | 250 + lib/tags.c | 76 + lib/thread-fp.cc | 58 + lib/thread-fp.h | 43 + lib/thread.cc | 742 +++ mime-node.c | 515 ++ notmuch-client-init.c | 18 + notmuch-client.h | 514 ++ notmuch-compact.c | 67 + notmuch-config.c | 760 +++ notmuch-count.c | 227 + notmuch-dump.c | 415 ++ notmuch-git.py | 1218 ++++ notmuch-insert.c | 619 ++ notmuch-new.c | 1325 +++++ notmuch-reindex.c | 137 + notmuch-reply.c | 781 +++ notmuch-restore.c | 467 ++ notmuch-search.c | 926 +++ notmuch-setup.c | 243 + notmuch-show.c | 1513 +++++ notmuch-tag.c | 317 + notmuch-time.c | 136 + notmuch.c | 622 ++ packaging/debian | 2 + packaging/fedora/notmuch.spec | 171 + parse-time-string/Makefile | 5 + parse-time-string/Makefile.local | 14 + parse-time-string/README | 9 + parse-time-string/parse-time-string.c | 1510 +++++ parse-time-string/parse-time-string.h | 102 + performance-test/.gitignore | 4 + performance-test/M00-new.sh | 15 + performance-test/M01-dump-restore.sh | 15 + performance-test/M02-show.sh | 13 + performance-test/M03-search.sh | 13 + performance-test/M04-reply.sh | 15 + performance-test/M05-reindex.sh | 11 + performance-test/M06-insert.sh | 16 + performance-test/Makefile | 7 + performance-test/Makefile.local | 44 + performance-test/README | 104 + performance-test/T00-new.sh | 41 + performance-test/T01-dump-restore.sh | 13 + performance-test/T02-tag.sh | 14 + performance-test/T03-reindex.sh | 41 + performance-test/T04-thread-subquery.sh | 13 + performance-test/T05-ruby.sh | 24 + performance-test/T06-emacs.sh | 84 + performance-test/T07-git.sh | 23 + performance-test/download/.gitignore | 2 + .../notmuch-email-corpus-0.3.tar.xz.asc | 9 + .../notmuch-email-corpus-0.4.tar.xz.asc | 14 + .../notmuch-email-corpus-0.5.tar.xz.asc | 16 + performance-test/notmuch-memory-test | 22 + performance-test/notmuch-time-test | 22 + performance-test/perf-test-lib.sh | 252 + performance-test/version.sh | 3 + query-string.c | 56 + sprinter-json.c | 203 + sprinter-sexp.c | 238 + sprinter-text.c | 159 + sprinter.h | 89 + status.c | 88 + tag-util.c | 426 ++ tag-util.h | 163 + test/.gitignore | 11 + test/Makefile | 7 + test/Makefile.local | 84 + test/README | 338 ++ test/T000-basic.sh | 78 + test/T010-help-test.sh | 32 + test/T020-compact.sh | 34 + test/T030-config.sh | 211 + test/T035-read-config.sh | 476 ++ test/T040-setup.sh | 47 + test/T050-new.sh | 473 ++ test/T051-new-renames.sh | 40 + test/T055-path-config.sh | 391 ++ test/T060-count.sh | 184 + test/T070-insert.sh | 300 + test/T080-search.sh | 199 + test/T081-sexpr-search.sh | 1322 +++++ test/T090-search-output.sh | 446 ++ test/T095-address.sh | 335 ++ test/T100-search-by-folder.sh | 166 + test/T110-search-position-overlap-bug.sh | 37 + test/T120-search-insufficient-from-quoting.sh | 44 + test/T130-search-limiting.sh | 71 + test/T131-show-limiting.sh | 81 + test/T140-excludes.sh | 469 ++ test/T150-tagging.sh | 357 ++ test/T160-json.sh | 205 + test/T170-sexp.sh | 63 + test/T180-text.sh | 88 + test/T190-multipart.sh | 833 +++ test/T200-thread-naming.sh | 212 + test/T205-author-naming.sh | 12 + test/T210-raw.sh | 78 + test/T220-reply.sh | 392 ++ test/T230-reply-to-sender.sh | 212 + test/T240-dump-restore.sh | 360 ++ test/T250-uuencode.sh | 34 + test/T260-thread-order.sh | 78 + test/T270-author-order.sh | 58 + test/T280-from-guessing.sh | 217 + test/T290-long-id.sh | 27 + test/T300-encoding.sh | 69 + test/T310-emacs.sh | 1111 ++++ test/T315-emacs-tagging.sh | 166 + test/T320-emacs-large-search-buffer.sh | 35 + test/T330-emacs-subject-to-filename.sh | 141 + test/T340-maildir-sync.sh | 210 + test/T350-crypto.sh | 485 ++ test/T351-pgpmime-mangling.sh | 32 + test/T355-smime.sh | 207 + test/T356-protected-headers.sh | 208 + test/T357-index-decryption.sh | 325 ++ test/T358-emacs-protected-headers.sh | 116 + test/T360-symbol-hiding.sh | 38 + test/T370-search-folder-coherence.sh | 46 + test/T380-atomicity.sh | 101 + test/T385-transactions.sh | 35 + test/T390-python.sh | 201 + test/T391-python-cffi.sh | 23 + test/T392-python-cffi-notmuch.sh | 48 + test/T395-ruby.sh | 107 + test/T400-hooks.sh | 236 + test/T405-external.sh | 54 + test/T410-argument-parsing.sh | 104 + test/T420-emacs-test-functions.sh | 10 + test/T430-emacs-address-cleaning.sh | 18 + test/T440-emacs-hello.sh | 89 + test/T450-emacs-show.sh | 415 ++ test/T453-emacs-reply.sh | 71 + test/T454-emacs-dont-reply-names.sh | 76 + test/T455-emacs-charsets.sh | 143 + test/T460-emacs-tree.sh | 249 + test/T465-emacs-unthreaded.sh | 85 + test/T470-missing-headers.sh | 166 + test/T480-hex-escaping.sh | 54 + test/T490-parse-time-string.sh | 96 + test/T500-search-date.sh | 29 + test/T510-thread-replies.sh | 237 + test/T520-show.sh | 84 + test/T530-upgrade.sh | 76 + test/T550-db-features.sh | 52 + test/T560-lib-error.sh | 334 ++ test/T562-lib-database.sh | 427 ++ test/T563-lib-directory.sh | 112 + test/T564-lib-query.sh | 254 + test/T565-lib-tags.sh | 85 + test/T566-lib-message.sh | 550 ++ test/T568-lib-thread.sh | 341 ++ test/T570-revision-tracking.sh | 125 + test/T580-thread-search.sh | 42 + test/T585-thread-subquery.sh | 46 + test/T590-libconfig.sh | 1045 ++++ test/T592-thread-breakage.sh | 128 + test/T595-reopen.sh | 123 + test/T600-named-queries.sh | 80 + test/T610-message-property.sh | 404 ++ test/T620-lock.sh | 84 + test/T630-emacs-draft.sh | 74 + test/T640-database-modified.sh | 72 + test/T650-regexp-query.sh | 206 + test/T660-bad-date.sh | 14 + test/T670-duplicate-mid.sh | 128 + test/T680-html-indexing.sh | 22 + test/T690-command-line-args.sh | 85 + test/T700-reindex.sh | 122 + test/T710-message-id.sh | 77 + test/T720-emacs-attachment-warnings.sh | 12 + test/T720-lib-lifetime.sh | 89 + test/T730-emacs-forwarding.sh | 44 + test/T740-body.sh | 43 + test/T750-gzip.sh | 177 + test/T750-user-header.sh | 122 + test/T760-as-text.sh | 77 + test/T800-asan.sh | 44 + test/T810-tsan.sh | 92 + test/T810-tsan.suppressions | 5 + test/T850-git.sh | 402 ++ test/aggregate-results.sh | 99 + test/arg-test.c | 87 + test/atomicity.py | 78 + test/corpora/README | 19 + test/corpora/attachment/x-gtar-compressed.eml | 136 + test/corpora/broken/broken-cc | 9 + test/corpora/broken/loop/loop-12 | 8 + test/corpora/broken/loop/loop-21 | 8 + test/corpora/crypto/basic-encrypted.eml | 27 + .../crypto/encrypted-rfc822-attachment | 47 + test/corpora/crypto/encrypted-signed.eml | 34 + test/corpora/crypto/simple-encrypted | 36 + test/corpora/default/01:2, | 34 + test/corpora/default/02:2, | 32 + test/corpora/default/bar/17:2, | 23 + test/corpora/default/bar/18:2, | 12 + test/corpora/default/bar/baz/05:2, | 104 + test/corpora/default/bar/baz/23:2, | 145 + test/corpora/default/bar/baz/24:2, | 204 + test/corpora/default/bar/baz/cur/25:2, | 32 + test/corpora/default/bar/baz/cur/26:2, | 121 + test/corpora/default/bar/baz/new/27:2, | 21 + test/corpora/default/bar/baz/new/28:2, | 38 + test/corpora/default/bar/cur/19:2, | 360 ++ test/corpora/default/bar/cur/20:2, | 101 + test/corpora/default/bar/new/21:2, | 102 + test/corpora/default/bar/new/22:2, | 84 + test/corpora/default/cur/29:2, | 21 + test/corpora/default/cur/30:2, | 75 + test/corpora/default/cur/31:2, | 31 + test/corpora/default/cur/32:2, | 165 + test/corpora/default/cur/33:2, | 13 + test/corpora/default/cur/34:2, | 46 + test/corpora/default/cur/35:2, | 24 + test/corpora/default/cur/36:2, | 25 + test/corpora/default/cur/37:2, | 22 + test/corpora/default/cur/38:2, | 40 + test/corpora/default/cur/39:2, | 32 + test/corpora/default/cur/40:2, | 31 + test/corpora/default/cur/41:2, | 37 + test/corpora/default/cur/42:2, | 30 + test/corpora/default/cur/43:2, | 26 + test/corpora/default/cur/44:2, | 29 + test/corpora/default/cur/45:2, | 41 + test/corpora/default/cur/46:2, | 57 + test/corpora/default/cur/47:2, | 84 + test/corpora/default/cur/48:2, | 17 + test/corpora/default/cur/49:2, | 33 + test/corpora/default/cur/50:2, | 39 + test/corpora/default/cur/51:2, | 12 + test/corpora/default/cur/52:2, | 39 + test/corpora/default/cur/53:2, | 20 + test/corpora/default/foo/06:2, | 36 + test/corpora/default/foo/baz/11:2, | 27 + test/corpora/default/foo/baz/12:2, | 27 + test/corpora/default/foo/baz/cur/13:2, | 178 + test/corpora/default/foo/baz/cur/14:2, | 39 + test/corpora/default/foo/baz/new/15:2, | 22 + test/corpora/default/foo/baz/new/16:2, | 27 + test/corpora/default/foo/cur/07:2, | 57 + test/corpora/default/foo/cur/08:2, | 87 + test/corpora/default/foo/new/03:2, | 93 + test/corpora/default/foo/new/09:2, | 33 + test/corpora/default/foo/new/10:2, | 54 + test/corpora/default/new/04:2, | 84 + test/corpora/duplicate/msg-1-1:2, | 85 + test/corpora/duplicate/msg-1-2:2, | 113 + test/corpora/duplicate/msg-2-1:2, | 43 + test/corpora/duplicate/msg-2-2:2, | 140 + test/corpora/duplicate/msg-3-1:2, | 183 + test/corpora/duplicate/msg-3-2:2, | 184 + test/corpora/duplicate/msg-3-3:2, | 178 + test/corpora/duplicate/msg-3-4:2, | 184 + test/corpora/duplicate/msg-3-5:2, | 179 + test/corpora/html/attribute-text | 15 + test/corpora/html/embedded-image | 69 + ...TCH-1-2-system_data_types.7-srcfix.txt:2,S | 282 + test/corpora/indexing/fake-pdf:2,S | 11 + test/corpora/insert/mbox-attachment.eml | 83 + test/corpora/lkml/cur/1354585346.000260:2, | 99 + test/corpora/lkml/cur/1354585346.000261:2, | 131 + test/corpora/lkml/cur/1354585346.000265:2, | 85 + test/corpora/lkml/cur/1354585346.000323:2, | 105 + test/corpora/lkml/cur/1354585346.000324:2, | 71 + test/corpora/lkml/cur/1354585346.000325:2, | 86 + test/corpora/lkml/cur/1354585346.000539:2, | 81 + test/corpora/lkml/cur/1354585346.000541:2, | 64 + test/corpora/lkml/cur/1382298587.001724:2, | 104 + test/corpora/lkml/cur/1382298587.001730:2, | 103 + test/corpora/lkml/cur/1382298587.001731:2, | 67 + test/corpora/lkml/cur/1382298587.001732:2, | 73 + test/corpora/lkml/cur/1382298587.001733:2, | 211 + test/corpora/lkml/cur/1382298587.001734:2, | 223 + test/corpora/lkml/cur/1382298587.001735:2, | 212 + test/corpora/lkml/cur/1382298587.001736:2, | 256 + test/corpora/lkml/cur/1382298587.001738:2, | 139 + test/corpora/lkml/cur/1382298587.001739:2, | 355 ++ test/corpora/lkml/cur/1382298587.001740:2, | 214 + test/corpora/lkml/cur/1382298587.001887:2, | 85 + test/corpora/lkml/cur/1382298587.001892:2, | 254 + test/corpora/lkml/cur/1382298587.001970:2, | 103 + test/corpora/lkml/cur/1382298587.002189:2, | 66 + test/corpora/lkml/cur/1382298587.002193:2, | 59 + test/corpora/lkml/cur/1382298587.002194:2, | 61 + test/corpora/lkml/cur/1382298587.002195:2, | 59 + test/corpora/lkml/cur/1382298587.002196:2, | 54 + test/corpora/lkml/cur/1382298587.002197:2, | 53 + test/corpora/lkml/cur/1382298587.002201:2, | 58 + test/corpora/lkml/cur/1382298587.002228:2, | 100 + test/corpora/lkml/cur/1382298587.002878:2, | 90 + test/corpora/lkml/cur/1382298587.002912:2, | 65 + test/corpora/lkml/cur/1382298587.002915:2, | 58 + test/corpora/lkml/cur/1382298587.002917:2, | 67 + test/corpora/lkml/cur/1382298587.002997:2, | 90 + test/corpora/lkml/cur/1382298587.003106:2, | 60 + test/corpora/lkml/cur/1382298587.003112:2, | 105 + test/corpora/lkml/cur/1382298587.003117:2, | 65 + test/corpora/lkml/cur/1382298587.003118:2, | 122 + test/corpora/lkml/cur/1382298587.003171:2, | 174 + test/corpora/lkml/cur/1382298587.003317:2, | 156 + test/corpora/lkml/cur/1382298587.003318:2, | 66 + test/corpora/lkml/cur/1382298587.003486:2, | 89 + test/corpora/lkml/cur/1382298587.004581:2, | 92 + test/corpora/lkml/cur/1382298587.004582:2, | 68 + test/corpora/lkml/cur/1382298770.001724:2, | 104 + test/corpora/lkml/cur/1382298770.001730:2, | 103 + test/corpora/lkml/cur/1382298770.001731:2, | 67 + test/corpora/lkml/cur/1382298770.001732:2, | 73 + test/corpora/lkml/cur/1382298770.001733:2, | 211 + test/corpora/lkml/cur/1382298770.001734:2, | 223 + test/corpora/lkml/cur/1382298770.001735:2, | 212 + test/corpora/lkml/cur/1382298770.001736:2, | 256 + test/corpora/lkml/cur/1382298770.001738:2, | 139 + test/corpora/lkml/cur/1382298770.001739:2, | 355 ++ test/corpora/lkml/cur/1382298770.001740:2, | 214 + test/corpora/lkml/cur/1382298770.001887:2, | 85 + test/corpora/lkml/cur/1382298770.001892:2, | 254 + test/corpora/lkml/cur/1382298770.002189:2, | 66 + test/corpora/lkml/cur/1382298770.002191:2, | 65 + test/corpora/lkml/cur/1382298770.002193:2, | 59 + test/corpora/lkml/cur/1382298770.002194:2, | 61 + test/corpora/lkml/cur/1382298770.002195:2, | 59 + test/corpora/lkml/cur/1382298770.002196:2, | 54 + test/corpora/lkml/cur/1382298770.002197:2, | 53 + test/corpora/lkml/cur/1382298770.002201:2, | 58 + test/corpora/lkml/cur/1382298770.002878:2, | 90 + test/corpora/lkml/cur/1382298770.002879:2, | 68 + test/corpora/lkml/cur/1382298770.002911:2, | 84 + test/corpora/lkml/cur/1382298770.002912:2, | 65 + test/corpora/lkml/cur/1382298770.002915:2, | 58 + test/corpora/lkml/cur/1382298770.002917:2, | 67 + test/corpora/lkml/cur/1382298770.002930:2, | 81 + test/corpora/lkml/cur/1382298770.002997:2, | 90 + test/corpora/lkml/cur/1382298770.003106:2, | 60 + test/corpora/lkml/cur/1382298770.003117:2, | 65 + test/corpora/lkml/cur/1382298770.003118:2, | 122 + test/corpora/lkml/cur/1382298770.003171:2, | 174 + test/corpora/lkml/cur/1382298770.003317:2, | 156 + test/corpora/lkml/cur/1382298770.003318:2, | 66 + test/corpora/lkml/cur/1382298770.003486:2, | 89 + test/corpora/lkml/cur/1382298770.003499:2, | 63 + test/corpora/lkml/cur/1382298770.004581:2, | 92 + test/corpora/lkml/cur/1382298770.004582:2, | 68 + test/corpora/lkml/cur/1382298775.002830:2, | 60 + test/corpora/lkml/cur/1382298775.002978:2, | 91 + test/corpora/lkml/cur/1382298775.002992:2, | 87 + test/corpora/lkml/cur/1382298775.002999:2, | 109 + test/corpora/lkml/cur/1382298775.003976:2, | 96 + test/corpora/lkml/cur/1382298775.004354:2, | 170 + test/corpora/lkml/cur/1382298775.004363:2, | 95 + test/corpora/lkml/cur/1382298775.004374:2, | 75 + test/corpora/lkml/cur/1382298793.002253:2, | 208 + test/corpora/lkml/cur/1382298793.002254:2, | 100 + test/corpora/lkml/cur/1382298793.002255:2, | 100 + test/corpora/lkml/cur/1382298793.002256:2, | 89 + test/corpora/lkml/cur/1382298793.002257:2, | 81 + test/corpora/lkml/cur/1382298793.002258:2, | 86 + test/corpora/lkml/cur/1382298793.002259:2, | 58 + test/corpora/lkml/cur/1382298793.002260:2, | 57 + test/corpora/lkml/cur/1382298793.002261:2, | 58 + test/corpora/lkml/cur/1382298793.002262:2, | 59 + test/corpora/lkml/cur/1382298793.002263:2, | 58 + test/corpora/lkml/cur/1382298793.002264:2, | 103 + test/corpora/lkml/cur/1382298793.002265:2, | 76 + test/corpora/lkml/cur/1382298793.002266:2, | 75 + test/corpora/lkml/cur/1382298793.002267:2, | 773 +++ test/corpora/lkml/cur/1382298793.002268:2, | 66 + test/corpora/lkml/cur/1382298793.002269:2, | 74 + test/corpora/lkml/cur/1382298793.002270:2, | 68 + test/corpora/lkml/cur/1382298793.002271:2, | 58 + test/corpora/lkml/cur/1382298793.002272:2, | 59 + test/corpora/lkml/cur/1382298793.002273:2, | 64 + test/corpora/lkml/cur/1382298793.002274:2, | 59 + test/corpora/lkml/cur/1382298793.002275:2, | 59 + test/corpora/lkml/cur/1382298793.002276:2, | 58 + test/corpora/lkml/cur/1382298793.002277:2, | 58 + test/corpora/lkml/cur/1382298793.002278:2, | 63 + test/corpora/lkml/cur/1382298793.002279:2, | 88 + test/corpora/lkml/cur/1382298793.002280:2, | 57 + test/corpora/lkml/cur/1382298793.002281:2, | 59 + test/corpora/lkml/cur/1382298793.002282:2, | 59 + test/corpora/lkml/cur/1382298793.002283:2, | 68 + test/corpora/lkml/cur/1382298793.002284:2, | 61 + test/corpora/lkml/cur/1382298793.002285:2, | 61 + test/corpora/lkml/cur/1382298793.002286:2, | 62 + test/corpora/lkml/cur/1382298793.002287:2, | 57 + test/corpora/lkml/cur/1382298793.002288:2, | 63 + test/corpora/lkml/cur/1382298793.002289:2, | 86 + test/corpora/lkml/cur/1382298793.002290:2, | 81 + test/corpora/lkml/cur/1382298793.002292:2, | 71 + test/corpora/lkml/cur/1382298793.002293:2, | 57 + test/corpora/lkml/cur/1382298793.002294:2, | 63 + test/corpora/lkml/cur/1382298793.002296:2, | 148 + test/corpora/lkml/cur/1382298793.002297:2, | 58 + test/corpora/lkml/cur/1382298793.002298:2, | 85 + test/corpora/lkml/cur/1382298793.002299:2, | 56 + test/corpora/lkml/cur/1382298793.002302:2, | 85 + test/corpora/lkml/cur/1382298793.002309:2, | 93 + test/corpora/lkml/cur/1382298793.002329:2, | 79 + test/corpora/lkml/cur/1382298793.002340:2, | 71 + test/corpora/lkml/cur/1382298793.002400:2, | 102 + test/corpora/lkml/cur/1382298793.002432:2, | 85 + test/corpora/lkml/cur/1382298793.002468:2, | 75 + test/corpora/lkml/cur/1382298793.002543:2, | 132 + test/corpora/lkml/cur/1382298793.002557:2, | 109 + test/corpora/lkml/cur/1382298793.002575:2, | 79 + test/corpora/lkml/cur/1382298793.002576:2, | 63 + test/corpora/lkml/cur/1382298793.002639:2, | 90 + test/corpora/lkml/cur/1382298793.002642:2, | 66 + test/corpora/lkml/cur/1382298793.002661:2, | 49 + test/corpora/lkml/cur/1382298793.002662:2, | 49 + test/corpora/lkml/cur/1382298793.002663:2, | 50 + test/corpora/lkml/cur/1382298793.002664:2, | 49 + test/corpora/lkml/cur/1382298793.002665:2, | 98 + test/corpora/lkml/cur/1382298793.002666:2, | 87 + test/corpora/lkml/cur/1382298793.002667:2, | 87 + test/corpora/lkml/cur/1382298793.002668:2, | 50 + test/corpora/lkml/cur/1382298793.002669:2, | 48 + test/corpora/lkml/cur/1382298793.002670:2, | 52 + test/corpora/lkml/cur/1382298793.002671:2, | 48 + test/corpora/lkml/cur/1382298793.002679:2, | 61 + test/corpora/lkml/cur/1382298793.002688:2, | 94 + test/corpora/lkml/cur/1382298793.002699:2, | 106 + test/corpora/lkml/cur/1382298793.003013:2, | 128 + test/corpora/lkml/cur/1382298793.003145:2, | 89 + test/corpora/lkml/cur/1382298793.003148:2, | 92 + test/corpora/lkml/cur/1382298793.003216:2, | 113 + test/corpora/lkml/cur/1382298793.003231:2, | 82 + test/corpora/lkml/cur/1382298793.003278:2, | 89 + test/corpora/lkml/cur/1382298793.003295:2, | 92 + test/corpora/lkml/cur/1382298793.003316:2, | 105 + test/corpora/lkml/cur/1382298793.003334:2, | 106 + test/corpora/lkml/cur/1382298793.003340:2, | 138 + test/corpora/lkml/cur/1382298793.003448:2, | 100 + test/corpora/lkml/cur/1382298793.003459:2, | 88 + test/corpora/lkml/cur/1382298793.003462:2, | 97 + test/corpora/lkml/cur/1382298793.003468:2, | 99 + test/corpora/lkml/cur/1382298793.003471:2, | 79 + test/corpora/lkml/cur/1382298793.003472:2, | 84 + test/corpora/lkml/cur/1382298793.003478:2, | 113 + test/corpora/lkml/cur/1382298793.003497:2, | 94 + test/corpora/lkml/cur/1382298793.003501:2, | 89 + test/corpora/lkml/cur/1382298793.003503:2, | 110 + test/corpora/lkml/cur/1382298793.003971:2, | 106 + test/corpora/lkml/cur/1382298793.004059:2, | 88 + test/corpora/lkml/cur/1382298793.004091:2, | 133 + test/corpora/lkml/cur/1382298793.004190:2, | 67 + test/corpora/lkml/cur/1382298795.000299:2, | 79 + test/corpora/lkml/cur/1382298795.001362:2, | 96 + test/corpora/lkml/cur/1382298795.002635:2, | 54 + test/corpora/lkml/cur/1382298796.001941:2, | 73 + test/corpora/lkml/cur/1382298805.004526:2, | 89 + test/corpora/lkml/cur/1382298805.004551:2, | 90 + test/corpora/lkml/cur/1382298805.004613:2, | 94 + test/corpora/lkml/cur/1382298805.004614:2, | 89 + test/corpora/lkml/cur/1382298805.004615:2, | 64 + test/corpora/lkml/cur/1382298805.004617:2, | 67 + test/corpora/lkml/cur/1382298805.004618:2, | 70 + test/corpora/lkml/cur/1382298805.004619:2, | 99 + test/corpora/lkml/cur/1382298805.004636:2, | 93 + test/corpora/lkml/cur/1382298805.004638:2, | 88 + test/corpora/lkml/cur/1382298805.004639:2, | 95 + test/corpora/lkml/cur/1382298805.004640:2, | 69 + test/corpora/lkml/cur/1382298805.004642:2, | 93 + test/corpora/lkml/cur/1382298805.004653:2, | 90 + test/corpora/lkml/cur/1382298805.004665:2, | 121 + test/corpora/lkml/cur/1382298805.004680:2, | 103 + test/corpora/lkml/cur/1382298805.004688:2, | 107 + test/corpora/lkml/cur/1382298805.004906:2, | 125 + test/corpora/mangling/mixed-up.eml | 29 + test/corpora/pkcs7/smime-onepart-signed.eml | 51 + ...le-wrapped-with-phony-protected-header.eml | 41 + ...pted-message-with-forwarded-attachment.eml | 38 + .../encrypted-signed-not-masked.eml | 33 + .../protected-headers/encrypted-signed.eml | 33 + .../misplaced-protected-header.eml | 43 + .../nested-rfc822-message.eml | 33 + .../no-protected-header-attribute.eml | 29 + .../phony-protected-header-bad-encryption.eml | 27 + .../protected-headers/protected-header.eml | 29 + .../protected-with-legacy-display.eml | 42 + .../signed-protected-header.eml | 28 + .../protected-headers/simple-signed-mail.eml | 27 + .../smime-enc+legacy-disp.eml | 50 + .../smime-multipart-signed.eml | 68 + .../smime-onepart-signed.eml | 54 + .../smime-sign+enc+legacy-disp.eml | 102 + .../protected-headers/smime-sign+enc.eml | 95 + .../subjectless-protected-header.eml | 28 + .../wrapped-protected-header.eml | 38 + .../1529425589.M615261P21663.len:2,S | 9 + .../1532672447.R3166642290392477575.len:2,S | 17 + .../1532672447.R6968667928580738175.len:2,S | 18 + test/corpora/threading/ghost-root/child | 9 + test/corpora/threading/ghost-root/fake-root | 9 + test/corpora/threading/ghost-root/grand-child | 9 + .../corpora/threading/ghost-root/grand-child2 | 9 + .../threading/ghost-root/great-grand-child | 9 + test/corpora/threading/ghost-root/real-root | 7 + .../threading/parent-priority/cur/child | 11 + .../threading/parent-priority/cur/grand-child | 10 + .../threading/parent-priority/cur/root | 7 + test/database-test.c | 71 + test/database-test.h | 21 + test/emacs-address-cleaning.el | 39 + test/emacs-attachment-warnings.el | 81 + .../notmuch-reply-duplicate-4 | 21 + .../notmuch-show-decrypted-message | 10 + .../notmuch-show-decrypted-message-no-crypto | 9 + .../notmuch-show-depth | 44 + .../notmuch-show-depth-1 | 119 + .../notmuch-show-duplicate-4 | 20 + ...tmuch-show-elide-non-matching-messages-off | 82 + ...otmuch-show-elide-non-matching-messages-on | 78 + .../notmuch-show-height-0 | 97 + .../notmuch-show-indent-thread-content-off | 82 + .../notmuch-show-multipart-alternative | 62 + ...notmuch-show-process-crypto-mime-parts-off | 31 + .../notmuch-show-process-crypto-mime-parts-on | 32 + .../notmuch-show-size | 64 + .../notmuch-show-size-450 | 89 + .../notmuch-show-undecryptable-message | 9 + test/emacs-tree.expected-output/inbox-outline | 25 + .../notmuch-tree-show-window | 41 + .../notmuch-tree-single-thread | 6 + .../notmuch-tree-tag-inbox | 53 + .../notmuch-tree-tag-inbox-tagged | 53 + .../notmuch-tree-tag-inbox-thread-tagged | 53 + .../result-format-function | 53 + .../result-format-function | 53 + test/emacs.expected-output/attachment | 32 + test/emacs.expected-output/notmuch-hello | 12 + .../notmuch-hello-all-tags | 11 + ...notmuch-hello-empty-custom-queries-section | 3 + .../notmuch-hello-empty-custom-tags-section | 5 + .../notmuch-hello-long-names | 15 + .../notmuch-hello-new-section | 4 + .../notmuch-hello-no-saved-searches | 8 + .../notmuch-hello-section-counts | 5 + .../notmuch-hello-section-hidden-tag | 4 + .../notmuch-hello-section-with-empty | 4 + .../notmuch-hello-view-inbox | 25 + .../notmuch-hello-with-empty | 12 + .../notmuch-search-tag-inbox | 25 + .../notmuch-show-message-with-headers-hidden | 22 + .../notmuch-show-message-with-headers-visible | 25 + .../notmuch-show-thread-maildir-storage | 218 + ...-maildir-storage-with-fourfold-indentation | 223 + ...thread-maildir-storage-without-indentation | 218 + ...ch-show-thread-with-all-messages-collapsed | 4 + ...-show-thread-with-all-messages-uncollapsed | 79 + .../notmuch-show-thread-with-hidden-messages | 4 + .../raw-message-cf0c4d-52ad0a | 104 + .../search-result-format-function | 25 + test/export-dirs.sh | 31 + test/gen-threads.py | 33 + test/ghost-report.cc | 17 + test/hex-xcode.c | 109 + test/json_check_nodes.py | 114 + test/make-db-version.cc | 37 + test/message-id-parse.c | 26 + test/notmuch-test | 107 + test/notmuch-test.h | 50 + test/openpgp4-secret-key.asc | 15 + test/openpgp4-secret-key.asc.NOTE | 5 + test/parse-time.c | 314 + test/random-corpus.c | 231 + .../config-with-comments | 81 + test/smime/0xE0972A47.p12 | 62 + test/smime/README | 9 + test/smime/bob.p12 | 58 + test/smime/ca.crt | 20 + test/smime/key+cert.pem | 56 + test/smime/test.crt | 19 + test/smtp-dummy.c | 287 + test/symbol-test.cc | 33 + test/test-lib-FREEBSD.sh | 9 + test/test-lib-common.sh | 357 ++ test/test-lib-emacs.sh | 226 + test/test-lib.el | 221 + test/test-lib.sh | 1102 ++++ test/test-vars.sh | 62 + test/test-verbose | 29 + test/test.expected-output/test-verbose-no | 21 + test/test.expected-output/test-verbose-yes | 25 + test/valgrind/suppressions | 6 + test/valgrind/valgrind.sh | 22 + util/Makefile | 5 + util/Makefile.local | 18 + util/crypto.c | 245 + util/crypto.h | 106 + util/error_util.c | 40 + util/error_util.h | 54 + util/gmime-extra.c | 221 + util/gmime-extra.h | 81 + util/hex-escape.c | 159 + util/hex-escape.h | 50 + util/path-util.c | 27 + util/path-util.h | 19 + util/repair.c | 158 + util/repair.h | 44 + util/string-util.c | 298 + util/string-util.h | 100 + util/talloc-extra.c | 14 + util/talloc-extra.h | 26 + util/unicode-util.c | 43 + util/unicode-util.h | 19 + util/util.c | 24 + util/util.h | 29 + util/xapian-extra.h | 15 + util/xutil.c | 139 + util/xutil.h | 60 + util/zlib-extra.c | 95 + util/zlib-extra.h | 39 + version.txt | 1 + vim/Makefile | 15 + vim/README | 62 + vim/notmuch.txt | 153 + vim/notmuch.vim | 973 ++++ vim/notmuch.yaml | 10 + vim/syntax/notmuch-compose.vim | 7 + vim/syntax/notmuch-folders.vim | 12 + vim/syntax/notmuch-git-diff.vim | 26 + vim/syntax/notmuch-search.vim | 12 + vim/syntax/notmuch-show.vim | 24 + 930 files changed, 137984 insertions(+) create mode 100644 .dir-locals.el create mode 100644 .gitignore create mode 100644 .mailmap create mode 100644 .travis.yml create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 COPYING-GPL-3 create mode 100644 INSTALL create mode 100644 Makefile create mode 100644 Makefile.global create mode 100644 Makefile.local create mode 100644 NEWS create mode 100644 README create mode 100644 README.rst create mode 100644 bindings/Makefile create mode 100644 bindings/Makefile.local create mode 100644 bindings/python-cffi/MANIFEST.in create mode 100644 bindings/python-cffi/notmuch2/__init__.py create mode 100644 bindings/python-cffi/notmuch2/_base.py create mode 100644 bindings/python-cffi/notmuch2/_build.py create mode 100644 bindings/python-cffi/notmuch2/_config.py create mode 100644 bindings/python-cffi/notmuch2/_database.py create mode 100644 bindings/python-cffi/notmuch2/_errors.py create mode 100644 bindings/python-cffi/notmuch2/_message.py create mode 100644 bindings/python-cffi/notmuch2/_query.py create mode 100644 bindings/python-cffi/notmuch2/_tags.py create mode 100644 bindings/python-cffi/notmuch2/_thread.py create mode 100644 bindings/python-cffi/setup.py create mode 100644 bindings/python-cffi/tests/conftest.py create mode 100644 bindings/python-cffi/tests/test_base.py create mode 100644 bindings/python-cffi/tests/test_config.py create mode 100644 bindings/python-cffi/tests/test_database.py create mode 100644 bindings/python-cffi/tests/test_errors.py create mode 100644 bindings/python-cffi/tests/test_message.py create mode 100644 bindings/python-cffi/tests/test_tags.py create mode 100644 bindings/python-cffi/tests/test_thread.py create mode 100644 bindings/python-cffi/tox.ini create mode 100644 bindings/python/.gitignore create mode 100644 bindings/python/MANIFEST.in create mode 100644 bindings/python/README create mode 100644 bindings/python/docs/COPYING create mode 100644 bindings/python/docs/Makefile create mode 100644 bindings/python/docs/source/conf.py create mode 100644 bindings/python/docs/source/database.rst create mode 100644 bindings/python/docs/source/filesystem.rst create mode 100644 bindings/python/docs/source/index.rst create mode 100644 bindings/python/docs/source/message.rst create mode 100644 bindings/python/docs/source/messages.rst create mode 100644 bindings/python/docs/source/notes.rst create mode 100644 bindings/python/docs/source/query.rst create mode 100644 bindings/python/docs/source/quickstart.rst create mode 100644 bindings/python/docs/source/status_and_errors.rst create mode 100644 bindings/python/docs/source/tags.rst create mode 100644 bindings/python/docs/source/thread.rst create mode 100644 bindings/python/docs/source/threads.rst create mode 100644 bindings/python/notmuch/__init__.py create mode 100644 bindings/python/notmuch/compat.py create mode 100644 bindings/python/notmuch/database.py create mode 100644 bindings/python/notmuch/directory.py create mode 100644 bindings/python/notmuch/errors.py create mode 100644 bindings/python/notmuch/filenames.py create mode 100644 bindings/python/notmuch/globals.py create mode 100644 bindings/python/notmuch/message.py create mode 100644 bindings/python/notmuch/messages.py create mode 100644 bindings/python/notmuch/query.py create mode 100644 bindings/python/notmuch/tag.py create mode 100644 bindings/python/notmuch/thread.py create mode 100644 bindings/python/notmuch/threads.py create mode 100644 bindings/python/notmuch/version.py create mode 100644 bindings/python/setup.py create mode 100644 bindings/ruby/.gitignore create mode 100644 bindings/ruby/README create mode 100644 bindings/ruby/database.c create mode 100644 bindings/ruby/defs.h create mode 100644 bindings/ruby/directory.c create mode 100644 bindings/ruby/extconf.rb create mode 100644 bindings/ruby/filenames.c create mode 100644 bindings/ruby/init.c create mode 100644 bindings/ruby/message.c create mode 100644 bindings/ruby/messages.c create mode 100644 bindings/ruby/query.c create mode 100755 bindings/ruby/rdoc.sh create mode 100644 bindings/ruby/status.c create mode 100644 bindings/ruby/tags.c create mode 100644 bindings/ruby/thread.c create mode 100644 bindings/ruby/threads.c create mode 100644 command-line-arguments.c create mode 100644 command-line-arguments.h create mode 100644 compat/.gitignore create mode 100644 compat/Makefile create mode 100644 compat/Makefile.local create mode 100644 compat/README create mode 100644 compat/check_asctime.c create mode 100644 compat/check_getpwuid.c create mode 100644 compat/compat.h create mode 100644 compat/function-attributes.h create mode 100644 compat/gen_zlib_pc.c create mode 100644 compat/getdelim.c create mode 100644 compat/getline.c create mode 100644 compat/have_canonicalize_file_name.c create mode 100644 compat/have_d_type.c create mode 100644 compat/have_getline.c create mode 100644 compat/have_strcasestr.c create mode 100644 compat/have_strsep.c create mode 100644 compat/have_timegm.c create mode 100644 compat/strcasestr.c create mode 100644 compat/strsep.c create mode 100644 compat/timegm.c create mode 100644 completion/Makefile create mode 100644 completion/Makefile.local create mode 100644 completion/README create mode 100644 completion/notmuch-completion.bash create mode 100644 completion/zsh/_email-notmuch create mode 100644 completion/zsh/_notmuch create mode 100755 configure create mode 100644 contrib/go/.gitignore create mode 100644 contrib/go/LICENSE create mode 100644 contrib/go/Makefile create mode 100644 contrib/go/README create mode 100644 contrib/go/src/notmuch-addrlookup/addrlookup.go create mode 100644 contrib/go/src/notmuch/notmuch.go create mode 100644 contrib/notmuch-mutt/.gitignore create mode 100644 contrib/notmuch-mutt/Makefile create mode 100644 contrib/notmuch-mutt/README create mode 100755 contrib/notmuch-mutt/notmuch-mutt create mode 100644 contrib/notmuch-mutt/notmuch-mutt.rc create mode 100644 debian/.gitignore create mode 100644 debian/NEWS create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/elpa-notmuch.elpa create mode 100644 debian/elpa-notmuch.info create mode 100644 debian/elpa-notmuch.lintian-overrides create mode 100644 debian/elpa-test create mode 100644 debian/gbp.conf create mode 100644 debian/libnotmuch-dev.install create mode 100644 debian/libnotmuch-dev.manpages create mode 100644 debian/libnotmuch5.install create mode 100644 debian/libnotmuch5.symbols create mode 100644 debian/not-installed create mode 100644 debian/notmuch-doc.install create mode 100644 debian/notmuch-emacs.README.Debian create mode 100644 debian/notmuch-emacs.maintscript create mode 100644 debian/notmuch-git.install create mode 100644 debian/notmuch-git.manpages create mode 100644 debian/notmuch-mutt.docs create mode 100644 debian/notmuch-mutt.install create mode 100644 debian/notmuch-mutt.manpages create mode 100644 debian/notmuch-vim.README.Debian create mode 100644 debian/notmuch-vim.dirs create mode 100644 debian/notmuch-vim.docs create mode 100644 debian/notmuch-vim.install create mode 100644 debian/notmuch.dirs create mode 100644 debian/notmuch.docs create mode 100644 debian/notmuch.install create mode 100644 debian/notmuch.maintscript create mode 100644 debian/notmuch.manpages create mode 100644 debian/ruby-notmuch.install create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 debian/source/options create mode 100644 debian/tests/control create mode 100644 debian/upstream/metadata create mode 100644 debugger.c create mode 100644 devel/RELEASING create mode 100644 devel/STYLE create mode 100644 devel/TODO create mode 100644 devel/author-scan.sh create mode 100755 devel/check-notmuch-commit create mode 100755 devel/check-out-of-tree-build.sh create mode 100644 devel/emacs-keybindings.org create mode 100755 devel/man-to-mdwn.pl create mode 100755 devel/news2wiki.pl create mode 100644 devel/nmbug/doc/.gitignore create mode 100644 devel/nmbug/doc/Makefile create mode 100644 devel/nmbug/doc/conf.py create mode 100644 devel/nmbug/doc/index.rst create mode 100644 devel/nmbug/doc/man1/notmuch-report.1.rst create mode 100644 devel/nmbug/doc/man5/notmuch-report.json.5.rst create mode 100755 devel/nmbug/notmuch-report create mode 100644 devel/nmbug/notmuch-report.json create mode 100644 devel/notmuch-web/nmgunicorn.py create mode 100755 devel/notmuch-web/nmweb.py create mode 120000 devel/notmuch-web/static/css/jquery-ui.css create mode 100644 devel/notmuch-web/static/css/notmuch-0.1.css create mode 120000 devel/notmuch-web/static/js/jquery-ui.js create mode 120000 devel/notmuch-web/static/js/jquery.js create mode 100644 devel/notmuch-web/static/js/notmuch-0.1.js create mode 100644 devel/notmuch-web/templates/base.html create mode 100644 devel/notmuch-web/templates/index.html create mode 100644 devel/notmuch-web/templates/search.html create mode 100644 devel/notmuch-web/templates/show.html create mode 100644 devel/notmuch-web/todo create mode 100755 devel/release-checks.sh create mode 100644 devel/schemata create mode 100755 devel/try-emacs-mua create mode 100644 devel/uncrustify.cfg create mode 100644 doc/.gitignore create mode 100644 doc/INSTALL create mode 100644 doc/Makefile create mode 100644 doc/Makefile.local create mode 100644 doc/command-line.rst create mode 100644 doc/conf.py create mode 100644 doc/doxygen.cfg create mode 100644 doc/elisp.py create mode 100644 doc/index.rst create mode 100644 doc/man1/notmuch-address.rst create mode 100644 doc/man1/notmuch-compact.rst create mode 100644 doc/man1/notmuch-config.rst create mode 100644 doc/man1/notmuch-count.rst create mode 100644 doc/man1/notmuch-dump.rst create mode 100644 doc/man1/notmuch-emacs-mua.rst create mode 100644 doc/man1/notmuch-git.rst create mode 100644 doc/man1/notmuch-insert.rst create mode 100644 doc/man1/notmuch-new.rst create mode 100644 doc/man1/notmuch-reindex.rst create mode 100644 doc/man1/notmuch-reply.rst create mode 100644 doc/man1/notmuch-restore.rst create mode 100644 doc/man1/notmuch-search.rst create mode 100644 doc/man1/notmuch-show.rst create mode 100644 doc/man1/notmuch-tag.rst create mode 100644 doc/man1/notmuch.rst create mode 100644 doc/man5/notmuch-hooks.rst create mode 100644 doc/man7/notmuch-properties.rst create mode 100644 doc/man7/notmuch-search-terms.rst create mode 100644 doc/man7/notmuch-sexp-queries.rst create mode 100644 doc/notmuch-emacs.rst create mode 100644 doc/python-bindings.rst create mode 100644 doc/queries.rst create mode 100644 emacs/.gitignore create mode 100644 emacs/Makefile create mode 100644 emacs/Makefile.local create mode 100644 emacs/coolj.el create mode 100644 emacs/make-deps.el create mode 100644 emacs/notmuch-address.el create mode 100644 emacs/notmuch-company.el create mode 100644 emacs/notmuch-compat.el create mode 100644 emacs/notmuch-crypto.el create mode 100644 emacs/notmuch-draft.el create mode 100755 emacs/notmuch-emacs-mua create mode 100644 emacs/notmuch-emacs-mua.desktop create mode 100644 emacs/notmuch-hello.el create mode 100644 emacs/notmuch-jump.el create mode 100644 emacs/notmuch-lib.el create mode 100644 emacs/notmuch-logo.svg create mode 100644 emacs/notmuch-maildir-fcc.el create mode 100644 emacs/notmuch-message.el create mode 100644 emacs/notmuch-mua.el create mode 100644 emacs/notmuch-parser.el create mode 100644 emacs/notmuch-pkg.el.tmpl create mode 100644 emacs/notmuch-print.el create mode 100644 emacs/notmuch-query.el create mode 100644 emacs/notmuch-show.el create mode 100644 emacs/notmuch-tag.el create mode 100644 emacs/notmuch-tree.el create mode 100644 emacs/notmuch-version.el.tmpl create mode 100644 emacs/notmuch-wash.el create mode 100644 emacs/notmuch.el create mode 100644 emacs/rstdoc.el create mode 100644 emacs/rstdoc.rsti create mode 100644 gmime-filter-reply.c create mode 100644 gmime-filter-reply.h create mode 100644 hooks.c create mode 100644 lib/Makefile create mode 100644 lib/Makefile.local create mode 100644 lib/add-message.cc create mode 100644 lib/built-with.c create mode 100644 lib/config.cc create mode 100644 lib/database-private.h create mode 100644 lib/database.cc create mode 100644 lib/directory.cc create mode 100644 lib/features.cc create mode 100644 lib/filenames.c create mode 100644 lib/index.cc create mode 100644 lib/indexopts.c create mode 100644 lib/init.cc create mode 100644 lib/lastmod-fp.cc create mode 100644 lib/lastmod-fp.h create mode 100644 lib/message-file.c create mode 100644 lib/message-id.c create mode 100644 lib/message-private.h create mode 100644 lib/message-property.cc create mode 100644 lib/message.cc create mode 100644 lib/messages.c create mode 100644 lib/notmuch-private.h create mode 100644 lib/notmuch.h create mode 100644 lib/notmuch.sym create mode 100644 lib/open.cc create mode 100644 lib/parse-sexp.cc create mode 100644 lib/parse-time-vrp.cc create mode 100644 lib/parse-time-vrp.h create mode 100644 lib/prefix.cc create mode 100644 lib/query-fp.cc create mode 100644 lib/query-fp.h create mode 100644 lib/query.cc create mode 100644 lib/regexp-fields.cc create mode 100644 lib/regexp-fields.h create mode 100644 lib/sexp-fp.cc create mode 100644 lib/sexp-fp.h create mode 100644 lib/sha1.c create mode 100644 lib/string-list.c create mode 100644 lib/string-map.c create mode 100644 lib/tags.c create mode 100644 lib/thread-fp.cc create mode 100644 lib/thread-fp.h create mode 100644 lib/thread.cc create mode 100644 mime-node.c create mode 100644 notmuch-client-init.c create mode 100644 notmuch-client.h create mode 100644 notmuch-compact.c create mode 100644 notmuch-config.c create mode 100644 notmuch-count.c create mode 100644 notmuch-dump.c create mode 100644 notmuch-git.py create mode 100644 notmuch-insert.c create mode 100644 notmuch-new.c create mode 100644 notmuch-reindex.c create mode 100644 notmuch-reply.c create mode 100644 notmuch-restore.c create mode 100644 notmuch-search.c create mode 100644 notmuch-setup.c create mode 100644 notmuch-show.c create mode 100644 notmuch-tag.c create mode 100644 notmuch-time.c create mode 100644 notmuch.c create mode 100644 packaging/debian create mode 100644 packaging/fedora/notmuch.spec create mode 100644 parse-time-string/Makefile create mode 100644 parse-time-string/Makefile.local create mode 100644 parse-time-string/README create mode 100644 parse-time-string/parse-time-string.c create mode 100644 parse-time-string/parse-time-string.h create mode 100644 performance-test/.gitignore create mode 100755 performance-test/M00-new.sh create mode 100755 performance-test/M01-dump-restore.sh create mode 100755 performance-test/M02-show.sh create mode 100755 performance-test/M03-search.sh create mode 100755 performance-test/M04-reply.sh create mode 100755 performance-test/M05-reindex.sh create mode 100755 performance-test/M06-insert.sh create mode 100644 performance-test/Makefile create mode 100644 performance-test/Makefile.local create mode 100644 performance-test/README create mode 100755 performance-test/T00-new.sh create mode 100755 performance-test/T01-dump-restore.sh create mode 100755 performance-test/T02-tag.sh create mode 100755 performance-test/T03-reindex.sh create mode 100755 performance-test/T04-thread-subquery.sh create mode 100755 performance-test/T05-ruby.sh create mode 100755 performance-test/T06-emacs.sh create mode 100755 performance-test/T07-git.sh create mode 100644 performance-test/download/.gitignore create mode 100644 performance-test/download/notmuch-email-corpus-0.3.tar.xz.asc create mode 100644 performance-test/download/notmuch-email-corpus-0.4.tar.xz.asc create mode 100644 performance-test/download/notmuch-email-corpus-0.5.tar.xz.asc create mode 100755 performance-test/notmuch-memory-test create mode 100755 performance-test/notmuch-time-test create mode 100644 performance-test/perf-test-lib.sh create mode 100644 performance-test/version.sh create mode 100644 query-string.c create mode 100644 sprinter-json.c create mode 100644 sprinter-sexp.c create mode 100644 sprinter-text.c create mode 100644 sprinter.h create mode 100644 status.c create mode 100644 tag-util.c create mode 100644 tag-util.h create mode 100644 test/.gitignore create mode 100644 test/Makefile create mode 100644 test/Makefile.local create mode 100644 test/README create mode 100755 test/T000-basic.sh create mode 100755 test/T010-help-test.sh create mode 100755 test/T020-compact.sh create mode 100755 test/T030-config.sh create mode 100755 test/T035-read-config.sh create mode 100755 test/T040-setup.sh create mode 100755 test/T050-new.sh create mode 100755 test/T051-new-renames.sh create mode 100755 test/T055-path-config.sh create mode 100755 test/T060-count.sh create mode 100755 test/T070-insert.sh create mode 100755 test/T080-search.sh create mode 100755 test/T081-sexpr-search.sh create mode 100755 test/T090-search-output.sh create mode 100755 test/T095-address.sh create mode 100755 test/T100-search-by-folder.sh create mode 100755 test/T110-search-position-overlap-bug.sh create mode 100755 test/T120-search-insufficient-from-quoting.sh create mode 100755 test/T130-search-limiting.sh create mode 100755 test/T131-show-limiting.sh create mode 100755 test/T140-excludes.sh create mode 100755 test/T150-tagging.sh create mode 100755 test/T160-json.sh create mode 100755 test/T170-sexp.sh create mode 100755 test/T180-text.sh create mode 100755 test/T190-multipart.sh create mode 100755 test/T200-thread-naming.sh create mode 100755 test/T205-author-naming.sh create mode 100755 test/T210-raw.sh create mode 100755 test/T220-reply.sh create mode 100755 test/T230-reply-to-sender.sh create mode 100755 test/T240-dump-restore.sh create mode 100755 test/T250-uuencode.sh create mode 100755 test/T260-thread-order.sh create mode 100755 test/T270-author-order.sh create mode 100755 test/T280-from-guessing.sh create mode 100755 test/T290-long-id.sh create mode 100755 test/T300-encoding.sh create mode 100755 test/T310-emacs.sh create mode 100755 test/T315-emacs-tagging.sh create mode 100755 test/T320-emacs-large-search-buffer.sh create mode 100755 test/T330-emacs-subject-to-filename.sh create mode 100755 test/T340-maildir-sync.sh create mode 100755 test/T350-crypto.sh create mode 100755 test/T351-pgpmime-mangling.sh create mode 100755 test/T355-smime.sh create mode 100755 test/T356-protected-headers.sh create mode 100755 test/T357-index-decryption.sh create mode 100755 test/T358-emacs-protected-headers.sh create mode 100755 test/T360-symbol-hiding.sh create mode 100755 test/T370-search-folder-coherence.sh create mode 100755 test/T380-atomicity.sh create mode 100755 test/T385-transactions.sh create mode 100755 test/T390-python.sh create mode 100755 test/T391-python-cffi.sh create mode 100755 test/T392-python-cffi-notmuch.sh create mode 100755 test/T395-ruby.sh create mode 100755 test/T400-hooks.sh create mode 100755 test/T405-external.sh create mode 100755 test/T410-argument-parsing.sh create mode 100755 test/T420-emacs-test-functions.sh create mode 100755 test/T430-emacs-address-cleaning.sh create mode 100755 test/T440-emacs-hello.sh create mode 100755 test/T450-emacs-show.sh create mode 100755 test/T453-emacs-reply.sh create mode 100755 test/T454-emacs-dont-reply-names.sh create mode 100755 test/T455-emacs-charsets.sh create mode 100755 test/T460-emacs-tree.sh create mode 100755 test/T465-emacs-unthreaded.sh create mode 100755 test/T470-missing-headers.sh create mode 100755 test/T480-hex-escaping.sh create mode 100755 test/T490-parse-time-string.sh create mode 100755 test/T500-search-date.sh create mode 100755 test/T510-thread-replies.sh create mode 100755 test/T520-show.sh create mode 100755 test/T530-upgrade.sh create mode 100755 test/T550-db-features.sh create mode 100755 test/T560-lib-error.sh create mode 100755 test/T562-lib-database.sh create mode 100755 test/T563-lib-directory.sh create mode 100755 test/T564-lib-query.sh create mode 100755 test/T565-lib-tags.sh create mode 100755 test/T566-lib-message.sh create mode 100755 test/T568-lib-thread.sh create mode 100755 test/T570-revision-tracking.sh create mode 100755 test/T580-thread-search.sh create mode 100755 test/T585-thread-subquery.sh create mode 100755 test/T590-libconfig.sh create mode 100755 test/T592-thread-breakage.sh create mode 100755 test/T595-reopen.sh create mode 100755 test/T600-named-queries.sh create mode 100755 test/T610-message-property.sh create mode 100755 test/T620-lock.sh create mode 100755 test/T630-emacs-draft.sh create mode 100755 test/T640-database-modified.sh create mode 100755 test/T650-regexp-query.sh create mode 100755 test/T660-bad-date.sh create mode 100755 test/T670-duplicate-mid.sh create mode 100755 test/T680-html-indexing.sh create mode 100755 test/T690-command-line-args.sh create mode 100755 test/T700-reindex.sh create mode 100755 test/T710-message-id.sh create mode 100755 test/T720-emacs-attachment-warnings.sh create mode 100755 test/T720-lib-lifetime.sh create mode 100755 test/T730-emacs-forwarding.sh create mode 100755 test/T740-body.sh create mode 100755 test/T750-gzip.sh create mode 100755 test/T750-user-header.sh create mode 100755 test/T760-as-text.sh create mode 100755 test/T800-asan.sh create mode 100755 test/T810-tsan.sh create mode 100644 test/T810-tsan.suppressions create mode 100755 test/T850-git.sh create mode 100755 test/aggregate-results.sh create mode 100644 test/arg-test.c create mode 100644 test/atomicity.py create mode 100644 test/corpora/README create mode 100644 test/corpora/attachment/x-gtar-compressed.eml create mode 100644 test/corpora/broken/broken-cc create mode 100644 test/corpora/broken/loop/loop-12 create mode 100644 test/corpora/broken/loop/loop-21 create mode 100644 test/corpora/crypto/basic-encrypted.eml create mode 100644 test/corpora/crypto/encrypted-rfc822-attachment create mode 100644 test/corpora/crypto/encrypted-signed.eml create mode 100644 test/corpora/crypto/simple-encrypted create mode 100644 test/corpora/default/01:2, create mode 100644 test/corpora/default/02:2, create mode 100644 test/corpora/default/bar/17:2, create mode 100644 test/corpora/default/bar/18:2, create mode 100644 test/corpora/default/bar/baz/05:2, create mode 100644 test/corpora/default/bar/baz/23:2, create mode 100644 test/corpora/default/bar/baz/24:2, create mode 100644 test/corpora/default/bar/baz/cur/25:2, create mode 100644 test/corpora/default/bar/baz/cur/26:2, create mode 100644 test/corpora/default/bar/baz/new/27:2, create mode 100644 test/corpora/default/bar/baz/new/28:2, create mode 100644 test/corpora/default/bar/cur/19:2, create mode 100644 test/corpora/default/bar/cur/20:2, create mode 100644 test/corpora/default/bar/new/21:2, create mode 100644 test/corpora/default/bar/new/22:2, create mode 100644 test/corpora/default/cur/29:2, create mode 100644 test/corpora/default/cur/30:2, create mode 100644 test/corpora/default/cur/31:2, create mode 100644 test/corpora/default/cur/32:2, create mode 100644 test/corpora/default/cur/33:2, create mode 100644 test/corpora/default/cur/34:2, create mode 100644 test/corpora/default/cur/35:2, create mode 100644 test/corpora/default/cur/36:2, create mode 100644 test/corpora/default/cur/37:2, create mode 100644 test/corpora/default/cur/38:2, create mode 100644 test/corpora/default/cur/39:2, create mode 100644 test/corpora/default/cur/40:2, create mode 100644 test/corpora/default/cur/41:2, create mode 100644 test/corpora/default/cur/42:2, create mode 100644 test/corpora/default/cur/43:2, create mode 100644 test/corpora/default/cur/44:2, create mode 100644 test/corpora/default/cur/45:2, create mode 100644 test/corpora/default/cur/46:2, create mode 100644 test/corpora/default/cur/47:2, create mode 100644 test/corpora/default/cur/48:2, create mode 100644 test/corpora/default/cur/49:2, create mode 100644 test/corpora/default/cur/50:2, create mode 100644 test/corpora/default/cur/51:2, create mode 100644 test/corpora/default/cur/52:2, create mode 100644 test/corpora/default/cur/53:2, create mode 100644 test/corpora/default/foo/06:2, create mode 100644 test/corpora/default/foo/baz/11:2, create mode 100644 test/corpora/default/foo/baz/12:2, create mode 100644 test/corpora/default/foo/baz/cur/13:2, create mode 100644 test/corpora/default/foo/baz/cur/14:2, create mode 100644 test/corpora/default/foo/baz/new/15:2, create mode 100644 test/corpora/default/foo/baz/new/16:2, create mode 100644 test/corpora/default/foo/cur/07:2, create mode 100644 test/corpora/default/foo/cur/08:2, create mode 100644 test/corpora/default/foo/new/03:2, create mode 100644 test/corpora/default/foo/new/09:2, create mode 100644 test/corpora/default/foo/new/10:2, create mode 100644 test/corpora/default/new/04:2, create mode 100644 test/corpora/duplicate/msg-1-1:2, create mode 100644 test/corpora/duplicate/msg-1-2:2, create mode 100644 test/corpora/duplicate/msg-2-1:2, create mode 100644 test/corpora/duplicate/msg-2-2:2, create mode 100644 test/corpora/duplicate/msg-3-1:2, create mode 100644 test/corpora/duplicate/msg-3-2:2, create mode 100644 test/corpora/duplicate/msg-3-3:2, create mode 100644 test/corpora/duplicate/msg-3-4:2, create mode 100644 test/corpora/duplicate/msg-3-5:2, create mode 100644 test/corpora/html/attribute-text create mode 100644 test/corpora/html/embedded-image create mode 100644 test/corpora/indexing/PATCH-1-2-system_data_types.7-srcfix.txt:2,S create mode 100644 test/corpora/indexing/fake-pdf:2,S create mode 100644 test/corpora/insert/mbox-attachment.eml create mode 100644 test/corpora/lkml/cur/1354585346.000260:2, create mode 100644 test/corpora/lkml/cur/1354585346.000261:2, create mode 100644 test/corpora/lkml/cur/1354585346.000265:2, create mode 100644 test/corpora/lkml/cur/1354585346.000323:2, create mode 100644 test/corpora/lkml/cur/1354585346.000324:2, create mode 100644 test/corpora/lkml/cur/1354585346.000325:2, create mode 100644 test/corpora/lkml/cur/1354585346.000539:2, create mode 100644 test/corpora/lkml/cur/1354585346.000541:2, create mode 100644 test/corpora/lkml/cur/1382298587.001724:2, create mode 100644 test/corpora/lkml/cur/1382298587.001730:2, create mode 100644 test/corpora/lkml/cur/1382298587.001731:2, create mode 100644 test/corpora/lkml/cur/1382298587.001732:2, create mode 100644 test/corpora/lkml/cur/1382298587.001733:2, create mode 100644 test/corpora/lkml/cur/1382298587.001734:2, create mode 100644 test/corpora/lkml/cur/1382298587.001735:2, create mode 100644 test/corpora/lkml/cur/1382298587.001736:2, create mode 100644 test/corpora/lkml/cur/1382298587.001738:2, create mode 100644 test/corpora/lkml/cur/1382298587.001739:2, create mode 100644 test/corpora/lkml/cur/1382298587.001740:2, create mode 100644 test/corpora/lkml/cur/1382298587.001887:2, create mode 100644 test/corpora/lkml/cur/1382298587.001892:2, create mode 100644 test/corpora/lkml/cur/1382298587.001970:2, create mode 100644 test/corpora/lkml/cur/1382298587.002189:2, create mode 100644 test/corpora/lkml/cur/1382298587.002193:2, create mode 100644 test/corpora/lkml/cur/1382298587.002194:2, create mode 100644 test/corpora/lkml/cur/1382298587.002195:2, create mode 100644 test/corpora/lkml/cur/1382298587.002196:2, create mode 100644 test/corpora/lkml/cur/1382298587.002197:2, create mode 100644 test/corpora/lkml/cur/1382298587.002201:2, create mode 100644 test/corpora/lkml/cur/1382298587.002228:2, create mode 100644 test/corpora/lkml/cur/1382298587.002878:2, create mode 100644 test/corpora/lkml/cur/1382298587.002912:2, create mode 100644 test/corpora/lkml/cur/1382298587.002915:2, create mode 100644 test/corpora/lkml/cur/1382298587.002917:2, create mode 100644 test/corpora/lkml/cur/1382298587.002997:2, create mode 100644 test/corpora/lkml/cur/1382298587.003106:2, create mode 100644 test/corpora/lkml/cur/1382298587.003112:2, create mode 100644 test/corpora/lkml/cur/1382298587.003117:2, create mode 100644 test/corpora/lkml/cur/1382298587.003118:2, create mode 100644 test/corpora/lkml/cur/1382298587.003171:2, create mode 100644 test/corpora/lkml/cur/1382298587.003317:2, create mode 100644 test/corpora/lkml/cur/1382298587.003318:2, create mode 100644 test/corpora/lkml/cur/1382298587.003486:2, create mode 100644 test/corpora/lkml/cur/1382298587.004581:2, create mode 100644 test/corpora/lkml/cur/1382298587.004582:2, create mode 100644 test/corpora/lkml/cur/1382298770.001724:2, create mode 100644 test/corpora/lkml/cur/1382298770.001730:2, create mode 100644 test/corpora/lkml/cur/1382298770.001731:2, create mode 100644 test/corpora/lkml/cur/1382298770.001732:2, create mode 100644 test/corpora/lkml/cur/1382298770.001733:2, create mode 100644 test/corpora/lkml/cur/1382298770.001734:2, create mode 100644 test/corpora/lkml/cur/1382298770.001735:2, create mode 100644 test/corpora/lkml/cur/1382298770.001736:2, create mode 100644 test/corpora/lkml/cur/1382298770.001738:2, create mode 100644 test/corpora/lkml/cur/1382298770.001739:2, create mode 100644 test/corpora/lkml/cur/1382298770.001740:2, create mode 100644 test/corpora/lkml/cur/1382298770.001887:2, create mode 100644 test/corpora/lkml/cur/1382298770.001892:2, create mode 100644 test/corpora/lkml/cur/1382298770.002189:2, create mode 100644 test/corpora/lkml/cur/1382298770.002191:2, create mode 100644 test/corpora/lkml/cur/1382298770.002193:2, create mode 100644 test/corpora/lkml/cur/1382298770.002194:2, create mode 100644 test/corpora/lkml/cur/1382298770.002195:2, create mode 100644 test/corpora/lkml/cur/1382298770.002196:2, create mode 100644 test/corpora/lkml/cur/1382298770.002197:2, create mode 100644 test/corpora/lkml/cur/1382298770.002201:2, create mode 100644 test/corpora/lkml/cur/1382298770.002878:2, create mode 100644 test/corpora/lkml/cur/1382298770.002879:2, create mode 100644 test/corpora/lkml/cur/1382298770.002911:2, create mode 100644 test/corpora/lkml/cur/1382298770.002912:2, create mode 100644 test/corpora/lkml/cur/1382298770.002915:2, create mode 100644 test/corpora/lkml/cur/1382298770.002917:2, create mode 100644 test/corpora/lkml/cur/1382298770.002930:2, create mode 100644 test/corpora/lkml/cur/1382298770.002997:2, create mode 100644 test/corpora/lkml/cur/1382298770.003106:2, create mode 100644 test/corpora/lkml/cur/1382298770.003117:2, create mode 100644 test/corpora/lkml/cur/1382298770.003118:2, create mode 100644 test/corpora/lkml/cur/1382298770.003171:2, create mode 100644 test/corpora/lkml/cur/1382298770.003317:2, create mode 100644 test/corpora/lkml/cur/1382298770.003318:2, create mode 100644 test/corpora/lkml/cur/1382298770.003486:2, create mode 100644 test/corpora/lkml/cur/1382298770.003499:2, create mode 100644 test/corpora/lkml/cur/1382298770.004581:2, create mode 100644 test/corpora/lkml/cur/1382298770.004582:2, create mode 100644 test/corpora/lkml/cur/1382298775.002830:2, create mode 100644 test/corpora/lkml/cur/1382298775.002978:2, create mode 100644 test/corpora/lkml/cur/1382298775.002992:2, create mode 100644 test/corpora/lkml/cur/1382298775.002999:2, create mode 100644 test/corpora/lkml/cur/1382298775.003976:2, create mode 100644 test/corpora/lkml/cur/1382298775.004354:2, create mode 100644 test/corpora/lkml/cur/1382298775.004363:2, create mode 100644 test/corpora/lkml/cur/1382298775.004374:2, create mode 100644 test/corpora/lkml/cur/1382298793.002253:2, create mode 100644 test/corpora/lkml/cur/1382298793.002254:2, create mode 100644 test/corpora/lkml/cur/1382298793.002255:2, create mode 100644 test/corpora/lkml/cur/1382298793.002256:2, create mode 100644 test/corpora/lkml/cur/1382298793.002257:2, create mode 100644 test/corpora/lkml/cur/1382298793.002258:2, create mode 100644 test/corpora/lkml/cur/1382298793.002259:2, create mode 100644 test/corpora/lkml/cur/1382298793.002260:2, create mode 100644 test/corpora/lkml/cur/1382298793.002261:2, create mode 100644 test/corpora/lkml/cur/1382298793.002262:2, create mode 100644 test/corpora/lkml/cur/1382298793.002263:2, create mode 100644 test/corpora/lkml/cur/1382298793.002264:2, create mode 100644 test/corpora/lkml/cur/1382298793.002265:2, create mode 100644 test/corpora/lkml/cur/1382298793.002266:2, create mode 100644 test/corpora/lkml/cur/1382298793.002267:2, create mode 100644 test/corpora/lkml/cur/1382298793.002268:2, create mode 100644 test/corpora/lkml/cur/1382298793.002269:2, create mode 100644 test/corpora/lkml/cur/1382298793.002270:2, create mode 100644 test/corpora/lkml/cur/1382298793.002271:2, create mode 100644 test/corpora/lkml/cur/1382298793.002272:2, create mode 100644 test/corpora/lkml/cur/1382298793.002273:2, create mode 100644 test/corpora/lkml/cur/1382298793.002274:2, create mode 100644 test/corpora/lkml/cur/1382298793.002275:2, create mode 100644 test/corpora/lkml/cur/1382298793.002276:2, create mode 100644 test/corpora/lkml/cur/1382298793.002277:2, create mode 100644 test/corpora/lkml/cur/1382298793.002278:2, create mode 100644 test/corpora/lkml/cur/1382298793.002279:2, create mode 100644 test/corpora/lkml/cur/1382298793.002280:2, create mode 100644 test/corpora/lkml/cur/1382298793.002281:2, create mode 100644 test/corpora/lkml/cur/1382298793.002282:2, create mode 100644 test/corpora/lkml/cur/1382298793.002283:2, create mode 100644 test/corpora/lkml/cur/1382298793.002284:2, create mode 100644 test/corpora/lkml/cur/1382298793.002285:2, create mode 100644 test/corpora/lkml/cur/1382298793.002286:2, create mode 100644 test/corpora/lkml/cur/1382298793.002287:2, create mode 100644 test/corpora/lkml/cur/1382298793.002288:2, create mode 100644 test/corpora/lkml/cur/1382298793.002289:2, create mode 100644 test/corpora/lkml/cur/1382298793.002290:2, create mode 100644 test/corpora/lkml/cur/1382298793.002292:2, create mode 100644 test/corpora/lkml/cur/1382298793.002293:2, create mode 100644 test/corpora/lkml/cur/1382298793.002294:2, create mode 100644 test/corpora/lkml/cur/1382298793.002296:2, create mode 100644 test/corpora/lkml/cur/1382298793.002297:2, create mode 100644 test/corpora/lkml/cur/1382298793.002298:2, create mode 100644 test/corpora/lkml/cur/1382298793.002299:2, create mode 100644 test/corpora/lkml/cur/1382298793.002302:2, create mode 100644 test/corpora/lkml/cur/1382298793.002309:2, create mode 100644 test/corpora/lkml/cur/1382298793.002329:2, create mode 100644 test/corpora/lkml/cur/1382298793.002340:2, create mode 100644 test/corpora/lkml/cur/1382298793.002400:2, create mode 100644 test/corpora/lkml/cur/1382298793.002432:2, create mode 100644 test/corpora/lkml/cur/1382298793.002468:2, create mode 100644 test/corpora/lkml/cur/1382298793.002543:2, create mode 100644 test/corpora/lkml/cur/1382298793.002557:2, create mode 100644 test/corpora/lkml/cur/1382298793.002575:2, create mode 100644 test/corpora/lkml/cur/1382298793.002576:2, create mode 100644 test/corpora/lkml/cur/1382298793.002639:2, create mode 100644 test/corpora/lkml/cur/1382298793.002642:2, create mode 100644 test/corpora/lkml/cur/1382298793.002661:2, create mode 100644 test/corpora/lkml/cur/1382298793.002662:2, create mode 100644 test/corpora/lkml/cur/1382298793.002663:2, create mode 100644 test/corpora/lkml/cur/1382298793.002664:2, create mode 100644 test/corpora/lkml/cur/1382298793.002665:2, create mode 100644 test/corpora/lkml/cur/1382298793.002666:2, create mode 100644 test/corpora/lkml/cur/1382298793.002667:2, create mode 100644 test/corpora/lkml/cur/1382298793.002668:2, create mode 100644 test/corpora/lkml/cur/1382298793.002669:2, create mode 100644 test/corpora/lkml/cur/1382298793.002670:2, create mode 100644 test/corpora/lkml/cur/1382298793.002671:2, create mode 100644 test/corpora/lkml/cur/1382298793.002679:2, create mode 100644 test/corpora/lkml/cur/1382298793.002688:2, create mode 100644 test/corpora/lkml/cur/1382298793.002699:2, create mode 100644 test/corpora/lkml/cur/1382298793.003013:2, create mode 100644 test/corpora/lkml/cur/1382298793.003145:2, create mode 100644 test/corpora/lkml/cur/1382298793.003148:2, create mode 100644 test/corpora/lkml/cur/1382298793.003216:2, create mode 100644 test/corpora/lkml/cur/1382298793.003231:2, create mode 100644 test/corpora/lkml/cur/1382298793.003278:2, create mode 100644 test/corpora/lkml/cur/1382298793.003295:2, create mode 100644 test/corpora/lkml/cur/1382298793.003316:2, create mode 100644 test/corpora/lkml/cur/1382298793.003334:2, create mode 100644 test/corpora/lkml/cur/1382298793.003340:2, create mode 100644 test/corpora/lkml/cur/1382298793.003448:2, create mode 100644 test/corpora/lkml/cur/1382298793.003459:2, create mode 100644 test/corpora/lkml/cur/1382298793.003462:2, create mode 100644 test/corpora/lkml/cur/1382298793.003468:2, create mode 100644 test/corpora/lkml/cur/1382298793.003471:2, create mode 100644 test/corpora/lkml/cur/1382298793.003472:2, create mode 100644 test/corpora/lkml/cur/1382298793.003478:2, create mode 100644 test/corpora/lkml/cur/1382298793.003497:2, create mode 100644 test/corpora/lkml/cur/1382298793.003501:2, create mode 100644 test/corpora/lkml/cur/1382298793.003503:2, create mode 100644 test/corpora/lkml/cur/1382298793.003971:2, create mode 100644 test/corpora/lkml/cur/1382298793.004059:2, create mode 100644 test/corpora/lkml/cur/1382298793.004091:2, create mode 100644 test/corpora/lkml/cur/1382298793.004190:2, create mode 100644 test/corpora/lkml/cur/1382298795.000299:2, create mode 100644 test/corpora/lkml/cur/1382298795.001362:2, create mode 100644 test/corpora/lkml/cur/1382298795.002635:2, create mode 100644 test/corpora/lkml/cur/1382298796.001941:2, create mode 100644 test/corpora/lkml/cur/1382298805.004526:2, create mode 100644 test/corpora/lkml/cur/1382298805.004551:2, create mode 100644 test/corpora/lkml/cur/1382298805.004613:2, create mode 100644 test/corpora/lkml/cur/1382298805.004614:2, create mode 100644 test/corpora/lkml/cur/1382298805.004615:2, create mode 100644 test/corpora/lkml/cur/1382298805.004617:2, create mode 100644 test/corpora/lkml/cur/1382298805.004618:2, create mode 100644 test/corpora/lkml/cur/1382298805.004619:2, create mode 100644 test/corpora/lkml/cur/1382298805.004636:2, create mode 100644 test/corpora/lkml/cur/1382298805.004638:2, create mode 100644 test/corpora/lkml/cur/1382298805.004639:2, create mode 100644 test/corpora/lkml/cur/1382298805.004640:2, create mode 100644 test/corpora/lkml/cur/1382298805.004642:2, create mode 100644 test/corpora/lkml/cur/1382298805.004653:2, create mode 100644 test/corpora/lkml/cur/1382298805.004665:2, create mode 100644 test/corpora/lkml/cur/1382298805.004680:2, create mode 100644 test/corpora/lkml/cur/1382298805.004688:2, create mode 100644 test/corpora/lkml/cur/1382298805.004906:2, create mode 100644 test/corpora/mangling/mixed-up.eml create mode 100644 test/corpora/pkcs7/smime-onepart-signed.eml create mode 100644 test/corpora/protected-headers/double-wrapped-with-phony-protected-header.eml create mode 100644 test/corpora/protected-headers/encrypted-message-with-forwarded-attachment.eml create mode 100644 test/corpora/protected-headers/encrypted-signed-not-masked.eml create mode 100644 test/corpora/protected-headers/encrypted-signed.eml create mode 100644 test/corpora/protected-headers/misplaced-protected-header.eml create mode 100644 test/corpora/protected-headers/nested-rfc822-message.eml create mode 100644 test/corpora/protected-headers/no-protected-header-attribute.eml create mode 100644 test/corpora/protected-headers/phony-protected-header-bad-encryption.eml create mode 100644 test/corpora/protected-headers/protected-header.eml create mode 100644 test/corpora/protected-headers/protected-with-legacy-display.eml create mode 100644 test/corpora/protected-headers/signed-protected-header.eml create mode 100644 test/corpora/protected-headers/simple-signed-mail.eml create mode 100644 test/corpora/protected-headers/smime-enc+legacy-disp.eml create mode 100644 test/corpora/protected-headers/smime-multipart-signed.eml create mode 100644 test/corpora/protected-headers/smime-onepart-signed.eml create mode 100644 test/corpora/protected-headers/smime-sign+enc+legacy-disp.eml create mode 100644 test/corpora/protected-headers/smime-sign+enc.eml create mode 100644 test/corpora/protected-headers/subjectless-protected-header.eml create mode 100644 test/corpora/protected-headers/wrapped-protected-header.eml create mode 100644 test/corpora/threading/ghost-root/1529425589.M615261P21663.len:2,S create mode 100644 test/corpora/threading/ghost-root/1532672447.R3166642290392477575.len:2,S create mode 100644 test/corpora/threading/ghost-root/1532672447.R6968667928580738175.len:2,S create mode 100644 test/corpora/threading/ghost-root/child create mode 100644 test/corpora/threading/ghost-root/fake-root create mode 100644 test/corpora/threading/ghost-root/grand-child create mode 100644 test/corpora/threading/ghost-root/grand-child2 create mode 100644 test/corpora/threading/ghost-root/great-grand-child create mode 100644 test/corpora/threading/ghost-root/real-root create mode 100644 test/corpora/threading/parent-priority/cur/child create mode 100644 test/corpora/threading/parent-priority/cur/grand-child create mode 100644 test/corpora/threading/parent-priority/cur/root create mode 100644 test/database-test.c create mode 100644 test/database-test.h create mode 100644 test/emacs-address-cleaning.el create mode 100644 test/emacs-attachment-warnings.el create mode 100644 test/emacs-reply.expected-output/notmuch-reply-duplicate-4 create mode 100644 test/emacs-show.expected-output/notmuch-show-decrypted-message create mode 100644 test/emacs-show.expected-output/notmuch-show-decrypted-message-no-crypto create mode 100644 test/emacs-show.expected-output/notmuch-show-depth create mode 100644 test/emacs-show.expected-output/notmuch-show-depth-1 create mode 100644 test/emacs-show.expected-output/notmuch-show-duplicate-4 create mode 100644 test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-off create mode 100644 test/emacs-show.expected-output/notmuch-show-elide-non-matching-messages-on create mode 100644 test/emacs-show.expected-output/notmuch-show-height-0 create mode 100644 test/emacs-show.expected-output/notmuch-show-indent-thread-content-off create mode 100644 test/emacs-show.expected-output/notmuch-show-multipart-alternative create mode 100644 test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-off create mode 100644 test/emacs-show.expected-output/notmuch-show-process-crypto-mime-parts-on create mode 100644 test/emacs-show.expected-output/notmuch-show-size create mode 100644 test/emacs-show.expected-output/notmuch-show-size-450 create mode 100644 test/emacs-show.expected-output/notmuch-show-undecryptable-message create mode 100644 test/emacs-tree.expected-output/inbox-outline create mode 100644 test/emacs-tree.expected-output/notmuch-tree-show-window create mode 100644 test/emacs-tree.expected-output/notmuch-tree-single-thread create mode 100644 test/emacs-tree.expected-output/notmuch-tree-tag-inbox create mode 100644 test/emacs-tree.expected-output/notmuch-tree-tag-inbox-tagged create mode 100644 test/emacs-tree.expected-output/notmuch-tree-tag-inbox-thread-tagged create mode 100644 test/emacs-tree.expected-output/result-format-function create mode 100644 test/emacs-unthreaded.expected-output/result-format-function create mode 100644 test/emacs.expected-output/attachment create mode 100644 test/emacs.expected-output/notmuch-hello create mode 100644 test/emacs.expected-output/notmuch-hello-all-tags create mode 100644 test/emacs.expected-output/notmuch-hello-empty-custom-queries-section create mode 100644 test/emacs.expected-output/notmuch-hello-empty-custom-tags-section create mode 100644 test/emacs.expected-output/notmuch-hello-long-names create mode 100644 test/emacs.expected-output/notmuch-hello-new-section create mode 100644 test/emacs.expected-output/notmuch-hello-no-saved-searches create mode 100644 test/emacs.expected-output/notmuch-hello-section-counts create mode 100644 test/emacs.expected-output/notmuch-hello-section-hidden-tag create mode 100644 test/emacs.expected-output/notmuch-hello-section-with-empty create mode 100644 test/emacs.expected-output/notmuch-hello-view-inbox create mode 100644 test/emacs.expected-output/notmuch-hello-with-empty create mode 100644 test/emacs.expected-output/notmuch-search-tag-inbox create mode 100644 test/emacs.expected-output/notmuch-show-message-with-headers-hidden create mode 100644 test/emacs.expected-output/notmuch-show-message-with-headers-visible create mode 100644 test/emacs.expected-output/notmuch-show-thread-maildir-storage create mode 100644 test/emacs.expected-output/notmuch-show-thread-maildir-storage-with-fourfold-indentation create mode 100644 test/emacs.expected-output/notmuch-show-thread-maildir-storage-without-indentation create mode 100644 test/emacs.expected-output/notmuch-show-thread-with-all-messages-collapsed create mode 100644 test/emacs.expected-output/notmuch-show-thread-with-all-messages-uncollapsed create mode 100644 test/emacs.expected-output/notmuch-show-thread-with-hidden-messages create mode 100644 test/emacs.expected-output/raw-message-cf0c4d-52ad0a create mode 100644 test/emacs.expected-output/search-result-format-function create mode 100644 test/export-dirs.sh create mode 100644 test/gen-threads.py create mode 100644 test/ghost-report.cc create mode 100644 test/hex-xcode.c create mode 100755 test/json_check_nodes.py create mode 100644 test/make-db-version.cc create mode 100644 test/message-id-parse.c create mode 100755 test/notmuch-test create mode 100644 test/notmuch-test.h create mode 100644 test/openpgp4-secret-key.asc create mode 100644 test/openpgp4-secret-key.asc.NOTE create mode 100644 test/parse-time.c create mode 100644 test/random-corpus.c create mode 100644 test/setup.expected-output/config-with-comments create mode 100644 test/smime/0xE0972A47.p12 create mode 100644 test/smime/README create mode 100644 test/smime/bob.p12 create mode 100644 test/smime/ca.crt create mode 100644 test/smime/key+cert.pem create mode 100644 test/smime/test.crt create mode 100644 test/smtp-dummy.c create mode 100644 test/symbol-test.cc create mode 100644 test/test-lib-FREEBSD.sh create mode 100644 test/test-lib-common.sh create mode 100644 test/test-lib-emacs.sh create mode 100644 test/test-lib.el create mode 100644 test/test-lib.sh create mode 100644 test/test-vars.sh create mode 100755 test/test-verbose create mode 100644 test/test.expected-output/test-verbose-no create mode 100644 test/test.expected-output/test-verbose-yes create mode 100644 test/valgrind/suppressions create mode 100755 test/valgrind/valgrind.sh create mode 100644 util/Makefile create mode 100644 util/Makefile.local create mode 100644 util/crypto.c create mode 100644 util/crypto.h create mode 100644 util/error_util.c create mode 100644 util/error_util.h create mode 100644 util/gmime-extra.c create mode 100644 util/gmime-extra.h create mode 100644 util/hex-escape.c create mode 100644 util/hex-escape.h create mode 100644 util/path-util.c create mode 100644 util/path-util.h create mode 100644 util/repair.c create mode 100644 util/repair.h create mode 100644 util/string-util.c create mode 100644 util/string-util.h create mode 100644 util/talloc-extra.c create mode 100644 util/talloc-extra.h create mode 100644 util/unicode-util.c create mode 100644 util/unicode-util.h create mode 100644 util/util.c create mode 100644 util/util.h create mode 100644 util/xapian-extra.h create mode 100644 util/xutil.c create mode 100644 util/xutil.h create mode 100644 util/zlib-extra.c create mode 100644 util/zlib-extra.h create mode 100644 version.txt create mode 100644 vim/Makefile create mode 100644 vim/README create mode 100644 vim/notmuch.txt create mode 100644 vim/notmuch.vim create mode 100644 vim/notmuch.yaml create mode 100644 vim/syntax/notmuch-compose.vim create mode 100644 vim/syntax/notmuch-folders.vim create mode 100644 vim/syntax/notmuch-git-diff.vim create mode 100644 vim/syntax/notmuch-search.vim create mode 100644 vim/syntax/notmuch-show.vim diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 00000000..b3ddffe8 --- /dev/null +++ b/.dir-locals.el @@ -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 index 00000000..f94d1480 --- /dev/null +++ b/.gitignore @@ -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 index 00000000..935c6eb0 --- /dev/null +++ b/.mailmap @@ -0,0 +1,8 @@ +Peter Feigl +Nate +Ali Polatel +Stefan +Patrick Totzke +Patrick Totzke +Patrick Totzke +Mark Walters diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..5bb03de6 --- /dev/null +++ b/.travis.yml @@ -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 index 00000000..6e872084 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,129 @@ +Carl Worth 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 ) + 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 index 00000000..4e744d26 --- /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 index 00000000..4c493545 --- /dev/null +++ b/COPYING-GPL-3 @@ -0,0 +1,676 @@ + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/INSTALL b/INSTALL new file mode 100644 index 00000000..8054fafa --- /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 index 00000000..d2010fe4 --- /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 index 00000000..7a7a3c6d --- /dev/null +++ b/Makefile.global @@ -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 index 00000000..7699c208 --- /dev/null +++ b/Makefile.local @@ -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 index 00000000..315f4136 --- /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:` is equivalent to `date:..`. + +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:..!` shorthand for +`date:..` + + 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:" + +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": "\n\n...", + "footer": "", + ... + }, + ... + } + +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:..`. Notmuch + supports a wide variety of expressions in `` and + ``. 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: + + 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 + + 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 ); do + "$file" + done + +`notmuch show --format=mbox ` + + 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]
. [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. + + + diff --git a/README b/README new file mode 100644 index 00000000..03bbb57f --- /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 index 00000000..7ff3198c --- /dev/null +++ b/README.rst @@ -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 index 00000000..de492a7c --- /dev/null +++ b/bindings/Makefile @@ -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 index 00000000..9875123a --- /dev/null +++ b/bindings/Makefile.local @@ -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 index 00000000..9ef81f24 --- /dev/null +++ b/bindings/python-cffi/MANIFEST.in @@ -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 index 00000000..f281edc1 --- /dev/null +++ b/bindings/python-cffi/notmuch2/__init__.py @@ -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 index 00000000..1cf03c88 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_base.py @@ -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 '' + else: + return '' diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py new file mode 100644 index 00000000..65d7dcb6 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_build.py @@ -0,0 +1,346 @@ +import cffi +from _notmuch_config import * + +ffibuilder = cffi.FFI() +ffibuilder.set_source( + 'notmuch2._capi', + r""" + #include + #include + #include + + #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 index 00000000..603fdcbf --- /dev/null +++ b/bindings/python-cffi/notmuch2/_config.py @@ -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 index 00000000..d7485b4d --- /dev/null +++ b/bindings/python-cffi/notmuch2/_database.py @@ -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 index 00000000..17c3ad9c --- /dev/null +++ b/bindings/python-cffi/notmuch2/_errors.py @@ -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 index 00000000..d4b34e91 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_message.py @@ -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: + :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 index 00000000..1db6ec96 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_query.py @@ -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 index 00000000..ee5d2a34 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_tags.py @@ -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 '' + else: + return '' diff --git a/bindings/python-cffi/notmuch2/_thread.py b/bindings/python-cffi/notmuch2/_thread.py new file mode 100644 index 00000000..e883f308 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_thread.py @@ -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 index 00000000..55fb2d24 --- /dev/null +++ b/bindings/python-cffi/setup.py @@ -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 index 00000000..fe90c787 --- /dev/null +++ b/bindings/python-cffi/tests/conftest.py @@ -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 index 00000000..d3280a67 --- /dev/null +++ b/bindings/python-cffi/tests/test_base.py @@ -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 index 00000000..2a7f42f0 --- /dev/null +++ b/bindings/python-cffi/tests/test_config.py @@ -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 index 00000000..f1d12ea6 --- /dev/null +++ b/bindings/python-cffi/tests/test_database.py @@ -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 index 00000000..c2519f86 --- /dev/null +++ b/bindings/python-cffi/tests/test_errors.py @@ -0,0 +1,8 @@ +from notmuch2 import _capi as capi +from notmuch2 import _errors as errors + +def test_status_no_message(): + exc = errors.NotmuchError(capi.lib.NOTMUCH_STATUS_PATH_ERROR) + assert exc.status == capi.lib.NOTMUCH_STATUS_PATH_ERROR + assert exc.message is None + assert str(exc) == 'Path supplied is illegal for this function' diff --git a/bindings/python-cffi/tests/test_message.py b/bindings/python-cffi/tests/test_message.py new file mode 100644 index 00000000..56701d05 --- /dev/null +++ b/bindings/python-cffi/tests/test_message.py @@ -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 index 00000000..f2c6209d --- /dev/null +++ b/bindings/python-cffi/tests/test_tags.py @@ -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 index 00000000..619d2aac --- /dev/null +++ b/bindings/python-cffi/tests/test_thread.py @@ -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 index 00000000..7cf93be0 --- /dev/null +++ b/bindings/python-cffi/tox.ini @@ -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 index 00000000..601acdd7 --- /dev/null +++ b/bindings/python/.gitignore @@ -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 index 00000000..c83be4b1 --- /dev/null +++ b/bindings/python/MANIFEST.in @@ -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 index 00000000..5bf076d2 --- /dev/null +++ b/bindings/python/README @@ -0,0 +1,17 @@ +notmuch -- The python interface to notmuch +========================================== + +This module makes the functionality of the notmuch library +(`https://notmuchmail.org`_) available to python. Successful import of +this module depends on a libnotmuch.so|dll being available on the +user's system. + +If you have downloaded the full source tarball, you can create the +documentation with sphinx installed, go to the docs directory and +"make html". A static version of the documentation is available at: + + https://notmuch.readthedocs.io/projects/notmuch-python/ + +To build the python bindings, do + + python setup.py install --prefix=path/to/your/preferred/location diff --git a/bindings/python/docs/COPYING b/bindings/python/docs/COPYING new file mode 100644 index 00000000..e6000869 --- /dev/null +++ b/bindings/python/docs/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/bindings/python/docs/Makefile b/bindings/python/docs/Makefile new file mode 100644 index 00000000..bd6de38c --- /dev/null +++ b/bindings/python/docs/Makefile @@ -0,0 +1,88 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf build/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) html + @echo + @echo "Build finished. The HTML pages are in html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) build/dirhtml + @echo + @echo "Build finished. The HTML pages are in build/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in build/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) build/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in build/qthelp, like this:" + @echo "# qcollectiongenerator build/qthelp/pyDNS.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile build/qthelp/pyDNS.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex + @echo + @echo "Build finished; the LaTeX files are in build/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes + @echo + @echo "The overview file is in build/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in build/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) build/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in build/doctest/output.txt." diff --git a/bindings/python/docs/source/conf.py b/bindings/python/docs/source/conf.py new file mode 100644 index 00000000..8b43c5ca --- /dev/null +++ b/bindings/python/docs/source/conf.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# +# pyDNS documentation build configuration file, created by +# sphinx-quickstart on Tue Feb 2 10:00:47 2010. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +from unittest.mock import Mock + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0,os.path.abspath('../..')) + +MOCK_MODULES = [ + 'ctypes', +] +for mod_name in MOCK_MODULES: + sys.modules[mod_name] = Mock() + + +from notmuch import __VERSION__,__AUTHOR__ +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo'] +autoclass_content = "both" + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'notmuch' +copyright = u'2010-2012, ' + __AUTHOR__ + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = __VERSION__ +# The full version, including alpha/beta/rc tags. +release = __VERSION__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = False + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +html_use_modindex = False + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'notmuchdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'notmuch.tex', u'notmuch Documentation', + u'notmuch contributors', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'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 index 00000000..660de91d --- /dev/null +++ b/bindings/python/docs/source/database.rst @@ -0,0 +1,54 @@ +:class:`Database` -- The underlying notmuch database +==================================================== + +.. currentmodule:: notmuch + +.. autoclass:: Database([path=None[, create=False[, mode=MODE.READ_ONLY]]]) + + .. automethod:: create + + .. automethod:: open(path, status=MODE.READ_ONLY) + + .. automethod:: close + + .. automethod:: get_path + + .. automethod:: get_version + + .. automethod:: needs_upgrade + + .. automethod:: upgrade + + .. automethod:: begin_atomic + + .. automethod:: end_atomic + + .. automethod:: get_directory + + .. automethod:: index_file + + .. automethod:: remove_message + + .. automethod:: find_message + + .. automethod:: find_message_by_filename + + .. automethod:: get_all_tags + + .. automethod:: create_query + + .. automethod:: get_config + + .. automethod:: get_configs + + .. automethod:: set_config + + .. attribute:: Database.MODE + + Defines constants that are used as the mode in which to open a database. + + MODE.READ_ONLY + Open the database in read-only mode + + MODE.READ_WRITE + Open the database in read-write mode diff --git a/bindings/python/docs/source/filesystem.rst b/bindings/python/docs/source/filesystem.rst new file mode 100644 index 00000000..13fe1194 --- /dev/null +++ b/bindings/python/docs/source/filesystem.rst @@ -0,0 +1,32 @@ +Files and directories +===================== + +.. currentmodule:: notmuch + +:class:`Filenames` -- An iterator over filenames +------------------------------------------------ + +.. autoclass:: Filenames + + .. method:: Filenames.__len__ + .. warning:: + :meth:`__len__` was removed in version 0.22 as it exhausted the + iterator and broke list(Filenames()). Use `len(list(names))` + instead. + +:class:`Directory` -- A directory entry in the database +------------------------------------------------------- + +.. autoclass:: Directory + + .. automethod:: Directory.get_child_files + + .. automethod:: Directory.get_child_directories + + .. automethod:: Directory.get_mtime + + .. automethod:: Directory.set_mtime + + .. autoattribute:: Directory.mtime + + .. autoattribute:: Directory.path diff --git a/bindings/python/docs/source/index.rst b/bindings/python/docs/source/index.rst new file mode 100644 index 00000000..bef7e60d --- /dev/null +++ b/bindings/python/docs/source/index.rst @@ -0,0 +1,36 @@ +Welcome to :mod:`notmuch`'s documentation +========================================= + +.. currentmodule:: notmuch + +The :mod:`notmuch` module provides an interface to the `notmuch +`_ functionality, directly interfacing to a +shared notmuch library. Within :mod:`notmuch`, the classes +:class:`Database`, :class:`Query` provide most of the core +functionality, returning :class:`Threads`, :class:`Messages` and +:class:`Tags`. + +.. moduleauthor:: Sebastian Spaeth + +:License: This module is covered under the GNU GPL v3 (or later). + +.. toctree:: + :maxdepth: 1 + + quickstart + notes + status_and_errors + database + query + messages + message + tags + threads + thread + filesystem + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/bindings/python/docs/source/message.rst b/bindings/python/docs/source/message.rst new file mode 100644 index 00000000..b0033924 --- /dev/null +++ b/bindings/python/docs/source/message.rst @@ -0,0 +1,54 @@ +:class:`Message` -- A single message +==================================== + +.. currentmodule:: notmuch + +.. autoclass:: Message + + .. automethod:: get_message_id + + .. automethod:: get_thread_id + + .. automethod:: get_replies + + .. automethod:: get_filename + + .. automethod:: get_filenames + + .. attribute:: FLAG + + FLAG.MATCH + This flag is automatically set by a + Query.search_threads on those messages that match the + query. This allows us to distinguish matches from the rest + of the messages in that thread. + + .. automethod:: get_flag + + .. automethod:: set_flag + + .. automethod:: get_date + + .. automethod:: get_header + + .. automethod:: get_tags + + .. automethod:: get_property + + .. automethod:: get_properties + + .. automethod:: maildir_flags_to_tags + + .. automethod:: tags_to_maildir_flags + + .. automethod:: remove_tag + + .. automethod:: add_tag + + .. automethod:: remove_all_tags + + .. automethod:: freeze + + .. automethod:: thaw + + .. automethod:: __str__ diff --git a/bindings/python/docs/source/messages.rst b/bindings/python/docs/source/messages.rst new file mode 100644 index 00000000..3ccf505c --- /dev/null +++ b/bindings/python/docs/source/messages.rst @@ -0,0 +1,15 @@ +:class:`Messages` -- A bunch of messages +======================================== + +.. currentmodule:: notmuch + +.. autoclass:: Messages + + .. automethod:: collect_tags + + .. method:: __len__() + + .. warning:: + + :meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke + list(Messages()). Use the :meth:`Query.count_messages` function or use `len(list(msgs))`. diff --git a/bindings/python/docs/source/notes.rst b/bindings/python/docs/source/notes.rst new file mode 100644 index 00000000..a7927485 --- /dev/null +++ b/bindings/python/docs/source/notes.rst @@ -0,0 +1,6 @@ +Interfacing with notmuch +======================== + +.. todo:: move the note about talloc out of the code + +.. automodule:: notmuch diff --git a/bindings/python/docs/source/query.rst b/bindings/python/docs/source/query.rst new file mode 100644 index 00000000..785e984b --- /dev/null +++ b/bindings/python/docs/source/query.rst @@ -0,0 +1,43 @@ +:class:`Query` -- A search query +================================ + +.. currentmodule:: notmuch + +.. autoclass:: Query + + .. automethod:: create + + .. attribute:: Query.SORT + + Defines constants that are used as the mode in which to open a database. + + SORT.OLDEST_FIRST + Sort by message date, oldest first. + + SORT.NEWEST_FIRST + Sort by message date, newest first. + + SORT.MESSAGE_ID + Sort by email message ID. + + SORT.UNSORTED + Do not apply a special sort order (returns results in document id + order). + + .. automethod:: set_sort + + .. attribute:: sort + + Instance attribute :attr:`sort` contains the sort order (see + :attr:`Query.SORT`) if explicitly specified via + :meth:`set_sort`. By default it is set to `None`. + + .. automethod:: exclude_tag + + .. automethod:: search_threads + + .. automethod:: search_messages + + .. automethod:: count_messages + + .. automethod:: count_threads diff --git a/bindings/python/docs/source/quickstart.rst b/bindings/python/docs/source/quickstart.rst new file mode 100644 index 00000000..609f42e1 --- /dev/null +++ b/bindings/python/docs/source/quickstart.rst @@ -0,0 +1,19 @@ +Quickstart and examples +======================= + +.. todo:: write a nice introduction +.. todo:: improve the examples + +Notmuch can be imported as:: + + import notmuch + +or:: + + from notmuch import Query, Database + + db = Database('path', create=True) + msgs = Query(db, 'from:myself').search_messages() + + for msg in msgs: + print(msg) diff --git a/bindings/python/docs/source/status_and_errors.rst b/bindings/python/docs/source/status_and_errors.rst new file mode 100644 index 00000000..68913f16 --- /dev/null +++ b/bindings/python/docs/source/status_and_errors.rst @@ -0,0 +1,57 @@ +.. currentmodule:: notmuch + +Status and Errors +================= + +Some methods return a status, indicating if an operation was successful and what the error was. Most of these status codes are expressed as a specific value, the :class:`notmuch.STATUS`. + +.. note:: + + Prior to version 0.12 the exception classes and the enumeration + :class:`notmuch.STATUS` were defined in `notmuch.globals`. They + have since then been moved into `notmuch.errors`. + +:class:`STATUS` -- Notmuch operation return value +-------------------------------------------------- + +.. autoclass:: notmuch.STATUS + :inherited-members: + +.. automethod:: notmuch.STATUS.status2str + +:exc:`NotmuchError` -- A Notmuch execution error +------------------------------------------------ +Whenever an error occurs, we throw a special Exception :exc:`NotmuchError`, or a more fine grained Exception which is derived from it. This means it is always safe to check for NotmuchErrors if you want to catch all errors. If you are interested in more fine grained exceptions, you can use those below. + +.. autoexception:: NotmuchError + +The following exceptions are all directly derived from NotmuchError. Each of them corresponds to a specific :class:`notmuch.STATUS` value. You can either check the :attr:`status` attribute of a NotmuchError to see if a specific error has occurred, or you can directly check for the following Exception types: + +.. autoexception:: OutOfMemoryError(message=None) + :members: +.. autoexception:: ReadOnlyDatabaseError(message=None) + :members: +.. autoexception:: XapianError(message=None) + :members: +.. autoexception:: FileError(message=None) + :members: +.. autoexception:: FileNotEmailError(message=None) + :members: +.. autoexception:: DuplicateMessageIdError(message=None) + :members: +.. autoexception:: NullPointerError(message=None) + :members: +.. autoexception:: TagTooLongError(message=None) + :members: +.. autoexception:: UnbalancedFreezeThawError(message=None) + :members: +.. autoexception:: UnbalancedAtomicError(message=None) + :members: +.. autoexception:: UnsupportedOperationError(message=None) + :members: +.. autoexception:: UpgradeRequiredError(message=None) + :members: +.. autoexception:: PathError(message=None) + :members: +.. autoexception:: NotInitializedError(message=None) + :members: diff --git a/bindings/python/docs/source/tags.rst b/bindings/python/docs/source/tags.rst new file mode 100644 index 00000000..31527d4b --- /dev/null +++ b/bindings/python/docs/source/tags.rst @@ -0,0 +1,17 @@ +:class:`Tags` -- Notmuch tags +----------------------------- + +.. currentmodule:: notmuch + +.. autoclass:: Tags + :members: + + .. method:: __len__ + + .. warning:: + + :meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke + list(Tags()). Use :meth:`len(list(msgs))` instead if you need to know the number of + tags. + + .. automethod:: __str__ diff --git a/bindings/python/docs/source/thread.rst b/bindings/python/docs/source/thread.rst new file mode 100644 index 00000000..40678725 --- /dev/null +++ b/bindings/python/docs/source/thread.rst @@ -0,0 +1,26 @@ +:class:`Thread` -- A single thread +================================== + +.. currentmodule:: notmuch + +.. autoclass:: Thread + + .. automethod:: get_thread_id + + .. automethod:: get_total_messages + + .. automethod:: get_toplevel_messages + + .. automethod:: get_matched_messages + + .. automethod:: get_authors + + .. automethod:: get_subject + + .. automethod:: get_oldest_date + + .. automethod:: get_newest_date + + .. automethod:: get_tags + + .. automethod:: __str__ diff --git a/bindings/python/docs/source/threads.rst b/bindings/python/docs/source/threads.rst new file mode 100644 index 00000000..46ce5be5 --- /dev/null +++ b/bindings/python/docs/source/threads.rst @@ -0,0 +1,14 @@ +:class:`Threads` -- Threads iterator +==================================== + +.. currentmodule:: notmuch + +.. autoclass:: Threads + + .. method:: __len__ + .. warning:: + :meth:`__len__` was removed in version 0.22 as it exhausted the + iterator and broke list(Threads()). Use `len(list(msgs))` + instead. + + .. automethod:: __str__ diff --git a/bindings/python/notmuch/__init__.py b/bindings/python/notmuch/__init__.py new file mode 100644 index 00000000..cf627ffa --- /dev/null +++ b/bindings/python/notmuch/__init__.py @@ -0,0 +1,84 @@ +"""The :mod:`notmuch` module provides most of the functionality that a user is +likely to need. + +.. note:: The underlying notmuch library is build on a hierarchical + memory allocator called talloc. All objects derive from a + top-level :class:`Database` object. + + This means that as soon as an object is deleted, all underlying + derived objects such as Queries, Messages, Message, and Tags will + be freed by the underlying library as well. Accessing these + objects will then lead to segfaults and other unexpected behavior. + + We implement reference counting, so that parent objects can be + automatically freed when they are not needed anymore. For + example:: + + db = Database('path',create=True) + msgs = Query(db,'from:myself').search_messages() + + This returns :class:`Messages` which internally contains a + reference to its parent :class:`Query` object. Otherwise the + Query() would be immediately freed, taking our *msgs* down with + it. + + In this case, the above Query() object will be automatically freed + whenever we delete all derived objects, ie in our case: + `del(msgs)` would also delete the parent Query. It would not + delete the parent Database() though, as that is still referenced + from the variable *db* in which it is stored. + + Pretty much the same is valid for all other objects in the + hierarchy, such as :class:`Query`, :class:`Messages`, + :class:`Message`, and :class:`Tags`. +""" + +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010-2011 Sebastian Spaeth +""" +from .database import Database +from .directory import Directory +from .filenames import Filenames +from .message import Message +from .messages import Messages +from .query import Query +from .tag import Tags +from .thread import Thread +from .threads import Threads +from .globals import nmlib +from .errors import ( + STATUS, + NotmuchError, + OutOfMemoryError, + ReadOnlyDatabaseError, + XapianError, + FileError, + FileNotEmailError, + DuplicateMessageIdError, + NullPointerError, + TagTooLongError, + UnbalancedFreezeThawError, + UnbalancedAtomicError, + NotInitializedError, + UnsupportedOperationError, + UpgradeRequiredError, + PathError, +) +from .version import __VERSION__ +__LICENSE__ = "GPL v3+" +__AUTHOR__ = 'Sebastian Spaeth ' diff --git a/bindings/python/notmuch/compat.py b/bindings/python/notmuch/compat.py new file mode 100644 index 00000000..4a94e05c --- /dev/null +++ b/bindings/python/notmuch/compat.py @@ -0,0 +1,74 @@ +''' +This file is part of notmuch. + +This module handles differences between python2.x and python3.x and +allows the notmuch bindings to support both version families with one +source tree. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +Copyright 2012 Justus Winter <4winter@informatik.uni-hamburg.de> +''' + +import sys + +if sys.version_info[0] == 2: + from ConfigParser import SafeConfigParser + + class Python3StringMixIn(object): + def __str__(self): + return unicode(self).encode('utf-8') + + def encode_utf8(value): + ''' + Ensure a nicely utf-8 encoded string to pass to wrapped + libnotmuch functions. + + C++ code expects strings to be well formatted and unicode + strings to have no null bytes. + ''' + if not isinstance(value, basestring): + raise TypeError('Expected str or unicode, got %s' % type(value)) + + if isinstance(value, unicode): + return value.encode('utf-8', 'replace') + + return value +else: + from configparser import ConfigParser as SafeConfigParser + + if not hasattr(SafeConfigParser, 'readfp'): # py >= 3.12 + SafeConfigParser.readfp = SafeConfigParser.read_file + + class Python3StringMixIn(object): + def __str__(self): + return self.__unicode__() + + def encode_utf8(value): + ''' + Ensure a nicely utf-8 encoded string to pass to wrapped + libnotmuch functions. + + C++ code expects strings to be well formatted and unicode + strings to have no null bytes. + ''' + if not isinstance(value, str): + raise TypeError('Expected str, got %s' % type(value)) + + return value.encode('utf-8', 'replace') + +# We import the SafeConfigParser class on behalf of other code to cope +# with the differences between Python 2 and 3. +SafeConfigParser # avoid warning about unused import diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py new file mode 100644 index 00000000..8fb507fa --- /dev/null +++ b/bindings/python/notmuch/database.py @@ -0,0 +1,789 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +import os +import codecs +import warnings +from ctypes import c_char_p, c_void_p, c_uint, byref, POINTER +from .compat import SafeConfigParser +from .globals import ( + nmlib, + Enum, + _str, + NotmuchConfigListP, + NotmuchDatabaseP, + NotmuchDirectoryP, + NotmuchIndexoptsP, + NotmuchMessageP, + NotmuchTagsP, +) +from .errors import ( + STATUS, + FileError, + NotmuchError, + NullPointerError, + NotInitializedError, +) +from .message import Message +from .tag import Tags +from .query import Query +from .directory import Directory + +class Database(object): + """The :class:`Database` is the highest-level object that notmuch + provides. It references a notmuch database, and can be opened in + read-only or read-write mode. A :class:`Query` can be derived from + or be applied to a specific database to find messages. Also adding + and removing messages to the database happens via this + object. Modifications to the database are not atmic by default (see + :meth:`begin_atomic`) and once a database has been modified, all + other database objects pointing to the same data-base will throw an + :exc:`XapianError` as the underlying database has been + modified. Close and reopen the database to continue working with it. + + :class:`Database` objects implement the context manager protocol + so you can use the :keyword:`with` statement to ensure that the + database is properly closed. See :meth:`close` for more + information. + + .. note:: + + Any function in this class can and will throw an + :exc:`NotInitializedError` if the database was not initialized + properly. + """ + _std_db_path = None + """Class attribute to cache user's default database""" + + MODE = Enum(['READ_ONLY', 'READ_WRITE']) + """Constants: Mode in which to open the database""" + + DECRYPTION_POLICY = Enum(['FALSE', 'TRUE', 'AUTO', 'NOSTASH']) + """Constants: policies for decrypting messages during indexing""" + + """notmuch_database_get_directory""" + _get_directory = nmlib.notmuch_database_get_directory + _get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)] + _get_directory.restype = c_uint + + """notmuch_database_get_path""" + _get_path = nmlib.notmuch_database_get_path + _get_path.argtypes = [NotmuchDatabaseP] + _get_path.restype = c_char_p + + """notmuch_database_get_version""" + _get_version = nmlib.notmuch_database_get_version + _get_version.argtypes = [NotmuchDatabaseP] + _get_version.restype = c_uint + + """notmuch_database_get_revision""" + _get_revision = nmlib.notmuch_database_get_revision + _get_revision.argtypes = [NotmuchDatabaseP, POINTER(c_char_p)] + _get_revision.restype = c_uint + + """notmuch_database_open""" + _open = nmlib.notmuch_database_open + _open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)] + _open.restype = c_uint + + """notmuch_database_upgrade""" + _upgrade = nmlib.notmuch_database_upgrade + _upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p] + _upgrade.restype = c_uint + + """ notmuch_database_find_message""" + _find_message = nmlib.notmuch_database_find_message + _find_message.argtypes = [NotmuchDatabaseP, c_char_p, + POINTER(NotmuchMessageP)] + _find_message.restype = c_uint + + """notmuch_database_find_message_by_filename""" + _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename + _find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p, + POINTER(NotmuchMessageP)] + _find_message_by_filename.restype = c_uint + + """notmuch_database_get_all_tags""" + _get_all_tags = nmlib.notmuch_database_get_all_tags + _get_all_tags.argtypes = [NotmuchDatabaseP] + _get_all_tags.restype = NotmuchTagsP + + """notmuch_database_create""" + _create = nmlib.notmuch_database_create + _create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)] + _create.restype = c_uint + + def __init__(self, path = None, create = False, + mode = MODE.READ_ONLY): + """If *path* is `None`, we will try to read a users notmuch + configuration and use his configured database. The location of the + configuration file can be specified through the environment variable + *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`. + + If *create* is `True`, the database will always be created in + :attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY. + + :param path: Directory to open/create the database in (see + above for behavior if `None`) + :type path: `str` or `None` + :param create: Pass `False` to open an existing, `True` to create a new + database. + :type create: bool + :param mode: Mode to open a database in. Is always + :attr:`MODE`.READ_WRITE when creating a new one. + :type mode: :attr:`MODE` + :raises: :exc:`NotmuchError` or derived exception in case of + failure. + """ + self._db = None + self.mode = mode + if path is None: + # no path specified. use a user's default database + if Database._std_db_path is None: + #the following line throws a NotmuchError if it fails + Database._std_db_path = self._get_user_default_db() + path = Database._std_db_path + + if create == False: + self.open(path, mode) + else: + self.create(path) + + _destroy = nmlib.notmuch_database_destroy + _destroy.argtypes = [NotmuchDatabaseP] + _destroy.restype = c_uint + + def __del__(self): + if self._db: + status = self._destroy(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + def _assert_db_is_initialized(self): + """Raises :exc:`NotInitializedError` if self._db is `None`""" + if not self._db: + raise NotInitializedError() + + def create(self, path): + """Creates a new notmuch database + + This function is used by __init__() and usually does not need + to be called directly. It wraps the underlying + *notmuch_database_create* function and creates a new notmuch + database at *path*. It will always return a database in :attr:`MODE` + .READ_WRITE mode as creating an empty database for + reading only does not make a great deal of sense. + + :param path: A directory in which we should create the database. + :type path: str + :raises: :exc:`NotmuchError` in case of any failure + (possibly after printing an error message on stderr). + """ + if self._db: + raise NotmuchError(message="Cannot create db, this Database() " + "already has an open one.") + + db = NotmuchDatabaseP() + status = Database._create(_str(path), byref(db)) + + if status != STATUS.SUCCESS: + raise NotmuchError(status) + self._db = db + return status + + def open(self, path, mode=0): + """Opens an existing database + + This function is used by __init__() and usually does not need + to be called directly. It wraps the underlying + *notmuch_database_open* function. + + :param status: Open the database in read-only or read-write mode + :type status: :attr:`MODE` + :raises: Raises :exc:`NotmuchError` in case of any failure + (possibly after printing an error message on stderr). + """ + db = NotmuchDatabaseP() + status = Database._open(_str(path), mode, byref(db)) + + if status != STATUS.SUCCESS: + raise NotmuchError(status) + self._db = db + return status + + _close = nmlib.notmuch_database_close + _close.argtypes = [NotmuchDatabaseP] + _close.restype = c_uint + + def close(self): + ''' + Closes the notmuch database. + + .. warning:: + + This function closes the notmuch database. From that point + on every method invoked on any object ever derived from + the closed database may cease to function and raise a + NotmuchError. + ''' + if self._db: + status = self._close(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + def __enter__(self): + ''' + Implements the context manager protocol. + ''' + return self + + def __exit__(self, exc_type, exc_value, traceback): + ''' + Implements the context manager protocol. + ''' + self.close() + + def get_path(self): + """Returns the file path of an open database""" + self._assert_db_is_initialized() + return Database._get_path(self._db).decode('utf-8') + + def get_version(self): + """Returns the database format version + + :returns: The database version as positive integer + """ + self._assert_db_is_initialized() + return Database._get_version(self._db) + + def get_revision (self): + """Returns the committed database revision and UUID + + :returns: (revision, uuid) The database revision as a positive integer + and the UUID of the database. + """ + self._assert_db_is_initialized() + uuid = c_char_p () + revision = Database._get_revision(self._db, byref (uuid)) + return (revision, uuid.value.decode ('utf-8')) + + _needs_upgrade = nmlib.notmuch_database_needs_upgrade + _needs_upgrade.argtypes = [NotmuchDatabaseP] + _needs_upgrade.restype = bool + + def needs_upgrade(self): + """Does this database need to be upgraded before writing to it? + + If this function returns `True` then no functions that modify the + database (:meth:`index_file`, + :meth:`Message.add_tag`, :meth:`Directory.set_mtime`, + etc.) will work unless :meth:`upgrade` is called successfully first. + + :returns: `True` or `False` + """ + self._assert_db_is_initialized() + return self._needs_upgrade(self._db) + + def upgrade(self): + """Upgrades the current database + + After opening a database in read-write mode, the client should + check if an upgrade is needed (notmuch_database_needs_upgrade) and + if so, upgrade with this function before making any modifications. + + NOT IMPLEMENTED: The optional progress_notify callback can be + used by the caller to provide progress indication to the + user. If non-NULL it will be called periodically with + 'progress' as a floating-point value in the range of [0.0..1.0] + indicating the progress made so far in the upgrade process. + + :TODO: catch exceptions, document return values and etc... + """ + self._assert_db_is_initialized() + status = Database._upgrade(self._db, None, None) + # TODO: catch exceptions, document return values and etc + return status + + _begin_atomic = nmlib.notmuch_database_begin_atomic + _begin_atomic.argtypes = [NotmuchDatabaseP] + _begin_atomic.restype = c_uint + + def begin_atomic(self): + """Begin an atomic database operation + + Any modifications performed between a successful + :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to + the database atomically. Note that, unlike a typical database + transaction, this only ensures atomicity, not durability; + neither begin nor end necessarily flush modifications to disk. + + :returns: :attr:`STATUS`.SUCCESS or raises + :raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION + Xapian exception occurred; atomic section not entered. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + status = self._begin_atomic(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return status + + _end_atomic = nmlib.notmuch_database_end_atomic + _end_atomic.argtypes = [NotmuchDatabaseP] + _end_atomic.restype = c_uint + + def end_atomic(self): + """Indicate the end of an atomic database operation + + See :meth:`begin_atomic` for details. + + :returns: :attr:`STATUS`.SUCCESS or raises + + :raises: + :exc:`NotmuchError`: + :attr:`STATUS`.XAPIAN_EXCEPTION + A Xapian exception occurred; atomic section not + ended. + :attr:`STATUS`.UNBALANCED_ATOMIC: + end_atomic has been called more times than begin_atomic. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + status = self._end_atomic(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return status + + def get_directory(self, path): + """Returns a :class:`Directory` of path, + + :param path: An unicode string containing the path relative to the path + of database (see :meth:`get_path`), or else should be an absolute + path with initial components that match the path of 'database'. + :returns: :class:`Directory` or raises an exception. + :raises: :exc:`FileError` if path is not relative database or absolute + with initial components same as database. + """ + self._assert_db_is_initialized() + + # sanity checking if path is valid, and make path absolute + if path and path[0] == os.sep: + # we got an absolute path + if not path.startswith(self.get_path()): + # but its initial components are not equal to the db path + raise FileError('Database().get_directory() called ' + 'with a wrong absolute path') + abs_dirpath = path + else: + #we got a relative path, make it absolute + abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path)) + + dir_p = NotmuchDirectoryP() + status = Database._get_directory(self._db, _str(path), byref(dir_p)) + + if status != STATUS.SUCCESS: + raise NotmuchError(status) + if not dir_p: + return None + + # return the Directory, init it with the absolute path + return Directory(abs_dirpath, dir_p, self) + + _get_default_indexopts = nmlib.notmuch_database_get_default_indexopts + _get_default_indexopts.argtypes = [NotmuchDatabaseP] + _get_default_indexopts.restype = NotmuchIndexoptsP + + _indexopts_set_decrypt_policy = nmlib.notmuch_indexopts_set_decrypt_policy + _indexopts_set_decrypt_policy.argtypes = [NotmuchIndexoptsP, c_uint] + _indexopts_set_decrypt_policy.restype = None + + _indexopts_destroy = nmlib.notmuch_indexopts_destroy + _indexopts_destroy.argtypes = [NotmuchIndexoptsP] + _indexopts_destroy.restype = None + + _index_file = nmlib.notmuch_database_index_file + _index_file.argtypes = [NotmuchDatabaseP, c_char_p, + c_void_p, + POINTER(NotmuchMessageP)] + _index_file.restype = c_uint + + def index_file(self, filename, sync_maildir_flags=False, decrypt_policy=None): + """Adds a new message to the database + + :param filename: should be a path relative to the path of the + open database (see :meth:`get_path`), or else should be an + absolute filename with initial components that match the + path of the database. + + The file should be a single mail message (not a + multi-message mbox) that is expected to remain at its + current location, since the notmuch database will reference + the filename, and will not copy the entire contents of the + file. + + :param sync_maildir_flags: If the message contains Maildir + flags, we will -depending on the notmuch configuration- sync + those tags to initial notmuch tags, if set to `True`. It is + `False` by default to remain consistent with the libnotmuch + API. You might want to look into the underlying method + :meth:`Message.maildir_flags_to_tags`. + + :param decrypt_policy: If the message contains any encrypted + parts, and decrypt_policy is set to + :attr:`DECRYPTION_POLICY`.TRUE, notmuch will try to + decrypt the message and index the cleartext, stashing any + discovered session keys. If it is set to + :attr:`DECRYPTION_POLICY`.FALSE, it will never try to + decrypt during indexing. If it is set to + :attr:`DECRYPTION_POLICY`.AUTO, then it will try to use + any stashed session keys it knows about, but will not try + to access the user's secret keys. + :attr:`DECRYPTION_POLICY`.NOSTASH behaves the same as + :attr:`DECRYPTION_POLICY`.TRUE except that no session keys + are stashed in the database. If decrypt_policy is set to + None (the default), then the database itself will decide + whether to decrypt, based on the `index.decrypt` + configuration setting (see notmuch-config(1)). + + :returns: On success, we return + + 1) a :class:`Message` object that can be used for things + such as adding tags to the just-added message. + 2) one of the following :attr:`STATUS` values: + + :attr:`STATUS`.SUCCESS + Message successfully added to database. + :attr:`STATUS`.DUPLICATE_MESSAGE_ID + Message has the same message ID as another message already + in the database. The new filename was successfully added + to the list of the filenames for the existing message. + + :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`) + + :raises: Raises a :exc:`NotmuchError` with the following meaning. + If such an exception occurs, nothing was added to the database. + + :attr:`STATUS`.FILE_ERROR + An error occurred trying to open the file, (such as + permission denied, or file not found, etc.). + :attr:`STATUS`.FILE_NOT_EMAIL + The contents of filename don't look like an email + message. + :attr:`STATUS`.READ_ONLY_DATABASE + Database was opened in read-only mode so no message can + be added. + """ + self._assert_db_is_initialized() + msg_p = NotmuchMessageP() + indexopts = c_void_p(None) + if decrypt_policy is not None: + indexopts = self._get_default_indexopts(self._db) + self._indexopts_set_decrypt_policy(indexopts, decrypt_policy) + + status = self._index_file(self._db, _str(filename), indexopts, byref(msg_p)) + + if indexopts: + self._indexopts_destroy(indexopts) + + if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]: + raise NotmuchError(status) + + #construct Message() and return + msg = Message(msg_p, self) + #automatic sync initial tags from Maildir flags + if sync_maildir_flags: + msg.maildir_flags_to_tags() + return (msg, status) + + def add_message(self, filename, sync_maildir_flags=False): + """Deprecated alias for :meth:`index_file` + """ + warnings.warn( + "This function is deprecated and will be removed in the future, use index_file.", DeprecationWarning) + + return self.index_file(filename, sync_maildir_flags=sync_maildir_flags) + + _remove_message = nmlib.notmuch_database_remove_message + _remove_message.argtypes = [NotmuchDatabaseP, c_char_p] + _remove_message.restype = c_uint + + def remove_message(self, filename): + """Removes a message (filename) from the given notmuch database + + Note that only this particular filename association is removed from + the database. If the same message (as determined by the message ID) + is still available via other filenames, then the message will + persist in the database for those filenames. When the last filename + is removed for a particular message, the database content for that + message will be entirely removed. + + :returns: A :attr:`STATUS` value with the following meaning: + + :attr:`STATUS`.SUCCESS + The last filename was removed and the message was removed + from the database. + :attr:`STATUS`.DUPLICATE_MESSAGE_ID + This filename was removed but the message persists in the + database with at least one other filename. + + :raises: Raises a :exc:`NotmuchError` with the following meaning. + If such an exception occurs, nothing was removed from the + database. + + :attr:`STATUS`.READ_ONLY_DATABASE + Database was opened in read-only mode so no message can be + removed. + """ + self._assert_db_is_initialized() + status = self._remove_message(self._db, _str(filename)) + if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]: + raise NotmuchError(status) + return status + + def find_message(self, msgid): + """Returns a :class:`Message` as identified by its message ID + + Wraps the underlying *notmuch_database_find_message* function. + + :param msgid: The message ID + :type msgid: unicode or str + :returns: :class:`Message` or `None` if no message is found. + :raises: + :exc:`OutOfMemoryError` + If an Out-of-memory occurred while constructing the message. + :exc:`XapianError` + In case of a Xapian Exception. These exceptions + include "Database modified" situations, e.g. when the + notmuch database has been modified by another program + in the meantime. In this case, you should close and + reopen the database and retry. + :exc:`NotInitializedError` if + the database was not initialized. + """ + self._assert_db_is_initialized() + msg_p = NotmuchMessageP() + status = Database._find_message(self._db, _str(msgid), byref(msg_p)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return msg_p and Message(msg_p, self) or None + + def find_message_by_filename(self, filename): + """Find a message with the given filename + + :returns: If the database contains a message with the given + filename, then a class:`Message:` is returned. This + function returns None if no message is found with the given + filename. + + :raises: :exc:`OutOfMemoryError` if an Out-of-memory occurred while + constructing the message. + :raises: :exc:`XapianError` in case of a Xapian Exception. + These exceptions include "Database modified" + situations, e.g. when the notmuch database has been + modified by another program in the meantime. In this + case, you should close and reopen the database and + retry. + :raises: :exc:`NotInitializedError` if the database was not + initialized. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + + msg_p = NotmuchMessageP() + status = Database._find_message_by_filename(self._db, _str(filename), + byref(msg_p)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return msg_p and Message(msg_p, self) or None + + def get_all_tags(self): + """Returns :class:`Tags` with a list of all tags found in the database + + :returns: :class:`Tags` + :exception: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER + on error + """ + self._assert_db_is_initialized() + tags_p = Database._get_all_tags(self._db) + if not tags_p: + raise NullPointerError() + return Tags(tags_p, self) + + def create_query(self, querystring): + """Returns a :class:`Query` derived from this database + + This is a shorthand method for doing:: + + # short version + # Automatically frees the Database() when 'q' is deleted + + q = Database(dbpath).create_query('from:"Biene Maja"') + + # long version, which is functionally equivalent but will keep the + # Database in the 'db' variable around after we delete 'q': + + db = Database(dbpath) + q = Query(db,'from:"Biene Maja"') + + This function is a python extension and not in the underlying C API. + """ + return Query(self, querystring) + + """notmuch_database_status_string""" + _status_string = nmlib.notmuch_database_status_string + _status_string.argtypes = [NotmuchDatabaseP] + _status_string.restype = c_char_p + + def status_string(self): + """Returns the status string of the database + + This is sometimes used for additional error reporting + """ + self._assert_db_is_initialized() + s = Database._status_string(self._db) + if s: + return s.decode('utf-8', 'ignore') + return s + + def __repr__(self): + return "'Notmuch DB " + self.get_path() + "'" + + def _get_user_default_db(self): + """ Reads a user's notmuch config and returns his db location + + Throws a NotmuchError if it cannot find it""" + config = SafeConfigParser() + conf_f = os.getenv('NOTMUCH_CONFIG', + os.path.expanduser('~/.notmuch-config')) + config.readfp(codecs.open(conf_f, 'r', 'utf-8')) + if not config.has_option('database', 'path'): + raise NotmuchError(message="No DB path specified" + " and no user default found") + db_path = config.get('database', 'path') + if not os.path.isabs(db_path): + db_path = os.path.expanduser(os.path.join("~", db_path)) + return db_path + + """notmuch_database_get_config""" + _get_config = nmlib.notmuch_database_get_config + _get_config.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(c_char_p)] + _get_config.restype = c_uint + + def get_config(self, key): + """Return the value of the given config key. + + Note that only config values that are stored in the database are + searched and returned. The config file is not read. + + :param key: the config key under which a value should be looked up, it + should probably be in the form "section.key" + :type key: str + :returns: the config value or the empty string if no value is present + for that key + :rtype: str + :raises: :exc:`NotmuchError` in case of failure. + + """ + self._assert_db_is_initialized() + return_string = c_char_p() + status = self._get_config(self._db, _str(key), byref(return_string)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return return_string.value.decode('utf-8') + + """notmuch_database_get_config_list""" + _get_config_list = nmlib.notmuch_database_get_config_list + _get_config_list.argtypes = [NotmuchDatabaseP, c_char_p, + POINTER(NotmuchConfigListP)] + _get_config_list.restype = c_uint + + _config_list_valid = nmlib.notmuch_config_list_valid + _config_list_valid.argtypes = [NotmuchConfigListP] + _config_list_valid.restype = bool + + _config_list_key = nmlib.notmuch_config_list_key + _config_list_key.argtypes = [NotmuchConfigListP] + _config_list_key.restype = c_char_p + + _config_list_value = nmlib.notmuch_config_list_value + _config_list_value.argtypes = [NotmuchConfigListP] + _config_list_value.restype = c_char_p + + _config_list_move_to_next = nmlib.notmuch_config_list_move_to_next + _config_list_move_to_next.argtypes = [NotmuchConfigListP] + _config_list_move_to_next.restype = None + + _config_list_destroy = nmlib.notmuch_config_list_destroy + _config_list_destroy.argtypes = [NotmuchConfigListP] + _config_list_destroy.restype = None + + def get_configs(self, prefix=''): + """Return a generator of key, value pairs where the start of key + matches the given prefix + + Note that only config values that are stored in the database are + searched and returned. The config file is not read. If no `prefix` is + given all config values are returned. + + This could be used to get all named queries into a dict for example:: + + queries = {k[6:]: v for k, v in db.get_configs('query.')} + + :param prefix: a string by which the keys should be selected + :type prefix: str + :yields: all key-value pairs where `prefix` matches the beginning + of the key + :ytype: pairs of str + :raises: :exc:`NotmuchError` in case of failure. + + """ + self._assert_db_is_initialized() + config_list_p = NotmuchConfigListP() + status = self._get_config_list(self._db, _str(prefix), + byref(config_list_p)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + while self._config_list_valid(config_list_p): + key = self._config_list_key(config_list_p).decode('utf-8') + value = self._config_list_value(config_list_p).decode('utf-8') + yield key, value + self._config_list_move_to_next(config_list_p) + + """notmuch_database_set_config""" + _set_config = nmlib.notmuch_database_set_config + _set_config.argtypes = [NotmuchDatabaseP, c_char_p, c_char_p] + _set_config.restype = c_uint + + def set_config(self, key, value): + """Set a config value in the notmuch database. + + If an empty string is provided as `value` the `key` is unset! + + :param key: the key to set + :type key: str + :param value: the value to store under `key` + :type value: str + :returns: None + :raises: :exc:`NotmuchError` in case of failure. + + """ + self._assert_db_is_initialized() + status = self._set_config(self._db, _str(key), _str(value)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) diff --git a/bindings/python/notmuch/directory.py b/bindings/python/notmuch/directory.py new file mode 100644 index 00000000..b30c9e35 --- /dev/null +++ b/bindings/python/notmuch/directory.py @@ -0,0 +1,185 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from ctypes import c_uint, c_long +from .globals import ( + nmlib, + NotmuchDirectoryP, + NotmuchFilenamesP +) +from .errors import ( + STATUS, + NotmuchError, + NotInitializedError, +) +from .filenames import Filenames + +class Directory(object): + """Represents a directory entry in the notmuch directory + + Modifying attributes of this object will modify the + database, not the real directory attributes. + + The Directory object is usually derived from another object + e.g. via :meth:`Database.get_directory`, and will automatically be + become invalid whenever that parent is deleted. You should + therefore initialized this object handing it a reference to the + parent, preventing the parent from automatically being garbage + collected. + """ + + """notmuch_directory_get_mtime""" + _get_mtime = nmlib.notmuch_directory_get_mtime + _get_mtime.argtypes = [NotmuchDirectoryP] + _get_mtime.restype = c_long + + """notmuch_directory_set_mtime""" + _set_mtime = nmlib.notmuch_directory_set_mtime + _set_mtime.argtypes = [NotmuchDirectoryP, c_long] + _set_mtime.restype = c_uint + + """notmuch_directory_get_child_files""" + _get_child_files = nmlib.notmuch_directory_get_child_files + _get_child_files.argtypes = [NotmuchDirectoryP] + _get_child_files.restype = NotmuchFilenamesP + + """notmuch_directory_get_child_directories""" + _get_child_directories = nmlib.notmuch_directory_get_child_directories + _get_child_directories.argtypes = [NotmuchDirectoryP] + _get_child_directories.restype = NotmuchFilenamesP + + def _assert_dir_is_initialized(self): + """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED) + if dir_p is None""" + if not self._dir_p: + raise NotInitializedError() + + def __init__(self, path, dir_p, parent): + """ + :param path: The absolute path of the directory object. + :param dir_p: The pointer to an internal notmuch_directory_t object. + :param parent: The object this Directory is derived from + (usually a :class:`Database`). We do not directly use + this, but store a reference to it as long as + this Directory object lives. This keeps the + parent object alive. + """ + self._path = path + self._dir_p = dir_p + self._parent = parent + + def set_mtime(self, mtime): + """Sets the mtime value of this directory in the database + + The intention is for the caller to use the mtime to allow efficient + identification of new messages to be added to the database. The + recommended usage is as follows: + + * Read the mtime of a directory from the filesystem + + * Call :meth:`Database.index_file` for all mail files in + the directory + + * Call notmuch_directory_set_mtime with the mtime read from the + filesystem. Then, when wanting to check for updates to the + directory in the future, the client can call :meth:`get_mtime` + and know that it only needs to add files if the mtime of the + directory and files are newer than the stored timestamp. + + .. note:: + + :meth:`get_mtime` function does not allow the caller to + distinguish a timestamp of 0 from a non-existent timestamp. So + don't store a timestamp of 0 unless you are comfortable with + that. + + :param mtime: A (time_t) timestamp + :raises: :exc:`XapianError` a Xapian exception occurred, mtime + not stored + :raises: :exc:`ReadOnlyDatabaseError` the database was opened + in read-only mode so directory mtime cannot be modified + :raises: :exc:`NotInitializedError` the directory object has not + been initialized + """ + self._assert_dir_is_initialized() + status = Directory._set_mtime(self._dir_p, mtime) + + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + def get_mtime(self): + """Gets the mtime value of this directory in the database + + Retrieves a previously stored mtime for this directory. + + :param mtime: A (time_t) timestamp + :raises: :exc:`NotmuchError`: + + :attr:`STATUS`.NOT_INITIALIZED + The directory has not been initialized + """ + self._assert_dir_is_initialized() + return Directory._get_mtime(self._dir_p) + + # Make mtime attribute a property of Directory() + mtime = property(get_mtime, set_mtime, doc="""Property that allows getting + and setting of the Directory *mtime* (read-write) + + See :meth:`get_mtime` and :meth:`set_mtime` for usage and + possible exceptions.""") + + def get_child_files(self): + """Gets a Filenames iterator listing all the filenames of + messages in the database within the given directory. + + The returned filenames will be the basename-entries only (not + complete paths. + """ + self._assert_dir_is_initialized() + files_p = Directory._get_child_files(self._dir_p) + return Filenames(files_p, self) + + def get_child_directories(self): + """Gets a :class:`Filenames` iterator listing all the filenames of + sub-directories in the database within the given directory + + The returned filenames will be the basename-entries only (not + complete paths. + """ + self._assert_dir_is_initialized() + files_p = Directory._get_child_directories(self._dir_p) + return Filenames(files_p, self) + + @property + def path(self): + """Returns the absolute path of this Directory (read-only)""" + return self._path + + def __repr__(self): + """Object representation""" + return "" % self._path + + _destroy = nmlib.notmuch_directory_destroy + _destroy.argtypes = [NotmuchDirectoryP] + _destroy.restype = None + + def __del__(self): + """Close and free the Directory""" + if self._dir_p: + self._destroy(self._dir_p) diff --git a/bindings/python/notmuch/errors.py b/bindings/python/notmuch/errors.py new file mode 100644 index 00000000..b7684ef6 --- /dev/null +++ b/bindings/python/notmuch/errors.py @@ -0,0 +1,204 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from ctypes import c_char_p, c_int + +from .globals import ( + nmlib, + Enum, + Python3StringMixIn, +) + +class Status(Enum): + """Enum with a string representation of a notmuch_status_t value.""" + _status2str = nmlib.notmuch_status_to_string + _status2str.restype = c_char_p + _status2str.argtypes = [c_int] + + def __init__(self, statuslist): + """It is initialized with a list of strings that are available as + Status().string1 - Status().stringn attributes. + """ + super(Status, self).__init__(statuslist) + + @classmethod + def status2str(self, status): + """Get a (unicode) string representation of a notmuch_status_t value.""" + # define strings for custom error messages + if status == STATUS.NOT_INITIALIZED: + return "Operation on uninitialized object impossible." + return unicode(Status._status2str(status)) + +STATUS = Status(['SUCCESS', + 'OUT_OF_MEMORY', + 'READ_ONLY_DATABASE', + 'XAPIAN_EXCEPTION', + 'FILE_ERROR', + 'FILE_NOT_EMAIL', + 'DUPLICATE_MESSAGE_ID', + 'NULL_POINTER', + 'TAG_TOO_LONG', + 'UNBALANCED_FREEZE_THAW', + 'UNBALANCED_ATOMIC', + 'UNSUPPORTED_OPERATION', + 'UPGRADE_REQUIRED', + 'PATH_ERROR', + 'NOT_INITIALIZED']) +"""STATUS is a class, whose attributes provide constants that serve as return +indicators for notmuch functions. Currently the following ones are defined. For +possible return values and specific meaning for each method, see the method +description. + + * SUCCESS + * OUT_OF_MEMORY + * READ_ONLY_DATABASE + * XAPIAN_EXCEPTION + * FILE_ERROR + * FILE_NOT_EMAIL + * DUPLICATE_MESSAGE_ID + * NULL_POINTER + * TAG_TOO_LONG + * UNBALANCED_FREEZE_THAW + * UNBALANCED_ATOMIC + * UNSUPPORTED_OPERATION + * UPGRADE_REQUIRED + * PATH_ERROR + * NOT_INITIALIZED + +Invoke the class method `notmuch.STATUS.status2str` with a status value as +argument to receive a human readable string""" +STATUS.__name__ = 'STATUS' + + +class NotmuchError(Exception, Python3StringMixIn): + """Is initiated with a (notmuch.STATUS[, message=None]). It will not + return an instance of the class NotmuchError, but a derived instance + of a more specific Error Message, e.g. OutOfMemoryError. Each status + but SUCCESS has a corresponding subclassed Exception.""" + + @classmethod + def get_exc_subclass(cls, status): + """Returns a fine grained Exception() type, + detailing the error status""" + subclasses = { + STATUS.OUT_OF_MEMORY: OutOfMemoryError, + STATUS.READ_ONLY_DATABASE: ReadOnlyDatabaseError, + STATUS.XAPIAN_EXCEPTION: XapianError, + STATUS.FILE_ERROR: FileError, + STATUS.FILE_NOT_EMAIL: FileNotEmailError, + STATUS.DUPLICATE_MESSAGE_ID: DuplicateMessageIdError, + STATUS.NULL_POINTER: NullPointerError, + STATUS.TAG_TOO_LONG: TagTooLongError, + STATUS.UNBALANCED_FREEZE_THAW: UnbalancedFreezeThawError, + STATUS.UNBALANCED_ATOMIC: UnbalancedAtomicError, + STATUS.UNSUPPORTED_OPERATION: UnsupportedOperationError, + STATUS.UPGRADE_REQUIRED: UpgradeRequiredError, + STATUS.PATH_ERROR: PathError, + STATUS.NOT_INITIALIZED: NotInitializedError, + } + assert 0 < status <= len(subclasses) + return subclasses[status] + + def __new__(cls, *args, **kwargs): + """Return a correct subclass of NotmuchError if needed + + We return a NotmuchError instance if status is None (or 0) and a + subclass that inherits from NotmuchError depending on the + 'status' parameter otherwise.""" + # get 'status'. Passed in as arg or kwarg? + status = args[0] if len(args) else kwargs.get('status', None) + # no 'status' or cls is subclass already, return 'cls' instance + if not status or cls != NotmuchError: + return super(NotmuchError, cls).__new__(cls) + subclass = cls.get_exc_subclass(status) # which class to use? + return subclass.__new__(subclass, *args, **kwargs) + + def __init__(self, status=None, message=None): + self.status = status + self.message = message + + def __unicode__(self): + if self.message is not None: + return self.message + elif self.status is not None: + return STATUS.status2str(self.status) + else: + return 'Unknown error' + + +# List of Subclassed exceptions that correspond to STATUS values and are +# subclasses of NotmuchError. +class OutOfMemoryError(NotmuchError): + status = STATUS.OUT_OF_MEMORY + + +class ReadOnlyDatabaseError(NotmuchError): + status = STATUS.READ_ONLY_DATABASE + + +class XapianError(NotmuchError): + status = STATUS.XAPIAN_EXCEPTION + + +class FileError(NotmuchError): + status = STATUS.FILE_ERROR + + +class FileNotEmailError(NotmuchError): + status = STATUS.FILE_NOT_EMAIL + + +class DuplicateMessageIdError(NotmuchError): + status = STATUS.DUPLICATE_MESSAGE_ID + + +class NullPointerError(NotmuchError): + status = STATUS.NULL_POINTER + + +class TagTooLongError(NotmuchError): + status = STATUS.TAG_TOO_LONG + + +class UnbalancedFreezeThawError(NotmuchError): + status = STATUS.UNBALANCED_FREEZE_THAW + + +class UnbalancedAtomicError(NotmuchError): + status = STATUS.UNBALANCED_ATOMIC + + +class UnsupportedOperationError(NotmuchError): + status = STATUS.UNSUPPORTED_OPERATION + + +class UpgradeRequiredError(NotmuchError): + status = STATUS.UPGRADE_REQUIRED + + +class PathError(NotmuchError): + status = STATUS.PATH_ERROR + + +class NotInitializedError(NotmuchError): + """Derived from NotmuchError, this occurs if the underlying data + structure (e.g. database is not initialized (yet) or an iterator has + been exhausted. You can test for NotmuchError with .status = + STATUS.NOT_INITIALIZED""" + status = STATUS.NOT_INITIALIZED diff --git a/bindings/python/notmuch/filenames.py b/bindings/python/notmuch/filenames.py new file mode 100644 index 00000000..3bbc22b9 --- /dev/null +++ b/bindings/python/notmuch/filenames.py @@ -0,0 +1,131 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" +from ctypes import c_char_p +from .globals import ( + nmlib, + NotmuchFilenamesP, + Python3StringMixIn, +) +from .errors import ( + NullPointerError, + NotInitializedError, +) + + +class Filenames(Python3StringMixIn): + """Represents a list of filenames as returned by notmuch + + Objects of this class implement the iterator protocol. + + .. note:: + + The underlying library only provides a one-time iterator (it + cannot reset the iterator to the start). Thus iterating over + the function will "exhaust" the list of tags, and a subsequent + iteration attempt will raise a + :exc:`NotInitializedError`. Also note, that any function that + uses iteration (nearly all) will also exhaust the tags. So + both:: + + for name in filenames: print name + + as well as:: + + list_of_names = list(names) + + and even a simple:: + + #str() iterates over all tags to construct a space separated list + print(str(filenames)) + + will "exhaust" the Filenames. However, you can use + :meth:`Message.get_filenames` repeatedly to get fresh + Filenames objects to perform various actions on filenames. + """ + + #notmuch_filenames_get + _get = nmlib.notmuch_filenames_get + _get.argtypes = [NotmuchFilenamesP] + _get.restype = c_char_p + + def __init__(self, files_p, parent): + """ + :param files_p: A pointer to an underlying *notmuch_tags_t* + structure. These are not publicly exposed, so a user + will almost never instantiate a :class:`Tags` object + herself. They are usually handed back as a result, + e.g. in :meth:`Database.get_all_tags`. *tags_p* must be + valid, we will raise an :exc:`NullPointerError` + if it is `None`. + :type files_p: :class:`ctypes.c_void_p` + :param parent: The parent object (ie :class:`Message` these + filenames are derived from, and saves a + reference to it, so we can automatically delete the db object + once all derived objects are dead. + """ + if not files_p: + raise NullPointerError() + + self._files_p = files_p + #save reference to parent object so we keep it alive + self._parent = parent + + def __iter__(self): + """ Make Filenames an iterator """ + return self + + _valid = nmlib.notmuch_filenames_valid + _valid.argtypes = [NotmuchFilenamesP] + _valid.restype = bool + + _move_to_next = nmlib.notmuch_filenames_move_to_next + _move_to_next.argtypes = [NotmuchFilenamesP] + _move_to_next.restype = None + + def __next__(self): + if not self._files_p: + raise NotInitializedError() + + if not self._valid(self._files_p): + self._files_p = None + raise StopIteration + + file_ = Filenames._get(self._files_p) + self._move_to_next(self._files_p) + return file_.decode('utf-8', 'ignore') + next = __next__ # python2.x iterator protocol compatibility + + def __unicode__(self): + """Represent Filenames() as newline-separated list of full paths + + .. note:: + + This method exhausts the iterator object, so you will not be able to + iterate over them again. + """ + return "\n".join(self) + + _destroy = nmlib.notmuch_filenames_destroy + _destroy.argtypes = [NotmuchFilenamesP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch filenames""" + if self._files_p: + self._destroy(self._files_p) diff --git a/bindings/python/notmuch/globals.py b/bindings/python/notmuch/globals.py new file mode 100644 index 00000000..11e328b7 --- /dev/null +++ b/bindings/python/notmuch/globals.py @@ -0,0 +1,105 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from ctypes import CDLL, Structure, POINTER +from notmuch.version import SOVERSION + +#----------------------------------------------------------------------------- +#package-global instance of the notmuch library +try: + from os import uname + if uname()[0] == 'Darwin': + nmlib = CDLL("libnotmuch.{0:s}.dylib".format(SOVERSION)) + else: + nmlib = CDLL("libnotmuch.so.{0:s}".format(SOVERSION)) +except: + raise ImportError("Could not find shared 'notmuch' library.") + +from .compat import Python3StringMixIn, encode_utf8 as _str + +# We import these on behalf of other modules. Silence warning about +# these symbols not being used. +Python3StringMixIn +_str + +class Enum(object): + """Provides ENUMS as "code=Enum(['a','b','c'])" where code.a=0 etc...""" + def __init__(self, names): + for number, name in enumerate(names): + setattr(self, name, number) + + +class NotmuchDatabaseS(Structure): + pass +NotmuchDatabaseP = POINTER(NotmuchDatabaseS) + + +class NotmuchQueryS(Structure): + pass +NotmuchQueryP = POINTER(NotmuchQueryS) + + +class NotmuchThreadsS(Structure): + pass +NotmuchThreadsP = POINTER(NotmuchThreadsS) + + +class NotmuchThreadS(Structure): + pass +NotmuchThreadP = POINTER(NotmuchThreadS) + + +class NotmuchMessagesS(Structure): + pass +NotmuchMessagesP = POINTER(NotmuchMessagesS) + + +class NotmuchMessageS(Structure): + pass +NotmuchMessageP = POINTER(NotmuchMessageS) + + +class NotmuchMessagePropertiesS(Structure): + pass +NotmuchMessagePropertiesP = POINTER(NotmuchMessagePropertiesS) + + +class NotmuchTagsS(Structure): + pass +NotmuchTagsP = POINTER(NotmuchTagsS) + + +class NotmuchDirectoryS(Structure): + pass +NotmuchDirectoryP = POINTER(NotmuchDirectoryS) + + +class NotmuchFilenamesS(Structure): + pass +NotmuchFilenamesP = POINTER(NotmuchFilenamesS) + + +class NotmuchConfigListS(Structure): + pass +NotmuchConfigListP = POINTER(NotmuchConfigListS) + + +class NotmuchIndexoptsS(Structure): + pass +NotmuchIndexoptsP = POINTER(NotmuchIndexoptsS) diff --git a/bindings/python/notmuch/message.py b/bindings/python/notmuch/message.py new file mode 100644 index 00000000..e71dbe3e --- /dev/null +++ b/bindings/python/notmuch/message.py @@ -0,0 +1,721 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth + Jesse Rosenthal +""" + + +from ctypes import c_char_p, c_long, c_uint, c_int, POINTER, byref +from datetime import date +from .globals import ( + nmlib, + Enum, + _str, + Python3StringMixIn, + NotmuchTagsP, + NotmuchMessageP, + NotmuchMessagesP, + NotmuchMessagePropertiesP, + NotmuchFilenamesP, +) +from .errors import ( + STATUS, + NotmuchError, + NullPointerError, + NotInitializedError, +) +from .tag import Tags +from .filenames import Filenames + +import email +import sys + + +class Message(Python3StringMixIn): + r"""Represents a single Email message + + Technically, this wraps the underlying *notmuch_message_t* + structure. A user will usually not create these objects themselves + but get them as search results. + + As it implements :meth:`__cmp__`, it is possible to compare two + :class:`Message`\s using `if msg1 == msg2: ...`. + """ + + """notmuch_message_get_filename (notmuch_message_t *message)""" + _get_filename = nmlib.notmuch_message_get_filename + _get_filename.argtypes = [NotmuchMessageP] + _get_filename.restype = c_char_p + + """return all filenames for a message""" + _get_filenames = nmlib.notmuch_message_get_filenames + _get_filenames.argtypes = [NotmuchMessageP] + _get_filenames.restype = NotmuchFilenamesP + + """notmuch_message_get_flag""" + _get_flag = nmlib.notmuch_message_get_flag + _get_flag.argtypes = [NotmuchMessageP, c_uint] + _get_flag.restype = bool + + """notmuch_message_set_flag""" + _set_flag = nmlib.notmuch_message_set_flag + _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int] + _set_flag.restype = None + + """notmuch_message_get_message_id (notmuch_message_t *message)""" + _get_message_id = nmlib.notmuch_message_get_message_id + _get_message_id.argtypes = [NotmuchMessageP] + _get_message_id.restype = c_char_p + + """notmuch_message_get_thread_id""" + _get_thread_id = nmlib.notmuch_message_get_thread_id + _get_thread_id.argtypes = [NotmuchMessageP] + _get_thread_id.restype = c_char_p + + """notmuch_message_get_replies""" + _get_replies = nmlib.notmuch_message_get_replies + _get_replies.argtypes = [NotmuchMessageP] + _get_replies.restype = NotmuchMessagesP + + """notmuch_message_get_tags (notmuch_message_t *message)""" + _get_tags = nmlib.notmuch_message_get_tags + _get_tags.argtypes = [NotmuchMessageP] + _get_tags.restype = NotmuchTagsP + + _get_date = nmlib.notmuch_message_get_date + _get_date.argtypes = [NotmuchMessageP] + _get_date.restype = c_long + + _get_header = nmlib.notmuch_message_get_header + _get_header.argtypes = [NotmuchMessageP, c_char_p] + _get_header.restype = c_char_p + + """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)""" + _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags + _tags_to_maildir_flags.argtypes = [NotmuchMessageP] + _tags_to_maildir_flags.restype = c_int + + """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)""" + _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags + _maildir_flags_to_tags.argtypes = [NotmuchMessageP] + _maildir_flags_to_tags.restype = c_int + + """notmuch_message_get_property""" + _get_property = nmlib.notmuch_message_get_property + _get_property.argtypes = [NotmuchMessageP, c_char_p, POINTER(c_char_p)] + _get_property.restype = c_int + + """notmuch_message_get_properties""" + _get_properties = nmlib.notmuch_message_get_properties + _get_properties.argtypes = [NotmuchMessageP, c_char_p, c_int] + _get_properties.restype = NotmuchMessagePropertiesP + + """notmuch_message_properties_valid""" + _properties_valid = nmlib.notmuch_message_properties_valid + _properties_valid.argtypes = [NotmuchMessagePropertiesP] + _properties_valid.restype = bool + + """notmuch_message_properties_value""" + _properties_value = nmlib.notmuch_message_properties_value + _properties_value.argtypes = [NotmuchMessagePropertiesP] + _properties_value.restype = c_char_p + + """notmuch_message_properties_key""" + _properties_key = nmlib.notmuch_message_properties_key + _properties_key.argtypes = [NotmuchMessagePropertiesP] + _properties_key.restype = c_char_p + + """notmuch_message_properties_move_to_next""" + _properties_move_to_next = nmlib.notmuch_message_properties_move_to_next + _properties_move_to_next.argtypes = [NotmuchMessagePropertiesP] + _properties_move_to_next.restype = None + + #Constants: Flags that can be set/get with set_flag + FLAG = Enum(['MATCH']) + + def __init__(self, msg_p, parent=None): + """ + :param msg_p: A pointer to an internal notmuch_message_t + Structure. If it is `None`, we will raise an + :exc:`NullPointerError`. + + :param parent: A 'parent' object is passed which this message is + derived from. We save a reference to it, so we can + automatically delete the parent object once all derived + objects are dead. + """ + if not msg_p: + raise NullPointerError() + self._msg = msg_p + #keep reference to parent, so we keep it alive + self._parent = parent + + def get_message_id(self): + """Returns the message ID + + :returns: String with a message ID + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + return Message._get_message_id(self._msg).decode('utf-8', 'ignore') + + def get_thread_id(self): + """Returns the thread ID + + The returned string belongs to 'message' will only be valid for as + long as the message is valid. + + This function will not return `None` since Notmuch ensures that every + message belongs to a single thread. + + :returns: String with a thread ID + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + + return Message._get_thread_id(self._msg).decode('utf-8', 'ignore') + + def get_replies(self): + """Gets all direct replies to this message as :class:`Messages` + iterator + + .. note:: + + This call only makes sense if 'message' was ultimately obtained from + a :class:`Thread` object, (such as by coming directly from the + result of calling :meth:`Thread.get_toplevel_messages` or by any + number of subsequent calls to :meth:`get_replies`). If this message + was obtained through some non-thread means, (such as by a call to + :meth:`Query.search_messages`), then this function will return + an empty Messages iterator. + + :returns: :class:`Messages`. + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + + msgs_p = Message._get_replies(self._msg) + + from .messages import Messages, EmptyMessagesResult + + if not msgs_p: + return EmptyMessagesResult(self) + + return Messages(msgs_p, self) + + def get_date(self): + """Returns time_t of the message date + + For the original textual representation of the Date header from the + message call notmuch_message_get_header() with a header value of + "date". + + :returns: A time_t timestamp. + :rtype: c_unit64 + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + return Message._get_date(self._msg) + + def get_header(self, header): + """Get the value of the specified header. + + The value will be read from the actual message file, not from + the notmuch database. The header name is case insensitive. + + Returns an empty string ("") if the message does not contain a + header line matching 'header'. + + :param header: The name of the header to be retrieved. + It is not case-sensitive. + :type header: str + :returns: The header value as string + :raises: :exc:`NotInitializedError` if the message is not + initialized + :raises: :exc:`NullPointerError` if any error occurred + """ + if not self._msg: + raise NotInitializedError() + + #Returns NULL if any error occurs. + header = Message._get_header(self._msg, _str(header)) + if header == None: + raise NullPointerError() + return header.decode('UTF-8', 'ignore') + + def get_filename(self): + """Returns the file path of the message file + + :returns: Absolute file path & name of the message file + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + return Message._get_filename(self._msg).decode('utf-8', 'ignore') + + def get_filenames(self): + """Get all filenames for the email corresponding to 'message' + + Returns a Filenames() generator with all absolute filepaths for + messages recorded to have the same Message-ID. These files must + not necessarily have identical content.""" + if not self._msg: + raise NotInitializedError() + + files_p = Message._get_filenames(self._msg) + + return Filenames(files_p, self) + + def get_flag(self, flag): + """Checks whether a specific flag is set for this message + + The method :meth:`Query.search_threads` sets + *Message.FLAG.MATCH* for those messages that match the + query. This method allows us to get the value of this flag. + + :param flag: One of the :attr:`Message.FLAG` values (currently only + *Message.FLAG.MATCH* + :returns: An unsigned int (0/1), indicating whether the flag is set. + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + return Message._get_flag(self._msg, flag) + + def set_flag(self, flag, value): + """Sets/Unsets a specific flag for this message + + :param flag: One of the :attr:`Message.FLAG` values (currently only + *Message.FLAG.MATCH* + :param value: A bool indicating whether to set or unset the flag. + + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._msg: + raise NotInitializedError() + self._set_flag(self._msg, flag, value) + + def get_tags(self): + """Returns the message tags + + :returns: A :class:`Tags` iterator. + :raises: :exc:`NotInitializedError` if the message is not + initialized + :raises: :exc:`NullPointerError` if any error occurred + """ + if not self._msg: + raise NotInitializedError() + + tags_p = Message._get_tags(self._msg) + if not tags_p: + raise NullPointerError() + return Tags(tags_p, self) + + _add_tag = nmlib.notmuch_message_add_tag + _add_tag.argtypes = [NotmuchMessageP, c_char_p] + _add_tag.restype = c_uint + + def add_tag(self, tag, sync_maildir_flags=False): + """Adds a tag to the given message + + Adds a tag to the current message. The maximal tag length is defined in + the notmuch library and is currently 200 bytes. + + :param tag: String with a 'tag' to be added. + + :param sync_maildir_flags: If notmuch configuration is set to do + this, add maildir flags corresponding to notmuch tags. See + underlying method :meth:`tags_to_maildir_flags`. Use False + if you want to add/remove many tags on a message without + having to physically rename the file every time. Do note, + that this will do nothing when a message is frozen, as tag + changes will not be committed to the database yet. + + :returns: STATUS.SUCCESS if the tag was successfully added. + Raises an exception otherwise. + :raises: :exc:`NullPointerError` if the `tag` argument is NULL + :raises: :exc:`TagTooLongError` if the length of `tag` exceeds + Message.NOTMUCH_TAG_MAX) + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._add_tag(self._msg, _str(tag)) + + # bail out on failure + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + if sync_maildir_flags: + self.tags_to_maildir_flags() + return STATUS.SUCCESS + + _remove_tag = nmlib.notmuch_message_remove_tag + _remove_tag.argtypes = [NotmuchMessageP, c_char_p] + _remove_tag.restype = c_uint + + def remove_tag(self, tag, sync_maildir_flags=False): + """Removes a tag from the given message + + If the message has no such tag, this is a non-operation and + will report success anyway. + + :param tag: String with a 'tag' to be removed. + :param sync_maildir_flags: If notmuch configuration is set to do + this, add maildir flags corresponding to notmuch tags. See + underlying method :meth:`tags_to_maildir_flags`. Use False + if you want to add/remove many tags on a message without + having to physically rename the file every time. Do note, + that this will do nothing when a message is frozen, as tag + changes will not be committed to the database yet. + + :returns: STATUS.SUCCESS if the tag was successfully removed or if + the message had no such tag. + Raises an exception otherwise. + :raises: :exc:`NullPointerError` if the `tag` argument is NULL + :raises: :exc:`TagTooLongError` if the length of `tag` exceeds + Message.NOTMUCH_TAG_MAX) + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._remove_tag(self._msg, _str(tag)) + # bail out on error + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + if sync_maildir_flags: + self.tags_to_maildir_flags() + return STATUS.SUCCESS + + _remove_all_tags = nmlib.notmuch_message_remove_all_tags + _remove_all_tags.argtypes = [NotmuchMessageP] + _remove_all_tags.restype = c_uint + + def remove_all_tags(self, sync_maildir_flags=False): + """Removes all tags from the given message. + + See :meth:`freeze` for an example showing how to safely + replace tag values. + + + :param sync_maildir_flags: If notmuch configuration is set to do + this, add maildir flags corresponding to notmuch tags. See + :meth:`tags_to_maildir_flags`. Use False if you want to + add/remove many tags on a message without having to + physically rename the file every time. Do note, that this + will do nothing when a message is frozen, as tag changes + will not be committed to the database yet. + + :returns: STATUS.SUCCESS if the tags were successfully removed. + Raises an exception otherwise. + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._remove_all_tags(self._msg) + + # bail out on error + if status != STATUS.SUCCESS: + raise NotmuchError(status) + + if sync_maildir_flags: + self.tags_to_maildir_flags() + return STATUS.SUCCESS + + _freeze = nmlib.notmuch_message_freeze + _freeze.argtypes = [NotmuchMessageP] + _freeze.restype = c_uint + + def get_property(self, prop): + """ Retrieve the value for a single property key + + :param prop: The name of the property to get. + :returns: String with the property value or None if there is no such + key. In the case of multiple values for the given key, the + first one is retrieved. + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + value = c_char_p() + status = Message._get_property(self._msg, _str(prop), byref(value)) + if status != 0: + raise NotmuchError(status) + + if value is None or value.value is None: + return None + return value.value.decode('utf-8') + + def get_properties(self, prop="", exact=False): + """ Get the properties of the message, returning a generator of + name, value pairs. + + The generator will yield once per value. There might be more than one + value on each name, so the generator might yield the same name several + times. + + :param prop: The name of the property to get. Otherwise it will return + the full list of properties of the message. + :param exact: if True, require exact match with key. Otherwise + treat as prefix. + :yields: Each property values as a pair of `name, value` + :ytype: pairs of str + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + properties = Message._get_properties(self._msg, _str(prop), exact) + while Message._properties_valid(properties): + key = Message._properties_key(properties) + value = Message._properties_value(properties) + yield key.decode("utf-8"), value.decode("utf-8") + Message._properties_move_to_next(properties) + + def freeze(self): + """Freezes the current state of 'message' within the database + + This means that changes to the message state, (via :meth:`add_tag`, + :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be + committed to the database until the message is :meth:`thaw` ed. + + Multiple calls to freeze/thaw are valid and these calls will + "stack". That is there must be as many calls to thaw as to freeze + before a message is actually thawed. + + The ability to do freeze/thaw allows for safe transactions to + change tag values. For example, explicitly setting a message to + have a given set of tags might look like this:: + + msg.freeze() + msg.remove_all_tags(False) + for tag in new_tags: + msg.add_tag(tag, False) + msg.thaw() + msg.tags_to_maildir_flags() + + With freeze/thaw used like this, the message in the database is + guaranteed to have either the full set of original tag values, or + the full set of new tag values, but nothing in between. + + Imagine the example above without freeze/thaw and the operation + somehow getting interrupted. This could result in the message being + left with no tags if the interruption happened after + :meth:`remove_all_tags` but before :meth:`add_tag`. + + :returns: STATUS.SUCCESS if the message was successfully frozen. + Raises an exception otherwise. + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._freeze(self._msg) + + if STATUS.SUCCESS == status: + # return on success + return status + + raise NotmuchError(status) + + _thaw = nmlib.notmuch_message_thaw + _thaw.argtypes = [NotmuchMessageP] + _thaw.restype = c_uint + + def thaw(self): + """Thaws the current 'message' + + Thaw the current 'message', synchronizing any changes that may have + occurred while 'message' was frozen into the notmuch database. + + See :meth:`freeze` for an example of how to use this + function to safely provide tag changes. + + Multiple calls to freeze/thaw are valid and these calls with + "stack". That is there must be as many calls to thaw as to freeze + before a message is actually thawed. + + :returns: STATUS.SUCCESS if the message was successfully frozen. + Raises an exception otherwise. + :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made + to thaw an unfrozen message. That is, there have been + an unbalanced number of calls to :meth:`freeze` and + :meth:`thaw`. + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._thaw(self._msg) + + if STATUS.SUCCESS == status: + # return on success + return status + + raise NotmuchError(status) + + def is_match(self): + """(Not implemented)""" + return self.get_flag(Message.FLAG.MATCH) + + def tags_to_maildir_flags(self): + """Synchronize notmuch tags to file Maildir flags + + 'D' if the message has the "draft" tag + 'F' if the message has the "flagged" tag + 'P' if the message has the "passed" tag + 'R' if the message has the "replied" tag + 'S' if the message does not have the "unread" tag + + Any existing flags unmentioned in the list above will be + preserved in the renaming. + + Also, if this filename is in a directory named "new", rename it + to be within the neighboring directory named "cur". + + Do note that calling this method while a message is frozen might + not work yet, as the modified tags have not been committed yet + to the database. + + :returns: a :class:`STATUS` value. In short, you want to see + notmuch.STATUS.SUCCESS here. See there for details.""" + if not self._msg: + raise NotInitializedError() + return Message._tags_to_maildir_flags(self._msg) + + def maildir_flags_to_tags(self): + """Synchronize file Maildir flags to notmuch tags + + Flag Action if present + ---- ----------------- + 'D' Adds the "draft" tag to the message + 'F' Adds the "flagged" tag to the message + 'P' Adds the "passed" tag to the message + 'R' Adds the "replied" tag to the message + 'S' Removes the "unread" tag from the message + + For each flag that is not present, the opposite action + (add/remove) is performed for the corresponding tags. If there + are multiple filenames associated with this message, the flag is + considered present if it appears in one or more filenames. (That + is, the flags from the multiple filenames are combined with the + logical OR operator.) + + As a convenience, you can set the sync_maildir_flags parameter in + :meth:`Database.index_file` to implicitly call this. + + :returns: a :class:`STATUS`. In short, you want to see + notmuch.STATUS.SUCCESS here. See there for details.""" + if not self._msg: + raise NotInitializedError() + return Message._maildir_flags_to_tags(self._msg) + + def __repr__(self): + """Represent a Message() object by str()""" + return self.__str__() + + def __unicode__(self): + format = "%s (%s) (%s)" + return format % (self.get_header('from'), + self.get_tags(), + date.fromtimestamp(self.get_date()), + ) + + def get_message_parts(self): + """Output like notmuch show""" + fp = open(self.get_filename(), 'rb') + if sys.version_info[0] < 3: + email_msg = email.message_from_file(fp) + else: + email_msg = email.message_from_binary_file(fp) + fp.close() + + out = [] + for msg in email_msg.walk(): + if not msg.is_multipart(): + out.append(msg) + return out + + def get_part(self, num): + """Returns the nth message body part""" + parts = self.get_message_parts() + if (num <= 0 or num > len(parts)): + return "" + else: + out_part = parts[(num - 1)] + return out_part.get_payload(decode=True) + + def __hash__(self): + """Implement hash(), so we can use Message() sets""" + file = self.get_filename() + if not file: + return None + return hash(file) + + def __cmp__(self, other): + """Implement cmp(), so we can compare Message()s + + 2 messages are considered equal if they point to the same + Message-Id and if they point to the same file names. If 2 + Messages derive from different queries where some files have + been added or removed, the same messages would not be considered + equal (as they do not point to the same set of files + any more).""" + res = cmp(self.get_message_id(), other.get_message_id()) + if res: + res = cmp(list(self.get_filenames()), list(other.get_filenames())) + return res + + _destroy = nmlib.notmuch_message_destroy + _destroy.argtypes = [NotmuchMessageP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch Message""" + if self._msg: + self._destroy(self._msg) diff --git a/bindings/python/notmuch/messages.py b/bindings/python/notmuch/messages.py new file mode 100644 index 00000000..3801c666 --- /dev/null +++ b/bindings/python/notmuch/messages.py @@ -0,0 +1,199 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth + Jesse Rosenthal +""" + +from .globals import ( + nmlib, + NotmuchTagsP, + NotmuchMessageP, + NotmuchMessagesP, +) +from .errors import ( + NullPointerError, + NotInitializedError, +) +from .tag import Tags +from .message import Message + +class Messages(object): + r"""Represents a list of notmuch messages + + This object provides an iterator over a list of notmuch messages + (Technically, it provides a wrapper for the underlying + *notmuch_messages_t* structure). Do note that the underlying library + only provides a one-time iterator (it cannot reset the iterator to + the start). Thus iterating over the function will "exhaust" the list + of messages, and a subsequent iteration attempt will raise a + :exc:`NotInitializedError`. If you need to + re-iterate over a list of messages you will need to retrieve a new + :class:`Messages` object or cache your :class:`Message`\s in a list + via:: + + msglist = list(msgs) + + You can store and reuse the single :class:`Message` objects as often + as you want as long as you keep the parent :class:`Messages` object + around. (Due to hierarchical memory allocation, all derived + :class:`Message` objects will be invalid when we delete the parent + :class:`Messages` object, even if it was already exhausted.) So + this works:: + + db = Database() + msgs = Query(db,'').search_messages() #get a Messages() object + msglist = list(msgs) + + # msgs is "exhausted" now and msgs.next() will raise an exception. + # However it will be kept alive until all retrieved Message() + # objects are also deleted. If you do e.g. an explicit del(msgs) + # here, the following lines would fail. + + # You can reiterate over *msglist* however as often as you want. + # It is simply a list with :class:`Message`s. + + print (msglist[0].get_filename()) + print (msglist[1].get_filename()) + print (msglist[0].get_message_id()) + + + As :class:`Message` implements both __hash__() and __cmp__(), it is + possible to make sets out of :class:`Messages` and use set + arithmetic (this happens in python and will of course be *much* + slower than redoing a proper query with the appropriate filters:: + + s1, s2 = set(msgs1), set(msgs2) + s.union(s2) + s1 -= s2 + ... + + Be careful when using set arithmetic between message sets derived + from different Databases (ie the same database reopened after + messages have changed). If messages have added or removed associated + files in the meantime, it is possible that the same message would be + considered as a different object (as it points to a different file). + """ + + #notmuch_messages_get + _get = nmlib.notmuch_messages_get + _get.argtypes = [NotmuchMessagesP] + _get.restype = NotmuchMessageP + + _collect_tags = nmlib.notmuch_messages_collect_tags + _collect_tags.argtypes = [NotmuchMessagesP] + _collect_tags.restype = NotmuchTagsP + + def __init__(self, msgs_p, parent=None): + """ + :param msgs_p: A pointer to an underlying *notmuch_messages_t* + structure. These are not publicly exposed, so a user + will almost never instantiate a :class:`Messages` object + herself. They are usually handed back as a result, + e.g. in :meth:`Query.search_messages`. *msgs_p* must be + valid, we will raise an :exc:`NullPointerError` if it is + `None`. + :type msgs_p: :class:`ctypes.c_void_p` + :param parent: The parent object + (ie :class:`Query`) these tags are derived from. It saves + a reference to it, so we can automatically delete the db + object once all derived objects are dead. + :TODO: Make the iterator work more than once and cache the tags in + the Python object.(?) + """ + if not msgs_p: + raise NullPointerError() + + self._msgs = msgs_p + #store parent, so we keep them alive as long as self is alive + self._parent = parent + + def collect_tags(self): + """Return the unique :class:`Tags` in the contained messages + + :returns: :class:`Tags` + :exceptions: :exc:`NotInitializedError` if not init'ed + + .. note:: + + :meth:`collect_tags` will iterate over the messages and therefore + will not allow further iterations. + """ + if not self._msgs: + raise NotInitializedError() + + # collect all tags (returns NULL on error) + tags_p = Messages._collect_tags(self._msgs) + #reset _msgs as we iterated over it and can do so only once + self._msgs = None + + if not tags_p: + raise NullPointerError() + return Tags(tags_p, self) + + def __iter__(self): + """ Make Messages an iterator """ + return self + + _valid = nmlib.notmuch_messages_valid + _valid.argtypes = [NotmuchMessagesP] + _valid.restype = bool + + _move_to_next = nmlib.notmuch_messages_move_to_next + _move_to_next.argtypes = [NotmuchMessagesP] + _move_to_next.restype = None + + def __next__(self): + if not self._msgs: + raise NotInitializedError() + + if not self._valid(self._msgs): + self._msgs = None + raise StopIteration + + msg = Message(Messages._get(self._msgs), self) + self._move_to_next(self._msgs) + return msg + next = __next__ # python2.x iterator protocol compatibility + + def __nonzero__(self): + ''' + Implement truth value testing. If __nonzero__ is not + implemented, the python runtime would fall back to `len(..) > + 0` thus exhausting the iterator. + + :returns: True if the wrapped iterator has at least one more object + left. + ''' + return self._msgs and self._valid(self._msgs) + + _destroy = nmlib.notmuch_messages_destroy + _destroy.argtypes = [NotmuchMessagesP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch Messages""" + if self._msgs: + self._destroy(self._msgs) + +class EmptyMessagesResult(Messages): + def __init__(self, parent): + self._msgs = None + self._parent = parent + + def __next__(self): + raise StopIteration() + next = __next__ diff --git a/bindings/python/notmuch/query.py b/bindings/python/notmuch/query.py new file mode 100644 index 00000000..ffb86df1 --- /dev/null +++ b/bindings/python/notmuch/query.py @@ -0,0 +1,237 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from ctypes import c_char_p, c_uint, POINTER, byref +from .globals import ( + nmlib, + Enum, + _str, + NotmuchQueryP, + NotmuchThreadsP, + NotmuchDatabaseP, + NotmuchMessagesP, +) +from .errors import ( + NotmuchError, + NullPointerError, + NotInitializedError, +) +from .threads import Threads +from .messages import Messages + + +class Query(object): + """Represents a search query on an opened :class:`Database`. + + A query selects and filters a subset of messages from the notmuch + database we derive from. + + :class:`Query` provides an instance attribute :attr:`sort`, which + contains the sort order (if specified via :meth:`set_sort`) or + `None`. + + Any function in this class may throw an :exc:`NotInitializedError` + in case the underlying query object was not set up correctly. + + .. note:: Do remember that as soon as we tear down this object, + all underlying derived objects such as threads, + messages, tags etc will be freed by the underlying library + as well. Accessing these objects will lead to segfaults and + other unexpected behavior. See above for more details. + """ + # constants + SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED']) + """Constants: Sort order in which to return results""" + + def __init__(self, db, querystr): + """ + :param db: An open database which we derive the Query from. + :type db: :class:`Database` + :param querystr: The query string for the message. + :type querystr: utf-8 encoded str or unicode + """ + self._db = None + self._query = None + self.sort = None + self.create(db, querystr) + + def _assert_query_is_initialized(self): + """Raises :exc:`NotInitializedError` if self._query is `None`""" + if not self._query: + raise NotInitializedError() + + """notmuch_query_create""" + _create = nmlib.notmuch_query_create + _create.argtypes = [NotmuchDatabaseP, c_char_p] + _create.restype = NotmuchQueryP + + def create(self, db, querystr): + """Creates a new query derived from a Database + + This function is utilized by __init__() and usually does not need to + be called directly. + + :param db: Database to create the query from. + :type db: :class:`Database` + :param querystr: The query string + :type querystr: utf-8 encoded str or unicode + :raises: + :exc:`NullPointerError` if the query creation failed + (e.g. too little memory). + :exc:`NotInitializedError` if the underlying db was not + initialized. + """ + db._assert_db_is_initialized() + # create reference to parent db to keep it alive + self._db = db + # create query, return None if too little mem available + query_p = Query._create(db._db, _str(querystr)) + if not query_p: + raise NullPointerError + self._query = query_p + + _set_sort = nmlib.notmuch_query_set_sort + _set_sort.argtypes = [NotmuchQueryP, c_uint] + _set_sort.restype = None + + def set_sort(self, sort): + """Set the sort order future results will be delivered in + + :param sort: Sort order (see :attr:`Query.SORT`) + """ + self._assert_query_is_initialized() + self.sort = sort + self._set_sort(self._query, sort) + + _exclude_tag = nmlib.notmuch_query_add_tag_exclude + _exclude_tag.argtypes = [NotmuchQueryP, c_char_p] + _exclude_tag.restype = None + + def exclude_tag(self, tagname): + """Add a tag that will be excluded from the query results by default. + + This exclusion will be overridden if this tag appears explicitly in the + query. + + :param tagname: Name of the tag to be excluded + """ + self._assert_query_is_initialized() + self._exclude_tag(self._query, _str(tagname)) + + """notmuch_query_search_threads""" + _search_threads = nmlib.notmuch_query_search_threads + _search_threads.argtypes = [NotmuchQueryP, POINTER(NotmuchThreadsP)] + _search_threads.restype = c_uint + + def search_threads(self): + r"""Execute a query for threads + + Execute a query for threads, returning a :class:`Threads` iterator. + The returned threads are owned by the query and as such, will only be + valid until the Query is deleted. + + The method sets :attr:`Message.FLAG`\.MATCH for those messages that + match the query. The method :meth:`Message.get_flag` allows us + to get the value of this flag. + + :returns: :class:`Threads` + :raises: :exc:`NullPointerError` if search_threads failed + """ + self._assert_query_is_initialized() + threads_p = NotmuchThreadsP() # == NULL + status = Query._search_threads(self._query, byref(threads_p)) + if status != 0: + raise NotmuchError(status) + + if not threads_p: + raise NullPointerError + return Threads(threads_p, self) + + """notmuch_query_search_messages_st""" + _search_messages = nmlib.notmuch_query_search_messages + _search_messages.argtypes = [NotmuchQueryP, POINTER(NotmuchMessagesP)] + _search_messages.restype = c_uint + + def search_messages(self): + """Filter messages according to the query and return + :class:`Messages` in the defined sort order + + :returns: :class:`Messages` + :raises: :exc:`NullPointerError` if search_messages failed + """ + self._assert_query_is_initialized() + msgs_p = NotmuchMessagesP() # == NULL + status = Query._search_messages(self._query, byref(msgs_p)) + if status != 0: + raise NotmuchError(status) + + if not msgs_p: + raise NullPointerError + return Messages(msgs_p, self) + + _count_messages = nmlib.notmuch_query_count_messages + _count_messages.argtypes = [NotmuchQueryP, POINTER(c_uint)] + _count_messages.restype = c_uint + + def count_messages(self): + ''' + This function performs a search and returns Xapian's best + guess as to the number of matching messages. + + :returns: the estimated number of messages matching this query + :rtype: int + ''' + self._assert_query_is_initialized() + count = c_uint(0) + status = Query._count_messages(self._query, byref(count)) + if status != 0: + raise NotmuchError(status) + return count.value + + _count_threads = nmlib.notmuch_query_count_threads + _count_threads.argtypes = [NotmuchQueryP, POINTER(c_uint)] + _count_threads.restype = c_uint + + def count_threads(self): + ''' + This function performs a search and returns the number of + unique thread IDs in the matching messages. This is the same + as number of threads matching a search. + + Note that this is a significantly heavier operation than + meth:`Query.count_messages`. + + :returns: the number of threads returned by this query + :rtype: int + ''' + self._assert_query_is_initialized() + count = c_uint(0) + status = Query._count_threads(self._query, byref(count)) + if status != 0: + raise NotmuchError(status) + return count.value + + _destroy = nmlib.notmuch_query_destroy + _destroy.argtypes = [NotmuchQueryP] + _destroy.restype = None + + def __del__(self): + """Close and free the Query""" + if self._query: + self._destroy(self._query) diff --git a/bindings/python/notmuch/tag.py b/bindings/python/notmuch/tag.py new file mode 100644 index 00000000..fbb18ce4 --- /dev/null +++ b/bindings/python/notmuch/tag.py @@ -0,0 +1,141 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" +from ctypes import c_char_p +from .globals import ( + nmlib, + Python3StringMixIn, + NotmuchTagsP, +) +from .errors import ( + NullPointerError, + NotInitializedError, +) + + +class Tags(Python3StringMixIn): + """Represents a list of notmuch tags + + This object provides an iterator over a list of notmuch tags (which + are unicode instances). + + Do note that the underlying library only provides a one-time + iterator (it cannot reset the iterator to the start). Thus iterating + over the function will "exhaust" the list of tags, and a subsequent + iteration attempt will raise a :exc:`NotInitializedError`. + Also note, that any function that uses iteration (nearly all) will + also exhaust the tags. So both:: + + for tag in tags: print tag + + as well as:: + + number_of_tags = len(tags) + + and even a simple:: + + #str() iterates over all tags to construct a space separated list + print(str(tags)) + + will "exhaust" the Tags. If you need to re-iterate over a list of + tags you will need to retrieve a new :class:`Tags` object. + """ + + #notmuch_tags_get + _get = nmlib.notmuch_tags_get + _get.argtypes = [NotmuchTagsP] + _get.restype = c_char_p + + def __init__(self, tags_p, parent=None): + """ + :param tags_p: A pointer to an underlying *notmuch_tags_t* + structure. These are not publicly exposed, so a user + will almost never instantiate a :class:`Tags` object + herself. They are usually handed back as a result, + e.g. in :meth:`Database.get_all_tags`. *tags_p* must be + valid, we will raise an :exc:`NullPointerError` if it is + `None`. + :type tags_p: :class:`ctypes.c_void_p` + :param parent: The parent object (ie :class:`Database` or + :class:`Message` these tags are derived from, and saves a + reference to it, so we can automatically delete the db object + once all derived objects are dead. + :TODO: Make the iterator optionally work more than once by + cache the tags in the Python object(?) + """ + if not tags_p: + raise NullPointerError() + + self._tags = tags_p + #save reference to parent object so we keep it alive + self._parent = parent + + def __iter__(self): + """ Make Tags an iterator """ + return self + + _valid = nmlib.notmuch_tags_valid + _valid.argtypes = [NotmuchTagsP] + _valid.restype = bool + + _move_to_next = nmlib.notmuch_tags_move_to_next + _move_to_next.argtypes = [NotmuchTagsP] + _move_to_next.restype = None + + def __next__(self): + if not self._tags: + raise NotInitializedError() + if not self._valid(self._tags): + self._tags = None + raise StopIteration + tag = Tags._get(self._tags).decode('UTF-8') + self._move_to_next(self._tags) + return tag + next = __next__ # python2.x iterator protocol compatibility + + def __nonzero__(self): + ''' + Implement truth value testing. If __nonzero__ is not + implemented, the python runtime would fall back to `len(..) > + 0` thus exhausting the iterator. + + :returns: True if the wrapped iterator has at least one more object + left. + ''' + return self._tags and self._valid(self._tags) + + def __unicode__(self): + """string representation of :class:`Tags`: a space separated list of tags + + .. note:: + + As this iterates over the tags, we will not be able to iterate over + them again (as in retrieve them)! If the tags have been exhausted + already, this will raise a :exc:`NotInitializedError`on subsequent + attempts. + """ + return " ".join(self) + + _destroy = nmlib.notmuch_tags_destroy + _destroy.argtypes = [NotmuchTagsP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch tags""" + if self._tags: + self._destroy(self._tags) diff --git a/bindings/python/notmuch/thread.py b/bindings/python/notmuch/thread.py new file mode 100644 index 00000000..9aa0e08c --- /dev/null +++ b/bindings/python/notmuch/thread.py @@ -0,0 +1,281 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from ctypes import c_char_p, c_long, c_int +from .globals import ( + nmlib, + NotmuchThreadP, + NotmuchMessagesP, + NotmuchTagsP, +) +from .errors import ( + NullPointerError, + NotInitializedError, +) +from .messages import Messages +from .tag import Tags +from datetime import date + +class Thread(object): + """Represents a single message thread.""" + + """notmuch_thread_get_thread_id""" + _get_thread_id = nmlib.notmuch_thread_get_thread_id + _get_thread_id.argtypes = [NotmuchThreadP] + _get_thread_id.restype = c_char_p + + """notmuch_thread_get_authors""" + _get_authors = nmlib.notmuch_thread_get_authors + _get_authors.argtypes = [NotmuchThreadP] + _get_authors.restype = c_char_p + + """notmuch_thread_get_subject""" + _get_subject = nmlib.notmuch_thread_get_subject + _get_subject.argtypes = [NotmuchThreadP] + _get_subject.restype = c_char_p + + """notmuch_thread_get_toplevel_messages""" + _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages + _get_toplevel_messages.argtypes = [NotmuchThreadP] + _get_toplevel_messages.restype = NotmuchMessagesP + + _get_newest_date = nmlib.notmuch_thread_get_newest_date + _get_newest_date.argtypes = [NotmuchThreadP] + _get_newest_date.restype = c_long + + _get_oldest_date = nmlib.notmuch_thread_get_oldest_date + _get_oldest_date.argtypes = [NotmuchThreadP] + _get_oldest_date.restype = c_long + + """notmuch_thread_get_tags""" + _get_tags = nmlib.notmuch_thread_get_tags + _get_tags.argtypes = [NotmuchThreadP] + _get_tags.restype = NotmuchTagsP + + def __init__(self, thread_p, parent=None): + """ + :param thread_p: A pointer to an internal notmuch_thread_t + Structure. These are not publicly exposed, so a user + will almost never instantiate a :class:`Thread` object + herself. They are usually handed back as a result, + e.g. when iterating through :class:`Threads`. *thread_p* + must be valid, we will raise an :exc:`NullPointerError` + if it is `None`. + + :param parent: A 'parent' object is passed which this message is + derived from. We save a reference to it, so we can + automatically delete the parent object once all derived + objects are dead. + """ + if not thread_p: + raise NullPointerError() + self._thread = thread_p + #keep reference to parent, so we keep it alive + self._parent = parent + + def get_thread_id(self): + """Get the thread ID of 'thread' + + The returned string belongs to 'thread' and will only be valid + for as long as the thread is valid. + + :returns: String with a message ID + :raises: :exc:`NotInitializedError` if the thread + is not initialized. + """ + if not self._thread: + raise NotInitializedError() + return Thread._get_thread_id(self._thread).decode('utf-8', 'ignore') + + _get_total_messages = nmlib.notmuch_thread_get_total_messages + _get_total_messages.argtypes = [NotmuchThreadP] + _get_total_messages.restype = c_int + + def get_total_messages(self): + """Get the total number of messages in 'thread' + + :returns: The number of all messages in the database + belonging to this thread. Contrast with + :meth:`get_matched_messages`. + :raises: :exc:`NotInitializedError` if the thread + is not initialized. + """ + if not self._thread: + raise NotInitializedError() + return self._get_total_messages(self._thread) + + def get_toplevel_messages(self): + """Returns a :class:`Messages` iterator for the top-level messages in + 'thread' + + This iterator will not necessarily iterate over all of the messages + in the thread. It will only iterate over the messages in the thread + which are not replies to other messages in the thread. + + :returns: :class:`Messages` + :raises: :exc:`NotInitializedError` if query is not initialized + :raises: :exc:`NullPointerError` if search_messages failed + """ + if not self._thread: + raise NotInitializedError() + + msgs_p = Thread._get_toplevel_messages(self._thread) + + if not msgs_p: + raise NullPointerError() + + return Messages(msgs_p, self) + + """notmuch_thread_get_messages""" + _get_messages = nmlib.notmuch_thread_get_messages + _get_messages.argtypes = [NotmuchThreadP] + _get_messages.restype = NotmuchMessagesP + + def get_messages(self): + """Returns a :class:`Messages` iterator for all messages in 'thread' + + :returns: :class:`Messages` + :raises: :exc:`NotInitializedError` if query is not initialized + :raises: :exc:`NullPointerError` if get_messages failed + """ + if not self._thread: + raise NotInitializedError() + + msgs_p = Thread._get_messages(self._thread) + + if not msgs_p: + raise NullPointerError() + + return Messages(msgs_p, self) + + _get_matched_messages = nmlib.notmuch_thread_get_matched_messages + _get_matched_messages.argtypes = [NotmuchThreadP] + _get_matched_messages.restype = c_int + + def get_matched_messages(self): + """Returns the number of messages in 'thread' that matched the query + + :returns: The number of all messages belonging to this thread that + matched the :class:`Query`from which this thread was created. + Contrast with :meth:`get_total_messages`. + :raises: :exc:`NotInitializedError` if the thread + is not initialized. + """ + if not self._thread: + raise NotInitializedError() + return self._get_matched_messages(self._thread) + + def get_authors(self): + """Returns the authors of 'thread' + + The returned string is a comma-separated list of the names of the + authors of mail messages in the query results that belong to this + thread. + + The returned string belongs to 'thread' and will only be valid for + as long as this Thread() is not deleted. + """ + if not self._thread: + raise NotInitializedError() + authors = Thread._get_authors(self._thread) + if not authors: + return None + return authors.decode('UTF-8', 'ignore') + + def get_subject(self): + """Returns the Subject of 'thread' + + The returned string belongs to 'thread' and will only be valid for + as long as this Thread() is not deleted. + """ + if not self._thread: + raise NotInitializedError() + subject = Thread._get_subject(self._thread) + if not subject: + return None + return subject.decode('UTF-8', 'ignore') + + def get_newest_date(self): + """Returns time_t of the newest message date + + :returns: A time_t timestamp. + :rtype: c_unit64 + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._thread: + raise NotInitializedError() + return Thread._get_newest_date(self._thread) + + def get_oldest_date(self): + """Returns time_t of the oldest message date + + :returns: A time_t timestamp. + :rtype: c_unit64 + :raises: :exc:`NotInitializedError` if the message + is not initialized. + """ + if not self._thread: + raise NotInitializedError() + return Thread._get_oldest_date(self._thread) + + def get_tags(self): + """ Returns the message tags + + In the Notmuch database, tags are stored on individual + messages, not on threads. So the tags returned here will be all + tags of the messages which matched the search and which belong to + this thread. + + The :class:`Tags` object is owned by the thread and as such, will only + be valid for as long as this :class:`Thread` is valid (e.g. until the + query from which it derived is explicitly deleted). + + :returns: A :class:`Tags` iterator. + :raises: :exc:`NotInitializedError` if query is not initialized + :raises: :exc:`NullPointerError` if search_messages failed + """ + if not self._thread: + raise NotInitializedError() + + tags_p = Thread._get_tags(self._thread) + if not tags_p: + raise NullPointerError() + return Tags(tags_p, self) + + def __unicode__(self): + frm = "thread:%s %12s [%d/%d] %s; %s (%s)" + + return frm % (self.get_thread_id(), + date.fromtimestamp(self.get_newest_date()), + self.get_matched_messages(), + self.get_total_messages(), + self.get_authors(), + self.get_subject(), + self.get_tags(), + ) + + _destroy = nmlib.notmuch_thread_destroy + _destroy.argtypes = [NotmuchThreadP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch Thread""" + if self._thread: + self._destroy(self._thread) diff --git a/bindings/python/notmuch/threads.py b/bindings/python/notmuch/threads.py new file mode 100644 index 00000000..0c382d5b --- /dev/null +++ b/bindings/python/notmuch/threads.py @@ -0,0 +1,152 @@ +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +from .globals import ( + nmlib, + Python3StringMixIn, + NotmuchThreadP, + NotmuchThreadsP, +) +from .errors import ( + NullPointerError, + NotInitializedError, +) +from .thread import Thread + +class Threads(Python3StringMixIn): + """Represents a list of notmuch threads + + This object provides an iterator over a list of notmuch threads + (Technically, it provides a wrapper for the underlying + *notmuch_threads_t* structure). Do note that the underlying + library only provides a one-time iterator (it cannot reset the + iterator to the start). Thus iterating over the function will + "exhaust" the list of threads, and a subsequent iteration attempt + will raise a :exc:`NotInitializedError`. Also + note, that any function that uses iteration will also + exhaust the messages. So both:: + + for thread in threads: print thread + + as well as:: + + list_of_threads = list(threads) + + will "exhaust" the threads. If you need to re-iterate over a list of + messages you will need to retrieve a new :class:`Threads` object. + + Things are not as bad as it seems though, you can store and reuse + the single Thread objects as often as you want as long as you + keep the parent Threads object around. (Recall that due to + hierarchical memory allocation, all derived Threads objects will + be invalid when we delete the parent Threads() object, even if it + was already "exhausted".) So this works:: + + db = Database() + threads = Query(db,'').search_threads() #get a Threads() object + threadlist = [] + for thread in threads: + threadlist.append(thread) + + # threads is "exhausted" now. + # However it will be kept around until all retrieved Thread() objects are + # also deleted. If you did e.g. an explicit del(threads) here, the + # following lines would fail. + + # You can reiterate over *threadlist* however as often as you want. + # It is simply a list with Thread objects. + + print (threadlist[0].get_thread_id()) + print (threadlist[1].get_thread_id()) + print (threadlist[0].get_total_messages()) + """ + + #notmuch_threads_get + _get = nmlib.notmuch_threads_get + _get.argtypes = [NotmuchThreadsP] + _get.restype = NotmuchThreadP + + def __init__(self, threads_p, parent=None): + """ + :param threads_p: A pointer to an underlying *notmuch_threads_t* + structure. These are not publicly exposed, so a user + will almost never instantiate a :class:`Threads` object + herself. They are usually handed back as a result, + e.g. in :meth:`Query.search_threads`. *threads_p* must be + valid, we will raise an :exc:`NullPointerError` if it is + `None`. + :type threads_p: :class:`ctypes.c_void_p` + :param parent: The parent object + (ie :class:`Query`) these tags are derived from. It saves + a reference to it, so we can automatically delete the db + object once all derived objects are dead. + :TODO: Make the iterator work more than once and cache the tags in + the Python object.(?) + """ + if not threads_p: + raise NullPointerError() + + self._threads = threads_p + #store parent, so we keep them alive as long as self is alive + self._parent = parent + + def __iter__(self): + """ Make Threads an iterator """ + return self + + _valid = nmlib.notmuch_threads_valid + _valid.argtypes = [NotmuchThreadsP] + _valid.restype = bool + + _move_to_next = nmlib.notmuch_threads_move_to_next + _move_to_next.argtypes = [NotmuchThreadsP] + _move_to_next.restype = None + + def __next__(self): + if not self._threads: + raise NotInitializedError() + + if not self._valid(self._threads): + self._threads = None + raise StopIteration + + thread = Thread(Threads._get(self._threads), self) + self._move_to_next(self._threads) + return thread + next = __next__ # python2.x iterator protocol compatibility + + def __nonzero__(self): + ''' + Implement truth value testing. If __nonzero__ is not + implemented, the python runtime would fall back to `len(..) > + 0` thus exhausting the iterator. + + :returns: True if the wrapped iterator has at least one more object + left. + ''' + return self._threads and self._valid(self._threads) + + _destroy = nmlib.notmuch_threads_destroy + _destroy.argtypes = [NotmuchThreadsP] + _destroy.restype = None + + def __del__(self): + """Close and free the notmuch Threads""" + if self._threads: + self._destroy(self._threads) diff --git a/bindings/python/notmuch/version.py b/bindings/python/notmuch/version.py new file mode 100644 index 00000000..f4fd171d --- /dev/null +++ b/bindings/python/notmuch/version.py @@ -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 index 00000000..6308b9f9 --- /dev/null +++ b/bindings/python/setup.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +""" +This file is part of notmuch. + +Notmuch is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +Notmuch is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with notmuch. If not, see . + +Copyright 2010 Sebastian Spaeth +""" + +import os +from distutils.core import setup + +# get the notmuch version number without importing the notmuch module +version_file = os.path.join(os.path.dirname(__file__), + 'notmuch', 'version.py') +exec(compile(open(version_file).read(), version_file, 'exec')) +assert '__VERSION__' in globals(), \ + 'Failed to read the notmuch binding version number' + +setup(name='notmuch', + version=__VERSION__, + description='Python binding of the notmuch mail search and indexing library.', + author='Sebastian Spaeth', + author_email='Sebastian@SSpaeth.de', + url='https://notmuchmail.org/', + download_url='https://notmuchmail.org/releases/notmuch-%s.tar.gz' % __VERSION__, + packages=['notmuch'], + keywords=['library', 'email'], + long_description='''Overview +======== + +The notmuch module provides an interface to the `notmuch +`_ functionality, directly interfacing with a +shared notmuch library. Notmuch provides a maildatabase that allows +for extremely quick searching and filtering of your email according to +various criteria. + +The documentation for the latest notmuch release can be `viewed +online `_. + +Requirements +------------ + +You need to have notmuch installed (or rather libnotmuch.so.1). Also, +notmuch makes use of the ctypes library, and has only been tested with +python >= 2.5. It will not work on earlier python versions. +''', + classifiers=['Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + 'Topic :: Communications :: Email', + 'Topic :: Software Development :: Libraries' + ], + platforms='', + license='https://www.gnu.org/licenses/gpl-3.0.txt', + ) diff --git a/bindings/ruby/.gitignore b/bindings/ruby/.gitignore new file mode 100644 index 00000000..c57ae63f --- /dev/null +++ b/bindings/ruby/.gitignore @@ -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 index 00000000..a2946b66 --- /dev/null +++ b/bindings/ruby/README @@ -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 index 00000000..ed224ef7 --- /dev/null +++ b/bindings/ruby/database.c @@ -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 + */ + +#include "defs.h" + +VALUE +notmuch_rb_database_alloc (VALUE klass) +{ + return Data_Wrap_Notmuch_Object (klass, ¬much_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, ¬much_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, ¬much_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, ¬much_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, ¬much_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, ¬much_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, ¬much_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, ¬much_rb_query_type, query); +} diff --git a/bindings/ruby/defs.h b/bindings/ruby/defs.h new file mode 100644 index 00000000..a2cb38c8 --- /dev/null +++ b/bindings/ruby/defs.h @@ -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 + */ + +#ifndef DEFS_H +#define DEFS_H + +#include +#include +#include + +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), ¬much_rb_database_type, (ptr)) + +#define Data_Get_Notmuch_Directory(obj, ptr) \ + Data_Get_Notmuch_Object ((obj), ¬much_rb_directory_type, (ptr)) + +#define Data_Get_Notmuch_Query(obj, ptr) \ + Data_Get_Notmuch_Object ((obj), ¬much_rb_query_type, (ptr)) + +#define Data_Get_Notmuch_Threads(obj, ptr) \ + Data_Get_Notmuch_Object ((obj), ¬much_rb_threads_type, (ptr)) + +#define Data_Get_Notmuch_Messages(obj, ptr) \ + Data_Get_Notmuch_Object ((obj), ¬much_rb_messages_type, (ptr)) + +#define Data_Get_Notmuch_Thread(obj, ptr) \ + Data_Get_Notmuch_Object ((obj), ¬much_rb_thread_type, (ptr)) + +#define Data_Get_Notmuch_Message(obj, ptr) \ + Data_Get_Notmuch_Object ((obj), ¬much_rb_message_type, (ptr)) + +#define Data_Get_Notmuch_Tags(obj, ptr) \ + Data_Get_Notmuch_Object ((obj), ¬much_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 index 00000000..f267d82f --- /dev/null +++ b/bindings/ruby/directory.c @@ -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 + */ + +#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, ¬much_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 index 00000000..d914537c --- /dev/null +++ b/bindings/ruby/extconf.rb @@ -0,0 +1,26 @@ +#!/usr/bin/env ruby +# coding: utf-8 +# Copyright 2010, 2011, 2012 Ali Polatel +# 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 index 00000000..60c3fb8b --- /dev/null +++ b/bindings/ruby/filenames.c @@ -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 index 00000000..2d1994af --- /dev/null +++ b/bindings/ruby/init.c @@ -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 + */ + +#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 = ¬much_rb_object_type, \ + .data = ¬much_ ## 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 notmuch 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 index 00000000..13c182f6 --- /dev/null +++ b/bindings/ruby/message.c @@ -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 + */ + +#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, ¬much_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, ¬much_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 index 00000000..6369d052 --- /dev/null +++ b/bindings/ruby/messages.c @@ -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 + */ + +#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, ¬much_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, ¬much_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 index 00000000..077def02 --- /dev/null +++ b/bindings/ruby/query.c @@ -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 + */ + +#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, ¬much_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, ¬much_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, ¬much_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 index 00000000..1e867ff9 --- /dev/null +++ b/bindings/ruby/rdoc.sh @@ -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 index 00000000..a0f88633 --- /dev/null +++ b/bindings/ruby/status.c @@ -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 + */ + +#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 index 00000000..b64874d1 --- /dev/null +++ b/bindings/ruby/tags.c @@ -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 index 00000000..b20ed893 --- /dev/null +++ b/bindings/ruby/thread.c @@ -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 + */ + +#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, ¬much_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, ¬much_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, ¬much_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 index 00000000..50280260 --- /dev/null +++ b/bindings/ruby/threads.c @@ -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 + */ + +#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, ¬much_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, ¬much_rb_thread_type, thread)); + } + + return self; +} diff --git a/command-line-arguments.c b/command-line-arguments.c new file mode 100644 index 00000000..5dea8281 --- /dev/null +++ b/command-line-arguments.c @@ -0,0 +1,320 @@ +#include +#include +#include +#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 index 00000000..606e5cd0 --- /dev/null +++ b/command-line-arguments.h @@ -0,0 +1,82 @@ +#ifndef NOTMUCH_OPTS_H +#define NOTMUCH_OPTS_H + +#include + +#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 index 00000000..7ede45e9 --- /dev/null +++ b/compat/.gitignore @@ -0,0 +1 @@ +/zlib.pc diff --git a/compat/Makefile b/compat/Makefile new file mode 100644 index 00000000..fa25832e --- /dev/null +++ b/compat/Makefile @@ -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 index 00000000..c58ca746 --- /dev/null +++ b/compat/Makefile.local @@ -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 index 00000000..12aacf42 --- /dev/null +++ b/compat/README @@ -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 index 00000000..62ad69d6 --- /dev/null +++ b/compat/check_asctime.c @@ -0,0 +1,12 @@ +#include +#include + +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 index 00000000..babeb742 --- /dev/null +++ b/compat/check_getpwuid.c @@ -0,0 +1,12 @@ +#include +#include + +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 index 00000000..59e91618 --- /dev/null +++ b/compat/compat.h @@ -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 + */ + +/* 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 +#include + +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_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 index 00000000..8f08bec4 --- /dev/null +++ b/compat/function-attributes.h @@ -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 index 00000000..7c0ee727 --- /dev/null +++ b/compat/gen_zlib_pc.c @@ -0,0 +1,19 @@ +#include +#include + +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 index 00000000..e5c1f07c --- /dev/null +++ b/compat/getdelim.c @@ -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 + +#include +#include +#include +#include + +#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 index 00000000..2fcaba14 --- /dev/null +++ b/compat/getline.c @@ -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 + +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 index 00000000..e5609793 --- /dev/null +++ b/compat/have_canonicalize_file_name.c @@ -0,0 +1,11 @@ +#define _GNU_SOURCE +#include + +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 index 00000000..5338ee4d --- /dev/null +++ b/compat/have_d_type.c @@ -0,0 +1,11 @@ +#include + +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 index 00000000..6952a3b3 --- /dev/null +++ b/compat/have_getline.c @@ -0,0 +1,14 @@ +#define _GNU_SOURCE +#include +#include + +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 index 00000000..8e004572 --- /dev/null +++ b/compat/have_strcasestr.c @@ -0,0 +1,12 @@ +#define _GNU_SOURCE +#include /* strcasecmp() in POSIX */ +#include /* 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 index 00000000..dd4aae75 --- /dev/null +++ b/compat/have_strsep.c @@ -0,0 +1,12 @@ +#define _GNU_SOURCE +#include + +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 index 00000000..8f7b380e --- /dev/null +++ b/compat/have_timegm.c @@ -0,0 +1,7 @@ +#include + +int +main () +{ + return (int) timegm ((struct tm *) 0); +} diff --git a/compat/strcasestr.c b/compat/strcasestr.c new file mode 100644 index 00000000..d4480bf3 --- /dev/null +++ b/compat/strcasestr.c @@ -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 + +#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 index 00000000..4b6926d9 --- /dev/null +++ b/compat/strsep.c @@ -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 + +/* 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 index 00000000..005a4239 --- /dev/null +++ b/compat/timegm.c @@ -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 +#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 index 00000000..de492a7c --- /dev/null +++ b/completion/Makefile @@ -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 index 00000000..54df463c --- /dev/null +++ b/completion/Makefile.local @@ -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 index 00000000..900e1c98 --- /dev/null +++ b/completion/README @@ -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 index 00000000..3748846e --- /dev/null +++ b/completion/notmuch-completion.bash @@ -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 +# +# +# 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 </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 index 00000000..1cd0d78f --- /dev/null +++ b/completion/zsh/_email-notmuch @@ -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 index 00000000..0bdd7f77 --- /dev/null +++ b/completion/zsh/_notmuch @@ -0,0 +1,295 @@ +#compdef notmuch -p notmuch-* + +# ZSH completion for `notmuch` +# Copyright © 2018 Vincent Breitmoser + +_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 index 00000000..7afd08c7 --- /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 <-- Currently ignored + --host=-- 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 < 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 </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 < +#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 < /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 < +#include + +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 < _check_gmime_cert.c < +#include + +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 < _verify_sig_with_session_key.c < +#include + +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 <= 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 <= 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 < /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 < _time_t.c +#include +#include +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 < sh.config <) +NOTMUCH_GMIME_EMITS_ANGLE_BRACKETS=${gmime_emits_angle_brackets} + +# Whether GMime can verify signatures when decrypting with a session key: +NOTMUCH_GMIME_VERIFY_WITH_SESSION_KEY=${gmime_verify_with_session_key} + +# Flags needed to compile and link against zlib +NOTMUCH_ZLIB_CFLAGS="${zlib_cflags}" +NOTMUCH_ZLIB_LDFLAGS="${zlib_ldflags}" + +# Does the C compiler support the sanitizers +NOTMUCH_HAVE_ASAN=${have_asan} +NOTMUCH_HAVE_TSAN=${have_tsan} + +# do we have man pages? +NOTMUCH_HAVE_MAN=$((have_sphinx)) + +# 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 < + Copyright (C) + + 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. + + , 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 index 00000000..1b9e7505 --- /dev/null +++ b/contrib/go/Makefile @@ -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 index 00000000..1825ae03 --- /dev/null +++ b/contrib/go/README @@ -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 index 00000000..916e5bb2 --- /dev/null +++ b/contrib/go/src/notmuch-addrlookup/addrlookup.go @@ -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*(("(\.|[^"])*"|[^,])*?)` + // pattern := "\\s*((\\\"(\\\\.|[^\\\\\"])*\\\"|[^,])*" + + // "\\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 index 00000000..5496198a --- /dev/null +++ b/contrib/go/src/notmuch/notmuch.go @@ -0,0 +1,1404 @@ +// Wrapper around the notmuch library + +package notmuch + +/* +#cgo LDFLAGS: -lnotmuch + +#include +#include +#include +#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__t see the various + * notmuch_ 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_ + +/* 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_ + +/* 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 index 00000000..116bb715 --- /dev/null +++ b/contrib/notmuch-mutt/.gitignore @@ -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 index 00000000..de933eaa --- /dev/null +++ b/contrib/notmuch-mutt/Makefile @@ -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 index 00000000..c7520228 --- /dev/null +++ b/contrib/notmuch-mutt/README @@ -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 + (Debian package: libdigest-sha-perl) +- Mail::Box + (Debian package: libmail-box-perl) +- Mail::Header + (Debian package: libmailtools-perl) +- 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 . + +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 +. + diff --git a/contrib/notmuch-mutt/notmuch-mutt b/contrib/notmuch-mutt/notmuch-mutt new file mode 100755 index 00000000..b81252c8 --- /dev/null +++ b/contrib/notmuch-mutt/notmuch-mutt @@ -0,0 +1,338 @@ +#!/usr/bin/env perl +# +# notmuch-mutt - notmuch (of a) helper for Mutt +# +# Copyright: © 2011-2015 Stefano Zacchiroli +# 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 () { # 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 [I