From: David Bremner Date: Sun, 13 Dec 2020 14:38:31 +0000 (-0400) Subject: Merge tag 'debian/0.31.2-3' into debian/buster-backports X-Git-Tag: archive/debian/0.31.4-2_bpo10+1~9 X-Git-Url: https://git.cworth.org/git?a=commitdiff_plain;h=7a9c97e8a57f2662b9069dae01b6e5cb2f650563;hp=b7ca3c23d17d247bda37645c7f861b3c0d04bf25;p=notmuch Merge tag 'debian/0.31.2-3' into debian/buster-backports notmuch release 0.31.2-3 for unstable (sid) [dgit] [dgit distro=debian no-split --quilt=linear] --- diff --git a/.dir-locals.el b/.dir-locals.el index fc75ae61..b3ddffe8 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -15,7 +15,7 @@ (emacs-lisp-mode (indent-tabs-mode . t) (tab-width . 8)) - (shell-mode + (sh-mode (indent-tabs-mode . t) (tab-width . 8) (sh-basic-offset . 4) diff --git a/.gitignore b/.gitignore index 468b660a..3edd1768 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,20 @@ +*.[ao] +*.stamp +*cscope* +*~ +.*.swp +/.deps /.first-build-message +/.stamps /Makefile.config +/bindings/python-cffi/build/ +/lib/libnotmuch*.dylib +/lib/libnotmuch.so* +/notmuch +/notmuch-shared +/releases /sh.config +/sphinx.config /version.stamp TAGS tags -*cscope* -/.deps -/notmuch -/notmuch-shared -/lib/libnotmuch.so* -/lib/libnotmuch*.dylib -*.[ao] -*~ -.*.swp -/releases -/.stamps -*.stamp diff --git a/.travis.yml b/.travis.yml index f9516bde..9dcec1ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,25 @@ language: c -dist: xenial +dist: bionic addons: apt: sources: - sourceline: 'ppa:xapian-backports/ppa' - - sourceline: 'ppa:notmuch/notmuch' 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 download-test-databases - make test notifications: diff --git a/AUTHORS b/AUTHORS index 5fe5006f..6e872084 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,5 +1,6 @@ -Carl Worth is the primary author of Notmuch. -But there's really not much that he's done. There's been a lot of +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 @@ -21,10 +22,108 @@ 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 -Keith Packard -Jamey Sharp + Martin Krafft + Jamey Sharp + +The Notmuch project acknowledges the contributions of the following +organizations via their employees + + Google LLC diff --git a/INSTALL b/INSTALL index f1236e71..40ea377a 100644 --- a/INSTALL +++ b/INSTALL @@ -95,7 +95,7 @@ dependencies with a single simple command line. For example: For Fedora and similar: - sudo yum install xapian-core-devel gmime-devel libtalloc-devel zlib-devel python3-sphinx texinfo info + 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.global b/Makefile.global index 0aee5876..8477468d 100644 --- a/Makefile.global +++ b/Makefile.global @@ -1,3 +1,4 @@ +# -*- makefile-gmake -*- # Here's the (hopefully simple) versioning scheme. # # Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We @@ -16,7 +17,7 @@ else DATE:=$(shell date +%F) endif -VERSION:=$(shell cat ${srcdir}/version) +VERSION:=$(shell cat ${srcdir}/version.txt) ELPA_VERSION:=$(subst ~,_,$(VERSION)) ifeq ($(filter release release-message pre-release update-versions,$(MAKECMDGOALS)),) ifeq ($(IS_GIT),yes) @@ -39,6 +40,7 @@ DEB_TAG=debian/$(UPSTREAM_TAG)-1 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 @@ -49,8 +51,7 @@ DETACHED_SIG_FILE=$(TAR_FILE).asc PV_FILE=bindings/python/notmuch/version.py # Smash together user's values with our extra values -STD_CFLAGS := -std=gnu99 -FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(STD_CFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(extra_cflags) $(CONFIGURE_CFLAGS) +FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(extra_cflags) $(CONFIGURE_CFLAGS) FINAL_CXXFLAGS = $(CPPFLAGS) $(CXXFLAGS) $(WARN_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) $(CONFIGURE_CXXFLAGS) FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Lutil -lnotmuch_util -Llib -lnotmuch ifeq ($(LIBDIR_IN_LDCONFIG),0) diff --git a/Makefile.local b/Makefile.local index 3c6dacbc..fb126294 100644 --- a/Makefile.local +++ b/Makefile.local @@ -1,7 +1,7 @@ -# -*- makefile -*- +# -*- makefile-gmake -*- .PHONY: all -all: notmuch notmuch-shared build-man build-info ruby-bindings +all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings ifeq ($(MAKECMDGOALS),) ifeq ($(shell cat .first-build-message 2>/dev/null),) @NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all @@ -19,7 +19,7 @@ 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 +version.stamp: $(srcdir)/version.txt echo $(VERSION) > $@ $(TAR_FILE): @@ -30,12 +30,12 @@ $(TAR_FILE): echo "Warning: No signed tag for $(VERSION)"; \ fi ; \ git archive --format=tar --prefix=$(PACKAGE)-$(VERSION)/ $$ref > $(TAR_FILE).tmp - echo $(VERSION) > version.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.tmp - rm version.tmp + --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)" @@ -54,6 +54,7 @@ update-versions: sed -i -e "s/^__VERSION__[[:blank:]]*=.*$$/__VERSION__ = \'${VERSION}\'/" \ -e "s/^SOVERSION[[:blank:]]*=.*$$/SOVERSION = \'${LIBNOTMUCH_VERSION_MAJOR}\'/" \ ${PV_FILE} + cp version.txt bindings/python-cffi # We invoke make recursively only to force ordering of our phony # targets in the case of parallel invocation of make (-j). @@ -66,6 +67,7 @@ update-versions: 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) @@ -79,6 +81,7 @@ ifeq ($(REALLY_UPLOAD),yes) git push origin $(VERSION) $(DEB_TAG) 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." @@ -88,7 +91,7 @@ pre-release: $(MAKE) VERSION=$(VERSION) test git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG) git tag -s -m "$(PACKAGE) Debian $(VERSION)-1 upload (same as $(VERSION))" $(DEB_TAG) - $(MAKE) VERSION=$(VERSION) $(TAR_FILE) + $(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 @@ -97,14 +100,16 @@ pre-release: .PHONY: debian-snapshot debian-snapshot: make VERSION=$(VERSION) clean - 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; \ - mv -f $${TMPFILE} debian/changelog; \ - echo '3.0 (quilt)' > debian/source/format + 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: @@ -290,7 +295,7 @@ 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 +DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config sh.config sphinx.config CPPCHECK_STAMPS := $(SRCS:%=.stamps/cppcheck/%) .PHONY: cppcheck diff --git a/NEWS b/NEWS index 66bb69f1..677c507d 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,154 @@ +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) =========================== @@ -72,7 +223,7 @@ information about cryptographic protections for the Subject header. Emacs ----- -Optionally check for missing attachements in outgoing messages (see +Optionally check for missing attachments in outgoing messages (see function `notmuch-mua-attachment-check`). Bind `B` to browse URLs in current message. diff --git a/bindings/Makefile.local b/bindings/Makefile.local index 18f95835..bc960bbc 100644 --- a/bindings/Makefile.local +++ b/bindings/Makefile.local @@ -1,4 +1,4 @@ -# -*- makefile -*- +# -*- makefile-gmake -*- dir := bindings @@ -13,6 +13,13 @@ ifeq ($(HAVE_RUBY_DEV),1) $(MAKE) -C $(dir)/ruby endif +python-cffi-bindings: 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 +endif + CLEAN += $(patsubst %,$(dir)/ruby/%, \ .RUBYARCHDIR.time \ Makefile database.o directory.o filenames.o\ @@ -20,3 +27,5 @@ CLEAN += $(patsubst %,$(dir)/ruby/%, \ status.o tags.o thread.o threads.o) CLEAN += bindings/ruby/.vendorarchdir.time + +CLEAN += bindings/python-cffi/build 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..f269f2a1 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_build.py @@ -0,0 +1,339 @@ +import cffi + + +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=['../../lib'], + library_dirs=['../../lib'], + 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_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_list notmuch_config_list_t; + typedef struct _notmuch_indexopts notmuch_indexopts_t; + + const char * + notmuch_status_to_string (notmuch_status_t status); + + notmuch_status_t + notmuch_database_create_verbose (const char *path, + notmuch_database_t **database, + char **error_message); + notmuch_status_t + notmuch_database_create (const char *path, notmuch_database_t **database); + notmuch_status_t + notmuch_database_open_verbose (const char *path, + notmuch_database_mode_t mode, + notmuch_database_t **database, + char **error_message); + notmuch_status_t + notmuch_database_open (const char *path, + notmuch_database_mode_t mode, + notmuch_database_t **database); + notmuch_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_status_t + notmuch_database_get_config_list (notmuch_database_t *db, const char *prefix, notmuch_config_list_t **out); + notmuch_bool_t + notmuch_config_list_valid (notmuch_config_list_t *config_list); + const char * + notmuch_config_list_key (notmuch_config_list_t *config_list); + const char * + notmuch_config_list_value (notmuch_config_list_t *config_list); + void + notmuch_config_list_move_to_next (notmuch_config_list_t *config_list); + void + notmuch_config_list_destroy (notmuch_config_list_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..29de6495 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_config.py @@ -0,0 +1,87 @@ +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_list_destroy, + fn_valid=capi.lib.notmuch_config_list_valid, + fn_get=capi.lib.notmuch_config_list_key, + fn_next=capi.lib.notmuch_config_list_move_to_next) + + def __next__(self): + item = super().__next__() + return base.BinString.from_cffi(item) + + +class ConfigMapping(base.NotmuchObject, collections.abc.MutableMapping): + """The config key/value pairs stored in the database. + + 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. + + :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. + """ + configlist_pp = capi.ffi.new('notmuch_config_list_t**') + ret = capi.lib.notmuch_database_get_config_list(self._ptr(), b'', configlist_pp) + if ret != capi.lib.NOTMUCH_STATUS_SUCCESS: + raise errors.NotmuchError(ret) + return ConfigIter(self._parent, configlist_pp[0]) + + 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..5ab0f20a --- /dev/null +++ b/bindings/python-cffi/notmuch2/_database.py @@ -0,0 +1,822 @@ +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 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 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 read from the user's + configuration file, respecting the ``NOTMUCH_CONFIG`` + environment variable if set. + :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. + + :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 + AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup']) + _db_p = base.MemoryPointer() + STR_MODE_MAP = { + 'ro': MODE.READ_ONLY, + 'rw': MODE.READ_WRITE, + } + + def __init__(self, path=None, mode=MODE.READ_ONLY): + if isinstance(mode, str): + mode = self.STR_MODE_MAP[mode] + self.mode = mode + if path is None: + path = self.default_path() + if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): + path = bytes(path) + db_pp = capi.ffi.new('notmuch_database_t **') + cmsg = capi.ffi.new('char**') + ret = capi.lib.notmuch_database_open_verbose(os.fsencode(path), + mode.value, db_pp, cmsg) + 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): + """Create and open database in READ_WRITE mode. + + This is creates a new notmuch database and returns an opened + instance in :attr:`MODE.READ_WRITE` mode. + + :param path: The directory of where the database is stored. If + ``None`` the location will be read from the user's + configuration file, respecting the ``NOTMUCH_CONFIG`` + environment variable if set. + :type path: str, bytes or os.PathLike + + :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. + """ + if path is None: + path = cls.default_path() + if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path): + path = bytes(path) + db_pp = capi.ffi.new('notmuch_database_t **') + cmsg = capi.ffi.new('char**') + ret = capi.lib.notmuch_database_create_verbose(os.fsencode(path), + db_pp, cmsg) + 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) + + @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 + :env:`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. + """ + 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 = str.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..13369445 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_errors.py @@ -0,0 +1,112 @@ +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, + } + 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: + return capi.lib.notmuch_status_to_string(self.status) + 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 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..2f232076 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_message.py @@ -0,0 +1,710 @@ +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.get_message(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 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:`Message` 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) + + 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..cda52338 --- /dev/null +++ b/bindings/python-cffi/setup.py @@ -0,0 +1,24 @@ +import setuptools + +with open('version.txt') 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..6835fd30 --- /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=5, + 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..1b2695f5 --- /dev/null +++ b/bindings/python-cffi/tests/test_config.py @@ -0,0 +1,56 @@ +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) as db0: + db0.config['spam'] = 'ham' + with dbmod.Database(maildir.path) 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): + assert list(db.config) == [] + db.config['spam'] = 'ham' + db.config['eggs'] = 'bacon' + assert set(db.config) == {'spam', 'eggs'} + assert set(db.config.keys()) == {'spam', 'eggs'} + assert set(db.config.values()) == {'ham', 'bacon'} + assert set(db.config.items()) == {('spam', 'ham'), ('eggs', 'bacon')} + + def test_len(self, db): + assert len(db.config) == 0 + db.config['spam'] = 'ham' + assert len(db.config) == 1 + db.config['eggs'] = 'bacon' + assert len(db.config) == 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..a2c69de6 --- /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) 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.FileError): + 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') 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_message.py b/bindings/python-cffi/tests/test_message.py new file mode 100644 index 00000000..532bf921 --- /dev/null +++ b/bindings/python-cffi/tests/test_message.py @@ -0,0 +1,226 @@ +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_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..faf3947b --- /dev/null +++ b/bindings/python-cffi/tests/test_tags.py @@ -0,0 +1,239 @@ +"""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) 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) 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) 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) 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) 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) 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..1f44b35d --- /dev/null +++ b/bindings/python-cffi/tests/test_thread.py @@ -0,0 +1,102 @@ +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) 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_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-cffi/version.txt b/bindings/python-cffi/version.txt new file mode 100644 index 00000000..c415e1c6 --- /dev/null +++ b/bindings/python-cffi/version.txt @@ -0,0 +1 @@ +0.31.2 diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py index 88ca836e..8fb507fa 100644 --- a/bindings/python/notmuch/database.py +++ b/bindings/python/notmuch/database.py @@ -65,7 +65,7 @@ class Database(object): .. note:: Any function in this class can and will throw an - :exc:`NotInitializedError` if the database was not intitialized + :exc:`NotInitializedError` if the database was not initialized properly. """ _std_db_path = None @@ -273,9 +273,9 @@ class Database(object): return Database._get_version(self._db) def get_revision (self): - """Returns the committed database revison and UUID + """Returns the committed database revision and UUID - :returns: (revison, uuid) The database revision as a positive integer + :returns: (revision, uuid) The database revision as a positive integer and the UUID of the database. """ self._assert_db_is_initialized() @@ -574,7 +574,7 @@ class Database(object): in the meantime. In this case, you should close and reopen the database and retry. :exc:`NotInitializedError` if - the database was not intitialized. + the database was not initialized. """ self._assert_db_is_initialized() msg_p = NotmuchMessageP() @@ -600,7 +600,7 @@ class Database(object): case, you should close and reopen the database and retry. :raises: :exc:`NotInitializedError` if the database was not - intitialized. + initialized. *Added in notmuch 0.9*""" self._assert_db_is_initialized() @@ -616,7 +616,7 @@ class Database(object): """Returns :class:`Tags` with a list of all tags found in the database :returns: :class:`Tags` - :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER + :exception: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER on error """ self._assert_db_is_initialized() diff --git a/bindings/python/notmuch/message.py b/bindings/python/notmuch/message.py index 6e32b5f7..e71dbe3e 100644 --- a/bindings/python/notmuch/message.py +++ b/bindings/python/notmuch/message.py @@ -46,7 +46,7 @@ import sys class Message(Python3StringMixIn): - """Represents a single Email message + r"""Represents a single Email message Technically, this wraps the underlying *notmuch_message_t* structure. A user will usually not create these objects themselves diff --git a/bindings/python/notmuch/messages.py b/bindings/python/notmuch/messages.py index cae5da50..3801c666 100644 --- a/bindings/python/notmuch/messages.py +++ b/bindings/python/notmuch/messages.py @@ -32,7 +32,7 @@ from .tag import Tags from .message import Message class Messages(object): - """Represents a list of notmuch messages + 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 diff --git a/bindings/python/notmuch/query.py b/bindings/python/notmuch/query.py index cc70e2aa..ffb86df1 100644 --- a/bindings/python/notmuch/query.py +++ b/bindings/python/notmuch/query.py @@ -95,7 +95,7 @@ class Query(object): :exc:`NullPointerError` if the query creation failed (e.g. too little memory). :exc:`NotInitializedError` if the underlying db was not - intitialized. + initialized. """ db._assert_db_is_initialized() # create reference to parent db to keep it alive @@ -140,7 +140,7 @@ class Query(object): _search_threads.restype = c_uint def search_threads(self): - """Execute a query for threads + 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 diff --git a/bindings/python/notmuch/version.py b/bindings/python/notmuch/version.py index e688b565..0d8fdd04 100644 --- a/bindings/python/notmuch/version.py +++ b/bindings/python/notmuch/version.py @@ -1,3 +1,3 @@ # this file should be kept in sync with ../../../version -__VERSION__ = '0.29.3' +__VERSION__ = '0.31.2' SOVERSION = '5' diff --git a/bindings/ruby/message.c b/bindings/ruby/message.c index c55cf6e2..6ea82afa 100644 --- a/bindings/ruby/message.c +++ b/bindings/ruby/message.c @@ -137,13 +137,18 @@ 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"); - return notmuch_message_get_flag (message, FIX2INT (flagv)) ? Qtrue : Qfalse; + status = notmuch_message_get_flag_st (message, FIX2INT (flagv), &is_set); + notmuch_rb_status_raise (status); + + return is_set ? Qtrue : Qfalse; } /* diff --git a/command-line-arguments.c b/command-line-arguments.c index d64aa85b..169b12a3 100644 --- a/command-line-arguments.c +++ b/command-line-arguments.c @@ -5,16 +5,16 @@ #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_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. -*/ + * 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, @@ -78,15 +78,17 @@ _process_boolean_arg (const notmuch_opt_desc_t *arg_desc, char next, return OPT_FAILED; } - *arg_desc->opt_bool = negate ? !value : value; + *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) { +_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; @@ -102,7 +104,8 @@ _process_int_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg } static opt_handled -_process_string_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str) { +_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); @@ -117,20 +120,22 @@ _process_string_arg (const notmuch_opt_desc_t *arg_desc, char next, const char * } /* Return number of non-NULL opt_* fields in opt_desc. */ -static int _opt_set_count (const notmuch_opt_desc_t *opt_desc) +static int +_opt_set_count (const notmuch_opt_desc_t *opt_desc) { return - !!opt_desc->opt_inherit + - !!opt_desc->opt_bool + - !!opt_desc->opt_int + - !!opt_desc->opt_keyword + - !!opt_desc->opt_flags + - !!opt_desc->opt_string + - !!opt_desc->opt_position; + (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) +static bool +_opt_valid (const notmuch_opt_desc_t *opt_desc) { int n = _opt_set_count (opt_desc); @@ -142,15 +147,17 @@ static bool _opt_valid (const notmuch_opt_desc_t *opt_desc) } /* - Search for the {pos_arg_index}th position argument, return false if - that does not exist. -*/ + * 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) { + 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) { @@ -176,12 +183,12 @@ parse_position_arg (const char *arg_str, int pos_arg_index, int parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index) { - assert(argv); + assert (argv); const char *_arg = argv[opt_index]; - assert(_arg); - assert(options); + assert (_arg); + assert (options); const char *arg = _arg + 2; /* _arg starts with -- */ const char *negative_arg = NULL; @@ -239,7 +246,7 @@ parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_ if (lookahead) { next = ' '; value = next_arg; - opt_index ++; + opt_index++; } opt_handled opt_status = OPT_FAILED; @@ -258,12 +265,12 @@ parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_ return -1; if (lookahead && opt_status == OPT_GIVEBACK) - opt_index --; + opt_index--; if (try->present) *try->present = true; - return opt_index+1; + return opt_index + 1; } return -1; } @@ -271,13 +278,14 @@ parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_ /* See command-line-arguments.h for description */ int parse_arguments (int argc, char **argv, - const notmuch_opt_desc_t *options, int opt_index) { + 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) { + if (strncmp (argv[opt_index], "--", 2) != 0) { more_args = parse_position_arg (argv[opt_index], pos_arg_index, options); @@ -290,7 +298,7 @@ parse_arguments (int argc, char **argv, int prev_opt_index = opt_index; if (strlen (argv[opt_index]) == 2) - return opt_index+1; + return opt_index + 1; opt_index = parse_option (argc, argv, options, opt_index); if (opt_index < 0) { diff --git a/command-line-arguments.h b/command-line-arguments.h index f722f97d..606e5cd0 100644 --- a/command-line-arguments.h +++ b/command-line-arguments.h @@ -45,20 +45,20 @@ typedef struct notmuch_opt_desc { /* - 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. - -*/ + * 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); @@ -71,12 +71,12 @@ parse_arguments (int argc, char **argv, const notmuch_opt_desc_t *options, int o */ int -parse_option (int argc, char **argv, const notmuch_opt_desc_t* options, int opt_index); +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); + const notmuch_opt_desc_t *options); #endif diff --git a/compat/Makefile.local b/compat/Makefile.local index bcb9f0ec..2ee1b399 100644 --- a/compat/Makefile.local +++ b/compat/Makefile.local @@ -1,4 +1,4 @@ -# -*- makefile -*- +# -*- makefile-gmake -*- dir := compat extra_cflags += -I$(srcdir)/$(dir) diff --git a/compat/canonicalize_file_name.c b/compat/canonicalize_file_name.c index e92c0f62..000f9e78 100644 --- a/compat/canonicalize_file_name.c +++ b/compat/canonicalize_file_name.c @@ -4,10 +4,10 @@ #include char * -canonicalize_file_name (const char * path) +canonicalize_file_name (const char *path) { #ifdef PATH_MAX - char *resolved_path = malloc (PATH_MAX+1); + char *resolved_path = malloc (PATH_MAX + 1); if (resolved_path == NULL) return NULL; diff --git a/compat/check_asctime.c b/compat/check_asctime.c index b0e56f0c..62ad69d6 100644 --- a/compat/check_asctime.c +++ b/compat/check_asctime.c @@ -1,7 +1,8 @@ #include #include -int main() +int +main () { struct tm tm; diff --git a/compat/check_getpwuid.c b/compat/check_getpwuid.c index c435eb89..babeb742 100644 --- a/compat/check_getpwuid.c +++ b/compat/check_getpwuid.c @@ -1,7 +1,8 @@ #include #include -int main() +int +main () { struct passwd passwd, *ignored; diff --git a/compat/compat.h b/compat/compat.h index 88bc4df4..8f15e585 100644 --- a/compat/compat.h +++ b/compat/compat.h @@ -30,14 +30,14 @@ extern "C" { #endif -#if !STD_GETPWUID +#if ! STD_GETPWUID #define _POSIX_PTHREAD_SEMANTICS 1 #endif -#if !STD_ASCTIME +#if ! STD_ASCTIME #define _POSIX_PTHREAD_SEMANTICS 1 #endif -#if !HAVE_CANONICALIZE_FILE_NAME +#if ! HAVE_CANONICALIZE_FILE_NAME /* we only call this function from C, and this makes testing easier */ #ifndef __cplusplus char * @@ -45,7 +45,7 @@ canonicalize_file_name (const char *path); #endif #endif -#if !HAVE_GETLINE +#if ! HAVE_GETLINE #include #include @@ -55,31 +55,31 @@ getline (char **lineptr, size_t *n, FILE *stream); ssize_t getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp); -#endif /* !HAVE_GETLINE */ +#endif /* !HAVE_GETLINE */ -#if !HAVE_STRCASESTR -char* strcasestr(const char *haystack, const char *needle); -#endif /* !HAVE_STRCASESTR */ +#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_STRSEP +char *strsep (char **stringp, const char *delim); +#endif /* !HAVE_STRSEP */ -#if !HAVE_TIMEGM +#if ! HAVE_TIMEGM #include time_t timegm (struct tm *tm); -#endif /* !HAVE_TIMEGM */ +#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); }) +#define IGNORE_RESULT(x) ({ __typeof__(x) __z = (x); (void) (__z = __z); }) #else /* !__GNUC__ */ #define IGNORE_RESULT(x) x -#endif /* __GNUC__ */ +#endif /* __GNUC__ */ #ifdef __cplusplus } #endif -#endif /* NOTMUCH_COMPAT_H */ +#endif /* NOTMUCH_COMPAT_H */ diff --git a/compat/function-attributes.h b/compat/function-attributes.h index 1945b5bf..8f08bec4 100644 --- a/compat/function-attributes.h +++ b/compat/function-attributes.h @@ -35,9 +35,9 @@ * provides support for testing for function attributes. */ #ifndef NORETURN_ATTRIBUTE -#if (__GNUC__ >= 3 || \ - (__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || \ - __has_attribute (noreturn)) +#if (__GNUC__ >= 3 || \ + (__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || \ + __has_attribute (noreturn)) #define NORETURN_ATTRIBUTE __attribute__ ((noreturn)) #else #define NORETURN_ATTRIBUTE diff --git a/compat/gen_zlib_pc.c b/compat/gen_zlib_pc.c index 198a727c..7c0ee727 100644 --- a/compat/gen_zlib_pc.c +++ b/compat/gen_zlib_pc.c @@ -2,17 +2,18 @@ #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"; + "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) +int +main (void) { - printf(template, ZLIB_VERSION); - return 0; + printf (template, ZLIB_VERSION); + return 0; } diff --git a/compat/getdelim.c b/compat/getdelim.c index 407f3d07..e5c1f07c 100644 --- a/compat/getdelim.c +++ b/compat/getdelim.c @@ -1,21 +1,21 @@ /* 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. */ + * 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. */ @@ -34,100 +34,92 @@ #if USE_UNLOCKED_IO # include "unlocked-io.h" -# define getc_maybe_unlocked(fp) getc(fp) -#elif !HAVE_FLOCKFILE || !HAVE_FUNLOCKFILE || !HAVE_DECL_GETC_UNLOCKED +# 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) +# define getc_maybe_unlocked(fp) getc (fp) #else -# define getc_maybe_unlocked(fp) getc_unlocked(fp) +# 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. */ + * 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; + ssize_t result = -1; + size_t cur_len = 0; - if (lineptr == NULL || n == NULL || fp == NULL) - { - errno = EINVAL; - return -1; + 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; + 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; + *lineptr = new_lineptr; } - for (;;) - { - int i; + for (;;) { + int i; - i = getc_maybe_unlocked (fp); - if (i == EOF) - { - result = -1; - break; + 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; + /* 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; + new_lineptr = (char *) realloc (*lineptr, needed); + if (new_lineptr == NULL) { + result = -1; + goto unlock_return; } - *lineptr = new_lineptr; - *n = needed; + *lineptr = new_lineptr; + *n = needed; } - (*lineptr)[cur_len] = i; - cur_len++; + (*lineptr)[cur_len] = i; + cur_len++; - if (i == delimiter) - break; + if (i == delimiter) + break; } - (*lineptr)[cur_len] = '\0'; - result = cur_len ? (ssize_t) cur_len : result; + (*lineptr)[cur_len] = '\0'; + result = cur_len ? (ssize_t) cur_len : result; - unlock_return: - funlockfile (fp); /* doesn't set errno */ + unlock_return: + funlockfile (fp); /* doesn't set errno */ - return result; + return result; } diff --git a/compat/getline.c b/compat/getline.c index 222e0f6c..2fcaba14 100644 --- a/compat/getline.c +++ b/compat/getline.c @@ -1,20 +1,20 @@ /* 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. */ + * 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. */ @@ -25,5 +25,5 @@ ssize_t getline (char **lineptr, size_t *n, FILE *stream) { - return getdelim (lineptr, n, '\n', stream); + return getdelim (lineptr, n, '\n', stream); } diff --git a/compat/have_canonicalize_file_name.c b/compat/have_canonicalize_file_name.c index 24c848ec..e5609793 100644 --- a/compat/have_canonicalize_file_name.c +++ b/compat/have_canonicalize_file_name.c @@ -1,7 +1,8 @@ #define _GNU_SOURCE #include -int main() +int +main () { char *found; char *string; diff --git a/compat/have_d_type.c b/compat/have_d_type.c index 9ca6c6e0..5338ee4d 100644 --- a/compat/have_d_type.c +++ b/compat/have_d_type.c @@ -1,6 +1,7 @@ #include -int main() +int +main () { struct dirent ent; diff --git a/compat/have_getline.c b/compat/have_getline.c index a8bcd17e..6952a3b3 100644 --- a/compat/have_getline.c +++ b/compat/have_getline.c @@ -2,12 +2,13 @@ #include #include -int main() +int +main () { ssize_t count = 0; size_t n = 0; char **lineptr = NULL; FILE *stream = NULL; - count = getline(lineptr, &n, stream); + count = getline (lineptr, &n, stream); } diff --git a/compat/have_strcasestr.c b/compat/have_strcasestr.c index c0fb7629..3cd1838d 100644 --- a/compat/have_strcasestr.c +++ b/compat/have_strcasestr.c @@ -1,10 +1,11 @@ #define _GNU_SOURCE #include -int main() +int +main () { char *found; const char *haystack, *needle; - found = strcasestr(haystack, needle); + found = strcasestr (haystack, needle); } diff --git a/compat/have_strsep.c b/compat/have_strsep.c index 2abab819..dd4aae75 100644 --- a/compat/have_strsep.c +++ b/compat/have_strsep.c @@ -1,11 +1,12 @@ #define _GNU_SOURCE #include -int main() +int +main () { char *found; char **stringp; const char *delim; - found = strsep(stringp, delim); + found = strsep (stringp, delim); } diff --git a/compat/have_timegm.c b/compat/have_timegm.c index 483fc3b6..8f7b380e 100644 --- a/compat/have_timegm.c +++ b/compat/have_timegm.c @@ -1,6 +1,7 @@ #include -int main() +int +main () { - return (int) timegm((struct tm *)0); + return (int) timegm ((struct tm *) 0); } diff --git a/compat/strcasestr.c b/compat/strcasestr.c index 62a3a545..d4480bf3 100644 --- a/compat/strcasestr.c +++ b/compat/strcasestr.c @@ -3,20 +3,20 @@ * 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. */ + * + * 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. */ @@ -24,17 +24,19 @@ #include "compat.h" -char *strcasestr(const char *haystack, const char *needle) +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; + 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--; - } + haystack++; + hay_len--; + } - return NULL; + return NULL; } diff --git a/compat/strsep.c b/compat/strsep.c index 78ab9e71..4b6926d9 100644 --- a/compat/strsep.c +++ b/compat/strsep.c @@ -1,65 +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. */ + * 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 * +strsep (char **stringp, const char *delim) { - char *begin, *end; + char *begin, *end; - begin = *stringp; - if (begin == NULL) - return NULL; + 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]; + /* 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') + if (ch == '\0') end = NULL; - else - end = strchr (begin + 1, ch); + 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; + } 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 index 3560c370..005a4239 100644 --- a/compat/timegm.c +++ b/compat/timegm.c @@ -1,19 +1,19 @@ /* 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. */ + * + * 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. */ @@ -35,20 +35,20 @@ leapyear (int year) time_t timegm (struct tm *tm) { - int monthlen[2][12] = { + 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; + int year, month, days; days = 365 * (tm->tm_year - 70); for (year = 70; year < tm->tm_year; year++) { - if (leapyear(1900 + year)) { + if (leapyear (1900 + year)) { days++; } } for (month = 0; month < tm->tm_mon; month++) { - days += monthlen[leapyear(1900 + year)][month]; + days += monthlen[leapyear (1900 + year)][month]; } days += tm->tm_mday - 1; diff --git a/completion/Makefile.local b/completion/Makefile.local index 8e86c9d2..54df463c 100644 --- a/completion/Makefile.local +++ b/completion/Makefile.local @@ -1,4 +1,4 @@ -# -*- makefile -*- +# -*- makefile-gmake -*- dir := completion diff --git a/configure b/configure index 6e73b22e..cfa9c09b 100755 --- a/configure +++ b/configure @@ -26,8 +26,29 @@ readonly DEFAULT_IFS="$IFS" 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 test/test-databases" +subdirs="${subdirs} performance-test test" subdirs="${subdirs} bindings" # For a non-srcdir configure invocation (such as ../configure), create @@ -49,6 +70,14 @@ if [ "$srcdir" != "." ]; then 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/ fi # Set several defaults (optionally specified by the user in @@ -79,6 +108,7 @@ PREFIX=/usr/local LIBDIR= WITH_DOCS=1 WITH_API_DOCS=1 +WITH_PYTHON_DOCS=1 WITH_EMACS=1 WITH_DESKTOP=1 WITH_BASH=1 @@ -147,7 +177,7 @@ Fine tuning of some installation directories is available: --emacslispdir=DIR Emacs code [PREFIX/share/emacs/site-lisp] --emacsetcdir=DIR Emacs miscellaneous files [PREFIX/share/emacs/site-lisp] --bashcompletiondir=DIR Bash completions files [PREFIX/share/bash-completion/completions] - --zshcompletiondir=DIR Zsh completions files [PREFIX/share/zsh/functions/Completion/Unix] + --zshcompletiondir=DIR Zsh completions files [PREFIX/share/zsh/site-functions] Some features can be disabled (--with-feature=no is equivalent to --without-feature) : @@ -401,15 +431,22 @@ else have_pkg_config=0 fi -printf "Checking for Xapian development files... " + + +printf "Checking for Xapian development files (>= 1.4.0)... " have_xapian=0 -for xapian_config in ${XAPIAN_CONFIG} xapian-config xapian-config-1.3; do +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/.* //') - printf "Yes (%s).\n" ${xapian_version} - have_xapian=1 - xapian_cxxflags=$(${xapian_config} --cxxflags) - xapian_ldflags=$(${xapian_config} --libs) + 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 @@ -418,81 +455,10 @@ if [ ${have_xapian} = "0" ]; then errors=$((errors + 1)) fi -have_xapian_compact=0 -have_xapian_field_processor=0 -if [ ${have_xapian} = "1" ]; then - printf "Checking for Xapian compaction support... " - cat>_compact.cc< -class TestCompactor : public Xapian::Compactor { }; -EOF - if ${CXX} ${CXXFLAGS_for_sh} ${xapian_cxxflags} -c _compact.cc -o _compact.o > /dev/null 2>&1 - then - have_xapian_compact=1 - printf "Yes.\n" - else - printf "No.\n" - errors=$((errors + 1)) - fi - - rm -f _compact.o _compact.cc - - printf "Checking for Xapian FieldProcessor API... " - cat>_field_processor.cc< -class TitleFieldProcessor : public Xapian::FieldProcessor { }; -EOF - if ${CXX} ${CXXFLAGS_for_sh} ${xapian_cxxflags} -c _field_processor.cc -o _field_processor.o > /dev/null 2>&1 - then - have_xapian_field_processor=1 - printf "Yes.\n" - else - printf "No. (optional)\n" - fi - - rm -f _field_processor.o _field_processor.cc - - default_xapian_backend="" - # DB_RETRY_LOCK is only supported on Xapian > 1.3.2 - have_xapian_db_retry_lock=0 - if [ $WITH_RETRY_LOCK = "1" ]; then - printf "Checking for Xapian lock retry support... " - cat>_retry.cc< -int flag = Xapian::DB_RETRY_LOCK; -EOF - if ${CXX} ${CXXFLAGS_for_sh} ${xapian_cxxflags} -c _retry.cc -o _retry.o > /dev/null 2>&1 - then - have_xapian_db_retry_lock=1 - printf "Yes.\n" - else - printf "No. (optional)\n" - fi - rm -f _retry.o _retry.cc - fi - - printf "Testing default Xapian backend... " - cat >_default_backend.cc < -int main(int argc, char** argv) { - Xapian::WritableDatabase db("test.db",Xapian::DB_CREATE_OR_OPEN); -} -EOF - ${CXX} ${CXXFLAGS_for_sh} ${xapian_cxxflags} _default_backend.cc -o _default_backend ${xapian_ldflags} - ./_default_backend - if [ -f test.db/iamglass ]; then - default_xapian_backend=glass - else - default_xapian_backend=chert - fi - printf "%s\n" "${default_xapian_backend}"; - rm -rf test.db _default_backend _default_backend.cc -fi - GMIME_MINVER=3.0.3 -printf "Checking for GMime development files... " -if pkg-config --exists "gmime-3.0 > $GMIME_MINVER"; then +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) @@ -513,11 +479,11 @@ int main () { g_mime_init (); parser = g_mime_parser_new (); - g_mime_parser_init_with_stream (parser, g_mime_stream_file_open("test/corpora/crypto/basic-encrypted.eml", "r", &error)); + 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"); + 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"); @@ -533,7 +499,7 @@ EOF printf 'No.\nCould not make tempdir for testing session-key support.\n' errors=$((errors + 1)) elif ${CC} ${CFLAGS} ${gmime_cflags} _check_session_keys.c ${gmime_ldflags} -o _check_session_keys \ - && GNUPGHOME=${TEMP_GPG} gpg --batch --quiet --import < test/gnupg-secret-key.asc \ + && GNUPGHOME=${TEMP_GPG} gpg --batch --quiet --import < "$srcdir"/test/gnupg-secret-key.asc \ && SESSION_KEY=$(GNUPGHOME=${TEMP_GPG} ./_check_session_keys) \ && [ $SESSION_KEY = 9:0BACD64099D1468AB07C796F0C0AC4851948A658A15B34E803865E9FC635F2F5 ] then @@ -559,6 +525,154 @@ EOF if [ -n "$TEMP_GPG" -a -d "$TEMP_GPG" ]; then rm -rf "$TEMP_GPG" fi + + # see https://github.com/jstedfast/gmime/pull/90 + # should be fixed in GMime in 3.2.7, but some distros might patch + printf "Checking for GMime X.509 certificate validity... " + + cat > _check_x509_validity.c < +#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; + GMimeValidity validity = GMIME_VALIDITY_UNKNOWN; + 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"); + 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); + + return 0; +} +EOF + if ! TEMP_GPG=$(mktemp -d "${TMPDIR:-/tmp}/notmuch.XXXXXX"); then + printf 'No.\nCould not make tempdir for testing X.509 certificate validity support.\n' + errors=$((errors + 1)) + elif ${CC} ${CFLAGS} ${gmime_cflags} _check_x509_validity.c ${gmime_ldflags} -o _check_x509_validity \ + && echo disable-crl-checks > "$TEMP_GPG/gpgsm.conf" \ + && echo "4D:E0:FF:63:C0:E9:EC:01:29:11:C8:7A:EE:DA:3A:9A:7F:6E:C1:0D S" >> "$TEMP_GPG/trustlist.txt" \ + && GNUPGHOME=${TEMP_GPG} gpgsm --batch --quiet --import < "$srcdir"/test/smime/ca.crt + 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:13607E4217515A70EC8DF9DBC16C5327B94577561D98AD1246FA8756659C7899", &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/gnupg-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 </dev/null 2>&1 && compat/gen_zlib_pc > compat/zlib.pc && - PKG_CONFIG_PATH="$PKG_CONFIG_PATH":compat && + PKG_CONFIG_PATH=${PKG_CONFIG_PATH:+$PKG_CONFIG_PATH:}compat && export PKG_CONFIG_PATH rm -f compat/gen_zlib_pc fi @@ -650,6 +764,43 @@ if [ $have_python -eq 0 ]; then 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_cffi=0 +have_python3_pytest=0 +if [ $have_python3 -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" @@ -677,13 +828,14 @@ if [ -z "${EMACSETCDIR-}" ]; then EMACSETCDIR="\$(prefix)/share/emacs/site-lisp" fi -printf "Checking if emacs (>= 24) is available... " -if emacs --quick --batch --eval '(if (< emacs-major-version 24) (kill-emacs 1))' > /dev/null 2>&1; then - printf "Yes.\n" - have_emacs=1 -else - printf "No (so will not byte-compile emacs code)\n" - have_emacs=0 +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 @@ -823,8 +975,8 @@ EOF if [ $have_python -eq 0 ]; then echo " python interpreter" fi - if [ $have_xapian -eq 0 -o $have_xapian_compact -eq 0 ]; then - echo " Xapian library (>= version 1.2.6, including development files such as headers)" + if [ $have_xapian -eq 0 ]; then + echo " Xapian library (>= version 1.4.0, including development files such as headers)" echo " https://xapian.org/" fi if [ $have_zlib -eq 0 ]; then @@ -859,7 +1011,7 @@ On Debian and similar systems: Or on Fedora and similar systems: - sudo yum install xapian-core-devel gmime-devel libtalloc-devel zlib-devel + sudo dnf install xapian-core-devel gmime30-devel libtalloc-devel zlib-devel On other systems, similar commands can be used, but the details of the package names may be different. @@ -874,7 +1026,7 @@ to install pkg-config with a command such as: sudo apt-get install pkg-config Or: - sudo yum install pkgconfig + sudo dnf install pkgconfig But if pkg-config is not available for your system, then you will need to modify the configure script to manually set the cflags and ldflags @@ -948,6 +1100,22 @@ else 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 @@ -1031,7 +1199,8 @@ for flag in -Wmissing-declarations; do done printf "\n\t%s\n" "${WARN_CFLAGS}" -rm -f minimal minimal.c _libversion.c _libversion _libversion.sh _check_session_keys.c _check_session_keys +rm -f minimal minimal.c _time_t.c _libversion.c _libversion _libversion.sh _check_session_keys.c _check_session_keys _check_x509_validity.c _check_x509_validity \ + _verify_sig_with_session_key.c _verify_sig_with_session_key # construct the Makefile.config cat > Makefile.config < sh.config < sphinx.config + # Finally, after everything configured, inform the user how to continue. cat < $@ install: all - mkdir -p $(DESTDIR)$(prefix)/bin + 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 -D -m 644 $(NAME).1 $(DESTDIR)$(mandir)/man1/$(NAME).1 - install -D -m 644 $(NAME).rc $(DESTDIR)$(sysconfdir)/Muttrc.d/$(NAME).rc + 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/notmuch-mutt b/contrib/notmuch-mutt/notmuch-mutt index 0e46a8c1..d1e2c084 100755 --- a/contrib/notmuch-mutt/notmuch-mutt +++ b/contrib/notmuch-mutt/notmuch-mutt @@ -12,6 +12,7 @@ use strict; use warnings; use File::Path; +use File::Basename; use Getopt::Long qw(:config no_getopt_compat); use Mail::Header; use Mail::Box::Maildir; @@ -41,16 +42,17 @@ sub search($$$) { my ($maildir, $remove_dups, $query) = @_; my $dup_option = ""; - $query = shell_quote($query); - - if ($remove_dups) { - $dup_option = "--duplicate=1"; - } + my @args = qw/notmuch search --output=files/; + push @args, "--duplicate=1" if $remove_dups; + push @args, $query; empty_maildir($maildir); - system("notmuch search --output=files $dup_option $query" - . " | sed -e 's: :\\\\ :g'" - . " | xargs -r -I searchoutput ln -s searchoutput $maildir/cur/"); + 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($$) { diff --git a/debian/changelog b/debian/changelog index 9273bbb4..63aaaa8a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,100 @@ +notmuch (0.31.2-3) unstable; urgency=medium + + * Switch to debhelper compat level 13 + + -- David Bremner Mon, 09 Nov 2020 13:59:47 -0400 + +notmuch (0.31.2-2) unstable; urgency=medium + + * Run tests in verbose mode + + -- David Bremner Mon, 09 Nov 2020 08:45:38 -0400 + +notmuch (0.31.2-1) unstable; urgency=medium + + * Delete stray "version" file in upstream source + + -- David Bremner Sun, 08 Nov 2020 11:32:45 -0400 + +notmuch (0.31.1-1) unstable; urgency=medium + + * New upstream bugfix release. + - Portability / C++20 fixes + - Fix initialization bug in library config handling. + + -- David Bremner Sun, 08 Nov 2020 07:48:22 -0400 + +notmuch (0.31-1) unstable; urgency=medium + + * New upstream release + * Compatibility fixes for Emacs 27.1 + + -- David Bremner Sat, 05 Sep 2020 21:47:42 -0300 + +notmuch (0.31~rc2-1) experimental; urgency=medium + + * New upstream release candidate + * Bug fix: "suggest elpa-mailscripts", thanks to Sean Whitton (Closes: + #944269). + * Bug fix: "suggest mailscripts", thanks to Sean Whitton (Closes: + #944270). + * Bug fix: "please drop transitional package notmuch-emacs from + src:notmuch", thanks to Holger Levsen (Closes: #940738). + + -- David Bremner Tue, 25 Aug 2020 07:51:33 -0300 + +notmuch (0.31~rc1-1) experimental; urgency=medium + + * Fix buggy test in T562-lib-database + * Clean up generated file in source package. + + -- David Bremner Mon, 17 Aug 2020 21:05:46 -0300 + +notmuch (0.31~rc0-1) experimental; urgency=medium + + * New upstream release candidate. + * Update notmuch-emacs for compatibility with GNU Emacs 27.1. + + -- David Bremner Sun, 16 Aug 2020 11:08:14 -0300 + +notmuch (0.30-1) unstable; urgency=medium + + * New upstream release + * Improvements to S/MIME handling + * Repairs to some mangled MIME messages + * New python bindings (notmuch2) compatible with current python 3 + + -- David Bremner Fri, 10 Jul 2020 22:24:14 -0300 + +notmuch (0.30~rc3-1) experimental; urgency=medium + + * New upstream release candidate + * Mark two tests broken on legacy (32 bit time_t) architectures. + * Drop -std=c99 + + -- David Bremner Fri, 03 Jul 2020 06:48:51 -0300 + +notmuch (0.30~rc2-1) experimental; urgency=medium + + * New upstream release candidate. + * Upstream fixes for new python bindings (python3-notmuch2). + * Update debian/copyright (one new author). + + -- David Bremner Tue, 16 Jun 2020 08:32:16 -0300 + +notmuch (0.30~rc1-1) experimental; urgency=medium + + * New upstream release candidate + * Update debian/changelog (new copyright holders) + + -- David Bremner Sat, 06 Jun 2020 08:06:56 -0300 + +notmuch (0.30~rc0-2) experimental; urgency=medium + + * New upstream release candidate + + -- David Bremner Mon, 01 Jun 2020 21:01:27 -0300 + notmuch (0.29.3-1~bpo10+1) buster-backports; urgency=medium * Rebuild for buster-backports. diff --git a/debian/compat b/debian/compat deleted file mode 100644 index b4de3947..00000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -11 diff --git a/debian/control b/debian/control index 84be657d..585ff778 100644 --- a/debian/control +++ b/debian/control @@ -4,39 +4,57 @@ Priority: optional Maintainer: Carl Worth Uploaders: Jameson Graef Rollins , - David Bremner -Build-Conflicts: ruby1.8, gdb-minimal, gdb [ia64 mips mips64el] + David Bremner , +Build-Conflicts: + gdb [ia64 mips mips64el], + gdb-minimal, + ruby1.8, Build-Depends: + bash-completion (>=1.9.0~), + debhelper-compat (= 13), + dh-elpa (>= 1.3), + dh-python, + desktop-file-utils, + doxygen, dpkg-dev (>= 1.17.14), - debhelper (>= 11~), - pkg-config, - libxapian-dev, + dtach (>= 0.8) , + emacs-nox | emacs-gtk | emacs-lucid | emacs25-nox | emacs25 (>=25~) | emacs25-lucid (>=25~) | emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~), + gdb [!ia64 !mips !mips64el !kfreebsd-any !alpha] , + gnupg , + gpgsm , libgmime-3.0-dev (>= 3.0.3~), + libpython3-all-dev, libtalloc-dev, + libxapian-dev, libz-dev, + pkg-config, python3-all (>= 3.1.2-7~), - dh-python, - dh-elpa (>= 1.3), + python3-cffi, + python3-pytest, + python3-pytest-cov, + python3-setuptools, python3-sphinx, - ruby, ruby-dev (>>1:1.9.3~), - emacs-nox | emacs-gtk | emacs-lucid | - emacs25-nox | emacs25 (>=25~) | emacs25-lucid (>=25~) | - emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~), - gdb [!ia64 !mips !mips64el !kfreebsd-any !alpha] , - dtach (>= 0.8) , - gpgsm , - gnupg , - bash-completion (>=1.9.0~), - texinfo -Standards-Version: 4.1.3 + ruby, + ruby-dev (>>1:1.9.3~), + texinfo, +Standards-Version: 4.4.1 Homepage: https://notmuchmail.org/ Vcs-Git: https://git.notmuchmail.org/git/notmuch -b release Vcs-Browser: https://git.notmuchmail.org/git/notmuch +Rules-Requires-Root: no Package: notmuch Architecture: any -Depends: libnotmuch5 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends} -Recommends: elpa-notmuch | notmuch-vim | notmuch-mutt | alot, gnupg-agent, gpgsm +Depends: + libnotmuch5 (= ${binary:Version}), + ${misc:Depends}, + ${shlibs:Depends}, +Recommends: + elpa-notmuch | notmuch-vim | notmuch-mutt | alot, + gnupg-agent, + gpgsm, +Suggests: + mailscripts Description: thread-based email index, search and tagging Notmuch is a system for indexing, searching, reading, and tagging large collections of email messages in maildir or mh format. It uses @@ -48,8 +66,11 @@ Description: thread-based email index, search and tagging Package: libnotmuch5 Section: libs Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends} -Pre-Depends: ${misc:Pre-Depends} +Depends: + ${misc:Depends}, + ${shlibs:Depends}, +Pre-Depends: + ${misc:Pre-Depends}, Description: thread-based email index, search and tagging (runtime) Notmuch is a system for indexing, searching, reading, and tagging large collections of email messages in maildir or mh format. It uses @@ -62,7 +83,9 @@ Description: thread-based email index, search and tagging (runtime) Package: libnotmuch-dev Section: libdevel Architecture: any -Depends: ${misc:Depends}, libnotmuch5 (= ${binary:Version}) +Depends: + libnotmuch5 (= ${binary:Version}), + ${misc:Depends}, Description: thread-based email index, search and tagging (development) Notmuch is a system for indexing, searching, reading, and tagging large collections of email messages in maildir or mh format. It uses @@ -75,7 +98,29 @@ Description: thread-based email index, search and tagging (development) Package: python3-notmuch Architecture: all Section: python -Depends: ${misc:Depends}, ${python3:Depends}, libnotmuch5 (>= ${source:Version}) +Depends: + libnotmuch5 (>= ${source:Version}), + ${misc:Depends}, + ${python3:Depends}, +Description: Python 3 legacy interface to the notmuch mail search and index library + Notmuch is a system for indexing, searching, reading, and tagging + large collections of email messages in maildir or mh format. It uses + the Xapian library to provide fast, full-text search with a very + convenient search syntax. + . + This package provides a legacy Python 3 interface to the notmuch + functionality, directly interfacing with a shared notmuch library. + . + New projects are encouraged to use python3-notmuch2 instead. + +Package: python3-notmuch2 +Architecture: any +Section: python +Depends: + libnotmuch5 (>= ${source:Version}), + ${misc:Depends}, + ${python3:Depends}, + ${shlibs:Depends}, Description: Python 3 interface to the notmuch mail search and index library Notmuch is a system for indexing, searching, reading, and tagging large collections of email messages in maildir or mh format. It uses @@ -83,12 +128,17 @@ Description: Python 3 interface to the notmuch mail search and index library convenient search syntax. . This package provides a Python 3 interface to the notmuch - functionality, directly interfacing with a shared notmuch library. + functionality using CFFI bindings, which interface with a shared + notmuch library. + . + This is the preferred way to use notmuch via Python. Package: ruby-notmuch Architecture: any Section: ruby -Depends: ${shlibs:Depends}, ${misc:Depends} +Depends: + ${misc:Depends}, + ${shlibs:Depends}, Description: Ruby interface to the notmuch mail search and index library Notmuch is a system for indexing, searching, reading, and tagging large collections of email messages in maildir or mh format. It uses @@ -98,16 +148,12 @@ Description: Ruby interface to the notmuch mail search and index library This package provides a Ruby interface to the notmuch functionality, directly interfacing with a shared notmuch library. -Package: notmuch-emacs -Section: oldlibs -Architecture: all -Depends: elpa-notmuch, ${misc:Depends} -Description: thread-based email index, search and tagging (transitional package) - This dummy package help ease transition to the new package elpa-notmuch - Package: elpa-notmuch Architecture: all -Depends: ${misc:Depends}, ${elpa:Depends} +Depends: + ${elpa:Depends}, + ${misc:Depends}, +Suggests: elpa-mailscripts Description: thread-based email index, search and tagging (emacs interface) Notmuch is a system for indexing, searching, reading, and tagging large collections of email messages in maildir or mh format. It uses @@ -119,10 +165,18 @@ Description: thread-based email index, search and tagging (emacs interface) Package: notmuch-vim Architecture: all -Breaks: notmuch (<<0.6~254~) -Replaces: notmuch (<<0.6~254~) -Depends: ${misc:Depends}, notmuch, vim-addon-manager, vim-ruby, ruby-notmuch -Recommends: ruby-mail +Breaks: + notmuch (<<0.6~254~), +Replaces: + notmuch (<<0.6~254~), +Depends: + notmuch, + ruby-notmuch, + vim-addon-manager, + vim-ruby, + ${misc:Depends}, +Recommends: + ruby-mail, Description: thread-based email index, search and tagging (vim interface) Notmuch is a system for indexing, searching, reading, and tagging large collections of email messages in maildir or mh format. It uses @@ -135,12 +189,18 @@ Description: thread-based email index, search and tagging (vim interface) Package: notmuch-mutt Architecture: all Depends: + libmail-box-perl, + libmailtools-perl, + libstring-shellquote-perl, + libterm-readline-gnu-perl, notmuch (>= 0.4), - libmail-box-perl, libmailtools-perl, - libstring-shellquote-perl, libterm-readline-gnu-perl, - ${misc:Depends} -Recommends: mutt -Enhances: notmuch, mutt + ${misc:Depends}, + ${perl:Depends}, +Recommends: + mutt, +Enhances: + mutt, + notmuch, Description: thread-based email index, search and tagging (Mutt interface) notmuch-mutt provides integration among the Mutt mail user agent and the Notmuch mail indexer. diff --git a/debian/copyright b/debian/copyright index 0931d9b9..ba221e6b 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,36 +1,100 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: notmuch -Source: git://notmuchmail.org/git/notmuch +Source: https://git.notmuchmail.org/git/notmuch Upstream-Contact: Notmuch Mailing List Files: * -Copyright: Copyright 2009 Carl Worth - Bart Trojanowski - Keith Packard - Alexander Botero-Lowry - Ingmar Vanhassel - Jed Brown - Jan Janak - Chris Wilson - Keith Amidon - Aneesh Kumar K.V - Mikhail Gusarov - Jeffrey C. Ollie - Jameson Graef Rollins - Stewart Smith - Adrian Perez - Kan-Ru Chen - James Rowe - Eric Anholt - Alec Berryman - Tassilo Horn - Stefan Schmidt - Rolland Santimano - Peter Wang - Lars Kellogg-Stedman - Holger Freyther - David Bremner - Alexander Botero-Lowry +Copyright: Copyright 2009-2020 + David Bremner + Carl Worth + Jani Nikula + Austin Clements + Daniel Kahn Gillmor + Mark Walters + Floris Bruynooghe + David Edmondson + Tomi Ollila + Sebastian Spaeth + Ali Polatel + Michal Sojka + Justus Winter + Sebastien Binet + W. Trevor King + Jameson Graef Rollins + Felipe Contreras + Pieter Praet + Peter Feigl + Dmitry Kurochkin + Peter Wang + Daniel Schoepe + Gregor Zattler + Keith Packard + Adam Wolfe Gordon + Stefano Zacchiroli + Vincent Breitmoser + laochailan + Ben Gamari + Aaron Ecay + Jesse Rosenthal + l-m-h@web.de + Thomas Jost + Dirk Hohndel + Blake Jones + Jonas Bernoulli + Damien Cassou + Vladimir Panteleev + Anton Khirnov + Matt Armstrong + Örjan Ekeberg + Jan Janak + Patrick Totzke + Chunyang Xu + rhn + Ruben Pollan + Ioan-Adrian Ratiu + Ethan Glasser-Camp + Todd + Chris Wilson + William Casarin + Yuri Volchkov + Cédric Cabessa + Mark Anderson + Jed Brown + Maxime Coste + Ludovic LANGE + Sebastian Poeplau + Mikhail + Gaute Hope + Keith Amidon + martin f. krafft + Jeffrey C. Ollie + Bart Trojanowski + Jameson Rollins + Scott Henson + Vladimir Marek + Servilio Afre Puentes + Kevin McCarthy + Tomas Carnecky + Kevin J. McCarthy + Scott Robinson + Wael M. Nasreddine + Charles Celerier + Olly Betts + Istvan Marko + Florian Klink + Thibaut Horel + Joel Borggrén-Franck + Ingmar Vanhassel + Olivier Taïbi + Ian Main + Alexander Botero-Lowry + Luis Ressel + Sergei Shilovsky + Trevor Jim + Jinwoo Lee + Uli Scholler + Matthew Lear + Amadeusz Żołnowski License: GPL-3+ Files: debian/* diff --git a/debian/elpa-notmuch.elpa b/debian/elpa-notmuch.elpa index 19e3ba51..4712b73f 100644 --- a/debian/elpa-notmuch.elpa +++ b/debian/elpa-notmuch.elpa @@ -1,3 +1,3 @@ -emacs/*.el -emacs/notmuch-logo.png -debian/tmp/usr/share/info/* +debian/tmp/usr/share/emacs/site-lisp/*.el +debian/tmp/usr/share/emacs/site-lisp/notmuch-logo.png +emacs/notmuch-pkg.el diff --git a/debian/elpa-notmuch.info b/debian/elpa-notmuch.info new file mode 100644 index 00000000..0ac0fbf6 --- /dev/null +++ b/debian/elpa-notmuch.info @@ -0,0 +1 @@ +usr/share/info/*.info diff --git a/debian/elpa-notmuch.lintian-overrides b/debian/elpa-notmuch.lintian-overrides new file mode 100644 index 00000000..aa275eda --- /dev/null +++ b/debian/elpa-notmuch.lintian-overrides @@ -0,0 +1,4 @@ +# elpa-notmuch is an elisp plugin for dealing with e-mail. We can +# already tell from the package name that it is an elisp package, so +# it belongs in Section: mail, and lintian is being too strict here. +elpa-notmuch: wrong-section-according-to-package-name elpa-notmuch => lisp diff --git a/debian/libnotmuch-dev.manpages b/debian/libnotmuch-dev.manpages new file mode 100644 index 00000000..9c4bd5d3 --- /dev/null +++ b/debian/libnotmuch-dev.manpages @@ -0,0 +1 @@ +usr/share/man/man3/notmuch.3.gz diff --git a/debian/libnotmuch5.symbols b/debian/libnotmuch5.symbols index 308567b8..ec01593b 100644 --- a/debian/libnotmuch5.symbols +++ b/debian/libnotmuch5.symbols @@ -1,4 +1,5 @@ libnotmuch.so.5 libnotmuch5 #MINVER# +* Build-Depends-Package: libnotmuch-dev notmuch_built_with@Base 0.23~rc0 notmuch_config_list_destroy@Base 0.23~rc0 notmuch_config_list_key@Base 0.23~rc0 @@ -55,6 +56,7 @@ libnotmuch.so.5 libnotmuch5 #MINVER# notmuch_message_get_filename@Base 0.3 notmuch_message_get_filenames@Base 0.5 notmuch_message_get_flag@Base 0.3 + notmuch_message_get_flag_st@Base 0.31~rc0 notmuch_message_get_header@Base 0.3 notmuch_message_get_message_id@Base 0.3 notmuch_message_get_properties@Base 0.23~rc0 @@ -63,6 +65,7 @@ libnotmuch.so.5 libnotmuch5 #MINVER# notmuch_message_get_tags@Base 0.3 notmuch_message_get_thread_id@Base 0.3 notmuch_message_has_maildir_flag@Base 0.26~rc0 + notmuch_message_has_maildir_flag_st@Base 0.31~rc0 notmuch_message_maildir_flags_to_tags@Base 0.5 notmuch_message_properties_destroy@Base 0.23~rc0 notmuch_message_properties_key@Base 0.23~rc0 @@ -128,8 +131,6 @@ libnotmuch.so.5 libnotmuch5 #MINVER# (c++)"typeinfo for Xapian::DatabaseError@Base" 0.24~rc0 (c++)"typeinfo for Xapian::DatabaseModifiedError@Base" 0.24~rc0 (c++|optional=present with Xapian 1.4)"typeinfo for Xapian::QueryParserError@Base" 0.23~rc0 - (c++)"typeinfo for Xapian::QueryParser::add_valuerangeprocessor(Xapian::ValueRangeProcessor*)::ShimRangeProcessor@Base" 0.25~rc0 - (c++)"typeinfo name for Xapian::QueryParser::add_valuerangeprocessor(Xapian::ValueRangeProcessor*)::ShimRangeProcessor@Base" 0.25~rc0 (c++)"typeinfo name for Xapian::LogicError@Base" 0.6.1 (c++)"typeinfo name for Xapian::RuntimeError@Base" 0.6.1 (c++)"typeinfo name for Xapian::DocNotFoundError@Base" 0.6.1 diff --git a/debian/not-installed b/debian/not-installed new file mode 100644 index 00000000..fd929459 --- /dev/null +++ b/debian/not-installed @@ -0,0 +1,3 @@ +usr/share/applications/mimeinfo.cache +usr/share/info/dir +usr/share/emacs/site-lisp/*.elc diff --git a/debian/notmuch-mutt.install b/debian/notmuch-mutt.install index 9b468bdb..8314f883 100644 --- a/debian/notmuch-mutt.install +++ b/debian/notmuch-mutt.install @@ -1,2 +1,2 @@ -usr/bin/notmuch-mutt etc/Muttrc.d/notmuch-mutt.rc +usr/bin/notmuch-mutt diff --git a/debian/notmuch-vim.dirs b/debian/notmuch-vim.dirs index c6373e42..2b531314 100644 --- a/debian/notmuch-vim.dirs +++ b/debian/notmuch-vim.dirs @@ -1,4 +1,4 @@ -usr/share/vim/registry -usr/share/vim/addons/plugin usr/share/vim/addons/doc +usr/share/vim/addons/plugin usr/share/vim/addons/syntax +usr/share/vim/registry diff --git a/debian/notmuch-vim.install b/debian/notmuch-vim.install index a1af708d..cf898738 100644 --- a/debian/notmuch-vim.install +++ b/debian/notmuch-vim.install @@ -1,4 +1,4 @@ -vim/notmuch.vim usr/share/vim/addons/plugin vim/notmuch.txt usr/share/vim/addons/doc -vim/syntax/notmuch-*.vim usr/share/vim/addons/syntax +vim/notmuch.vim usr/share/vim/addons/plugin vim/notmuch.yaml usr/share/vim/registry +vim/syntax/notmuch-*.vim usr/share/vim/addons/syntax diff --git a/debian/notmuch.install b/debian/notmuch.install index 0cce21bd..60f09712 100644 --- a/debian/notmuch.install +++ b/debian/notmuch.install @@ -1,5 +1,5 @@ usr/bin/notmuch usr/bin/notmuch-emacs-mua +usr/share/applications/notmuch-emacs-mua.desktop usr/share/bash-completion usr/share/zsh/vendor-completions -emacs/notmuch-emacs-mua.desktop usr/share/applications diff --git a/debian/notmuch.manpages b/debian/notmuch.manpages index f9fcb54a..3ac2ff5b 100644 --- a/debian/notmuch.manpages +++ b/debian/notmuch.manpages @@ -1,18 +1,19 @@ -usr/share/man/man5/notmuch-hooks.5.gz -usr/share/man/man1/notmuch-dump.1.gz -usr/share/man/man1/notmuch-count.1.gz +usr/share/man/man1/notmuch-address.1.gz usr/share/man/man1/notmuch-compact.1.gz +usr/share/man/man1/notmuch-config.1.gz +usr/share/man/man1/notmuch-count.1.gz +usr/share/man/man1/notmuch-dump.1.gz usr/share/man/man1/notmuch-emacs-mua.1.gz +usr/share/man/man1/notmuch-insert.1.gz usr/share/man/man1/notmuch-new.1.gz -usr/share/man/man1/notmuch.1.gz usr/share/man/man1/notmuch-reindex.1.gz -usr/share/man/man1/notmuch-address.1.gz -usr/share/man/man1/notmuch-tag.1.gz usr/share/man/man1/notmuch-reply.1.gz -usr/share/man/man1/notmuch-search.1.gz usr/share/man/man1/notmuch-restore.1.gz -usr/share/man/man1/notmuch-insert.1.gz +usr/share/man/man1/notmuch-search.1.gz +usr/share/man/man1/notmuch-setup.1.gz usr/share/man/man1/notmuch-show.1.gz -usr/share/man/man1/notmuch-config.1.gz +usr/share/man/man1/notmuch-tag.1.gz +usr/share/man/man1/notmuch.1.gz +usr/share/man/man5/notmuch-hooks.5.gz usr/share/man/man7/notmuch-properties.7.gz usr/share/man/man7/notmuch-search-terms.7.gz diff --git a/debian/python-notmuch.install b/debian/python-notmuch.install deleted file mode 100644 index b2cc1360..00000000 --- a/debian/python-notmuch.install +++ /dev/null @@ -1 +0,0 @@ -usr/lib/python2* diff --git a/debian/rules b/debian/rules index 5a378f6e..fa0551a9 100755 --- a/debian/rules +++ b/debian/rules @@ -1,6 +1,6 @@ #!/usr/bin/make -f -export PYBUILD_NAME=notmuch +export DEB_BUILD_MAINT_OPTIONS = hardening=+all %: dh $@ --with python3,elpa @@ -15,19 +15,25 @@ override_dh_auto_configure: --zshcompletiondir=/usr/share/zsh/vendor-completions \ --localstatedir=/var +override_dh_auto_test: + dh_auto_test -- V=1 + override_dh_auto_build: dh_auto_build -- V=1 - dh_auto_build --buildsystem=pybuild --sourcedirectory bindings/python + PYBUILD_NAME=notmuch dh_auto_build --buildsystem=pybuild --sourcedirectory bindings/python + PYBUILD_NAME=notmuch2 dh_auto_build --buildsystem=pybuild --sourcedirectory bindings/python-cffi $(MAKE) -C contrib/notmuch-mutt override_dh_auto_clean: dh_auto_clean - dh_auto_clean --buildsystem=pybuild --sourcedirectory bindings/python + PYBUILD_NAME=notmuch dh_auto_clean --buildsystem=pybuild --sourcedirectory bindings/python + PYBUILD_NAME=notmuch2 dh_auto_clean --buildsystem=pybuild --sourcedirectory bindings/python-cffi dh_auto_clean --sourcedirectory bindings/ruby $(MAKE) -C contrib/notmuch-mutt clean override_dh_auto_install: dh_auto_install - dh_auto_install --buildsystem=pybuild --sourcedirectory bindings/python + PYBUILD_NAME=notmuch dh_auto_install --buildsystem=pybuild --sourcedirectory bindings/python + PYBUILD_NAME=notmuch2 dh_auto_install --buildsystem=pybuild --sourcedirectory bindings/python-cffi $(MAKE) -C contrib/notmuch-mutt DESTDIR=$(CURDIR)/debian/tmp install dh_auto_install --sourcedirectory bindings/ruby diff --git a/debian/upstream/metadata b/debian/upstream/metadata new file mode 100644 index 00000000..8f266aa8 --- /dev/null +++ b/debian/upstream/metadata @@ -0,0 +1,6 @@ +Bug-Database: https://nmbug.notmuchmail.org/status/ +Bug-Submit: mailto:notmuch@notmuchmail.org +FAQ: https://notmuchmail.org/faq/ +Repository: https://git.notmuchmail.org/git/notmuch +Repository-Browse: https://git.notmuchmail.org/git/notmuch +Screenshots: https://notmuchmail.org/screenshots/ diff --git a/debugger.c b/debugger.c index 0febf170..5f47a1d7 100644 --- a/debugger.c +++ b/debugger.c @@ -39,8 +39,7 @@ debugger_is_active (void) sprintf (buf, "/proc/%d/exe", getppid ()); if (readlink (buf, buf2, sizeof (buf2)) != -1 && - strncmp (basename (buf2), "gdb", 3) == 0) - { + strncmp (basename (buf2), "gdb", 3) == 0) { return true; } diff --git a/devel/STYLE b/devel/STYLE index da653124..b18a9573 100644 --- a/devel/STYLE +++ b/devel/STYLE @@ -53,11 +53,19 @@ function (param_type param, param_type param) if/for/while test) and are preceded by a space. The opening brace of functions is the exception, and starts on a new line. -* Comments are always C-style /* */ block comments. They should start - with a capital letter and generally be written in complete - sentences. Public library functions are documented immediately - before their prototype in lib/notmuch.h. Internal functions are - typically documented immediately before their definition. +* Opening parens also cuddle, even if the first argument does not fit + on the same line. + +* Ternary operators that span a line should be parenthesized like as + "a ? (\n b ) : c". This is mainly to keep the indentation tools + happy. + +* Comments are always C-style /* */ block comments, with a leading * + each line. They should start with a capital letter and generally be + written in complete sentences. Public library functions are + documented immediately before their prototype in lib/notmuch.h. + Internal functions are typically documented immediately before their + definition. * Code lines should be less than 80 columns and comments should be wrapped at 70 columns. diff --git a/devel/author-scan.sh b/devel/author-scan.sh new file mode 100644 index 00000000..2d9c4af8 --- /dev/null +++ b/devel/author-scan.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +FILE_EXCLUDE='corpora' +AUTHOR_EXCLUDE='uncrustify' +# based on the FSF guideline, for want of a better idea. +THRESHOLD=15 + +git ls-files | grep -v -e "$FILE_EXCLUDE" | xargs -n 1 -d \\n \ + git blame -w --line-porcelain -- | \ + sed -n "/$AUTHOR_EXCLUDE/d; s/^[aA][uU][tT][hH][Oo][rR] //p" | \ + sort -fd | uniq -ic | awk "\$1 >= $THRESHOLD" | sort -nr diff --git a/devel/emacs-keybindings.org b/devel/emacs-keybindings.org index 464b9467..65dfe0eb 100644 --- a/devel/emacs-keybindings.org +++ b/devel/emacs-keybindings.org @@ -20,7 +20,7 @@ | q | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | | r | notmuch-search-reply-to-thread-sender | notmuch-show-reply-sender | notmuch-show-reply-sender | | s | notmuch-search | notmuch-search | notmuch-search | -| t | notmuch-search-filter-by-tag | toggle-truncate-lines | | +| t | notmuch-search-filter-by-tag | toggle-truncate-lines | notmuch-search-by-tag | | u | | | | | v | | | notmuch-show-view-all-mime-parts | | w | | notmuch-show-save-attachments | notmuch-show-save-attachments | diff --git a/devel/gen-testdb.sh b/devel/gen-testdb.sh deleted file mode 100755 index 61ae48a3..00000000 --- a/devel/gen-testdb.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash -# -# NAME -# gen-testdb.sh - generate test databases -# -# SYNOPSIS -# gen-testdb.sh -v NOTMUCH-VERSION [-c CORPUS-PATH] [-s TAR-SUFFIX] -# -# DESCRIPTION -# Generate a tarball containing the specified test corpus and -# the corresponding notmuch database, indexed using a specific -# version of notmuch, resulting in a specific version of the -# database. -# -# The specific version of notmuch will be built on the fly. -# Therefore the script must be run within a git repository to be -# able to build the old versions of notmuch. -# -# This script reuses the test infrastructure, and the script -# must be run from within the test directory. -# -# The output tarballs, named database-.tar.gz, are -# placed in the test/test-databases directory. -# -# OPTIONS -# -v NOTMUCH-VERSION -# Notmuch version in terms of a git tag or commit to use -# for generating the database. Required. -# -# -c CORPUS-PATH -# Path to a corpus to use for generating the -# database. Due to CWD changes within the test -# infrastructure, use absolute paths. Defaults to the -# test corpus. -# -# -s TAR-SUFFIX -# Suffix for the tarball basename. Empty by default. -# -# EXAMPLE -# -# Generate a database indexed with notmuch 0.17. Use the default -# test corpus. Name the tarball database-v1.tar.gz to reflect -# the fact that notmuch 0.17 used database version 1. -# -# $ cd test -# $ ../devel/gen-testdb.sh -v 0.17 -s v1 -# -# CAVEATS -# Test infrastructure options won't work. -# -# Any existing databases with the same name will be overwritten. -# -# It may not be possible to build old versions of notmuch with -# the set of dependencies that satisfy building the current -# version of notmuch. -# -# AUTHOR -# Jani Nikula -# -# LICENSE -# Same as notmuch test infrastructure (GPLv2+). -# - -test_description="database generation abusing test infrastructure" - -# immediate exit on subtest failure; see test_failure_ in test-lib.sh -immediate=t - -VERSION= -CORPUS= -SUFFIX= - -while getopts v:c:s: opt; do - case "$opt" in - v) VERSION="$OPTARG";; - c) CORPUS="$OPTARG";; - s) SUFFIX="-$OPTARG";; - esac -done -shift `expr $OPTIND - 1` - -. ./test-lib.sh || exit 1 - -SHORT_CORPUS=$(basename ${CORPUS:-database}) -DBNAME=${SHORT_CORPUS}${SUFFIX} -TARBALLNAME=${DBNAME}.tar.xz - -CORPUS=${CORPUS:-${TEST_DIRECTORY}/corpus} - -test_expect_code 0 "notmuch version specified on the command line" \ - "test -n ${VERSION}" - -test_expect_code 0 "the specified version ${VERSION} refers to a commit" \ - "git show ${VERSION} >/dev/null 2>&1" - -BUILD_DIR="notmuch-${VERSION}" -test_expect_code 0 "generate snapshot of notmuch version ${VERSION}" \ - "git -C $TEST_DIRECTORY/.. archive --prefix=${BUILD_DIR}/ --format=tar ${VERSION} | tar x" - -# force version string -git describe --match '[0-9.]*' ${VERSION} > ${BUILD_DIR}/version - -test_expect_code 0 "configure and build notmuch version ${VERSION}" \ - "make -C ${BUILD_DIR}" - -# use the newly built notmuch -export PATH=./${BUILD_DIR}:$PATH - -test_begin_subtest "verify the newly built notmuch version" -test_expect_equal "`notmuch --version`" "notmuch `cat ${BUILD_DIR}/version`" - -# replace the existing mails, if any, with the specified corpus -rm -rf ${MAIL_DIR} -cp -a ${CORPUS} ${MAIL_DIR} - -test_expect_code 0 "index the corpus" \ - "notmuch new" - -# wrap the resulting mail store and database in a tarball - -cp -a ${MAIL_DIR} ${TMP_DIRECTORY}/${DBNAME} -tar Jcf ${TMP_DIRECTORY}/${TARBALLNAME} -C ${TMP_DIRECTORY} ${DBNAME} -mkdir -p ${TEST_DIRECTORY}/test-databases -cp -a ${TMP_DIRECTORY}/${TARBALLNAME} ${TEST_DIRECTORY}/test-databases -test_expect_code 0 "create the output tarball ${TARBALLNAME}" \ - "test -f ${TEST_DIRECTORY}/test-databases/${TARBALLNAME}" - -# generate a checksum file -test_expect_code 0 "compute checksum" \ - "(cd ${TEST_DIRECTORY}/test-databases/ && sha256sum ${TARBALLNAME} > ${TARBALLNAME}.sha256)" -test_done diff --git a/devel/nmbug/nmbug b/devel/nmbug/nmbug index c35dd75d..043c1863 100755 --- a/devel/nmbug/nmbug +++ b/devel/nmbug/nmbug @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (c) 2011-2014 David Bremner # W. Trevor King diff --git a/devel/nmbug/notmuch-report b/devel/nmbug/notmuch-report index eaceb2ce..18a0bc70 100755 --- a/devel/nmbug/notmuch-report +++ b/devel/nmbug/notmuch-report @@ -1,10 +1,9 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Copyright (c) 2011-2012 David Bremner # # dependencies -# - python 2.6 for json -# - argparse; either python 2.7, or install separately +# - python3 or python2.7 # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/devel/printmimestructure b/devel/printmimestructure deleted file mode 100755 index 70e0a5c0..00000000 --- a/devel/printmimestructure +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Author: Daniel Kahn Gillmor -# License: GPLv3+ - -# This script reads a MIME message from stdin and produces a treelike -# representation on it stdout. - -# Example: -# -# 0 dkg@alice:~$ printmimestructure < 'Maildir/cur/1269025522.M338697P12023.monkey,S=6459,W=6963:2,Sa' -# └┬╴multipart/signed 6546 bytes -# ├─╴text/plain inline 895 bytes -# └─╴application/pgp-signature inline [signature.asc] 836 bytes -# 0 dkg@alice:~$ - - -# If you want to number the parts, i suggest piping the output through -# something like "cat -n" - -from __future__ import print_function - -import email -import sys - -def print_part(z, prefix): - fname = '' if z.get_filename() is None else ' [' + z.get_filename() + ']' - cset = '' if z.get_charset() is None else ' (' + z.get_charset() + ')' - disp = z.get_params(None, header='Content-Disposition') - if (disp is None): - disposition = '' - else: - disposition = '' - for d in disp: - if d[0] in [ 'attachment', 'inline' ]: - disposition = ' ' + d[0] - if z.is_multipart(): - nbytes = len(z.as_string()) - else: - nbytes = len(z.get_payload()) - - print('{}{}{}{}{} {:d} bytes'.format( - prefix, - z.get_content_type(), - cset, - disposition, - fname, - nbytes, - )) - -def test(z, prefix=''): - if (z.is_multipart()): - print_part(z, prefix+'┬╴') - if prefix.endswith('└'): - prefix = prefix.rpartition('└')[0] + ' ' - if prefix.endswith('├'): - prefix = prefix.rpartition('├')[0] + '│' - parts = z.get_payload() - i = 0 - while (i < parts.__len__()-1): - test(parts[i], prefix + '├') - i += 1 - test(parts[i], prefix + '└') - # FIXME: show epilogue? - else: - print_part(z, prefix+'─╴') - -test(email.message_from_file(sys.stdin), '└') diff --git a/devel/release-checks.sh b/devel/release-checks.sh index 7ba94822..23c29eaa 100755 --- a/devel/release-checks.sh +++ b/devel/release-checks.sh @@ -29,7 +29,7 @@ append_emsg () emsgs="${emsgs:+$emsgs\n} $1" } -for f in ./version debian/changelog NEWS "$PV_FILE" +for f in ./version.txt debian/changelog NEWS "$PV_FILE" do if [ ! -f "$f" ]; then append_emsg "File '$f' is missing" elif [ ! -r "$f" ]; then append_emsg "File '$f' is unreadable" @@ -53,7 +53,7 @@ then else echo "Reading './version' file failed (surprisingly!)" exit 1 -fi < ./version +fi < ./version.txt readonly VERSION @@ -109,7 +109,7 @@ else fi echo -n "Checking that python bindings version is $VERSION... " -py_version=`python -c "with open('$PV_FILE') as vf: exec(vf.read()); print(__VERSION__)"` +py_version=`python3 -c "with open('$PV_FILE') as vf: exec(vf.read()); print(__VERSION__)"` if [ "$py_version" = "$VERSION" ] then echo Yes. @@ -178,10 +178,7 @@ esac year=`exec date +%Y` echo -n "Checking that copyright in documentation contains 2009-$year... " # Read the value of variable `copyright' defined in 'doc/conf.py'. -# As __file__ is not defined when python command is given from command line, -# it is defined before contents of 'doc/conf.py' (which dereferences __file__) -# is executed. -copyrightline=`exec python -c "with open('doc/conf.py') as cf: __file__ = ''; exec(cf.read()); print(copyright)"` +copyrightline=$(grep ^copyright doc/conf.py) case $copyrightline in *2009-$year*) echo Yes. ;; diff --git a/devel/try-emacs-mua b/devel/try-emacs-mua index 041f6216..585d6242 100755 --- a/devel/try-emacs-mua +++ b/devel/try-emacs-mua @@ -44,28 +44,10 @@ while looking at: " pdir "emacs\n\nexit emacs (y or n)? ") try-notmuch-emacs-directory (concat pdir "emacs/") load-path (cons try-notmuch-emacs-directory load-path))) -;; they say advice doesn't work for primitives (functions from c source) -;; well, these 'before' advice works for emacs 23.1 - 24.5 (at least) -;; ...and for our purposes 24.3 is enough (there is no load-prefer-newer there) -;; note also that the old, "obsolete" defadvice mechanism was used, but that -;; is the only one available for emacs 23 and 24 up to 24.3. - -(if (boundp 'load-prefer-newer) - (defadvice require (before before-require activate) - (unless (featurep feature) - (message "require: %s" feature))) - ;; else: special require "short-circuit"; after load feature is provided... - ;; ... in notmuch sources we always use require and there are no loops - (defadvice require (before before-require activate) - (unless (featurep feature) - (message "require: %s" feature) - (let ((name (symbol-name feature))) - (if (and (string-match "^notmuch" name) - (file-newer-than-file-p - (concat try-notmuch-emacs-directory name ".el") - (concat try-notmuch-emacs-directory name ".elc"))) - (load (concat try-notmuch-emacs-directory name ".el") nil nil t t) - ))))) +(define-advice require + (:before (feature &optional _filename _noerror) notmuch) + (unless (featurep feature) + (message "require: %s" feature))) (insert "Found notmuch emacs client in " try-notmuch-emacs-directory "\n") diff --git a/devel/uncrustify.cfg b/devel/uncrustify.cfg index 6a8769c6..c36c33d6 100644 --- a/devel/uncrustify.cfg +++ b/devel/uncrustify.cfg @@ -117,3 +117,5 @@ align_right_cmt_span = 8 # align comments span this much in func cmt_star_cont = true # indent_brace = 0 + +indent_class = true diff --git a/doc/Makefile.local b/doc/Makefile.local index d733b51e..60bd7184 100644 --- a/doc/Makefile.local +++ b/doc/Makefile.local @@ -1,10 +1,10 @@ -# -*- makefile -*- +# -*- makefile-gmake -*- dir := doc # You can set these variables from the command line. SPHINXOPTS := -q -SPHINXBUILD = HAVE_EMACS=${HAVE_EMACS} WITH_EMACS=${WITH_EMACS} sphinx-build +SPHINXBUILD = sphinx-build DOCBUILDDIR := $(dir)/_build # Internal variables. @@ -29,8 +29,8 @@ MAN1_TEXI := $(patsubst $(srcdir)/doc/man1/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$ MAN5_TEXI := $(patsubst $(srcdir)/doc/man5/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$(MAN5_RST)) MAN7_TEXI := $(patsubst $(srcdir)/doc/man7/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$(MAN7_RST)) INFO_TEXI_FILES := $(MAN1_TEXI) $(MAN5_TEXI) $(MAN7_TEXI) -ifeq ($(HAVE_EMACS)$(WITH_EMACS),11) - INFO_TEXI_FILES := $(INFO_TEXI_FILES) $(DOCBUILDDIR)/texinfo/notmuch-emacs.texi +ifeq ($(WITH_EMACS),1) + INFO_TEXI_FILES += $(DOCBUILDDIR)/texinfo/notmuch-emacs.texi endif INFO_INFO_FILES := $(INFO_TEXI_FILES:.texi=.info) @@ -40,7 +40,7 @@ INFO_INFO_FILES := $(INFO_TEXI_FILES:.texi=.info) .PHONY: install-man build-man apidocs install-apidocs %.gz: % - rm -f $@ && gzip --stdout $^ > $@ + rm -f $@ && gzip --no-name --stdout $^ > $@ ifeq ($(WITH_EMACS),1) $(DOCBUILDDIR)/.roff.stamp sphinx-html sphinx-texinfo: docstring.stamp diff --git a/doc/conf.py b/doc/conf.py index 8afff929..11bed51d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -4,6 +4,8 @@ import sys import os +extensions = [ 'sphinx.ext.autodoc' ] + # The suffix of source filenames. source_suffix = '.rst' @@ -12,16 +14,26 @@ master_doc = 'index' # General information about the project. project = u'notmuch' -copyright = u'2009-2019, Carl Worth and many others' +copyright = u'2009-2020, Carl Worth and many others' location = os.path.dirname(__file__) for pathdir in ['.', '..']: - version_file = os.path.join(location,pathdir,'version') + version_file = os.path.join(location,pathdir,'version.txt') if os.path.exists(version_file): with open(version_file,'r') as infile: version=infile.read().replace('\n','') +# for autodoc +sys.path.insert(0, os.path.join(location, '..', 'bindings', 'python-cffi', 'notmuch2')) + +# read generated config +for pathdir in ['.', '..']: + conf_file = os.path.join(location,pathdir,'sphinx.config') + if os.path.exists(conf_file): + with open(conf_file,'r') as infile: + exec(''.join(infile.readlines())) + # The full version, including alpha/beta/rc tags. release = version @@ -29,12 +41,23 @@ release = version # directories to ignore when looking for source files. exclude_patterns = ['_build'] -# If we don't have emacs (or the user configured --without-emacs), -# don't build the notmuch-emacs docs, as they need emacs to generate -# the docstring include files -if os.environ.get('HAVE_EMACS') != '1' or os.environ.get('WITH_EMACS') != '1': +if tags.has('WITH_EMACS'): + # Hacky reimplementation of include to workaround limitations of + # sphinx-doc + lines = ['.. include:: /../emacs/rstdoc.rsti\n\n'] # in the source tree + for file in ('notmuch.rsti', 'notmuch-lib.rsti', 'notmuch-show.rsti', 'notmuch-tag.rsti'): + lines.extend(open(rsti_dir+'/'+file)) + rst_epilog = ''.join(lines) + del lines +else: + # If we don't have emacs (or the user configured --without-emacs), + # don't build the notmuch-emacs docs, as they need emacs to generate + # the docstring include files exclude_patterns.append('notmuch-emacs.rst') +if not tags.has('WITH_PYTHON'): + exclude_patterns.append('python-bindings.rst') + # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' diff --git a/doc/doxygen.cfg b/doc/doxygen.cfg index 2ca15d41..a2c4fd07 100644 --- a/doc/doxygen.cfg +++ b/doc/doxygen.cfg @@ -264,12 +264,10 @@ GENERATE_TAGFILE = ALLEXTERNALS = NO EXTERNAL_GROUPS = NO EXTERNAL_PAGES = NO -PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- CLASS_DIAGRAMS = NO -MSCGEN_PATH = HIDE_UNDOC_RELATIONS = YES HAVE_DOT = NO DOT_NUM_THREADS = 0 diff --git a/doc/index.rst b/doc/index.rst index 4440d93a..a3bf3480 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -26,6 +26,7 @@ Contents: man7/notmuch-search-terms man1/notmuch-show man1/notmuch-tag + python-bindings Indices and tables ================== diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst index 28487079..769f336a 100644 --- a/doc/man1/notmuch-config.rst +++ b/doc/man1/notmuch-config.rst @@ -38,7 +38,7 @@ programmatically as described in the SYNOPSIS above. Every configuration item is printed to stdout, each on a separate line of the form:: - *section*.\ *item*\ =\ *value* + section.item=value No additional whitespace surrounds the dot or equals sign characters. In a multiple-value item (a list), the values are @@ -134,14 +134,6 @@ The available configuration items are described below. Default: ``true``. -**crypto.gpg_path** - Name (or full path) of gpg binary to use in verification and - decryption of PGP/MIME messages. NOTE: This configuration item is - deprecated, and will be ignored if notmuch is built against GMime - 3.0 or later. - - Default: ``gpg``. - **index.decrypt** **[STORED IN DATABASE]** Policy for decrypting encrypted messages during indexing. Must be one of: ``false``, ``auto``, ``nostash``, or ``true``. @@ -206,8 +198,9 @@ The available configuration items are described below. **built_with.** Compile time feature . Current possibilities include - "compact" (see **notmuch-compact(1)**) and "field_processor" (see - **notmuch-search-terms(7)**). + "retry_lock" (configure option, included by default). + (since notmuch 0.30, "compact" and "field_processor" are + always included.) **query.** **[STORED IN DATABASE]** Expansion for named query called . See diff --git a/doc/man1/notmuch.rst b/doc/man1/notmuch.rst index d2cd8da5..fecfd08a 100644 --- a/doc/man1/notmuch.rst +++ b/doc/man1/notmuch.rst @@ -128,9 +128,9 @@ OPTION SYNTAX ------------- All options accepting an argument can be used with '=' or ':' as a -separator. For the cases where it's not ambiguous (in particular -excluding boolean options), a space can also be used. The following -are all equivalent: +separator. Except for boolean options (which would be ambiguous), a +space can also be used as a separator. The following are all +equivalent: :: diff --git a/doc/man7/notmuch-properties.rst b/doc/man7/notmuch-properties.rst index 802e6763..a7d91d67 100644 --- a/doc/man7/notmuch-properties.rst +++ b/doc/man7/notmuch-properties.rst @@ -109,6 +109,30 @@ of its normal activity. example, an AES-128 key might be stashed in a notmuch property as: ``session-key=7:14B16AF65536C28AF209828DFE34C9E0``. +**index.repaired** + + Some messages arrive in forms that are confusing to view; they can + be mangled by mail transport agents, or the sending mail user + agent may structure them in a way that is confusing. If notmuch + knows how to both detect and repair such a problematic message, it + will do so during indexing. + + If it applies a message repair during indexing, it will use the + ``index.repaired`` property to note the type of repair(s) it + performed. + + ``index.repaired=skip-protected-headers-legacy-display`` indicates + that when indexing the cleartext of an encrypted message, notmuch + skipped over a "legacy-display" text/rfc822-headers part that it + found in that message, since it was able to index the built-in + protected headers directly. + + ``index.repaired=mixedup`` indicates the repair of a "Mixed Up" + encrypted PGP/MIME message, a mangling typically produced by + Microsoft's Exchange MTA. See + https://tools.ietf.org/html/draft-dkg-openpgp-pgpmime-message-mangling + for more information. + SEE ALSO ======== diff --git a/doc/man7/notmuch-search-terms.rst b/doc/man7/notmuch-search-terms.rst index 1dd2dc58..28fca737 100644 --- a/doc/man7/notmuch-search-terms.rst +++ b/doc/man7/notmuch-search-terms.rst @@ -37,9 +37,8 @@ In addition to free text, the following prefixes can be used to force terms to match against specific portions of an email, (where indicate user-supplied values). -If notmuch is built with **Xapian Field Processors** (see below) some -of the prefixes with forms can be also used to restrict the -results to those whose value matches a regular expression (see +Some of the prefixes with forms can be also used to restrict +the results to those whose value matches a regular expression (see **regex(7)**) delimited with //, for example:: notmuch search 'from:"/bob@.*[.]example[.]com/"' @@ -87,8 +86,7 @@ thread: of output from **notmuch search** thread:{} - If notmuch is built with **Xapian Field Processors** (see below), - threads may be searched for indirectly by providing an arbitrary + Threads may be searched for indirectly by providing an arbitrary notmuch query in **{}**. For example, the following returns threads containing a message from mallory and one (not necessarily the same message) with Subject containing the word "crypto". @@ -158,9 +156,7 @@ lastmod:.. query: The **query:** prefix allows queries to refer to previously saved - queries added with **notmuch-config(1)**. Named queries are only - available if notmuch is built with **Xapian Field Processors** - (see below). + queries added with **notmuch-config(1)**. property:= The **property:** prefix searches for messages with a particular @@ -353,23 +349,21 @@ since 1970-01-01 00:00:00 UTC. For example: date:@..@ -date:..! can be used as a shorthand for date:... The -expansion takes place before interpretation, and thus, for example, -date:monday..! matches from the beginning of Monday until the end of -Monday. -With **Xapian Field Processor** support (see below), non-range -date queries such as date:yesterday will work, but otherwise -will give unexpected results; if in doubt use date:yesterday..! - -Currently, we do not support spaces in range expressions. You can +Currently, spaces in range expressions are not supported. You can replace the spaces with '\_', or (in most cases) '-', or (in some cases) leave the spaces out altogether. Examples in this man page use spaces for clarity. -Open-ended ranges are supported (since Xapian 1.2.1), i.e. it's possible -to specify date:.. or date:.. to not limit the start or -end time, respectively. Pre-1.2.1 Xapian does not report an error on -open ended ranges, but it does not work as expected either. +Open-ended ranges are supported. I.e. it's possible to specify +date:.. or date:.. to not limit the start or +end time, respectively. + +Single expression +----------------- + +date: works as a shorthand for date:... +For example, date:monday matches from the beginning of Monday until +the end of Monday. Relative date and time ---------------------- @@ -446,24 +440,6 @@ Time zones Some time zone codes, e.g. UTC, EET. -XAPIAN FIELD PROCESSORS -======================= - -Certain optional features of the notmuch query processor rely on the -presence of the Xapian field processor API. You can determine if your -notmuch was built against a sufficiently recent version of Xapian by running - -:: - - % notmuch config get built_with.field_processor - -Currently the following features require field processor support: - -- non-range date queries, e.g. "date:today" -- named queries e.g. "query:my_special_query" -- regular expression searches, e.g. "subject:/^\\[SPAM\\]/" -- thread subqueries, e.g. "thread:{from:bob}" - SEE ALSO ======== diff --git a/doc/notmuch-emacs.rst b/doc/notmuch-emacs.rst index 1655e2f0..de47b726 100644 --- a/doc/notmuch-emacs.rst +++ b/doc/notmuch-emacs.rst @@ -377,13 +377,3 @@ suffix exist it will be read instead (just one of these, chosen in this order). Most often users create ``~/.emacs.d/notmuch-config.el`` and just work with it. If Emacs was invoked with the ``-q`` or ``--no-init-file`` options, ``notmuch-init-file`` is not read. - -.. include:: ../emacs/rstdoc.rsti - -.. include:: ../emacs/notmuch.rsti - -.. include:: ../emacs/notmuch-lib.rsti - -.. include:: ../emacs/notmuch-show.rsti - -.. include:: ../emacs/notmuch-tag.rsti diff --git a/doc/python-bindings.rst b/doc/python-bindings.rst new file mode 100644 index 00000000..e1ad26ad --- /dev/null +++ b/doc/python-bindings.rst @@ -0,0 +1,5 @@ +Python Bindings +=============== + +.. automodule:: notmuch2 + :members: diff --git a/emacs/Makefile.local b/emacs/Makefile.local index 40b7c14f..d1b320c3 100644 --- a/emacs/Makefile.local +++ b/emacs/Makefile.local @@ -1,4 +1,4 @@ -# -*- makefile -*- +# -*- makefile-gmake -*- dir := emacs emacs_sources := \ @@ -47,7 +47,7 @@ emacs_images := \ emacs_bytecode = $(emacs_sources:.el=.elc) emacs_docstrings = $(emacs_sources:.el=.rsti) -ifneq ($(HAVE_SPHINX)$(HAVE_EMACS),11) +ifneq ($(HAVE_SPHINX)$(WITH_EMACS),11) docstring.stamp: @echo "Missing prerequisites, not collecting docstrings" else @@ -60,7 +60,7 @@ endif # the byte compiler may load an old .elc file when processing a # "require" or we may fail to rebuild a .elc that depended on a macro # from an updated file. -ifeq ($(HAVE_EMACS),1) +ifeq ($(WITH_EMACS),1) $(dir)/.eldeps: $(dir)/Makefile.local $(dir)/make-deps.el $(emacs_sources) $(call quiet,EMACS) --directory emacs -batch -l make-deps.el \ -f batch-make-deps $(emacs_sources) > $@.tmp && \ @@ -82,7 +82,7 @@ $(dir)/notmuch-lib.elc: $(dir)/notmuch-version.elc endif CLEAN+=$(dir)/.eldeps $(dir)/.eldeps.tmp $(dir)/.eldeps.x -ifeq ($(HAVE_EMACS),1) +ifeq ($(WITH_EMACS),1) %.elc: %.el $(global_deps) $(call quiet,EMACS) --directory emacs -batch -f batch-byte-compile $< %.rsti: %.el @@ -103,10 +103,8 @@ endif rm -r .elpa-build ifeq ($(WITH_EMACS),1) -ifeq ($(HAVE_EMACS),1) all: $(emacs_bytecode) $(emacs_docstrings) install-emacs: $(emacs_bytecode) -endif install: install-emacs endif @@ -115,7 +113,7 @@ endif install-emacs: $(emacs_sources) $(emacs_images) mkdir -p "$(DESTDIR)$(emacslispdir)" install -m0644 $(emacs_sources) "$(DESTDIR)$(emacslispdir)" -ifeq ($(HAVE_EMACS),1) +ifeq ($(WITH_EMACS),1) install -m0644 $(emacs_bytecode) "$(DESTDIR)$(emacslispdir)" endif mkdir -p "$(DESTDIR)$(emacsetcdir)" diff --git a/emacs/coolj.el b/emacs/coolj.el index 350d537f..39a8de2b 100644 --- a/emacs/coolj.el +++ b/emacs/coolj.el @@ -1,6 +1,6 @@ ;;; coolj.el --- automatically wrap long lines -*- coding:utf-8 -*- -;; Copyright (C) 2000, 2001, 2004, 2005, 2006, 2007, 2008, 2009 Free Software Foundation, Inc. +;; Copyright (C) 2000, 2001, 2004-2009 Free Software Foundation, Inc. ;; Authors: Kai Grossjohann ;; Alex Schroeder @@ -107,12 +107,12 @@ not need to be wrapped, move point to the next line and return t." If the line should not be broken, return nil; point remains on the line." (move-to-column fill-column) - (if (and (re-search-forward "[^ ]" (line-end-position) 1) - (> (current-column) fill-column)) - ;; This line is too long. Can we break it? - (or (coolj-find-break-backward prefix) - (progn (move-to-column fill-column) - (coolj-find-break-forward))))) + (and (re-search-forward "[^ ]" (line-end-position) 1) + (> (current-column) fill-column) + ;; This line is too long. Can we break it? + (or (coolj-find-break-backward prefix) + (progn (move-to-column fill-column) + (coolj-find-break-forward))))) (defun coolj-find-break-backward (prefix) "Move point backward to the first available breakpoint and return t. @@ -135,12 +135,12 @@ If no breakpoint is found, return nil." If no break point is found, return nil." (and (search-forward " " (line-end-position) 1) (progn (skip-chars-forward " " (line-end-position)) - (null (eolp))) + (null (eolp))) (if (and fill-nobreak-predicate - (run-hook-with-args-until-success - 'fill-nobreak-predicate)) - (coolj-find-break-forward) - t))) + (run-hook-with-args-until-success + 'fill-nobreak-predicate)) + (coolj-find-break-forward) + t))) (provide 'coolj) diff --git a/emacs/make-deps.el b/emacs/make-deps.el index 5b6db698..a7699fb1 100644 --- a/emacs/make-deps.el +++ b/emacs/make-deps.el @@ -1,4 +1,4 @@ -;; make-deps.el --- compute make dependencies for Elisp sources +;;; make-deps.el --- compute make dependencies for Elisp sources ;; ;; Copyright © Austin Clements ;; @@ -23,7 +23,6 @@ (defun batch-make-deps () "Invoke `make-deps' for each file on the command line." - (setq debug-on-error t) (dolist (file command-line-args-left) (let ((default-directory command-line-default-directory)) @@ -37,8 +36,8 @@ This prints make dependencies to `standard-output' based on the top-level `require' expressions in the current buffer. Paths in rules will be given relative to DIR, or `default-directory'." - - (setq dir (or dir default-directory)) + (unless dir + (setq dir default-directory)) (save-excursion (goto-char (point-min)) (condition-case nil diff --git a/emacs/notmuch-address.el b/emacs/notmuch-address.el index 64887a43..8a6d299c 100644 --- a/emacs/notmuch-address.el +++ b/emacs/notmuch-address.el @@ -29,16 +29,16 @@ (declare-function company-manual-begin "company") (defvar notmuch-address-last-harvest 0 - "Time of last address harvest") + "Time of last address harvest.") (defvar notmuch-address-completions (make-hash-table :test 'equal) "Hash of email addresses for completion during email composition. - This variable is set by calling `notmuch-address-harvest'.") +This variable is set by calling `notmuch-address-harvest'.") (defvar notmuch-address-full-harvest-finished nil - "t indicates that full completion address harvesting has been -finished. Use notmuch-address--harvest-ready to access as that -will load a saved hash if necessary (and available).") + "t indicates that full completion address harvesting has been finished. +Use notmuch-address--harvest-ready to access as that will load a +saved hash if necessary (and available).") (defun notmuch-address--harvest-ready () "Return t if there is a full address hash available. @@ -54,9 +54,9 @@ If it is a string then that string should be an external program which must take a single argument (searched string) and output a list of completion candidates, one per line. -Alternatively, it can be the symbol 'internal, in which case +Alternatively, it can be the symbol `internal', in which case internal completion is used; the variable -`notmuch-address-internal-completion` can be used to customize +`notmuch-address-internal-completion' can be used to customize this case. Finally, if this variable is nil then address completion is @@ -72,12 +72,12 @@ disabled." (defcustom notmuch-address-internal-completion '(sent nil) "Determines how internal address completion generates candidates. -This should be a list of the form '(DIRECTION FILTER), where - DIRECTION is either sent or received and specifies whether the - candidates are searched in messages sent by the user or received - by the user (note received by is much faster), and FILTER is - either nil or a filter-string, such as \"date:1y..\" to append - to the query." +This should be a list of the form (DIRECTION FILTER), where +DIRECTION is either sent or received and specifies whether the +candidates are searched in messages sent by the user or received +by the user (note received by is much faster), and FILTER is +either nil or a filter-string, such as \"date:1y..\" to append to +the query." :type '(list :tag "Use internal address completion" (radio :tag "Base completion on messages you have" @@ -101,8 +101,8 @@ This should be a list of the form '(DIRECTION FILTER), where "Filename to save the cached completion addresses. All the addresses notmuch uses for address completion will be -cached in this file. This has obvious privacy implications so you -should make sure it is not somewhere publicly readable." +cached in this file. This has obvious privacy implications so +you should make sure it is not somewhere publicly readable." :type '(choice (const :tag "Off" nil) (file :tag "Filename")) :group 'notmuch-send @@ -110,12 +110,14 @@ should make sure it is not somewhere publicly readable." :group 'notmuch-external) (defcustom notmuch-address-selection-function 'notmuch-address-selection-function - "The function to select address from given list. The function is -called with PROMPT, COLLECTION, and INITIAL-INPUT as arguments -(subset of what `completing-read' can be called with). -While executed the value of `completion-ignore-case' is t. -See documentation of function `notmuch-address-selection-function' -to know how address selection is made by default." + "The function to select address from given list. + +The function is called with PROMPT, COLLECTION, and INITIAL-INPUT +as arguments (subset of what `completing-read' can be called +with). While executed the value of `completion-ignore-case' +is t. See documentation of function +`notmuch-address-selection-function' to know how address +selection is made by default." :type 'function :group 'notmuch-send :group 'notmuch-address @@ -126,8 +128,7 @@ to know how address selection is made by default." The completed address is passed as an argument to each function. Note that this hook will be invoked for completion in headers -matching `notmuch-address-completion-headers-regexp'. -" +matching `notmuch-address-completion-headers-regexp'." :type 'hook :group 'notmuch-address :group 'notmuch-hooks) @@ -147,21 +148,21 @@ matching `notmuch-address-completion-headers-regexp'. (message "calling notmuch-address-message-insinuate is no longer needed")) (defcustom notmuch-address-use-company t - "If available, use company mode for address completion" + "If available, use company mode for address completion." :type 'boolean :group 'notmuch-send :group 'notmuch-address) (defun notmuch-address-setup () (let* ((setup-company (and notmuch-address-use-company - (require 'company nil t))) + (require 'company nil t))) (pair (cons notmuch-address-completion-headers-regexp - #'notmuch-address-expand-name))) - (when setup-company - (notmuch-company-setup)) - (unless (member pair message-completion-alist) - (setq message-completion-alist - (push pair message-completion-alist))))) + #'notmuch-address-expand-name))) + (when setup-company + (notmuch-company-setup)) + (unless (member pair message-completion-alist) + (setq message-completion-alist + (push pair message-completion-alist))))) (defun notmuch-address-toggle-internal-completion () "Toggle use of internal completion for current buffer. @@ -171,11 +172,11 @@ toggles the setting in this buffer." (interactive) (if (local-variable-p 'notmuch-address-command) (kill-local-variable 'notmuch-address-command) - (notmuch-setq-local notmuch-address-command 'internal)) - (if (boundp 'company-idle-delay) - (if (local-variable-p 'company-idle-delay) - (kill-local-variable 'company-idle-delay) - (notmuch-setq-local company-idle-delay nil)))) + (setq-local notmuch-address-command 'internal)) + (when (boundp 'company-idle-delay) + (if (local-variable-p 'company-idle-delay) + (kill-local-variable 'company-idle-delay) + (setq-local company-idle-delay nil)))) (defun notmuch-address-matching (substring) "Returns a list of completion candidates matching SUBSTRING. @@ -189,17 +190,18 @@ The candidates are taken from `notmuch-address-completions'." candidates)) (defun notmuch-address-options (original) - "Returns a list of completion candidates. Uses either -elisp-based implementation or older implementation requiring -external commands." + "Return a list of completion candidates. +Use either elisp-based implementation or older implementation +requiring external commands." (cond ((eq notmuch-address-command 'internal) (unless (notmuch-address--harvest-ready) ;; First, run quick synchronous harvest based on what the user - ;; entered so far + ;; entered so far. (notmuch-address-harvest original t)) (prog1 (notmuch-address-matching original) - ;; Then start the (potentially long-running) full asynchronous harvest if necessary + ;; Then start the (potentially long-running) full asynchronous + ;; harvest if necessary. (notmuch-address-harvest-trigger))) (t (process-lines notmuch-address-command original)))) @@ -242,7 +244,8 @@ external commands." (push chosen notmuch-address-history) (delete-region beg end) (insert chosen) - (run-hook-with-args 'notmuch-address-post-completion-functions chosen)) + (run-hook-with-args 'notmuch-address-post-completion-functions + chosen)) (message "No matches.") (ding)))) (t nil))) @@ -251,20 +254,20 @@ external commands." (defun notmuch-address-locate-command (command) "Return non-nil if `command' is an executable either on `exec-path' or an absolute pathname." - (when (stringp command) - (if (and (file-name-absolute-p command) - (file-executable-p command)) - command - (setq command (file-name-nondirectory command)) - (catch 'found-command - (let (bin) - (dolist (dir exec-path) - (setq bin (expand-file-name command dir)) - (when (or (and (file-executable-p bin) - (not (file-directory-p bin))) - (and (file-executable-p (setq bin (concat bin ".exe"))) - (not (file-directory-p bin)))) - (throw 'found-command bin)))))))) + (and (stringp command) + (if (and (file-name-absolute-p command) + (file-executable-p command)) + command + (setq command (file-name-nondirectory command)) + (catch 'found-command + (let (bin) + (dolist (dir exec-path) + (setq bin (expand-file-name command dir)) + (when (or (and (file-executable-p bin) + (not (file-directory-p bin))) + (and (file-executable-p (setq bin (concat bin ".exe"))) + (not (file-directory-p bin)))) + (throw 'found-command bin)))))))) (defun notmuch-address-harvest-addr (result) (let ((name-addr (plist-get result :name-addr))) @@ -285,7 +288,7 @@ external commands." (defvar notmuch-address-harvest-procs '(nil . nil) "The currently running harvests. -The car is a partial harvest, and the cdr is a full harvest") +The car is a partial harvest, and the cdr is a full harvest.") (defun notmuch-address-harvest (&optional addr-prefix synchronous callback) "Collect addresses completion candidates. @@ -301,21 +304,22 @@ matching ADDR-PREFIX*' are queried. Address harvesting may take some time so the address collection runs asynchronously unless SYNCHRONOUS is t. In case of asynchronous execution, CALLBACK is called when harvesting finishes." - (let* ((sent (eq (car notmuch-address-internal-completion) 'sent)) (config-query (cadr notmuch-address-internal-completion)) - (prefix-query (when addr-prefix - (format "%s:%s*" (if sent "to" "from") addr-prefix))) + (prefix-query (and addr-prefix + (format "%s:%s*" + (if sent "to" "from") + addr-prefix))) (from-or-to-me-query (mapconcat (lambda (x) (concat (if sent "from:" "to:") x)) (notmuch-user-emails) " or ")) (query (if (or prefix-query config-query) (concat (format "(%s)" from-or-to-me-query) - (when prefix-query - (format " and (%s)" prefix-query)) - (when config-query - (format " and (%s)" config-query))) + (and prefix-query + (format " and (%s)" prefix-query)) + (and config-query + (format " and (%s)" config-query))) from-or-to-me-query)) (args `("address" "--format=sexp" "--format-version=4" ,(if sent "--output=recipients" "--output=sender") @@ -323,7 +327,7 @@ execution, CALLBACK is called when harvesting finishes." ,query))) (if synchronous (mapc #'notmuch-address-harvest-addr - (apply 'notmuch-call-notmuch-sexp args)) + (apply 'notmuch-call-notmuch-sexp args)) ;; Asynchronous (let* ((current-proc (if addr-prefix (car notmuch-address-harvest-procs) @@ -334,7 +338,6 @@ execution, CALLBACK is called when harvesting finishes." ;; Kill any existing process (when current-proc (kill-buffer (process-buffer current-proc))) ; this also kills the process - (setq current-proc (apply 'notmuch-start-notmuch proc-name proc-buf callback ; process sentinel @@ -351,25 +354,25 @@ execution, CALLBACK is called when harvesting finishes." "Version format of the save hash.") (defun notmuch-address--get-address-hash () - "Returns the saved address hash as a plist. + "Return the saved address hash as a plist. Returns nil if the save file does not exist, or it does not seem to be a saved address hash." - (when notmuch-address-save-filename - (condition-case nil - (with-temp-buffer - (insert-file-contents notmuch-address-save-filename) - (let ((name (read (current-buffer))) - (plist (read (current-buffer)))) - ;; We do two simple sanity checks on the loaded file. We just - ;; check a version is specified, not that it is the current - ;; version, as we are allowed to over-write and a save-file with - ;; an older version. - (when (and (string= name "notmuch-address-hash") - (plist-get plist :version)) - plist))) - ;; The error case catches any of the reads failing. - (error nil)))) + (and notmuch-address-save-filename + (condition-case nil + (with-temp-buffer + (insert-file-contents notmuch-address-save-filename) + (let ((name (read (current-buffer))) + (plist (read (current-buffer)))) + ;; We do two simple sanity checks on the loaded file. + ;; We just check a version is specified, not that + ;; it is the current version, as we are allowed to + ;; over-write and a save-file with an older version. + (and (string= name "notmuch-address-hash") + (plist-get plist :version) + plist))) + ;; The error case catches any of the reads failing. + (error nil)))) (defun notmuch-address--load-address-hash () "Read the saved address hash and set the corresponding variables." @@ -382,22 +385,23 @@ to be a saved address hash." notmuch-address-internal-completion) (equal (plist-get load-plist :version) notmuch-address--save-hash-version)) - (setq notmuch-address-last-harvest (plist-get load-plist :last-harvest) - notmuch-address-completions (plist-get load-plist :completions) - notmuch-address-full-harvest-finished t) + (setq notmuch-address-last-harvest (plist-get load-plist :last-harvest)) + (setq notmuch-address-completions (plist-get load-plist :completions)) + (setq notmuch-address-full-harvest-finished t) ;; Return t to say load was successful. t))) (defun notmuch-address--save-address-hash () (when notmuch-address-save-filename (if (or (not (file-exists-p notmuch-address-save-filename)) - ;; The file exists, check it is a file we saved + ;; The file exists, check it is a file we saved (notmuch-address--get-address-hash)) (with-temp-file notmuch-address-save-filename - (let ((save-plist (list :version notmuch-address--save-hash-version - :completion-settings notmuch-address-internal-completion - :last-harvest notmuch-address-last-harvest - :completions notmuch-address-completions))) + (let ((save-plist + (list :version notmuch-address--save-hash-version + :completion-settings notmuch-address-internal-completion + :last-harvest notmuch-address-last-harvest + :completions notmuch-address-completions))) (print "notmuch-address-hash" (current-buffer)) (print save-plist (current-buffer)))) (message "\ @@ -409,16 +413,17 @@ appear to be an address savefile. Not overwriting." (let ((now (float-time))) (when (> (- now notmuch-address-last-harvest) 86400) (setq notmuch-address-last-harvest now) - (notmuch-address-harvest nil nil - (lambda (proc event) - ;; If harvest fails, we want to try - ;; again when the trigger is next - ;; called - (if (string= event "finished\n") - (progn - (notmuch-address--save-address-hash) - (setq notmuch-address-full-harvest-finished t)) - (setq notmuch-address-last-harvest 0))))))) + (notmuch-address-harvest + nil nil + (lambda (proc event) + ;; If harvest fails, we want to try + ;; again when the trigger is next + ;; called + (if (string= event "finished\n") + (progn + (notmuch-address--save-address-hash) + (setq notmuch-address-full-harvest-finished t)) + (setq notmuch-address-last-harvest 0))))))) ;; diff --git a/emacs/notmuch-company.el b/emacs/notmuch-company.el index 3e12e7a9..9ee8ceca 100644 --- a/emacs/notmuch-company.el +++ b/emacs/notmuch-company.el @@ -1,33 +1,39 @@ ;;; notmuch-company.el --- Mail address completion for notmuch via company-mode -*- lexical-binding: t -*- - -;; Authors: Trevor Jim -;; Michal Sojka ;; -;; Keywords: mail, 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 +;; Copyright © Trevor Jim +;; Copyright © Michal Sojka +;; +;; This file is part of Notmuch. +;; +;; Notmuch is free software: you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. - -;; 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. - +;; +;; 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 this program. If not, see . +;; along with Notmuch. If not, see . +;; +;; Authors: Trevor Jim +;; Michal Sojka +;; Keywords: mail, completion ;;; Commentary: -;; To enable this, install company mode (https://company-mode.github.io/) +;; Mail address completion for notmuch via company-mode. To enable +;; this, install company mode from . ;; ;; NB company-minimum-prefix-length defaults to 3 so you don't get -;; completion unless you type 3 characters +;; completion unless you type 3 characters. ;;; Code: -(eval-when-compile (require 'cl)) +(eval-when-compile (require 'cl-lib)) + (require 'notmuch-lib) (defvar notmuch-company-last-prefix nil) @@ -56,7 +62,7 @@ ;; internal completion) can still be accessed via standard company ;; functions, e.g., company-complete. (unless (eq notmuch-address-command 'internal) - (notmuch-setq-local company-idle-delay nil))) + (setq-local company-idle-delay nil))) ;;;###autoload (defun notmuch-company (command &optional arg &rest _ignore) @@ -65,12 +71,14 @@ (require 'company) (let ((case-fold-search t) (completion-ignore-case t)) - (case command + (cl-case command (interactive (company-begin-backend 'notmuch-company)) (prefix (and (derived-mode-p 'message-mode) - (looking-back (concat notmuch-address-completion-headers-regexp ".*") - (line-beginning-position)) - (setq notmuch-company-last-prefix (company-grab "[:,][ \t]*\\(.*\\)" 1 (point-at-bol))))) + (looking-back + (concat notmuch-address-completion-headers-regexp ".*") + (line-beginning-position)) + (setq notmuch-company-last-prefix + (company-grab "[:,][ \t]*\\(.*\\)" 1 (point-at-bol))))) (candidates (cond ((notmuch-address--harvest-ready) ;; Update harvested addressed from time to time @@ -79,17 +87,20 @@ (t (cons :async (lambda (callback) - ;; First run quick asynchronous harvest based on what the user entered so far + ;; First run quick asynchronous harvest + ;; based on what the user entered so far (notmuch-address-harvest arg nil (lambda (_proc _event) (funcall callback (notmuch-address-matching arg)) - ;; Then start the (potentially long-running) full asynchronous harvest if necessary + ;; Then start the (potentially long-running) + ;; full asynchronous harvest if necessary (notmuch-address-harvest-trigger)))))))) (match (if (string-match notmuch-company-last-prefix arg) (match-end 0) 0)) - (post-completion (run-hook-with-args 'notmuch-address-post-completion-functions arg)) + (post-completion + (run-hook-with-args 'notmuch-address-post-completion-functions arg)) (no-cache t)))) diff --git a/emacs/notmuch-compat.el b/emacs/notmuch-compat.el index 2cedd39d..3ede6b36 100644 --- a/emacs/notmuch-compat.el +++ b/emacs/notmuch-compat.el @@ -1,10 +1,26 @@ -;; Compatibility functions for earlier versions of emacs - +;;; notmuch-compat.el --- compatibility functions for earlier versions of emacs +;; ;; The functions in this file are copied from more modern versions of ;; emacs and are Copyright (C) 1985-1986, 1992, 1994-1995, 1999-2017 ;; Free Software Foundation, Inc. +;; +;; This file is part of Notmuch. +;; +;; Notmuch is free software: you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. +;; +;; Notmuch is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with Notmuch. If not, see . + +;;; Code: -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; emacs master has a bugfix for folding long headers when sending ;; messages. Include the fix for earlier versions of emacs. To avoid ;; interfering with gnus we only run the hook when called from @@ -24,70 +40,8 @@ (unless (fboundp 'message--fold-long-headers) (add-hook 'message-header-hook 'notmuch-message--fold-long-headers)) -(if (fboundp 'setq-local) - (defalias 'notmuch-setq-local 'setq-local) - (defmacro notmuch-setq-local (var val) - "Set variable VAR to value VAL in current buffer. - -Backport of setq-local for emacs without setq-local (pre 24.3)." - `(set (make-local-variable ',var) ,val))) - -(if (fboundp 'read-char-choice) - (defalias 'notmuch-read-char-choice 'read-char-choice) - (defun notmuch-read-char-choice (prompt chars &optional inhibit-keyboard-quit) - "Read and return one of CHARS, prompting for PROMPT. -Any input that is not one of CHARS is ignored. - -If optional argument INHIBIT-KEYBOARD-QUIT is non-nil, ignore -keyboard-quit events while waiting for a valid input. - -This is an exact copy of this function from emacs 24 for use on -emacs 23, except with the one emacs 24 only function it calls -inlined." - (unless (consp chars) - (error "Called `read-char-choice' without valid char choices")) - (let (char done show-help (helpbuf " *Char Help*")) - (let ((cursor-in-echo-area t) - (executing-kbd-macro executing-kbd-macro) - (esc-flag nil)) - (save-window-excursion ; in case we call help-form-show - (while (not done) - (unless (get-text-property 0 'face prompt) - (setq prompt (propertize prompt 'face 'minibuffer-prompt))) - (setq char (let ((inhibit-quit inhibit-keyboard-quit)) - (read-key prompt))) - (and show-help (buffer-live-p (get-buffer helpbuf)) - (kill-buffer helpbuf)) - (cond - ((not (numberp char))) - ;; If caller has set help-form, that's enough. - ;; They don't explicitly have to add help-char to chars. - ((and help-form - (eq char help-char) - (setq show-help t) - ;; This is an inlined copy of help-form-show as that - ;; was introduced in emacs 24 too. - (let ((msg (eval help-form))) - (if (stringp msg) - (with-output-to-temp-buffer " *Char Help*" - (princ msg)))))) - ((memq char chars) - (setq done t)) - ((and executing-kbd-macro (= char -1)) - ;; read-event returns -1 if we are in a kbd macro and - ;; there are no more events in the macro. Attempt to - ;; get an event interactively. - (setq executing-kbd-macro nil)) - ((not inhibit-keyboard-quit) - (cond - ((and (null esc-flag) (eq char ?\e)) - (setq esc-flag t)) - ((memq char '(?\C-g ?\e)) - (keyboard-quit)))))))) - ;; Display the question with the answer. But without cursor-in-echo-area. - (message "%s%s" prompt (char-to-string char)) - char))) - ;; End of compatibility functions (provide 'notmuch-compat) + +;;; notmuch-compat.el ends here diff --git a/emacs/notmuch-crypto.el b/emacs/notmuch-crypto.el index 4216f583..276c9859 100644 --- a/emacs/notmuch-crypto.el +++ b/emacs/notmuch-crypto.el @@ -1,4 +1,4 @@ -;;; notmuch-crypto.el --- functions for handling display of cryptographic metadata. +;;; notmuch-crypto.el --- functions for handling display of cryptographic metadata ;; ;; Copyright © Jameson Rollins ;; @@ -24,6 +24,8 @@ (require 'epg) (require 'notmuch-lib) +(declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare)) + (defcustom notmuch-crypto-process-mime t "Should cryptographic MIME parts be processed? @@ -43,6 +45,16 @@ mode." :package-version '(notmuch . "0.25") :group 'notmuch-crypto) +(defcustom notmuch-crypto-get-keys-asynchronously t + "Retrieve gpg keys asynchronously." + :type 'boolean + :group 'notmuch-crypto) + +(defcustom notmuch-crypto-gpg-program epg-gpg-program + "The gpg executable." + :type 'string + :group 'notmuch-crypto) + (defface notmuch-crypto-part-header '((((class color) (background dark)) @@ -91,34 +103,35 @@ mode." :supertype 'notmuch-button-type) (defun notmuch-crypto-insert-sigstatus-button (sigstatus from) + "Insert a button describing the signature status SIGSTATUS sent +by user FROM." (let* ((status (plist-get sigstatus :status)) - (help-msg nil) (show-button t) - (label nil) (face 'notmuch-crypto-signature-unknown) - (button-action (lambda (button) (message (button-get button 'help-echo))))) + (button-action (lambda (button) (message (button-get button 'help-echo)))) + (keyid (concat "0x" (plist-get sigstatus :keyid))) + label help-msg) (cond ((string= status "good") - (let ((fingerprint (concat "0x" (plist-get sigstatus :fingerprint)))) - ;; if userid present, userid has full or greater validity - (if (plist-member sigstatus :userid) - (let ((userid (plist-get sigstatus :userid))) + (let ((fingerprint (concat "0x" (plist-get sigstatus :fingerprint))) + (userid (plist-get sigstatus :userid))) + ;; If userid is present it has full or greater validity. + (if userid + (progn (setq label (concat "Good signature by: " userid)) (setq face 'notmuch-crypto-signature-good)) - (progn - (setq label (concat "Good signature by key: " fingerprint)) - (setq face 'notmuch-crypto-signature-good-key))) + (setq label (concat "Good signature by key: " fingerprint)) + (setq face 'notmuch-crypto-signature-good-key)) (setq button-action 'notmuch-crypto-sigstatus-good-callback) (setq help-msg (concat "Click to list key ID 0x" fingerprint ".")))) ((string= status "error") - (let ((keyid (concat "0x" (plist-get sigstatus :keyid)))) - (setq label (concat "Unknown key ID " keyid " or unsupported algorithm")) - (setq button-action 'notmuch-crypto-sigstatus-error-callback) - (setq help-msg (concat "Click to retrieve key ID " keyid " from keyserver and redisplay.")))) + (setq label (concat "Unknown key ID " keyid " or unsupported algorithm")) + (setq button-action 'notmuch-crypto-sigstatus-error-callback) + (setq help-msg (concat "Click to retrieve key ID " keyid + " from key server."))) ((string= status "bad") - (let ((keyid (concat "0x" (plist-get sigstatus :keyid)))) - (setq label (concat "Bad signature (claimed key ID " keyid ")")) - (setq face 'notmuch-crypto-signature-bad))) + (setq label (concat "Bad signature (claimed key ID " keyid ")")) + (setq face 'notmuch-crypto-signature-bad)) (status (setq label (concat "Unknown signature status: " status))) (t @@ -135,53 +148,117 @@ mode." :notmuch-from from) (insert "\n")))) -(declare-function notmuch-show-refresh-view "notmuch-show" (&optional reset-state)) - (defun notmuch-crypto-sigstatus-good-callback (button) - (let* ((sigstatus (button-get button :notmuch-sigstatus)) + (let* ((id (notmuch-show-get-message-id)) + (sigstatus (button-get button :notmuch-sigstatus)) (fingerprint (concat "0x" (plist-get sigstatus :fingerprint))) (buffer (get-buffer-create "*notmuch-crypto-gpg-out*")) - (window (display-buffer buffer t nil))) + (window (display-buffer buffer))) (with-selected-window window (with-current-buffer buffer (goto-char (point-max)) - (call-process epg-gpg-program nil t t "--batch" "--no-tty" "--list-keys" fingerprint)) + (insert (format "-- Key %s in message %s:\n" + fingerprint id)) + (call-process notmuch-crypto-gpg-program nil t t + "--batch" "--no-tty" "--list-keys" fingerprint)) (recenter -1)))) +(declare-function notmuch-show-refresh-view "notmuch-show" (&optional reset-state)) +(declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare)) + +(defun notmuch-crypto--async-key-sentinel (process event) + "When the user asks for a GPG key to be retrieved +asynchronously, handle completion of that task. + +If the retrieval is successful, the thread where the retrieval +was initiated is still displayed and the cursor has not moved, +redisplay the thread." + (let ((status (process-status process)) + (exit-status (process-exit-status process)) + (keyid (process-get process :gpg-key-id))) + (when (memq status '(exit signal)) + (message "Getting the GPG key %s asynchronously...%s." + keyid + (if (= exit-status 0) + "completed" + "failed")) + ;; If the original buffer is still alive and point didn't move + ;; (i.e. the user didn't move on or away), refresh the buffer to + ;; show the updated signature status. + (let ((show-buffer (process-get process :notmuch-show-buffer)) + (show-point (process-get process :notmuch-show-point))) + (when (and (bufferp show-buffer) + (buffer-live-p show-buffer) + (= show-point + (with-current-buffer show-buffer + (point)))) + (with-current-buffer show-buffer + (notmuch-show-refresh-view))))))) + +(defun notmuch-crypto--set-button-label (button label) + "Set the text displayed in BUTTON to LABEL." + (save-excursion + (let ((inhibit-read-only t)) + ;; This knows rather too much about how we typically format + ;; buttons. + (goto-char (button-start button)) + (forward-char 2) + (delete-region (point) (- (button-end button) 2)) + (insert label)))) + (defun notmuch-crypto-sigstatus-error-callback (button) + "When signature validation has failed, try to retrieve the +corresponding key when the status button is pressed." (let* ((sigstatus (button-get button :notmuch-sigstatus)) (keyid (concat "0x" (plist-get sigstatus :keyid))) - (buffer (get-buffer-create "*notmuch-crypto-gpg-out*")) - (window (display-buffer buffer t nil))) - (with-selected-window window - (with-current-buffer buffer - (goto-char (point-max)) - (call-process epg-gpg-program nil t t "--batch" "--no-tty" "--recv-keys" keyid) - (insert "\n") - (call-process epg-gpg-program nil t t "--batch" "--no-tty" "--list-keys" keyid)) - (recenter -1)) - (notmuch-show-refresh-view))) + (buffer (get-buffer-create "*notmuch-crypto-gpg-out*"))) + (if notmuch-crypto-get-keys-asynchronously + (progn + (notmuch-crypto--set-button-label + button (format "Retrieving key %s asynchronously..." keyid)) + (with-current-buffer buffer + (goto-char (point-max)) + (insert (format "--- Retrieving key %s:\n" keyid))) + (let ((p (make-process + :name "notmuch GPG key retrieval" + :connection-type 'pipe + :buffer buffer + :stderr buffer + :command (list notmuch-crypto-gpg-program "--recv-keys" keyid) + :sentinel #'notmuch-crypto--async-key-sentinel))) + (process-put p :gpg-key-id keyid) + (process-put p :notmuch-show-buffer (current-buffer)) + (process-put p :notmuch-show-point (point)) + (message "Getting the GPG key %s asynchronously..." keyid))) + (let ((window (display-buffer buffer))) + (with-selected-window window + (with-current-buffer buffer + (goto-char (point-max)) + (insert (format "--- Retrieving key %s:\n" keyid)) + (call-process notmuch-crypto-gpg-program nil t t "--recv-keys" keyid) + (insert "\n") + (call-process notmuch-crypto-gpg-program nil t t "--list-keys" keyid)) + (recenter -1)) + (notmuch-show-refresh-view))))) (defun notmuch-crypto-insert-encstatus-button (encstatus) - (let* ((status (plist-get encstatus :status)) - (help-msg nil) - (label "Decryption not attempted") - (face 'notmuch-crypto-decryption)) - (cond - ((string= status "good") - (setq label "Decryption successful")) - ((string= status "bad") - (setq label "Decryption error")) - (t - (setq label (concat "Unknown encryption status" - (if status (concat ": " status)))))) - (insert-button - (concat "[ " label " ]") - :type 'notmuch-crypto-status-button-type - 'help-echo help-msg - 'face face - 'mouse-face face) - (insert "\n"))) + "Insert a button describing the encryption status ENCSTATUS." + (insert-button + (concat "[ " + (let ((status (plist-get encstatus :status))) + (cond + ((string= status "good") + "Decryption successful") + ((string= status "bad") + "Decryption error") + (t + (concat "Unknown encryption status" + (and status (concat ": " status)))))) + " ]") + :type 'notmuch-crypto-status-button-type + 'face 'notmuch-crypto-decryption + 'mouse-face 'notmuch-crypto-decryption) + (insert "\n")) ;; diff --git a/emacs/notmuch-draft.el b/emacs/notmuch-draft.el index e22e0d16..283830ad 100644 --- a/emacs/notmuch-draft.el +++ b/emacs/notmuch-draft.el @@ -76,7 +76,7 @@ postponing and resuming a message." (defcustom notmuch-draft-save-plaintext 'ask "Should notmuch save/postpone in plaintext messages that seem - like they are intended to be sent encrypted +like they are intended to be sent encrypted (i.e with an mml encryption tag in it)." :type '(radio (const :tag "Never" nil) @@ -87,10 +87,10 @@ postponing and resuming a message." (defvar notmuch-draft-encryption-tag-regex "<#\\(part encrypt\\|secure.*mode=.*encrypt>\\)" - "Regular expression matching mml tags indicating encryption of part or message") + "Regular expression matching mml tags indicating encryption of part or message.") (defvar notmuch-draft-id nil - "Message-id of the most recent saved draft of this message") + "Message-id of the most recent saved draft of this message.") (make-variable-buffer-local 'notmuch-draft-id) (defun notmuch-draft--mark-deleted () @@ -152,16 +152,18 @@ Used when a new version is saved, or the message is sent." "Checks if we should save a message that should be encrypted. `notmuch-draft-save-plaintext' controls the behaviour." - (case notmuch-draft-save-plaintext - ((ask) - (unless (yes-or-no-p "(Customize `notmuch-draft-save-plaintext' to avoid this warning) + (cl-case notmuch-draft-save-plaintext + ((ask) + (unless (yes-or-no-p + "(Customize `notmuch-draft-save-plaintext' to avoid this warning) This message contains mml tags that suggest it is intended to be encrypted. Really save and index an unencrypted copy? ") - (error "Save aborted"))) - ((nil) - (error "Refusing to save draft with encryption tags (see `notmuch-draft-save-plaintext')")) - ((t) - (ignore)))) + (error "Save aborted"))) + ((nil) + (error "Refusing to save draft with encryption tags (see `%s')" + 'notmuch-draft-save-plaintext)) + ((t) + (ignore)))) (defun notmuch-draft--make-message-id () ;; message-make-message-id gives the id inside a "<" ">" pair, @@ -192,14 +194,16 @@ applied to newly inserted messages)." (message-remove-header "Message-ID") (message-add-header (concat "Message-ID: <" id ">"))) (t - (message "You have customized emacs so Message-ID is not a deletable header, so not changing it") + (message "You have customized emacs so Message-ID is not a %s" + "deletable header, so not changing it") (setq id nil))) (cond ((member 'Date message-deletable-headers) (message-remove-header "Date") (message-add-header (concat "Date: " (message-make-date)))) (t - (message "You have customized emacs so Date is not a deletable header, so not changing it"))) + (message "You have customized emacs so Date is not a deletable %s" + "header, so not changing it"))) (message-add-header "X-Notmuch-Emacs-Draft: True") (notmuch-draft-quote-some-mml) (notmuch-maildir-setup-message-for-saving) @@ -228,7 +232,8 @@ applied to newly inserted messages)." (draft (equal tags (notmuch-update-tags tags notmuch-draft-tags)))) (when (or draft (yes-or-no-p "Message does not appear to be a draft: edit as new? ")) - (switch-to-buffer (get-buffer-create (concat "*notmuch-draft-" id "*"))) + (pop-to-buffer-same-window + (get-buffer-create (concat "*notmuch-draft-" id "*"))) (setq buffer-read-only nil) (erase-buffer) (let ((coding-system-for-read 'no-conversion)) @@ -259,7 +264,7 @@ applied to newly inserted messages)." ;; If the resumed message was a draft then set the draft ;; message-id so that we can delete the current saved draft if the ;; message is resaved or sent. - (setq notmuch-draft-id (when draft id))))) + (setq notmuch-draft-id (and draft id))))) (add-hook 'message-send-hook 'notmuch-draft--mark-deleted) diff --git a/emacs/notmuch-emacs-mua.desktop b/emacs/notmuch-emacs-mua.desktop index 0d9af2a4..752a1d7b 100644 --- a/emacs/notmuch-emacs-mua.desktop +++ b/emacs/notmuch-emacs-mua.desktop @@ -8,3 +8,4 @@ Icon=emblem-mail Terminal=false Type=Application Categories=Network;Email; +Keywords=Mail;E-mail;Email; diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index aff8beb5..bb60a890 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -21,17 +21,22 @@ ;;; Code: -(eval-when-compile (require 'cl)) +(eval-when-compile (require 'cl-lib)) + (require 'widget) (require 'wid-edit) ; For `widget-forward'. (require 'notmuch-lib) (require 'notmuch-mua) -(declare-function notmuch-search "notmuch" (&optional query oldest-first target-thread target-line continuation)) +(declare-function notmuch-search "notmuch" + (&optional query oldest-first target-thread target-line continuation)) (declare-function notmuch-poll "notmuch" ()) (declare-function notmuch-tree "notmuch-tree" - (&optional query query-context target buffer-name open-target)) + (&optional query query-context target buffer-name open-target unthreaded)) +(declare-function notmuch-unthreaded + (&optional query query-context target buffer-name open-target)) + (defun notmuch-saved-search-get (saved-search field) "Get FIELD from SAVED-SEARCH. @@ -44,17 +49,19 @@ lists (NAME QUERY COUNT-QUERY)." ((keywordp (car saved-search)) (plist-get saved-search field)) ;; It is not a plist so it is an old-style entry. - ((consp (cdr saved-search)) ;; It is a list (NAME QUERY COUNT-QUERY) - (case field - (:name (first saved-search)) - (:query (second saved-search)) - (:count-query (third saved-search)) - (t nil))) - (t ;; It is a cons-cell (NAME . QUERY) - (case field - (:name (car saved-search)) - (:query (cdr saved-search)) - (t nil))))) + ((consp (cdr saved-search)) + (pcase-let ((`(,name ,query ,count-query) saved-search)) + (cl-case field + (:name name) + (:query query) + (:count-query count-query) + (t nil)))) + (t + (pcase-let ((`(,name . ,query) saved-search)) + (cl-case field + (:name name) + (:query query) + (t nil)))))) (defun notmuch-hello-saved-search-to-plist (saved-search) "Return a copy of SAVED-SEARCH in plist form. @@ -63,7 +70,7 @@ If saved search is a plist then just return a copy. In other cases, for backwards compatibility, convert to plist form and return that." (if (keywordp (car saved-search)) - (copy-seq saved-search) + (copy-sequence saved-search) (let ((fields (list :name :query :count-query)) plist-search) (dolist (field fields plist-search) @@ -85,21 +92,32 @@ searches so they still work in customize." :tag "Saved Search" :args '((list :inline t :format "%v" - (group :format "%v" :inline t (const :format " Name: " :name) (string :format "%v")) - (group :format "%v" :inline t (const :format " Query: " :query) (string :format "%v"))) + (group :format "%v" :inline t + (const :format " Name: " :name) + (string :format "%v")) + (group :format "%v" :inline t + (const :format " Query: " :query) + (string :format "%v"))) (checklist :inline t :format "%v" - (group :format "%v" :inline t (const :format "Shortcut key: " :key) (key-sequence :format "%v")) - (group :format "%v" :inline t (const :format "Count-Query: " :count-query) (string :format "%v")) - (group :format "%v" :inline t (const :format "" :sort-order) + (group :format "%v" :inline t + (const :format "Shortcut key: " :key) + (key-sequence :format "%v")) + (group :format "%v" :inline t + (const :format "Count-Query: " :count-query) + (string :format "%v")) + (group :format "%v" :inline t + (const :format "" :sort-order) (choice :tag " Sort Order" (const :tag "Default" nil) (const :tag "Oldest-first" oldest-first) (const :tag "Newest-first" newest-first))) - (group :format "%v" :inline t (const :format "" :search-type) + (group :format "%v" :inline t + (const :format "" :search-type) (choice :tag " Search Type" (const :tag "Search mode" nil) - (const :tag "Tree mode" tree)))))) + (const :tag "Tree mode" tree) + (const :tag "Unthreaded mode" unthreaded)))))) (defcustom notmuch-saved-searches `((:name "inbox" :query "tag:inbox" :key ,(kbd "i")) @@ -122,17 +140,16 @@ a plist. Supported properties are :sort-order Specify the sort order to be used for the search. Possible values are 'oldest-first 'newest-first or nil. Nil means use the default sort order. - :search-type Specify whether to run the search in search-mode - or tree mode. Set to 'tree to specify tree - mode, set to nil (or anything except tree) to - specify search mode. + :search-type Specify whether to run the search in search-mode, + tree mode or unthreaded mode. Set to 'tree to specify tree + mode, 'unthreaded to specify unthreaded mode, and set to nil + (or anything except tree and unthreaded) to specify search mode. Other accepted forms are a cons cell of the form (NAME . QUERY) or a list of the form (NAME QUERY COUNT-QUERY)." -;; The saved-search format is also used by the all-tags notmuch-hello -;; section. This section generates its own saved-search list in one of -;; the latter two forms. - + ;; The saved-search format is also used by the all-tags notmuch-hello + ;; section. This section generates its own saved-search list in one of + ;; the latter two forms. :get 'notmuch-hello--saved-searches-to-plist :type '(repeat notmuch-saved-search-plist) :tag "List of Saved Searches" @@ -352,7 +369,7 @@ supported for \"Customized queries section\" items." :type 'boolean) (defvar notmuch-hello-hidden-sections nil - "List of sections titles whose contents are hidden") + "List of sections titles whose contents are hidden.") (defvar notmuch-hello-first-run t "True if `notmuch-hello' is run for the first time, set to nil @@ -365,10 +382,10 @@ afterwards.") (setq n (/ n 1000))) (setq result (or result '(0))) (apply #'concat - (number-to-string (car result)) - (mapcar (lambda (elem) - (format "%s%03d" notmuch-hello-thousands-separator elem)) - (cdr result))))) + (number-to-string (car result)) + (mapcar (lambda (elem) + (format "%s%03d" notmuch-hello-thousands-separator elem)) + (cdr result))))) (defun notmuch-hello-trim (search) "Trim whitespace." @@ -392,10 +409,10 @@ afterwards.") notmuch-saved-searches))) ;; If an existing saved search with this name exists, remove it. (setq notmuch-saved-searches - (loop for elem in notmuch-saved-searches - if (not (equal name - (notmuch-saved-search-get elem :name))) - collect elem)) + (cl-loop for elem in notmuch-saved-searches + if (not (equal name + (notmuch-saved-search-get elem :name))) + collect elem)) ;; Add the new one. (customize-save-variable 'notmuch-saved-searches (add-to-list 'notmuch-saved-searches @@ -413,37 +430,42 @@ afterwards.") (notmuch-hello-update))) (defun notmuch-hello-longest-label (searches-alist) - (or (loop for elem in searches-alist - maximize (length (notmuch-saved-search-get elem :name))) + (or (cl-loop for elem in searches-alist + maximize (length (notmuch-saved-search-get elem :name))) 0)) (defun notmuch-hello-reflect-generate-row (ncols nrows row list) (let ((len (length list))) - (loop for col from 0 to (- ncols 1) - collect (let ((offset (+ (* nrows col) row))) - (if (< offset len) - (nth offset list) - ;; Don't forget to insert an empty slot in the - ;; output matrix if there is no corresponding - ;; value in the input matrix. - nil))))) + (cl-loop for col from 0 to (- ncols 1) + collect (let ((offset (+ (* nrows col) row))) + (if (< offset len) + (nth offset list) + ;; Don't forget to insert an empty slot in the + ;; output matrix if there is no corresponding + ;; value in the input matrix. + nil))))) (defun notmuch-hello-reflect (list ncols) "Reflect a `ncols' wide matrix represented by `list' along the diagonal." ;; Not very lispy... (let ((nrows (ceiling (length list) ncols))) - (loop for row from 0 to (- nrows 1) - append (notmuch-hello-reflect-generate-row ncols nrows row list)))) + (cl-loop for row from 0 to (- nrows 1) + append (notmuch-hello-reflect-generate-row ncols nrows row list)))) (defun notmuch-hello-widget-search (widget &rest ignore) - (if (widget-get widget :notmuch-search-type) - (notmuch-tree (widget-get widget - :notmuch-search-terms)) + (cond + ((eq (widget-get widget :notmuch-search-type) 'tree) + (notmuch-tree (widget-get widget + :notmuch-search-terms))) + ((eq (widget-get widget :notmuch-search-type) 'unthreaded) + (notmuch-unthreaded (widget-get widget + :notmuch-search-terms))) + (t (notmuch-search (widget-get widget :notmuch-search-terms) (widget-get widget - :notmuch-search-oldest-first)))) + :notmuch-search-oldest-first))))) (defun notmuch-saved-search-count (search) (car (process-lines notmuch-command "count" search))) @@ -459,19 +481,17 @@ should be. Returns a cons cell `(tags-per-line width)'." ;; Count is 9 wide (8 digits plus space), 1 for the space ;; after the name. (+ 9 1 (max notmuch-column-control widest))))) - ((floatp notmuch-column-control) (let* ((available-width (- (window-width) notmuch-hello-indent)) - (proposed-width (max (* available-width notmuch-column-control) widest))) + (proposed-width (max (* available-width notmuch-column-control) + widest))) (floor available-width proposed-width))) - (t (max 1 (/ (- (window-width) notmuch-hello-indent) ;; Count is 9 wide (8 digits plus space), 1 for the space ;; after the name. (+ 9 1 widest))))))) - (cons tags-per-line (/ (max 1 (- (window-width) notmuch-hello-indent ;; Count is 9 wide (8 digits plus @@ -489,8 +509,7 @@ If FILTER is a function, it is called with QUERY as a parameter and the string it returns is used as the query. If nil is returned, the entry is hidden. -Otherwise, FILTER is ignored. -" +Otherwise, FILTER is ignored." (cond ((functionp filter) (funcall filter query)) ((stringp filter) @@ -521,17 +540,15 @@ options will be handled as specified for (notmuch-hello-filtered-query count-query (or (plist-get options :filter-count) (plist-get options :filter)))) - "\n"))) - + "\n"))) (unless (= (call-process-region (point-min) (point-max) notmuch-command t t nil "count" "--batch") 0) - (notmuch-logged-error "notmuch count --batch failed" - "Please check that the notmuch CLI is new enough to support `count + (notmuch-logged-error + "notmuch count --batch failed" + "Please check that the notmuch CLI is new enough to support `count --batch'. In general we recommend running matching versions of the CLI and emacs interface.")) - (goto-char (point-min)) - (notmuch-remove-if-not #'identity (mapcar @@ -542,7 +559,8 @@ the CLI and emacs interface.")) search-query (plist-get options :filter))) (message-count (prog1 (read (current-buffer)) (forward-line 1)))) - (when (and filtered-query (or (plist-get options :show-empty-searches) (> message-count 0))) + (when (and filtered-query (or (plist-get options :show-empty-searches) + (> message-count 0))) (setq elem-plist (plist-put elem-plist :query filtered-query)) (plist-put elem-plist :count message-count)))) query-list)))) @@ -571,15 +589,15 @@ with `notmuch-hello-query-counts'." (mapc (lambda (elem) ;; (not elem) indicates an empty slot in the matrix. (when elem - (if (> column-indent 0) - (widget-insert (make-string column-indent ? ))) + (when (> column-indent 0) + (widget-insert (make-string column-indent ? ))) (let* ((name (plist-get elem :name)) (query (plist-get elem :query)) - (oldest-first (case (plist-get elem :sort-order) + (oldest-first (cl-case (plist-get elem :sort-order) (newest-first nil) (oldest-first t) (otherwise notmuch-search-oldest-first))) - (search-type (eq (plist-get elem :search-type) 'tree)) + (search-type (plist-get elem :search-type)) (msg-count (plist-get elem :count))) (widget-insert (format "%8s " (notmuch-hello-nice-number msg-count))) @@ -591,12 +609,11 @@ with `notmuch-hello-query-counts'." name) (setq column-indent (1+ (max 0 (- column-width (length name))))))) - (setq count (1+ count)) + (cl-incf count) (when (eq (% count tags-per-line) 0) (setq column-indent 0) (widget-insert "\n"))) reordered-list) - ;; If the last line was not full (and hence did not include a ;; carriage return), insert one now. (unless (eq (% count tags-per-line) 0) @@ -621,7 +638,7 @@ with `notmuch-hello-query-counts'." (dolist (window (window-list)) (let ((last-buf (window-parameter window 'notmuch-hello-last-buffer)) (cur-buf (window-buffer window))) - (when (not (eq last-buf cur-buf)) + (unless (eq last-buf cur-buf) ;; This window changed or is new. Update recorded buffer ;; for next time. (set-window-parameter window 'notmuch-hello-last-buffer cur-buf) @@ -635,7 +652,7 @@ with `notmuch-hello-query-counts'." ;; 24, we can't do it right here because something in this ;; hook's call stack overrides hello's point placement. (run-at-time nil nil #'notmuch-hello t)) - (when (null hello-buf) + (unless hello-buf ;; Clean up hook (remove-hook 'window-configuration-change-hook #'notmuch-hello-window-configuration-change)))) @@ -644,7 +661,7 @@ with `notmuch-hello-query-counts'." (defvar notmuch-emacs-version) (defun notmuch-hello-versions () - "Display the notmuch version(s)" + "Display the notmuch version(s)." (interactive) (let ((notmuch-cli-version (notmuch-cli-version))) (message "notmuch version %s" @@ -670,10 +687,9 @@ with `notmuch-hello-query-counts'." (define-key map (kbd "") 'widget-backward) map) "Keymap for \"notmuch hello\" buffers.") -(fset 'notmuch-hello-mode-map notmuch-hello-mode-map) (define-derived-mode notmuch-hello-mode fundamental-mode "notmuch-hello" - "Major mode for convenient notmuch navigation. This is your entry portal into notmuch. + "Major mode for convenient notmuch navigation. This is your entry portal into notmuch. Saved searches are \"bookmarks\" for arbitrary queries. Hit RET or click on a saved search to view matching threads. Edit saved @@ -703,9 +719,9 @@ The screen may be customized via `\\[customize]'. Complete list of currently available key bindings: \\{notmuch-hello-mode-map}" - (setq notmuch-buffer-refresh-function #'notmuch-hello-update) - ;;(setq buffer-read-only t) -) + (setq notmuch-buffer-refresh-function #'notmuch-hello-update) + ;;(setq buffer-read-only t) + ) (defun notmuch-hello-generate-tag-alist (&optional hide-tags) "Return an alist from tags to queries to display in the all-tags section." @@ -729,7 +745,9 @@ Complete list of currently available key bindings: ;; dark background. (setq image (cons 'image (append (cdr image) - (list :background (face-background 'notmuch-hello-logo-background))))) + (list :background + (face-background + 'notmuch-hello-logo-background))))) (insert-image image)) (widget-insert " ")) @@ -749,10 +767,10 @@ Complete list of currently available key bindings: (notmuch-hello-update)) :help-echo "Refresh" (notmuch-hello-nice-number - (string-to-number (car (process-lines notmuch-command "count"))))) + (string-to-number + (car (process-lines notmuch-command "count"))))) (widget-insert " messages.\n"))) - (defun notmuch-hello-insert-saved-searches () "Insert the saved-searches section." (let ((searches (notmuch-hello-query-counts @@ -803,48 +821,48 @@ Complete list of currently available key bindings: "clear") (widget-insert "\n\n") (let ((start (point))) - (loop for i from 1 to notmuch-hello-recent-searches-max - for search in notmuch-search-history do - (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i)))) - (set widget-symbol - (widget-create 'editable-field - ;; Don't let the search boxes be - ;; less than 8 characters wide. - :size (max 8 - (- (window-width) - ;; Leave some space - ;; at the start and - ;; end of the - ;; boxes. - (* 2 notmuch-hello-indent) - ;; 1 for the space - ;; before the - ;; `[save]' button. 6 - ;; for the `[save]' - ;; button. - 1 6 - ;; 1 for the space - ;; before the `[del]' - ;; button. 5 for the - ;; `[del]' button. - 1 5)) - :action (lambda (widget &rest ignore) - (notmuch-hello-search (widget-value widget))) - search)) - (widget-insert " ") - (widget-create 'push-button - :notify (lambda (widget &rest ignore) - (notmuch-hello-add-saved-search widget)) - :notmuch-saved-search-widget widget-symbol - "save") - (widget-insert " ") - (widget-create 'push-button - :notify (lambda (widget &rest ignore) - (when (y-or-n-p "Are you sure you want to delete this search? ") - (notmuch-hello-delete-search-from-history widget))) - :notmuch-saved-search-widget widget-symbol - "del")) - (widget-insert "\n")) + (cl-loop for i from 1 to notmuch-hello-recent-searches-max + for search in notmuch-search-history do + (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i)))) + (set widget-symbol + (widget-create 'editable-field + ;; Don't let the search boxes be + ;; less than 8 characters wide. + :size (max 8 + (- (window-width) + ;; Leave some space + ;; at the start and + ;; end of the + ;; boxes. + (* 2 notmuch-hello-indent) + ;; 1 for the space + ;; before the + ;; `[save]' button. 6 + ;; for the `[save]' + ;; button. + 1 6 + ;; 1 for the space + ;; before the `[del]' + ;; button. 5 for the + ;; `[del]' button. + 1 5)) + :action (lambda (widget &rest ignore) + (notmuch-hello-search (widget-value widget))) + search)) + (widget-insert " ") + (widget-create 'push-button + :notify (lambda (widget &rest ignore) + (notmuch-hello-add-saved-search widget)) + :notmuch-saved-search-widget widget-symbol + "save") + (widget-insert " ") + (widget-create 'push-button + :notify (lambda (widget &rest ignore) + (when (y-or-n-p "Are you sure you want to delete this search? ") + (notmuch-hello-delete-search-from-history widget))) + :notmuch-saved-search-widget widget-symbol + "del")) + (widget-insert "\n")) (indent-rigidly start (point) notmuch-hello-indent)) nil)) @@ -871,8 +889,8 @@ Supports the following entries in OPTIONS as a plist: the same values as :filter. If :filter and :filter-count are specified, this will be used instead of :filter, not in conjunction with it." (widget-insert title ": ") - (if (and notmuch-hello-first-run (plist-get options :initially-hidden)) - (add-to-list 'notmuch-hello-hidden-sections title)) + (when (and notmuch-hello-first-run (plist-get options :initially-hidden)) + (add-to-list 'notmuch-hello-hidden-sections title)) (let ((is-hidden (member title notmuch-hello-hidden-sections)) (start (point))) (if is-hidden @@ -889,7 +907,7 @@ Supports the following entries in OPTIONS as a plist: (notmuch-hello-update)) "hide")) (widget-insert "\n") - (when (not is-hidden) + (unless is-hidden (let ((searches (apply 'notmuch-hello-query-counts query-list options))) (when (or (not (plist-get options :hide-if-empty)) searches) @@ -911,7 +929,7 @@ following: options)) (defun notmuch-hello-insert-inbox () - "Show an entry for each saved search and inboxed messages for each tag" + "Show an entry for each saved search and inboxed messages for each tag." (notmuch-hello-insert-searches "What's in your inbox" (append notmuch-saved-searches @@ -919,7 +937,7 @@ following: :filter "tag:inbox")) (defun notmuch-hello-insert-alltags () - "Insert a section displaying all tags and associated message counts" + "Insert a section displaying all tags and associated message counts." (notmuch-hello-insert-tags-section nil :initially-hidden (not notmuch-show-all-tags-list) @@ -949,40 +967,32 @@ following: (defun notmuch-hello (&optional no-display) "Run notmuch and display saved searches, known tags, etc." (interactive) - (notmuch-assert-cli-sane) ;; This may cause a window configuration change, so if the ;; auto-refresh hook is already installed, avoid recursive refresh. (let ((notmuch-hello-auto-refresh nil)) (if no-display (set-buffer "*notmuch-hello*") - (switch-to-buffer "*notmuch-hello*"))) - + (pop-to-buffer-same-window "*notmuch-hello*"))) ;; Install auto-refresh hook (when notmuch-hello-auto-refresh (add-hook 'window-configuration-change-hook #'notmuch-hello-window-configuration-change)) - (let ((target-line (line-number-at-pos)) (target-column (current-column)) (inhibit-read-only t)) - ;; Delete all editable widget fields. Editable widget fields are ;; tracked in a buffer local variable `widget-field-list' (and ;; others). If we do `erase-buffer' without properly deleting the ;; widgets, some widget-related functions are confused later. (mapc 'widget-delete widget-field-list) - (erase-buffer) - (unless (eq major-mode 'notmuch-hello-mode) (notmuch-hello-mode)) - (let ((all (overlay-lists))) ;; Delete all the overlays. (mapc 'delete-overlay (car all)) (mapc 'delete-overlay (cdr all))) - (mapc (lambda (section) (let ((point-before (point))) @@ -995,7 +1005,6 @@ following: (widget-insert "\n")))) notmuch-hello-sections) (widget-setup) - ;; Move point back to where it was before refresh. Use line and ;; column instead of point directly to be insensitive to additions ;; and removals of text within earlier lines. diff --git a/emacs/notmuch-jump.el b/emacs/notmuch-jump.el index 3e20b8c7..1e2d0497 100644 --- a/emacs/notmuch-jump.el +++ b/emacs/notmuch-jump.el @@ -22,7 +22,9 @@ ;;; Code: -(eval-when-compile (require 'cl)) +(eval-when-compile + (require 'cl-lib) + (require 'pcase)) (require 'notmuch-lib) (require 'notmuch-hello) @@ -41,7 +43,6 @@ keys configured in the :key property of `notmuch-saved-searches'. Typically these shortcuts are a single key long, so this is a fast way to jump to a saved search from anywhere in Notmuch." (interactive) - ;; Build the action map (let (action-map) (dolist (saved-search notmuch-saved-searches) @@ -51,23 +52,28 @@ fast way to jump to a saved search from anywhere in Notmuch." (let ((name (plist-get saved-search :name)) (query (plist-get saved-search :query)) (oldest-first - (case (plist-get saved-search :sort-order) + (cl-case (plist-get saved-search :sort-order) (newest-first nil) (oldest-first t) (otherwise (default-value 'notmuch-search-oldest-first))))) (push (list key name - (if (eq (plist-get saved-search :search-type) 'tree) - `(lambda () (notmuch-tree ',query)) - `(lambda () (notmuch-search ',query ',oldest-first)))) + (cond + ((eq (plist-get saved-search :search-type) 'tree) + `(lambda () (notmuch-tree ',query))) + ((eq (plist-get saved-search :search-type) 'unthreaded) + `(lambda () (notmuch-unthreaded ',query))) + (t + `(lambda () (notmuch-search ',query ',oldest-first))))) action-map))))) (setq action-map (nreverse action-map)) - (if action-map (notmuch-jump action-map "Search: ") - (error "To use notmuch-jump, please customize shortcut keys in notmuch-saved-searches.")))) + (error "To use notmuch-jump, \ +please customize shortcut keys in notmuch-saved-searches.")))) (defvar notmuch-jump--action nil) +;;;###autoload (defun notmuch-jump (action-map prompt) "Interactively prompt for one of the keys in ACTION-MAP. @@ -82,9 +88,7 @@ ACTION-MAP must be a list of triples of the form where KEY is a key binding, LABEL is a string label to display in the buffer, and ACTION is a nullary function to call. LABEL may be null, in which case the action will still be bound, but will -not appear in the pop-up buffer. -" - +not appear in the pop-up buffer." (let* ((items (notmuch-jump--format-actions action-map)) ;; Format the table of bindings and the full prompt (table @@ -109,7 +113,6 @@ not appear in the pop-up buffer. (notmuch-jump--action nil)) ;; Read the action (read-from-minibuffer full-prompt nil minibuffer-map) - ;; If we got an action, do it (when notmuch-jump--action (funcall notmuch-jump--action)))) @@ -120,21 +123,18 @@ not appear in the pop-up buffer. Returns a list of strings, one for each item with a label in ACTION-MAP. These strings can be inserted into a tabular buffer." - ;; Compute the maximum key description width (let ((key-width 1)) - (dolist (entry action-map) + (pcase-dolist (`(,key ,desc) action-map) (setq key-width (max key-width - (string-width (format-kbd-macro (first entry)))))) + (string-width (format-kbd-macro key))))) ;; Format each action - (mapcar (lambda (entry) - (let ((key (format-kbd-macro (first entry))) - (desc (second entry))) - (concat - (propertize key 'face 'minibuffer-prompt) - (make-string (- key-width (length key)) ? ) - " " desc))) + (mapcar (pcase-lambda (`(,key ,desc)) + (setq key (format-kbd-macro key)) + (concat (propertize key 'face 'minibuffer-prompt) + (make-string (- key-width (length key)) ? ) + " " desc)) action-map))) (defun notmuch-jump--insert-items (width items) @@ -169,39 +169,38 @@ buffer." "Translate ACTION-MAP into a minibuffer keymap." (let ((map (make-sparse-keymap))) (set-keymap-parent map notmuch-jump-minibuffer-map) - (dolist (action action-map) - (if (= (length (first action)) 1) - (define-key map (first action) - `(lambda () (interactive) - (setq notmuch-jump--action ',(third action)) - (exit-minibuffer))))) + (pcase-dolist (`(,key ,name ,fn) action-map) + (when (= (length key) 1) + (define-key map key + `(lambda () (interactive) + (setq notmuch-jump--action ',fn) + (exit-minibuffer))))) ;; By doing this in two passes (and checking if we already have a ;; binding) we avoid problems if the user specifies a binding which ;; is a prefix of another binding. - (dolist (action action-map) - (if (> (length (first action)) 1) - (let* ((key (elt (first action) 0)) - (keystr (string key)) - (new-prompt (concat prompt (format-kbd-macro keystr) " ")) - (action-submap nil)) - (unless (lookup-key map keystr) - (dolist (act action-map) - (when (= key (elt (first act) 0)) - (push (list (substring (first act) 1) - (second act) - (third act)) - action-submap))) - ;; We deal with backspace specially - (push (list (kbd "DEL") - "Backup" - (apply-partially #'notmuch-jump action-map prompt)) - action-submap) - (setq action-submap (nreverse action-submap)) - (define-key map keystr - `(lambda () (interactive) - (setq notmuch-jump--action - ',(apply-partially #'notmuch-jump action-submap new-prompt)) - (exit-minibuffer))))))) + (pcase-dolist (`(,key ,name ,fn) action-map) + (when (> (length key) 1) + (let* ((key (elt key 0)) + (keystr (string key)) + (new-prompt (concat prompt (format-kbd-macro keystr) " ")) + (action-submap nil)) + (unless (lookup-key map keystr) + (pcase-dolist (`(,k ,n ,f) action-map) + (when (= key (elt k 0)) + (push (list (substring k 1) n f) action-submap))) + ;; We deal with backspace specially + (push (list (kbd "DEL") + "Backup" + (apply-partially #'notmuch-jump action-map prompt)) + action-submap) + (setq action-submap (nreverse action-submap)) + (define-key map keystr + `(lambda () (interactive) + (setq notmuch-jump--action + ',(apply-partially #'notmuch-jump + action-submap + new-prompt)) + (exit-minibuffer))))))) map)) ;; diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index 8acad267..118faf1e 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -19,23 +19,20 @@ ;; ;; Authors: Carl Worth -;; This is an part of an emacs-based interface to the notmuch mail system. - ;;; Code: +(require 'cl-lib) + (require 'mm-util) (require 'mm-view) (require 'mm-decode) -(require 'cl) + (require 'notmuch-compat) (unless (require 'notmuch-version nil t) (defconst notmuch-emacs-version "unknown" "Placeholder variable when notmuch-version.el[c] is not available.")) -(autoload 'notmuch-jump-search "notmuch-jump" - "Jump to a saved search by shortcut key." t) - (defgroup notmuch nil "Notmuch mail reader for Emacs." :group 'mail) @@ -54,9 +51,8 @@ (defgroup notmuch-send nil "Sending messages from Notmuch." - :group 'notmuch) - -(custom-add-to-group 'notmuch-send 'message 'custom-group) + :group 'notmuch + :group 'message) (defgroup notmuch-tag nil "Tags and tagging in Notmuch." @@ -153,7 +149,9 @@ For example, if you wanted to remove an \"inbox\" tag and add an (define-key map "?" 'notmuch-help) (define-key map "q" 'notmuch-bury-or-kill-this-buffer) (define-key map "s" 'notmuch-search) + (define-key map "t" 'notmuch-search-by-tag) (define-key map "z" 'notmuch-tree) + (define-key map "u" 'notmuch-unthreaded) (define-key map "m" 'notmuch-mua-new-mail) (define-key map "g" 'notmuch-refresh-this-buffer) (define-key map "=" 'notmuch-refresh-this-buffer) @@ -185,7 +183,7 @@ If notmuch exits with a non-zero status, output from the process will appear in a buffer named \"*Notmuch errors*\" and an error will be signaled. -Otherwise the output will be returned" +Otherwise the output will be returned." (with-temp-buffer (let* ((status (apply #'call-process notmuch-command nil t nil args)) (output (buffer-string))) @@ -207,7 +205,7 @@ Otherwise the output will be returned" (unless (notmuch-cli-sane-p) (notmuch-logged-error "notmuch cli seems misconfigured or unconfigured." -"Perhaps you haven't run \"notmuch setup\" yet? Try running this + "Perhaps you haven't run \"notmuch setup\" yet? Try running this on the command line, and then retry your notmuch command"))) (defun notmuch-cli-version () @@ -255,11 +253,13 @@ on the command line, and then retry your notmuch command"))) Invokes `notmuch-poll-script', \"notmuch new\", or does nothing depending on the value of `notmuch-poll-script'." (interactive) + (message "Polling mail...") (if (stringp notmuch-poll-script) (unless (string= notmuch-poll-script "") (unless (equal (call-process notmuch-poll-script nil nil) 0) (error "Notmuch: poll script `%s' failed!" notmuch-poll-script))) - (notmuch-call-notmuch-process "new"))) + (notmuch-call-notmuch-process "new")) + (message "Polling mail...done")) (defun notmuch-bury-or-kill-this-buffer () "Undisplay the current buffer. @@ -295,7 +295,7 @@ This is basically just `format-kbd-macro' but we also convert ESC to M-." (defun notmuch-describe-key (actual-key binding prefix ua-keys tail) - "Prepend cons cells describing prefix-arg ACTUAL-KEY and ACTUAL-KEY to TAIL + "Prepend cons cells describing prefix-arg ACTUAL-KEY and ACTUAL-KEY to TAIL. It does not prepend if ACTUAL-KEY is already listed in TAIL." (let ((key-string (concat prefix (key-description actual-key)))) @@ -312,10 +312,12 @@ It does not prepend if ACTUAL-KEY is already listed in TAIL." tail))) ;; Documentation for command (push (cons key-string - (or (and (symbolp binding) (get binding 'notmuch-doc)) - (and (functionp binding) (notmuch-documentation-first-line binding)))) + (or (and (symbolp binding) + (get binding 'notmuch-doc)) + (and (functionp binding) + (notmuch-documentation-first-line binding)))) tail))) - tail) + tail) (defun notmuch-describe-remaps (remap-keymap ua-keys base-keymap prefix tail) ;; Remappings are represented as a binding whose first "event" is @@ -323,13 +325,13 @@ It does not prepend if ACTUAL-KEY is already listed in TAIL." ;; binding whose "key" is 'remap, and whose "binding" is itself a ;; keymap that maps not from keys to commands, but from old (remapped) ;; functions to the commands to use in their stead. - (map-keymap - (lambda (command binding) - (mapc - (lambda (actual-key) - (setq tail (notmuch-describe-key actual-key binding prefix ua-keys tail))) - (where-is-internal command base-keymap))) - remap-keymap) + (map-keymap (lambda (command binding) + (mapc (lambda (actual-key) + (setq tail + (notmuch-describe-key actual-key binding + prefix ua-keys tail))) + (where-is-internal command base-keymap))) + remap-keymap) tail) (defun notmuch-describe-keymap (keymap ua-keys base-keymap &optional prefix tail) @@ -352,9 +354,13 @@ prefix argument. PREFIX and TAIL are used internally." (notmuch-describe-remaps binding ua-keys base-keymap prefix tail) (notmuch-describe-keymap - binding ua-keys base-keymap (notmuch-prefix-key-description key) tail)))) + binding ua-keys base-keymap + (notmuch-prefix-key-description key) + tail)))) (binding - (setq tail (notmuch-describe-key (vector key) binding prefix ua-keys tail))))) + (setq tail + (notmuch-describe-key (vector key) + binding prefix ua-keys tail))))) keymap) tail) @@ -364,11 +370,15 @@ prefix argument. PREFIX and TAIL are used internally." (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg) (let ((desc (save-match-data - (let* ((keymap-name (substring doc (match-beginning 1) (match-end 1))) + (let* ((keymap-name (substring doc + (match-beginning 1) + (match-end 1))) (keymap (symbol-value (intern keymap-name))) (ua-keys (where-is-internal 'universal-argument keymap t)) (desc-alist (notmuch-describe-keymap keymap ua-keys keymap)) - (desc-list (mapcar (lambda (arg) (concat (car arg) "\t" (cdr arg))) desc-alist))) + (desc-list (mapcar (lambda (arg) + (concat (car arg) "\t" (cdr arg))) + desc-alist))) (mapconcat #'identity desc-list "\n"))))) (setq doc (replace-match desc 1 1 doc))) (setq beg (match-end 0))) @@ -387,7 +397,8 @@ its prefixed behavior by setting the 'notmuch-prefix-doc property of its command symbol." (interactive) (let* ((mode major-mode) - (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t))))) + (doc (substitute-command-keys + (notmuch-substitute-command-keys (documentation mode t))))) (with-current-buffer (generate-new-buffer "*notmuch-help*") (insert doc) (goto-char (point-min)) @@ -398,17 +409,18 @@ of its command symbol." "Show help for a subkeymap." (interactive) (let* ((key (this-command-keys-vector)) - (prefix (make-vector (1- (length key)) nil)) - (i 0)) + (prefix (make-vector (1- (length key)) nil)) + (i 0)) (while (< i (length prefix)) (aset prefix i (aref key i)) - (setq i (1+ i))) - + (cl-incf i)) (let* ((subkeymap (key-binding prefix)) (ua-keys (where-is-internal 'universal-argument nil t)) (prefix-string (notmuch-prefix-key-description prefix)) - (desc-alist (notmuch-describe-keymap subkeymap ua-keys subkeymap prefix-string)) - (desc-list (mapcar (lambda (arg) (concat (car arg) "\t" (cdr arg))) desc-alist)) + (desc-alist (notmuch-describe-keymap + subkeymap ua-keys subkeymap prefix-string)) + (desc-list (mapcar (lambda (arg) (concat (car arg) "\t" (cdr arg))) + desc-alist)) (desc (mapconcat #'identity desc-list "\n"))) (with-help-window (help-buffer) (with-current-buffer standard-output @@ -469,7 +481,6 @@ This includes newlines, tabs, and other funny characters." The caller is responsible for prepending the term prefix and a colon. This performs minimal escaping in order to produce user-friendly queries." - (save-match-data (if (or (equal term "") ;; To be pessimistic, only pass through terms composed @@ -512,7 +523,7 @@ This replaces spaces, percents, and double quotes in STR with (let (out) (while list (when (funcall predicate (car list)) - (push (car list) out)) + (push (car list) out)) (setq list (cdr list))) (nreverse out))) @@ -526,11 +537,11 @@ This replaces spaces, percents, and double quotes in STR with (cdr xplist))) (defun notmuch-split-content-type (content-type) - "Split content/type into 'content' and 'type'" + "Split content/type into 'content' and 'type'." (split-string content-type "/")) (defun notmuch-match-content-type (t1 t2) - "Return t if t1 and t2 are matching content types, taking wildcards into account" + "Return t if t1 and t2 are matching content types, taking wildcards into account." (let ((st1 (notmuch-split-content-type t1)) (st2 (notmuch-split-content-type t2))) (if (or (string= (cadr st1) "*") @@ -540,12 +551,11 @@ This replaces spaces, percents, and double quotes in STR with (string= (downcase t1) (downcase t2))))) (defvar notmuch-multipart/alternative-discouraged - '( - ;; Avoid HTML parts. + '(;; Avoid HTML parts. "text/html" - ;; multipart/related usually contain a text/html part and some associated graphics. - "multipart/related" - )) + ;; multipart/related usually contain a text/html part and some + ;; associated graphics. + "multipart/related")) (defun notmuch-multipart/alternative-determine-discouraged (msg) "Return the discouraged alternatives for the specified message." @@ -572,7 +582,7 @@ for this message, if present." (defun notmuch-parts-filter-by-type (parts type) "Given a list of message parts, return a list containing the ones matching the given type." - (remove-if-not + (cl-remove-if-not (lambda (part) (notmuch-match-content-type (plist-get part :content-type) type)) parts)) @@ -594,12 +604,14 @@ the given type." (set-buffer-multibyte nil)) (let ((args `("show" "--format=raw" ,(format "--part=%s" (plist-get part :id)) - ,@(when process-crypto '("--decrypt=true")) + ,@(and process-crypto '("--decrypt=true")) ,(notmuch-id-to-query (plist-get msg :id)))) (coding-system-for-read - (if binaryp 'no-conversion - (let ((coding-system (mm-charset-to-coding-system - (plist-get part :content-charset)))) + (if binaryp + 'no-conversion + (let ((coding-system + (mm-charset-to-coding-system + (plist-get part :content-charset)))) ;; Sadly, ;; `mm-charset-to-coding-system' seems ;; to return things that are not @@ -611,7 +623,8 @@ the given type." ;; charset is US-ASCII. RFC6657 ;; complicates this somewhat. 'us-ascii))))) - (apply #'call-process notmuch-command nil '(t nil) nil args) + (apply #'call-process + notmuch-command nil '(t nil) nil args) (buffer-string)))))) (when (and cache data) (plist-put part plist-elem data)) @@ -643,16 +656,14 @@ MSG (if it isn't already)." ;; Workaround: The call to `mm-display-part' below triggers a bug in ;; Emacs 24 if it attempts to use the shr renderer to display an HTML ;; part with images in it (demonstrated in 24.1 and 24.2 on Debian and -;; Fedora 17, though unreproducable in other configurations). +;; Fedora 17, though unreproducible in other configurations). ;; `mm-shr' references the variable `gnus-inhibit-images' without ;; first loading gnus-art, which defines it, resulting in a ;; void-variable error. Hence, we advise `mm-shr' to ensure gnus-art ;; is loaded. -(if (>= emacs-major-version 24) - (defadvice mm-shr (before load-gnus-arts activate) - (require 'gnus-art nil t) - (ad-disable-advice 'mm-shr 'before 'load-gnus-arts) - (ad-activate 'mm-shr))) +(define-advice mm-shr (:before (_handle) notmuch--load-gnus-args) + "Require `gnus-art' since we use its variables." + (require 'gnus-art nil t)) (defun notmuch-mm-display-part-inline (msg part content-type process-crypto) "Use the mm-decode/mm-view functions to display a part in the @@ -664,9 +675,11 @@ current buffer, if possible." ;; `gnus-decoded' charset. Otherwise, we'll fetch the binary ;; part content and let mm-* decode it. (let* ((have-content (plist-member part :content)) - (charset (if have-content 'gnus-decoded + (charset (if have-content + 'gnus-decoded (plist-get part :content-charset))) - (handle (mm-make-handle (current-buffer) `(,content-type (charset . ,charset))))) + (handle (mm-make-handle (current-buffer) + `(,content-type (charset . ,charset))))) ;; If the user wants the part inlined, insert the content and ;; test whether we are able to inline it (which includes both ;; capability and suitability tests). @@ -683,8 +696,8 @@ current buffer, if possible." ;; have symbols of the form :Header as keys, and the resulting alist will have ;; symbols of the form 'Header as keys. (defun notmuch-headers-plist-to-alist (plist) - (loop for (key value . rest) on plist by #'cddr - collect (cons (intern (substring (symbol-name key) 1)) value))) + (cl-loop for (key value . rest) on plist by #'cddr + collect (cons (intern (substring (symbol-name key) 1)) value))) (defun notmuch-face-ensure-list-form (face) "Return FACE in face list form. @@ -710,7 +723,6 @@ must be a face name (a symbol or string), a property list of face attributes, or a list of these. If START and/or END are omitted, they default to the beginning/end of OBJECT. For convenience when applied to strings, this returns OBJECT." - ;; A face property can have three forms: a face name (a string or ;; symbol), a property list, or a list of these two forms. In the ;; list case, the faces will be combined, with the earlier faces @@ -753,7 +765,6 @@ This logs MSG and EXTRA to the *Notmuch errors* buffer and signals MSG as an error. If EXTRA is non-nil, text referring the user to the *Notmuch errors* buffer will be appended to the signaled error. This function does not return." - (with-current-buffer (get-buffer-create "*Notmuch errors*") (goto-char (point-max)) (unless (bobp) @@ -766,8 +777,7 @@ signaled error. This function does not return." (insert extra) (unless (bolp) (newline))))) - (error "%s" (concat msg (when extra - " (see *Notmuch errors* for more details)")))) + (error "%s%s" msg (if extra " (see *Notmuch errors* for more details)" ""))) (defun notmuch-check-async-exit-status (proc msg &optional command err) "If PROC exited abnormally, pop up an error buffer and signal an error. @@ -778,11 +788,12 @@ arguments passed to the sentinel. COMMAND and ERR, if provided, are passed to `notmuch-check-exit-status'. If COMMAND is not provided, it is taken from `process-command'." (let ((exit-status - (case (process-status proc) + (cl-case (process-status proc) ((exit) (process-exit-status proc)) ((signal) msg)))) (when exit-status - (notmuch-check-exit-status exit-status (or command (process-command proc)) + (notmuch-check-exit-status exit-status + (or command (process-command proc)) nil err)))) (defun notmuch-check-exit-status (exit-status command &optional output err) @@ -797,7 +808,6 @@ command and its arguments. OUTPUT, if provided, is a string giving the output of command. ERR, if provided, is the error output of command. OUTPUT and ERR will be included in the error message." - (cond ((eq exit-status 0) t) ((eq exit-status 20) @@ -818,24 +828,22 @@ You may need to restart Emacs or upgrade your notmuch package.")) command " ")) (extra (concat "command: " command-string "\n" - (if (integerp exit-status) - (format "exit status: %s\n" exit-status) - (format "exit signal: %s\n" exit-status)) - (when err - (concat "stderr:\n" err)) - (when output - (concat "stdout:\n" output))))) - (if err - ;; We have an error message straight from the CLI. - (notmuch-logged-error - (replace-regexp-in-string "[ \n\r\t\f]*\\'" "" err) extra) - ;; We only have combined output from the CLI; don't inundate - ;; the user with it. Mimic `process-lines'. - (notmuch-logged-error (format "%s exited with status %s" - (car command) exit-status) - extra)) - ;; `notmuch-logged-error' does not return. - )))) + (if (integerp exit-status) + (format "exit status: %s\n" exit-status) + (format "exit signal: %s\n" exit-status)) + (and err (concat "stderr:\n" err)) + (and output (concat "stdout:\n" output))))) + (if err + ;; We have an error message straight from the CLI. + (notmuch-logged-error + (replace-regexp-in-string "[ \n\r\t\f]*\\'" "" err) extra) + ;; We only have combined output from the CLI; don't inundate + ;; the user with it. Mimic `process-lines'. + (notmuch-logged-error (format "%s exited with status %s" + (car command) exit-status) + extra)) + ;; `notmuch-logged-error' does not return. + )))) (defun notmuch-call-notmuch--helper (destination args) "Helper for synchronous notmuch invocation commands. @@ -843,12 +851,11 @@ You may need to restart Emacs or upgrade your notmuch package.")) This wraps `call-process'. DESTINATION has the same meaning as for `call-process'. ARGS is as described for `notmuch-call-notmuch-process'." - (let (stdin-string) (while (keywordp (car args)) - (case (car args) - (:stdin-string (setq stdin-string (cadr args) - args (cddr args))) + (cl-case (car args) + (:stdin-string (setq stdin-string (cadr args)) + (setq args (cddr args))) (otherwise (error "Unknown keyword argument: %s" (car args))))) (if (null stdin-string) @@ -881,7 +888,6 @@ notmuch's output as an S-expression and returns the parsed value. Like `notmuch-call-notmuch-process', if notmuch exits with a non-zero status, this will report its output and signal an error." - (with-temp-buffer (let ((err-file (make-temp-file "nmerr"))) (unwind-protect @@ -909,11 +915,10 @@ when the process exits, or nil for none. The caller must *not* invoke `set-process-sentinel' directly on the returned process, as that will interfere with the handling of stderr and the exit status." - (let (err-file err-buffer proc err-proc - ;; Find notmuch using Emacs' `exec-path' - (command (or (executable-find notmuch-command) - (error "Command not found: %s" notmuch-command)))) + ;; Find notmuch using Emacs' `exec-path' + (command (or (executable-find notmuch-command) + (error "Command not found: %s" notmuch-command)))) (if (fboundp 'make-process) (progn (setq err-buffer (generate-new-buffer " *notmuch-stderr*")) @@ -927,14 +932,13 @@ status." :buffer buffer :command (cons command args) :connection-type 'pipe - :stderr err-buffer) - err-proc (get-buffer-process err-buffer)) + :stderr err-buffer)) + (setq err-proc (get-buffer-process err-buffer)) (process-put proc 'err-buffer err-buffer) (process-put err-proc 'err-file err-file) (process-put err-proc 'err-buffer err-buffer) (set-process-sentinel err-proc #'notmuch-start-notmuch-error-sentinel)) - ;; On Emacs versions before 25, there is no way to capture ;; stdout and stderr separately for asynchronous processes, or ;; even to redirect stderr to a file, so we use a trivial shell @@ -947,7 +951,6 @@ status." "exec 2>\"$1\"; shift; exec \"$0\" \"$@\"" command err-file args))) (process-put proc 'err-file err-file)) - (process-put proc 'sub-sentinel sentinel) (process-put proc 'real-command (cons notmuch-command args)) (set-process-sentinel proc #'notmuch-start-notmuch-sentinel) @@ -958,8 +961,8 @@ status." (let* ((err-file (process-get proc 'err-file)) (err-buffer (or (process-get proc 'err-buffer) (find-file-noselect err-file))) - (err (when (not (zerop (buffer-size err-buffer))) - (with-current-buffer err-buffer (buffer-string)))) + (err (and (not (zerop (buffer-size err-buffer))) + (with-current-buffer err-buffer (buffer-string)))) (sub-sentinel (process-get proc 'sub-sentinel)) (real-command (process-get proc 'real-command))) (condition-case err @@ -977,16 +980,17 @@ status." ;; If that didn't signal an error, then any error output was ;; really warning output. Show warnings, if any. (let ((warnings - (when err - (with-current-buffer err-buffer - (goto-char (point-min)) - (end-of-line) - ;; Show first line; stuff remaining lines in the - ;; errors buffer. - (let ((l1 (buffer-substring (point-min) (point)))) - (skip-chars-forward "\n") - (cons l1 (unless (eobp) - (buffer-substring (point) (point-max))))))))) + (and err + (with-current-buffer err-buffer + (goto-char (point-min)) + (end-of-line) + ;; Show first line; stuff remaining lines in the + ;; errors buffer. + (let ((l1 (buffer-substring (point-min) (point)))) + (skip-chars-forward "\n") + (cons l1 (and (not (eobp)) + (buffer-substring (point) + (point-max))))))))) (when warnings (notmuch-logged-error (car warnings) (cdr warnings))))) (error @@ -1018,14 +1022,10 @@ region if the region is active, or both `point' otherwise." (list (point) (point)))) (define-obsolete-function-alias - 'notmuch-search-interactive-region - 'notmuch-interactive-region + 'notmuch-search-interactive-region + 'notmuch-interactive-region "notmuch 0.29") (provide 'notmuch-lib) -;; Local Variables: -;; byte-compile-warnings: (not cl-functions) -;; End: - ;;; notmuch-lib.el ends here diff --git a/emacs/notmuch-maildir-fcc.el b/emacs/notmuch-maildir-fcc.el index ae56bacd..a9103a20 100644 --- a/emacs/notmuch-maildir-fcc.el +++ b/emacs/notmuch-maildir-fcc.el @@ -1,28 +1,28 @@ -;;; notmuch-maildir-fcc.el --- - -;; This file 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, 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. - +;;; notmuch-maildir-fcc.el --- inserting using a fcc handler + +;; Copyright © Jesse Rosenthal +;; +;; This file is part of Notmuch. +;; +;; Notmuch is free software: you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. +;; +;; Notmuch is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. +;; ;; You should have received a copy of the GNU General Public License -;; along with GNU Emacs; see the file COPYING. If not, write to the -;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -;; Boston, MA 02110-1301, USA. - -;;; Commentary: - -;; To use this as the fcc handler for message-mode, -;; customize the notmuch-fcc-dirs variable +;; along with Notmuch. If not, see . +;; +;; Authors: Jesse Rosenthal ;;; Code: -(eval-when-compile (require 'cl)) +(eval-when-compile (require 'cl-lib)) + (require 'message) (require 'notmuch-lib) @@ -30,7 +30,7 @@ (defvar notmuch-maildir-fcc-count 0) (defcustom notmuch-fcc-dirs "sent" - "Determines the Fcc Header which says where to save outgoing mail. + "Determines the Fcc Header which says where to save outgoing mail. Three types of values are permitted: @@ -68,16 +68,16 @@ database.path option in the notmuch configuration file). In all cases you will be prompted to create the folder or directory if it does not exist yet when sending a mail." - :type '(choice - (const :tag "No FCC header" nil) - (string :tag "A single folder") - (repeat :tag "A folder based on the From header" - (cons regexp (string :tag "Folder")))) - :require 'notmuch-fcc-initialization - :group 'notmuch-send) + :type '(choice + (const :tag "No FCC header" nil) + (string :tag "A single folder") + (repeat :tag "A folder based on the From header" + (cons regexp (string :tag "Folder")))) + :require 'notmuch-fcc-initialization + :group 'notmuch-send) (defcustom notmuch-maildir-use-notmuch-insert 't - "Should fcc use notmuch insert instead of simple fcc" + "Should fcc use notmuch insert instead of simple fcc." :type '(choice :tag "Fcc Method" (const :tag "Use notmuch insert" t) (const :tag "Use simple fcc" nil)) @@ -92,23 +92,19 @@ directory if it does not exist yet when sending a mail." Sets the Fcc header based on the values of `notmuch-fcc-dirs'. Originally intended to be use a hook function, but now called directly -by notmuch-mua-mail" - +by notmuch-mua-mail." (let ((subdir (cond ((or (not notmuch-fcc-dirs) (message-field-value "Fcc")) ;; Nothing set or an existing header. nil) - ((stringp notmuch-fcc-dirs) notmuch-fcc-dirs) - ((and (listp notmuch-fcc-dirs) (stringp (car notmuch-fcc-dirs))) ;; Old style - no longer works. (error "Invalid `notmuch-fcc-dirs' setting (old style)")) - ((listp notmuch-fcc-dirs) (let* ((from (message-field-value "From")) (match @@ -120,10 +116,8 @@ by notmuch-mua-mail" (cdr match) (message "No Fcc header added.") nil))) - (t (error "Invalid `notmuch-fcc-dirs' setting (neither string nor list)"))))) - (when subdir (if notmuch-maildir-use-notmuch-insert (notmuch-maildir-add-notmuch-insert-style-fcc-header subdir) @@ -132,10 +126,10 @@ by notmuch-mua-mail" (defun notmuch-maildir-add-notmuch-insert-style-fcc-header (subdir) ;; Notmuch insert does not accept absolute paths, so check the user ;; really want this header inserted. - (when (or (not (= (elt subdir 0) ?/)) - (y-or-n-p (format "Fcc header %s is an absolute path and notmuch insert is requested.\nInsert header anyway? " - subdir))) + (y-or-n-p + (format "Fcc header %s is an absolute path and notmuch insert is requested. +Insert header anyway? " subdir))) (message-add-header (concat "Fcc: " subdir)))) (defun notmuch-maildir-add-file-style-fcc-header (subdir) @@ -220,7 +214,7 @@ This inserts the current buffer as a message into the notmuch database in folder FOLDER. If CREATE is non-nil it will supply the --create-folder flag to create the folder if necessary. TAGS should be a list of tag changes to apply to the inserted message." - (let* ((args (append (when create (list "--create-folder")) + (let* ((args (append (and create (list "--create-folder")) (list (concat "--folder=" folder)) tags))) (apply 'notmuch-call-notmuch-process @@ -248,15 +242,14 @@ If CREATE is non-nil then create the folder if necessary." ;; typo, or just the user want a new folder, let the user decide ;; how to deal with it. (error - (let ((response (notmuch-read-char-choice - "Insert failed: (r)etry, (c)reate folder, (i)gnore, or (e)dit the header? " - '(?r ?c ?i ?e)))) - (case response - (?r (notmuch-maildir-fcc-with-notmuch-insert fcc-header)) - (?c (notmuch-maildir-fcc-with-notmuch-insert fcc-header 't)) - (?i 't) - (?e (notmuch-maildir-fcc-with-notmuch-insert - (read-from-minibuffer "Fcc header: " fcc-header))))))))) + (let ((response (read-char-choice "Insert failed: \ +\(r)etry, (c)reate folder, (i)gnore, or (e)dit the header? " '(?r ?c ?i ?e)))) + (cl-case response + (?r (notmuch-maildir-fcc-with-notmuch-insert fcc-header)) + (?c (notmuch-maildir-fcc-with-notmuch-insert fcc-header 't)) + (?i 't) + (?e (notmuch-maildir-fcc-with-notmuch-insert + (read-from-minibuffer "Fcc header: " fcc-header))))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -273,16 +266,16 @@ If CREATE is non-nil then create the folder if necessary." t)) (defun notmuch-maildir-fcc-make-uniq-maildir-id () - (let* ((ftime (float-time)) - (microseconds (mod (* 1000000 ftime) 1000000)) - (hostname (notmuch-maildir-fcc-host-fixer (system-name)))) - (setq notmuch-maildir-fcc-count (+ notmuch-maildir-fcc-count 1)) - (format "%d.%d_%d_%d.%s" - ftime - (emacs-pid) - microseconds - notmuch-maildir-fcc-count - hostname))) + (let* ((ftime (float-time)) + (microseconds (mod (* 1000000 ftime) 1000000)) + (hostname (notmuch-maildir-fcc-host-fixer (system-name)))) + (setq notmuch-maildir-fcc-count (+ notmuch-maildir-fcc-count 1)) + (format "%d.%d_%d_%d.%s" + ftime + (emacs-pid) + microseconds + notmuch-maildir-fcc-count + hostname))) (defun notmuch-maildir-fcc-dir-is-maildir-p (dir) (and (file-exists-p (concat dir "/cur/")) @@ -321,7 +314,7 @@ if successful, nil if not." (defun notmuch-maildir-fcc-move-tmp-to-cur (destdir msg-id &optional mark-seen) (add-name-to-file (concat destdir "/tmp/" msg-id) - (concat destdir "/cur/" msg-id ":2," (when mark-seen "S")))) + (concat destdir "/cur/" msg-id ":2," (and mark-seen "S")))) (defun notmuch-maildir-fcc-file-fcc (fcc-header) "Write the message to the file specified by FCC-HEADER. @@ -332,19 +325,19 @@ if needed." (notmuch-maildir-fcc-write-buffer-to-maildir fcc-header 't) ;; The fcc-header is not a valid maildir see if the user wants to ;; fix it in some way. - (let* ((prompt (format "Fcc %s is not a maildir: (r)etry, (c)reate folder, (i)gnore, or (e)dit the header? " - fcc-header)) - (response (notmuch-read-char-choice prompt '(?r ?c ?i ?e)))) - (case response - (?r (notmuch-maildir-fcc-file-fcc fcc-header)) - (?c (if (file-writable-p fcc-header) - (notmuch-maildir-fcc-create-maildir fcc-header) - (message "No permission to create %s." fcc-header) - (sit-for 2)) - (notmuch-maildir-fcc-file-fcc fcc-header)) - (?i 't) - (?e (notmuch-maildir-fcc-file-fcc - (read-from-minibuffer "Fcc header: " fcc-header))))))) + (let* ((prompt (format "Fcc %s is not a maildir: \ +\(r)etry, (c)reate folder, (i)gnore, or (e)dit the header? " fcc-header)) + (response (read-char-choice prompt '(?r ?c ?i ?e)))) + (cl-case response + (?r (notmuch-maildir-fcc-file-fcc fcc-header)) + (?c (if (file-writable-p fcc-header) + (notmuch-maildir-fcc-create-maildir fcc-header) + (message "No permission to create %s." fcc-header) + (sit-for 2)) + (notmuch-maildir-fcc-file-fcc fcc-header)) + (?i 't) + (?e (notmuch-maildir-fcc-file-fcc + (read-from-minibuffer "Fcc header: " fcc-header))))))) (defun notmuch-maildir-fcc-write-buffer-to-maildir (destdir &optional mark-seen) "Writes the current buffer to maildir destdir. If mark-seen is diff --git a/emacs/notmuch-message.el b/emacs/notmuch-message.el index 0164472f..c2242070 100644 --- a/emacs/notmuch-message.el +++ b/emacs/notmuch-message.el @@ -60,7 +60,8 @@ the first is a notmuch query and the rest are the tag changes to be applied to the matching messages.") (defun notmuch-message-apply-queued-tag-changes () - ;; Apply the tag changes queued in the buffer-local variable notmuch-message-queued-tag-changes. + ;; Apply the tag changes queued in the buffer-local variable + ;; notmuch-message-queued-tag-changes. (dolist (query-and-tags notmuch-message-queued-tag-changes) (notmuch-tag (car query-and-tags) (cdr query-and-tags)))) diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el index 7fdd76bc..03c7cc97 100644 --- a/emacs/notmuch-mua.el +++ b/emacs/notmuch-mua.el @@ -21,6 +21,8 @@ ;;; Code: +(eval-when-compile (require 'cl-lib)) + (require 'message) (require 'mm-view) (require 'format-spec) @@ -30,8 +32,6 @@ (require 'notmuch-draft) (require 'notmuch-message) -(eval-when-compile (require 'cl)) - (declare-function notmuch-show-insert-body "notmuch-show" (msg body depth)) (declare-function notmuch-fcc-header-setup "notmuch-maildir-fcc" ()) (declare-function notmuch-maildir-message-do-fcc "notmuch-maildir-fcc" ()) @@ -40,7 +40,7 @@ ;; -(defcustom notmuch-mua-send-hook '(notmuch-mua-message-send-hook) +(defcustom notmuch-mua-send-hook nil "Hook run before sending messages." :type 'hook :group 'notmuch-send @@ -58,7 +58,7 @@ window/frame that will be destroyed when the buffer is killed. You may want to customize `message-kill-buffer-on-exit' accordingly." (when (< emacs-major-version 24) - " Due to a known bug in Emacs 23, you should not set + " Due to a known bug in Emacs 23, you should not set this to `new-window' if `message-kill-buffer-on-exit' is disabled: this would result in an incorrect behavior.")) :group 'notmuch-send @@ -106,13 +106,13 @@ Note that these functions use `mail-citation-hook' if that is non-nil." This function specifies which parts of a mime message with multiple parts get a header." :type '(radio (const :tag "No part headers" - notmuch-show-reply-insert-header-p-never) + notmuch-show-reply-insert-header-p-never) (const :tag "All except multipart/* and hidden parts" - notmuch-show-reply-insert-header-p-trimmed) + notmuch-show-reply-insert-header-p-trimmed) (const :tag "Only for included text parts" - notmuch-show-reply-insert-header-p-minimal) + notmuch-show-reply-insert-header-p-minimal) (const :tag "Exactly as in show view" - notmuch-show-insert-header-p) + notmuch-show-insert-header-p) (function :tag "Other")) :group 'notmuch-reply) @@ -137,17 +137,21 @@ Typically this is added to `notmuch-mua-send-hook'." ;; When the message mentions attachment... (save-excursion (message-goto-body) - (loop while (re-search-forward notmuch-mua-attachment-regexp (point-max) t) - ;; For every instance of the "attachment" string - ;; found, examine the text properties. If the text - ;; has either a `face' or `syntax-table' property - ;; then it is quoted text and should *not* cause the - ;; user to be asked about a missing attachment. - if (let ((props (text-properties-at (match-beginning 0)))) - (not (or (memq 'syntax-table props) - (memq 'face props)))) - return t - finally return nil)) + ;; Limit search from reaching other possible parts of the message + (let ((search-limit (search-forward "\n<#" nil t))) + (message-goto-body) + (cl-loop while (re-search-forward notmuch-mua-attachment-regexp + search-limit t) + ;; For every instance of the "attachment" string + ;; found, examine the text properties. If the text + ;; has either a `face' or `syntax-table' property + ;; then it is quoted text and should *not* cause the + ;; user to be asked about a missing attachment. + if (let ((props (text-properties-at (match-beginning 0)))) + (not (or (memq 'syntax-table props) + (memq 'face props)))) + return t + finally return nil))) ;; ...but doesn't have a part with a filename... (save-excursion (message-goto-body) @@ -194,17 +198,19 @@ Typically this is added to `notmuch-mua-send-hook'." (defun notmuch-mua-add-more-hidden-headers () "Add some headers to the list that are hidden by default." (mapc (lambda (header) - (when (not (member header message-hidden-headers)) + (unless (member header message-hidden-headers) (push header message-hidden-headers))) notmuch-mua-hidden-headers)) (defun notmuch-mua-reply-crypto (parts) "Add mml sign-encrypt flag if any part of original message is encrypted." - (loop for part in parts - if (notmuch-match-content-type (plist-get part :content-type) "multipart/encrypted") - do (mml-secure-message-sign-encrypt) - else if (notmuch-match-content-type (plist-get part :content-type) "multipart/*") - do (notmuch-mua-reply-crypto (plist-get part :content)))) + (cl-loop for part in parts + if (notmuch-match-content-type (plist-get part :content-type) + "multipart/encrypted") + do (mml-secure-message-sign-encrypt) + else if (notmuch-match-content-type (plist-get part :content-type) + "multipart/*") + do (notmuch-mua-reply-crypto (plist-get part :content)))) ;; There is a bug in emacs 23's message.el that results in a newline ;; not being inserted after the References header, so the next header @@ -222,22 +228,17 @@ Typically this is added to `notmuch-mua-send-hook'." original) (when process-crypto (setq args (append args '("--decrypt=true")))) - (if reply-all (setq args (append args '("--reply-to=all"))) (setq args (append args '("--reply-to=sender")))) (setq args (append args (list query-string))) - ;; Get the reply object as SEXP, and parse it into an elisp object. (setq reply (apply #'notmuch-call-notmuch-sexp args)) - ;; Extract the original message to simplify the following code. (setq original (plist-get reply :original)) - ;; Extract the headers of both the reply and the original message. (let* ((original-headers (plist-get original :headers)) (reply-headers (plist-get reply :reply-headers))) - ;; If sender is non-nil, set the From: header to its value. (when sender (plist-put reply-headers :From sender)) @@ -245,28 +246,27 @@ Typically this is added to `notmuch-mua-send-hook'." ;; Overlay the composition window on that being used to read ;; the original message. ((same-window-regexps '("\\*mail .*"))) - - ;; We modify message-header-format-alist to get around a bug in message.el. - ;; See the comment above on notmuch-mua-insert-references. + ;; We modify message-header-format-alist to get around + ;; a bug in message.el. See the comment above on + ;; notmuch-mua-insert-references. (let ((message-header-format-alist - (loop for pair in message-header-format-alist - if (eq (car pair) 'References) - collect (cons 'References - (apply-partially - 'notmuch-mua-insert-references - (cdr pair))) - else - collect pair))) + (cl-loop for pair in message-header-format-alist + if (eq (car pair) 'References) + collect (cons 'References + (apply-partially + 'notmuch-mua-insert-references + (cdr pair))) + else + collect pair))) (notmuch-mua-mail (plist-get reply-headers :To) (notmuch-sanitize (plist-get reply-headers :Subject)) (notmuch-headers-plist-to-alist reply-headers) nil (notmuch-mua-get-switch-function)))) - - ;; Create a buffer-local queue for tag changes triggered when sending the reply + ;; Create a buffer-local queue for tag changes triggered when + ;; sending the reply. (when notmuch-message-replied-tags (setq-local notmuch-message-queued-tag-changes (list (cons query-string notmuch-message-replied-tags)))) - ;; Insert the message body - but put it in front of the signature ;; if one is present, and after any other content ;; message*setup-hooks may have added to the message body already. @@ -275,62 +275,57 @@ Typically this is added to `notmuch-mua-send-hook'." (narrow-to-region (point) (point-max)) (goto-char (point-max)) (if (re-search-backward message-signature-separator nil t) - (if message-signature-insert-empty-line - (forward-line -1)) + (when message-signature-insert-empty-line + (forward-line -1)) (goto-char (point-max)))) - (let ((from (plist-get original-headers :From)) (date (plist-get original-headers :Date)) (start (point))) - ;; notmuch-mua-cite-function constructs a citation line based ;; on the From and Date headers of the original message, which ;; are assumed to be in the buffer. (insert "From: " from "\n") (insert "Date: " date "\n\n") - - (insert (with-temp-buffer - (let - ;; Don't attempt to clean up messages, excerpt - ;; citations, etc. in the original message before - ;; quoting. - ((notmuch-show-insert-text/plain-hook nil) - ;; Don't omit long parts. - (notmuch-show-max-text-part-size 0) - ;; Insert headers for parts as appropriate for replying. - (notmuch-show-insert-header-p-function notmuch-mua-reply-insert-header-p-function) - ;; Ensure that any encrypted parts are - ;; decrypted during the generation of the reply - ;; text. - (notmuch-show-process-crypto process-crypto) - ;; Don't indent multipart sub-parts. - (notmuch-show-indent-multipart nil)) - ;; We don't want sigstatus buttons (an information leak and usually wrong anyway). - (letf (((symbol-function 'notmuch-crypto-insert-sigstatus-button) #'ignore) - ((symbol-function 'notmuch-crypto-insert-encstatus-button) #'ignore)) - (notmuch-show-insert-body original (plist-get original :body) 0) - (buffer-substring-no-properties (point-min) (point-max)))))) - + (insert + (with-temp-buffer + (let + ;; Don't attempt to clean up messages, excerpt + ;; citations, etc. in the original message before + ;; quoting. + ((notmuch-show-insert-text/plain-hook nil) + ;; Don't omit long parts. + (notmuch-show-max-text-part-size 0) + ;; Insert headers for parts as appropriate for replying. + (notmuch-show-insert-header-p-function + notmuch-mua-reply-insert-header-p-function) + ;; Ensure that any encrypted parts are + ;; decrypted during the generation of the reply + ;; text. + (notmuch-show-process-crypto process-crypto) + ;; Don't indent multipart sub-parts. + (notmuch-show-indent-multipart nil)) + ;; We don't want sigstatus buttons (an information leak and usually wrong anyway). + (cl-letf (((symbol-function 'notmuch-crypto-insert-sigstatus-button) #'ignore) + ((symbol-function 'notmuch-crypto-insert-encstatus-button) #'ignore)) + (notmuch-show-insert-body original (plist-get original :body) 0) + (buffer-substring-no-properties (point-min) (point-max)))))) (set-mark (point)) (goto-char start) ;; Quote the original message according to the user's configured style. (funcall notmuch-mua-cite-function))) - ;; Crypto processing based crypto content of the original message (when process-crypto (notmuch-mua-reply-crypto (plist-get original :body)))) - ;; Push mark right before signature, if any. (message-goto-signature) (unless (eobp) (end-of-line -1)) (push-mark) - (message-goto-body) (set-buffer-modified-p nil)) (define-derived-mode notmuch-message-mode message-mode "Message[Notmuch]" - "Notmuch message composition mode. Mostly like `message-mode'" + "Notmuch message composition mode. Mostly like `message-mode'." (notmuch-address-setup)) (put 'notmuch-message-mode 'flyspell-mode-predicate 'mail-mode-flyspell-verify) @@ -342,7 +337,7 @@ Typically this is added to `notmuch-mua-send-hook'." (defun notmuch-mua-pop-to-buffer (name switch-function) "Pop to buffer NAME, and warn if it already exists and is -modified. This function is notmuch addaptation of +modified. This function is notmuch adaptation of `message-pop-to-buffer'." (let ((buffer (get-buffer name))) (if (and buffer @@ -371,18 +366,18 @@ modified. This function is notmuch addaptation of return-action &rest ignored) "Invoke the notmuch mail composition window." (interactive) - (when notmuch-mua-user-agent-function (let ((user-agent (funcall notmuch-mua-user-agent-function))) - (when (not (string= "" user-agent)) + (unless (string= "" user-agent) (push (cons 'User-Agent user-agent) other-headers)))) - (unless (assq 'From other-headers) (push (cons 'From (message-make-from - (notmuch-user-name) (notmuch-user-primary-email))) other-headers)) - + (notmuch-user-name) + (notmuch-user-primary-email))) + other-headers)) (notmuch-mua-pop-to-buffer (message-buffer-name "mail" to) - (or switch-function (notmuch-mua-get-switch-function))) + (or switch-function + (notmuch-mua-get-switch-function))) (let ((headers (append ;; The following is copied from `message-mail' @@ -393,7 +388,8 @@ modified. This function is notmuch addaptation of ;; https://lists.gnu.org/archive/html/emacs-devel/2011-01/msg00337.html ;; We need to convert any string input, eg from rmail-start-mail. (dolist (h other-headers other-headers) - (if (stringp (car h)) (setcar h (intern (capitalize (car h)))))))) + (when (stringp (car h)) + (setcar h (intern (capitalize (car h)))))))) (args (list yank-action send-actions)) ;; Cause `message-setup-1' to do things relevant for mail, ;; such as observe `message-default-mail-headers'. @@ -409,7 +405,6 @@ modified. This function is notmuch addaptation of (message-hide-headers) (set-buffer-modified-p nil) (notmuch-mua-maybe-set-window-dedicated) - (message-goto-to)) (defcustom notmuch-identities nil @@ -430,19 +425,6 @@ the From: header is already filled in by notmuch." (defvar notmuch-mua-sender-history nil) -;; Workaround: Running `ido-completing-read' in emacs 23.1, 23.2 and 23.3 -;; without some explicit initialization fill freeze the operation. -;; Hence, we advice `ido-completing-read' to ensure required initialization -;; is done. -(if (and (= emacs-major-version 23) (< emacs-minor-version 4)) - (defadvice ido-completing-read (before notmuch-ido-mode-init activate) - (ido-init-completion-maps) - (add-hook 'minibuffer-setup-hook 'ido-minibuffer-setup) - (add-hook 'choose-completion-string-functions - 'ido-choose-completion-string) - (ad-disable-advice 'ido-completing-read 'before 'notmuch-ido-mode-init) - (ad-activate 'ido-completing-read))) - (defun notmuch-mua-prompt-for-sender () "Prompt for a sender from the user's configured identities." (if notmuch-identities @@ -466,8 +448,8 @@ If PROMPT-FOR-SENDER is non-nil, the user will be prompted for the From: address first." (interactive "P") (let ((other-headers - (when (or prompt-for-sender notmuch-always-prompt-for-sender) - (list (cons 'From (notmuch-mua-prompt-for-sender)))))) + (and (or prompt-for-sender notmuch-always-prompt-for-sender) + (list (cons 'From (notmuch-mua-prompt-for-sender)))))) (notmuch-mua-mail nil nil other-headers nil (notmuch-mua-get-switch-function)))) (defun notmuch-mua-new-forward-messages (messages &optional prompt-for-sender) @@ -476,16 +458,16 @@ the From: address first." If PROMPT-FOR-SENDER is non-nil, the user will be prompteed for the From: address." (let* ((other-headers - (when (or prompt-for-sender notmuch-always-prompt-for-sender) - (list (cons 'From (notmuch-mua-prompt-for-sender))))) - forward-subject ;; Comes from the first message and is - ;; applied later. - forward-references ;; List of accumulated message-references of forwarded messages - forward-queries) ;; List of corresponding message-query - + (and (or prompt-for-sender notmuch-always-prompt-for-sender) + (list (cons 'From (notmuch-mua-prompt-for-sender))))) + ;; Comes from the first message and is applied later. + forward-subject + ;; List of accumulated message-references of forwarded messages. + forward-references + ;; List of corresponding message-query. + forward-queries) ;; Generate the template for the outgoing message. (notmuch-mua-mail nil "" other-headers nil (notmuch-mua-get-switch-function)) - (save-excursion ;; Insert all of the forwarded messages. (mapc (lambda (id) @@ -495,7 +477,8 @@ the From: address." (with-current-buffer temp-buffer (erase-buffer) (let ((coding-system-for-read 'no-conversion)) - (call-process notmuch-command nil t nil "show" "--format=raw" id)) + (call-process notmuch-command nil t nil + "show" "--format=raw" id)) ;; Because we process the messages in reverse order, ;; always generate a forwarded subject, then use the ;; last (i.e. first) one. @@ -510,7 +493,6 @@ the From: address." ;; `message-forward-make-body' always puts the message at ;; the top, so do them in reverse order. (reverse messages)) - ;; Add in the appropriate subject. (save-restriction (message-narrow-to-headers) @@ -519,15 +501,13 @@ the From: address." (message-remove-header "References") (message-add-header (concat "References: " (mapconcat 'identity forward-references " ")))) - - ;; Create a buffer-local queue for tag changes triggered when sending the message + ;; Create a buffer-local queue for tag changes triggered when + ;; sending the message. (when notmuch-message-forwarded-tags (setq-local notmuch-message-queued-tag-changes - (loop for id in forward-queries - collect - (cons id - notmuch-message-forwarded-tags)))) - + (cl-loop for id in forward-queries + collect + (cons id notmuch-message-forwarded-tags)))) ;; `message-forward-make-body' shows the User-agent header. Hide ;; it again. (message-hide-headers) @@ -539,22 +519,19 @@ the From: address." If PROMPT-FOR-SENDER is non-nil, the user will be prompted for the From: address first. If REPLY-ALL is non-nil, the message will be addressed to all recipients of the source message." - -;; In current emacs (24.3) select-active-regions is set to t by -;; default. The reply insertion code sets the region to the quoted -;; message to make it easy to delete (kill-region or C-w). These two -;; things combine to put the quoted message in the primary selection. -;; -;; This is not what the user wanted and is a privacy risk (accidental -;; pasting of the quoted message). We can avoid some of the problems -;; by let-binding select-active-regions to nil. This fixes if the -;; primary selection was previously in a non-emacs window but not if -;; it was in an emacs window. To avoid the problem in the latter case -;; we deactivate mark. - - (let ((sender - (when prompt-for-sender - (notmuch-mua-prompt-for-sender))) + ;; In current emacs (24.3) select-active-regions is set to t by + ;; default. The reply insertion code sets the region to the quoted + ;; message to make it easy to delete (kill-region or C-w). These two + ;; things combine to put the quoted message in the primary selection. + ;; + ;; This is not what the user wanted and is a privacy risk (accidental + ;; pasting of the quoted message). We can avoid some of the problems + ;; by let-binding select-active-regions to nil. This fixes if the + ;; primary selection was previously in a non-emacs window but not if + ;; it was in an emacs window. To avoid the problem in the latter case + ;; we deactivate mark. + (let ((sender (and prompt-for-sender + (notmuch-mua-prompt-for-sender))) (select-active-regions nil)) (notmuch-mua-reply query-string sender reply-all) (deactivate-mark))) @@ -606,10 +583,11 @@ unencrypted. Really send? ")))) (run-hooks 'notmuch-mua-send-hook) (when (and (notmuch-mua-check-no-misplaced-secure-tag) (notmuch-mua-check-secure-tag-has-newline)) - (letf (((symbol-function 'message-do-fcc) #'notmuch-maildir-message-do-fcc)) - (if exit - (message-send-and-exit arg) - (message-send arg))))) + (cl-letf (((symbol-function 'message-do-fcc) + #'notmuch-maildir-message-do-fcc)) + (if exit + (message-send-and-exit arg) + (message-send arg))))) (defun notmuch-mua-send-and-exit (&optional arg) (interactive "P") @@ -623,11 +601,6 @@ unencrypted. Really send? ")))) (interactive) (message-kill-buffer)) -(defun notmuch-mua-message-send-hook () - "The default function used for `notmuch-mua-send-hook', this -simply runs the corresponding `message-mode' hook functions." - (run-hooks 'message-send-hook)) - ;; (define-mail-user-agent 'notmuch-user-agent diff --git a/emacs/notmuch-parser.el b/emacs/notmuch-parser.el index bb0379c1..3aa5bd8f 100644 --- a/emacs/notmuch-parser.el +++ b/emacs/notmuch-parser.el @@ -21,7 +21,7 @@ ;;; Code: -(require 'cl) +(eval-when-compile (require 'cl-lib)) (defun notmuch-sexp-create-parser () "Return a new streaming S-expression parser. @@ -38,14 +38,10 @@ can return 'retry to indicate that not enough input is available. The parser always consumes input from point in the current buffer. Hence, the caller is allowed to delete any data before point and may resynchronize after an error by moving point." - (vector 'notmuch-sexp-parser - ;; List depth - 0 - ;; Partial parse position marker - nil - ;; Partial parse state - nil)) + 0 ; List depth + nil ; Partial parse position marker + nil)) ; Partial parse state (defmacro notmuch-sexp--depth (sp) `(aref ,sp 1)) (defmacro notmuch-sexp--partial-pos (sp) `(aref ,sp 2)) @@ -60,7 +56,6 @@ parser is currently inside a list and the next token ends the list, this moves point just past the terminator and returns 'end. Otherwise, this moves point to just past the end of the value and returns the value." - (skip-chars-forward " \n\r\t") (cond ((eobp) 'retry) ((= (char-after) ?\)) @@ -70,7 +65,7 @@ returns the value." ;; error to be consistent with all other code paths. (read (current-buffer)) ;; Go up a level and return an end token - (decf (notmuch-sexp--depth sp)) + (cl-decf (notmuch-sexp--depth sp)) (forward-char) 'end)) ((= (char-after) ?\() @@ -80,7 +75,7 @@ returns the value." ;; parse, extend the partial parse to figure out when we ;; have a complete list. (catch 'return - (when (null (notmuch-sexp--partial-state sp)) + (unless (notmuch-sexp--partial-state sp) (let ((start (point))) (condition-case nil (throw 'return (read (current-buffer))) @@ -94,8 +89,8 @@ returns the value." (notmuch-sexp--partial-state sp))) ;; A complete value is available if we've ;; reached depth 0. - (depth (first new-state))) - (assert (>= depth 0)) + (depth (car new-state))) + (cl-assert (>= depth 0)) (if (= depth 0) ;; Reset partial parse state (setf (notmuch-sexp--partial-state sp) nil @@ -134,12 +129,11 @@ a list, it moves point past the token that opens the list and returns t. Later calls to `notmuch-sexp-read' will return the elements inside the list. If the input in buffer is not the beginning of a list, throw invalid-read-syntax." - (skip-chars-forward " \n\r\t") (cond ((eobp) 'retry) ((= (char-after) ?\() (forward-char) - (incf (notmuch-sexp--depth sp)) + (cl-incf (notmuch-sexp--depth sp)) t) (t ;; Skip over the bad character like `read' does @@ -151,7 +145,6 @@ beginning of a list, throw invalid-read-syntax." Moves point to the beginning of any trailing data or to the end of the buffer if there is only trailing whitespace." - (skip-chars-forward " \n\r\t") (unless (eobp) (error "Trailing garbage following expression"))) @@ -173,7 +166,6 @@ complete value in the list. It operates incrementally and should be called whenever the input buffer has been extended with additional data. The caller just needs to ensure it does not move point in the input buffer." - ;; Set up the initial state (unless (local-variable-p 'notmuch-sexp--parser) (set (make-local-variable 'notmuch-sexp--parser) @@ -181,7 +173,7 @@ move point in the input buffer." (set (make-local-variable 'notmuch-sexp--state) 'begin)) (let (done) (while (not done) - (case notmuch-sexp--state + (cl-case notmuch-sexp--state (begin ;; Enter the list (if (eq (notmuch-sexp-begin-list notmuch-sexp--parser) 'retry) @@ -190,7 +182,7 @@ move point in the input buffer." (result ;; Parse a result (let ((result (notmuch-sexp-read notmuch-sexp--parser))) - (case result + (cl-case result (retry (setq done t)) (end (setq notmuch-sexp--state 'end)) (t (with-current-buffer result-buffer @@ -204,8 +196,4 @@ move point in the input buffer." (provide 'notmuch-parser) -;; Local Variables: -;; byte-compile-warnings: (not cl-functions) -;; End: - ;;; notmuch-parser.el ends here diff --git a/emacs/notmuch-pkg.el.tmpl b/emacs/notmuch-pkg.el.tmpl index de97baac..85c631de 100644 --- a/emacs/notmuch-pkg.el.tmpl +++ b/emacs/notmuch-pkg.el.tmpl @@ -3,4 +3,4 @@ "notmuch" %VERSION% "Emacs based front-end (MUA) for notmuch" - nil) + '((emacs "25.1"))) diff --git a/emacs/notmuch-print.el b/emacs/notmuch-print.el index d9b3d449..6dd9f775 100644 --- a/emacs/notmuch-print.el +++ b/emacs/notmuch-print.el @@ -1,4 +1,4 @@ -;;; notmuch-print.el --- printing messages from notmuch. +;;; notmuch-print.el --- printing messages from notmuch ;; ;; Copyright © David Edmondson ;; diff --git a/emacs/notmuch-query.el b/emacs/notmuch-query.el index 563e4acf..3cfccbc3 100644 --- a/emacs/notmuch-query.el +++ b/emacs/notmuch-query.el @@ -28,11 +28,10 @@ A thread is a forest or list of trees. A tree is a two element list where the first element is a message, and the second element -is a possibly empty forest of replies. -" +is a possibly empty forest of replies." (let ((args '("show" "--format=sexp" "--format-version=4"))) - (if notmuch-show-process-crypto - (setq args (append args '("--decrypt=true")))) + (when notmuch-show-process-crypto + (setq args (append args '("--decrypt=true")))) (setq args (append args search-terms)) (apply #'notmuch-call-notmuch-sexp args))) @@ -40,37 +39,36 @@ is a possibly empty forest of replies. ;; Mapping functions across collections of messages. (defun notmuch-query-map-aux (mapper function seq) - "private function to do the actual mapping and flattening" + "Private function to do the actual mapping and flattening." (apply 'append (mapcar - (lambda (tree) - (funcall mapper function tree)) - seq))) + (lambda (tree) + (funcall mapper function tree)) + seq))) (defun notmuch-query-map-threads (fn threads) - "apply FN to every thread in THREADS. Flatten results to a list. - -See the function notmuch-query-get-threads for more information." + "Apply function FN to every thread in THREADS. +Flatten results to a list. See the function +`notmuch-query-get-threads' for more information." (notmuch-query-map-aux 'notmuch-query-map-forest fn threads)) (defun notmuch-query-map-forest (fn forest) - "apply function to every message in a forest. Flatten results to a list. - -See the function notmuch-query-get-threads for more information. -" + "Apply function FN to every message in FOREST. +Flatten results to a list. See the function +`notmuch-query-get-threads' for more information." (notmuch-query-map-aux 'notmuch-query-map-tree fn forest)) (defun notmuch-query-map-tree (fn tree) - "Apply function FN to every message in TREE. Flatten results to a list - -See the function notmuch-query-get-threads for more information." + "Apply function FN to every message in TREE. +Flatten results to a list. See the function +`notmuch-query-get-threads' for more information." (cons (funcall fn (car tree)) (notmuch-query-map-forest fn (cadr tree)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Predefined queries (defun notmuch-query-get-message-ids (&rest search-terms) - "Return a list of message-ids of messages that match SEARCH-TERMS" + "Return a list of message-ids of messages that match SEARCH-TERMS." (notmuch-query-map-threads (lambda (msg) (plist-get msg :id)) (notmuch-query-get-threads search-terms))) diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index e13ca3d7..b08ceb97 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -1,4 +1,4 @@ -;;; notmuch-show.el --- displaying notmuch forests. +;;; notmuch-show.el --- displaying notmuch forests ;; ;; Copyright © Carl Worth ;; Copyright © David Edmondson @@ -23,7 +23,10 @@ ;;; Code: -(eval-when-compile (require 'cl)) +(eval-when-compile + (require 'cl-lib) + (require 'pcase)) + (require 'mm-view) (require 'message) (require 'mm-decode) @@ -48,8 +51,11 @@ (declare-function notmuch-count-attachments "notmuch" (mm-handle)) (declare-function notmuch-save-attachments "notmuch" (mm-handle &optional queryp)) (declare-function notmuch-tree "notmuch-tree" - (&optional query query-context target buffer-name open-target)) + (&optional query query-context target buffer-name + open-target unthreaded)) (declare-function notmuch-tree-get-message-properties "notmuch-tree" nil) +(declare-function notmuch-unthreaded + (&optional query query-context target buffer-name open-target)) (declare-function notmuch-read-query "notmuch" (prompt)) (declare-function notmuch-draft-resume "notmuch-draft" (id)) @@ -90,10 +96,11 @@ visible for any given message." :group 'notmuch-show :group 'notmuch-hooks) -(defcustom notmuch-show-insert-text/plain-hook '(notmuch-wash-wrap-long-lines - notmuch-wash-tidy-citations - notmuch-wash-elide-blank-lines - notmuch-wash-excerpt-citations) +(defcustom notmuch-show-insert-text/plain-hook + '(notmuch-wash-wrap-long-lines + notmuch-wash-tidy-citations + notmuch-wash-elide-blank-lines + notmuch-wash-excerpt-citations) "Functions used to improve the display of text/plain parts." :type 'hook :options '(notmuch-wash-convert-inline-patch-to-part @@ -174,7 +181,7 @@ indentation." (make-variable-buffer-local 'notmuch-show-indent-content) (defvar notmuch-show-attachment-debug nil - "If t log stdout and stderr from attachment handlers + "If t log stdout and stderr from attachment handlers. When set to nil (the default) stdout and stderr from attachment handlers is discarded. When set to t the stdout and stderr from @@ -261,11 +268,11 @@ position of the message in the thread." :group 'notmuch-show) (defmacro with-current-notmuch-show-message (&rest body) - "Evaluate body with current buffer set to the text of current message" + "Evaluate body with current buffer set to the text of current message." `(save-excursion (let ((id (notmuch-show-get-message-id))) (let ((buf (generate-new-buffer (concat "*notmuch-msg-" id "*")))) - (with-current-buffer buf + (with-current-buffer buf (let ((coding-system-for-read 'no-conversion)) (call-process notmuch-command nil t nil "show" "--format=raw" id)) ,@body) @@ -290,13 +297,12 @@ position of the message in the thread." ;; ;; Any MIME part not explicitly mentioned here will be handled by an ;; external viewer as configured in the various mailcap files. - (let ((mm-inline-media-tests '( - ("text/.*" ignore identity) - ("application/pgp-signature" ignore identity) - ("multipart/alternative" ignore identity) - ("multipart/mixed" ignore identity) - ("multipart/related" ignore identity) - ))) + (let ((mm-inline-media-tests + '(("text/.*" ignore identity) + ("application/pgp-signature" ignore identity) + ("multipart/alternative" ignore identity) + ("multipart/mixed" ignore identity) + ("multipart/related" ignore identity)))) (mm-display-parts (mm-dissect-buffer))))) (defun notmuch-show-save-attachments () @@ -313,7 +319,6 @@ position of the message in the thread." FN is called with one argument, the message properties. It should operation on the contents of the current buffer." - ;; Remake the header to ensure that all information is available. (let* ((to (notmuch-show-get-to)) (cc (notmuch-show-get-cc)) @@ -322,7 +327,6 @@ operation on the contents of the current buffer." (date (notmuch-show-get-date)) (tags (notmuch-show-get-tags)) (depth (notmuch-show-get-depth)) - (header (concat "Subject: " subject "\n" "To: " to "\n" @@ -342,8 +346,10 @@ operation on the contents of the current buffer." (indenting notmuch-show-indent-content)) (with-temp-buffer (insert all) - (if indenting - (indent-rigidly (point-min) (point-max) (- (* notmuch-show-indent-messages-width depth)))) + (when indenting + (indent-rigidly (point-min) + (point-max) + (- (* notmuch-show-indent-messages-width depth)))) ;; Remove the original header. (goto-char (point-min)) (re-search-forward "^$" (point-max) nil) @@ -366,7 +372,6 @@ operation on the contents of the current buffer." 'message-header-subject) (t 'message-header-other)))) - (overlay-put (make-overlay (point) (re-search-forward ":")) 'face 'message-header-name) (overlay-put (make-overlay (point) (re-search-forward ".*$")) @@ -387,69 +392,62 @@ operation on the contents of the current buffer." "Update the displayed tags of the current message." (save-excursion (goto-char (notmuch-show-message-top)) - (if (re-search-forward "(\\([^()]*\\))$" (line-end-position) t) - (let ((inhibit-read-only t)) - (replace-match (concat "(" - (notmuch-tag-format-tags tags (notmuch-show-get-prop :orig-tags)) - ")")))))) + (when (re-search-forward "(\\([^()]*\\))$" (line-end-position) t) + (let ((inhibit-read-only t)) + (replace-match (concat "(" + (notmuch-tag-format-tags + tags + (notmuch-show-get-prop :orig-tags)) + ")")))))) (defun notmuch-clean-address (address) "Try to clean a single email ADDRESS for display. Return a cons cell of (AUTHOR_EMAIL AUTHOR_NAME). Return (ADDRESS nil) if parsing fails." (condition-case nil - (let (p-name p-address) - ;; It would be convenient to use `mail-header-parse-address', - ;; but that expects un-decoded mailbox parts, whereas our - ;; mailbox parts are already decoded (and hence may contain - ;; UTF-8). Given that notmuch should handle most of the awkward - ;; cases, some simple string deconstruction should be sufficient - ;; here. - (cond - ;; "User " style. - ((string-match "\\(.*\\) <\\(.*\\)>" address) - (setq p-name (match-string 1 address) - p-address (match-string 2 address))) - - ;; "" style. - ((string-match "<\\(.*\\)>" address) - (setq p-address (match-string 1 address))) - - ;; Everything else. - (t - (setq p-address address))) - - (when p-name - ;; Remove elements of the mailbox part that are not relevant for - ;; display, even if they are required during transport: - ;; - ;; Backslashes. - (setq p-name (replace-regexp-in-string "\\\\" "" p-name)) - - ;; Outer single and double quotes, which might be nested. - (loop - with start-of-loop - do (setq start-of-loop p-name) - - when (string-match "^\"\\(.*\\)\"$" p-name) - do (setq p-name (match-string 1 p-name)) - - when (string-match "^'\\(.*\\)'$" p-name) - do (setq p-name (match-string 1 p-name)) - - until (string= start-of-loop p-name))) - - ;; If the address is 'foo@bar.com ' then show just - ;; 'foo@bar.com'. - (when (string= p-name p-address) - (setq p-name nil)) - - (cons p-address p-name)) + (let (p-name p-address) + ;; It would be convenient to use `mail-header-parse-address', + ;; but that expects un-decoded mailbox parts, whereas our + ;; mailbox parts are already decoded (and hence may contain + ;; UTF-8). Given that notmuch should handle most of the awkward + ;; cases, some simple string deconstruction should be sufficient + ;; here. + (cond + ;; "User " style. + ((string-match "\\(.*\\) <\\(.*\\)>" address) + (setq p-name (match-string 1 address)) + (setq p-address (match-string 2 address))) + + ;; "" style. + ((string-match "<\\(.*\\)>" address) + (setq p-address (match-string 1 address))) + ;; Everything else. + (t + (setq p-address address))) + (when p-name + ;; Remove elements of the mailbox part that are not relevant for + ;; display, even if they are required during transport: + ;; + ;; Backslashes. + (setq p-name (replace-regexp-in-string "\\\\" "" p-name)) + ;; Outer single and double quotes, which might be nested. + (cl-loop with start-of-loop + do (setq start-of-loop p-name) + when (string-match "^\"\\(.*\\)\"$" p-name) + do (setq p-name (match-string 1 p-name)) + when (string-match "^'\\(.*\\)'$" p-name) + do (setq p-name (match-string 1 p-name)) + until (string= start-of-loop p-name))) + ;; If the address is 'foo@bar.com ' then show just + ;; 'foo@bar.com'. + (when (string= p-name p-address) + (setq p-name nil)) + (cons p-address p-name)) (error (cons address nil)))) (defun notmuch-show-clean-address (address) - "Try to clean a single email ADDRESS for display. Return -unchanged ADDRESS if parsing fails." + "Try to clean a single email ADDRESS for display. +Return unchanged ADDRESS if parsing fails." (let* ((clean-address (notmuch-clean-address address)) (p-address (car clean-address)) (p-name (cdr clean-address))) @@ -462,16 +460,26 @@ unchanged ADDRESS if parsing fails." (defun notmuch-show-insert-headerline (headers date tags depth) "Insert a notmuch style headerline based on HEADERS for a message at DEPTH in the current thread." - (let ((start (point))) - (insert (notmuch-show-spaces-n (* notmuch-show-indent-messages-width depth)) - (notmuch-sanitize - (notmuch-show-clean-address (plist-get headers :From))) + (let ((start (point)) + (from (notmuch-sanitize + (notmuch-show-clean-address (plist-get headers :From))))) + (when (string-match "\\cR" from) + ;; If the From header has a right-to-left character add + ;; invisible U+200E LEFT-TO-RIGHT MARK character which forces + ;; the header paragraph as left-to-right text. + (insert (propertize (string ?\x200e) 'invisible t))) + (insert (if notmuch-show-indent-content + (notmuch-show-spaces-n (* notmuch-show-indent-messages-width + depth)) + "") + from " (" date ") (" (notmuch-tag-format-tags tags tags) ")\n") - (overlay-put (make-overlay start (point)) 'face 'notmuch-message-summary-face))) + (overlay-put (make-overlay start (point)) + 'face 'notmuch-message-summary-face))) (defun notmuch-show-insert-header (header header-value) "Insert a single header." @@ -483,9 +491,9 @@ message at DEPTH in the current thread." (mapc (lambda (header) (let* ((header-symbol (intern (concat ":" header))) (header-value (plist-get headers header-symbol))) - (if (and header-value - (not (string-equal "" header-value))) - (notmuch-show-insert-header header header-value)))) + (when (and header-value + (not (string-equal "" header-value))) + (notmuch-show-insert-header header header-value)))) notmuch-message-headers) (save-excursion (save-restriction @@ -498,23 +506,19 @@ message at DEPTH in the current thread." 'face 'message-mml :supertype 'notmuch-button-type) -(defun notmuch-show-insert-part-header (nth content-type declared-type &optional name comment) - (let ((button) - (base-label (concat (when name (concat name ": ")) +(defun notmuch-show-insert-part-header (nth content-type declared-type + &optional name comment) + (let ((base-label (concat (and name (concat name ": ")) declared-type - (unless (string-equal declared-type content-type) - (concat " (as " content-type ")")) + (and (not (string-equal declared-type content-type)) + (concat " (as " content-type ")")) comment))) - - (setq button - (insert-button - (concat "[ " base-label " ]") - :base-label base-label - :type 'notmuch-show-part-button-type - :notmuch-part-hidden nil)) - (insert "\n") - ;; return button - button)) + (prog1 (insert-button + (concat "[ " base-label " ]") + :base-label base-label + :type 'notmuch-show-part-button-type + :notmuch-part-hidden nil) + (insert "\n")))) (defun notmuch-show-toggle-part-invisibility (&optional button) (interactive) @@ -522,8 +526,9 @@ message at DEPTH in the current thread." (when button (let ((overlay (button-get button 'overlay)) (lazy-part (button-get button :notmuch-lazy-part))) - ;; We have a part to toggle if there is an overlay or if there is a lazy part. - ;; If neither is present we cannot toggle the part so we just return nil. + ;; We have a part to toggle if there is an overlay or if there + ;; is a lazy part. If neither is present we cannot toggle the + ;; part so we just return nil. (when (or overlay lazy-part) (let* ((show (button-get button :notmuch-part-hidden)) (new-start (button-start button)) @@ -571,13 +576,13 @@ message at DEPTH in the current thread." ;; Recurse on sub-parts (let ((ctype (notmuch-split-content-type (downcase (plist-get part :content-type))))) - (cond ((equal (first ctype) "multipart") + (cond ((equal (car ctype) "multipart") (mapc (apply-partially #'notmuch-show--register-cids msg) (plist-get part :content))) ((equal ctype '("message" "rfc822")) (notmuch-show--register-cids msg - (first (plist-get (first (plist-get part :content)) :body))))))) + (car (plist-get (car (plist-get part :content)) :body))))))) (defun notmuch-show--get-cid-content (cid) "Return a list (CID-content content-type) or nil. @@ -588,8 +593,8 @@ enclosing angle brackets, a cid: prefix, or URL encoding. This will return nil if the CID is unknown or cannot be retrieved." (let ((descriptor (cdr (assoc cid notmuch-show--cids)))) (when descriptor - (let* ((msg (first descriptor)) - (part (second descriptor)) + (let* ((msg (car descriptor)) + (part (cadr descriptor)) ;; Request caching for this content, as some messages ;; reference the same cid: part many times (hundreds!). (content (notmuch-get-bodypart-binary @@ -600,10 +605,10 @@ will return nil if the CID is unknown or cannot be retrieved." (defun notmuch-show-setup-w3m () "Instruct w3m how to retrieve content from a \"related\" part of a message." (interactive) - (if (boundp 'w3m-cid-retrieve-function-alist) - (unless (assq 'notmuch-show-mode w3m-cid-retrieve-function-alist) - (push (cons 'notmuch-show-mode #'notmuch-show--cid-w3m-retrieve) - w3m-cid-retrieve-function-alist))) + (when (and (boundp 'w3m-cid-retrieve-function-alist) + (not (assq 'notmuch-show-mode w3m-cid-retrieve-function-alist))) + (push (cons 'notmuch-show-mode #'notmuch-show--cid-w3m-retrieve) + w3m-cid-retrieve-function-alist)) (setq mm-html-inhibit-images nil)) (defvar w3m-current-buffer) ;; From `w3m.el'. @@ -614,8 +619,8 @@ will return nil if the CID is unknown or cannot be retrieved." (with-current-buffer w3m-current-buffer (notmuch-show--get-cid-content cid)))) (when content-and-type - (insert (first content-and-type)) - (second content-and-type)))) + (insert (car content-and-type)) + (cadr content-and-type)))) ;; MIME part renderers @@ -624,7 +629,8 @@ will return nil if the CID is unknown or cannot be retrieved." (plist-get part :content))) (defun notmuch-show-insert-part-multipart/alternative (msg part content-type nth depth button) - (let ((chosen-type (car (notmuch-multipart/alternative-choose msg (notmuch-show-multipart/*-to-list part)))) + (let ((chosen-type (car (notmuch-multipart/alternative-choose + msg (notmuch-show-multipart/*-to-list part)))) (inner-parts (plist-get part :content)) (start (point))) ;; This inserts all parts of the chosen type rather than just one, @@ -632,8 +638,8 @@ will return nil if the CID is unknown or cannot be retrieved." ;; should be chosen if there are more than one that match? (mapc (lambda (inner-part) (let* ((inner-type (plist-get inner-part :content-type)) - (hide (not (or notmuch-show-all-multipart/alternative-parts - (string= chosen-type inner-type))))) + (hide (not (or notmuch-show-all-multipart/alternative-parts + (string= chosen-type inner-type))))) (notmuch-show-insert-bodypart msg inner-part depth hide))) inner-parts) @@ -644,14 +650,12 @@ will return nil if the CID is unknown or cannot be retrieved." (defun notmuch-show-insert-part-multipart/related (msg part content-type nth depth button) (let ((inner-parts (plist-get part :content)) (start (point))) - ;; Render the primary part. FIXME: Support RFC 2387 Start header. (notmuch-show-insert-bodypart msg (car inner-parts) depth) ;; Add hidden buttons for the rest (mapc (lambda (inner-part) (notmuch-show-insert-bodypart msg inner-part depth t)) (cdr inner-parts)) - (when notmuch-show-indent-multipart (indent-rigidly start (point) 1))) t) @@ -659,18 +663,15 @@ will return nil if the CID is unknown or cannot be retrieved." (defun notmuch-show-insert-part-multipart/signed (msg part content-type nth depth button) (when button (button-put button 'face 'notmuch-crypto-part-header)) - ;; Insert a button detailing the signature status. (notmuch-crypto-insert-sigstatus-button (car (plist-get part :sigstatus)) (notmuch-show-get-header :From msg)) - (let ((inner-parts (plist-get part :content)) (start (point))) ;; Show all of the parts. (mapc (lambda (inner-part) (notmuch-show-insert-bodypart msg inner-part depth)) inner-parts) - (when notmuch-show-indent-multipart (indent-rigidly start (point) 1))) t) @@ -678,21 +679,17 @@ will return nil if the CID is unknown or cannot be retrieved." (defun notmuch-show-insert-part-multipart/encrypted (msg part content-type nth depth button) (when button (button-put button 'face 'notmuch-crypto-part-header)) - ;; Insert a button detailing the encryption status. (notmuch-crypto-insert-encstatus-button (car (plist-get part :encstatus))) - ;; Insert a button detailing the signature status. (notmuch-crypto-insert-sigstatus-button (car (plist-get part :sigstatus)) (notmuch-show-get-header :From msg)) - (let ((inner-parts (plist-get part :content)) (start (point))) ;; Show all of the parts. (mapc (lambda (inner-part) (notmuch-show-insert-bodypart msg inner-part depth)) inner-parts) - (when notmuch-show-indent-multipart (indent-rigidly start (point) 1))) t) @@ -707,7 +704,6 @@ will return nil if the CID is unknown or cannot be retrieved." (mapc (lambda (inner-part) (notmuch-show-insert-bodypart msg inner-part depth)) inner-parts) - (when notmuch-show-indent-multipart (indent-rigidly start (point) 1))) t) @@ -716,19 +712,15 @@ will return nil if the CID is unknown or cannot be retrieved." (let* ((message (car (plist-get part :content))) (body (car (plist-get message :body))) (start (point))) - ;; Override `notmuch-message-headers' to force `From' to be ;; displayed. (let ((notmuch-message-headers '("From" "Subject" "To" "Cc" "Date"))) (notmuch-show-insert-headers (plist-get message :headers))) - ;; Blank line after headers to be compatible with the normal ;; message display. (insert "\n") - ;; Show the body (notmuch-show-insert-bodypart msg body depth) - (when notmuch-show-indent-multipart (indent-rigidly start (point) 1))) t) @@ -760,7 +752,8 @@ will return nil if the CID is unknown or cannot be retrieved." (unwind-protect (progn (unless (icalendar-import-buffer file t) - (error "Icalendar import error. See *icalendar-errors* for more information")) + (error "Icalendar import error. %s" + "See *icalendar-errors* for more information")) (set-buffer (get-file-buffer file)) (setq result (buffer-substring (point-min) (point-max))) (set-buffer-modified-p nil) @@ -773,42 +766,41 @@ will return nil if the CID is unknown or cannot be retrieved." (defun notmuch-show-insert-part-text/x-vcalendar (msg part content-type nth depth button) (notmuch-show-insert-part-text/calendar msg part content-type nth depth button)) -(if (version< emacs-version "25.3") - ;; https://bugs.gnu.org/28350 - ;; - ;; For newer emacs, we fall back to notmuch-show-insert-part-*/* - ;; (see notmuch-show-handlers-for) - (defun notmuch-show-insert-part-text/enriched (msg part content-type nth depth button) - ;; By requiring enriched below, we ensure that the function enriched-decode-display-prop - ;; is defined before it will be shadowed by the letf below. Otherwise the version - ;; in enriched.el may be loaded a bit later and used instead (for the first time). - (require 'enriched) - (letf (((symbol-function 'enriched-decode-display-prop) - (lambda (start end &optional param) (list start end)))) - (notmuch-show-insert-part-*/* msg part content-type nth depth button)))) +(when (version< emacs-version "25.3") + ;; https://bugs.gnu.org/28350 + ;; + ;; For newer emacs, we fall back to notmuch-show-insert-part-*/* + ;; (see notmuch-show-handlers-for) + (defun notmuch-show-insert-part-text/enriched + (msg part content-type nth depth button) + ;; By requiring enriched below, we ensure that the function + ;; enriched-decode-display-prop is defined before it will be + ;; shadowed by the letf below. Otherwise the version in + ;; enriched.el may be loaded a bit later and used instead (for + ;; the first time). + (require 'enriched) + (cl-letf (((symbol-function 'enriched-decode-display-prop) + (lambda (start end &optional param) (list start end)))) + (notmuch-show-insert-part-*/* msg part content-type nth depth button)))) (defun notmuch-show-get-mime-type-of-application/octet-stream (part) ;; If we can deduce a MIME type from the filename of the attachment, ;; we return that. - (if (plist-get part :filename) - (let ((extension (file-name-extension (plist-get part :filename))) - mime-type) - (if extension - (progn - (mailcap-parse-mimetypes) - (setq mime-type (mailcap-extension-to-mime extension)) - (if (and mime-type - (not (string-equal mime-type "application/octet-stream"))) - mime-type - nil)) - nil)))) + (and (plist-get part :filename) + (let ((extension (file-name-extension (plist-get part :filename)))) + (and extension + (progn + (mailcap-parse-mimetypes) + (let ((mime-type (mailcap-extension-to-mime extension))) + (and mime-type + (not (string-equal mime-type "application/octet-stream")) + mime-type))))))) (defun notmuch-show-insert-part-text/html (msg part content-type nth depth button) (if (eq mm-text-html-renderer 'shr) ;; It's easier to drive shr ourselves than to work around the ;; goofy things `mm-shr' does (like irreversibly taking over ;; content ID handling). - ;; FIXME: If we block an image, offer a button to load external ;; images. (let ((shr-blocked-images notmuch-show-text/html-blocked-images)) @@ -841,7 +833,7 @@ will return nil if the CID is unknown or cannot be retrieved." ;; shr strips the "cid:" part of URL, but doesn't ;; URL-decode it (see RFC 2392). (let ((cid (url-unhex-string url))) - (first (notmuch-show--get-cid-content cid)))))) + (car (notmuch-show--get-cid-content cid)))))) (shr-insert-document dom) t)) @@ -856,8 +848,8 @@ will return nil if the CID is unknown or cannot be retrieved." "Return a list of content handlers for a part of type CONTENT-TYPE." (let (result) (mapc (lambda (func) - (if (functionp func) - (push func result))) + (when (functionp func) + (push func result))) ;; Reverse order of prefrence. (list (intern (concat "notmuch-show-insert-part-*/*")) (intern (concat @@ -871,19 +863,19 @@ will return nil if the CID is unknown or cannot be retrieved." (defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth button) ;; Run the handlers until one of them succeeds. - (loop for handler in (notmuch-show-handlers-for content-type) - until (condition-case err - (funcall handler msg part content-type nth depth button) - ;; Specifying `debug' here lets the debugger run if - ;; `debug-on-error' is non-nil. - ((debug error) - (insert "!!! Bodypart handler `" (prin1-to-string handler) "' threw an error:\n" - "!!! " (error-message-string err) "\n") - nil)))) + (cl-loop for handler in (notmuch-show-handlers-for content-type) + until (condition-case err + (funcall handler msg part content-type nth depth button) + ;; Specifying `debug' here lets the debugger run if + ;; `debug-on-error' is non-nil. + ((debug error) + (insert "!!! Bodypart handler `" (prin1-to-string handler) + "' threw an error:\n" + "!!! " (error-message-string err) "\n") + nil)))) (defun notmuch-show-create-part-overlays (button beg end) - "Add an overlay to the part between BEG and END" - + "Add an overlay to the part between BEG and END." ;; If there is no button (i.e., the part is text/plain and the first ;; part) or if the part has no content then we don't make the part ;; toggleable. @@ -893,8 +885,7 @@ will return nil if the CID is unknown or cannot be retrieved." t)) (defun notmuch-show-record-part-information (part beg end) - "Store PART as a text property from BEG to END" - + "Store PART as a text property from BEG to END." ;; Record part information. Since we already inserted subparts, ;; don't override existing :notmuch-part properties. (notmuch-map-text-property beg end :notmuch-part @@ -905,13 +896,15 @@ will return nil if the CID is unknown or cannot be retrieved." ;; watch out for sticky specs of t, which means all properties are ;; front-sticky/rear-nonsticky. (notmuch-map-text-property beg end 'front-sticky - (lambda (v) (if (listp v) - (pushnew :notmuch-part v) - v))) + (lambda (v) + (if (listp v) + (cl-pushnew :notmuch-part v) + v))) (notmuch-map-text-property beg end 'rear-nonsticky - (lambda (v) (if (listp v) - (pushnew :notmuch-part v) - v)))) + (lambda (v) + (if (listp v) + (cl-pushnew :notmuch-part v) + v)))) (defun notmuch-show-lazy-part (part-args button) ;; Insert the lazy part after the button for the part. We would just @@ -936,10 +929,12 @@ will return nil if the CID is unknown or cannot be retrieved." (narrow-to-region part-beg part-end) (delete-region part-beg part-end) (apply #'notmuch-show-insert-bodypart-internal part-args) - (indent-rigidly part-beg part-end (* notmuch-show-indent-messages-width depth))) + (indent-rigidly part-beg + part-end + (* notmuch-show-indent-messages-width depth))) (goto-char part-end) (delete-char 1) - (notmuch-show-record-part-information (second part-args) + (notmuch-show-record-part-information (cadr part-args) (button-start button) part-end) ;; Create the overlay. If the lazy-part turned out to be empty/not @@ -988,33 +983,32 @@ this part.") HIDE determines whether to show or hide the part and the button as follows: If HIDE is nil, show the part and the button. If HIDE is t, hide the part initially and show the button." - (let* ((content-type (downcase (plist-get part :content-type))) (mime-type (notmuch-show-mime-type part)) (nth (plist-get part :id)) (long (and (notmuch-match-content-type mime-type "text/*") (> notmuch-show-max-text-part-size 0) - (> (length (plist-get part :content)) notmuch-show-max-text-part-size))) + (> (length (plist-get part :content)) + notmuch-show-max-text-part-size))) (beg (point)) ;; This default header-p function omits the part button for ;; the first (or only) part if this is text/plain. - (button (when (funcall notmuch-show-insert-header-p-function part hide) - (notmuch-show-insert-part-header nth mime-type content-type (plist-get part :filename)))) + (button (and (funcall notmuch-show-insert-header-p-function part hide) + (notmuch-show-insert-part-header + nth mime-type content-type + (plist-get part :filename)))) ;; Hide the part initially if HIDE is t, or if it is too long ;; and we have a button to allow toggling. (show-part (not (or (equal hide t) (and long button)))) (content-beg (point))) - ;; Store the computed mime-type for later use (e.g. by attachment handlers). (plist-put part :computed-type mime-type) - (if show-part - (notmuch-show-insert-bodypart-internal msg part mime-type nth depth button) + (notmuch-show-insert-bodypart-internal msg part mime-type nth depth button) (when button (button-put button :notmuch-lazy-part (list msg part mime-type nth depth button)))) - ;; Some of the body part handlers leave point somewhere up in the ;; part, so we make sure that we're down at the end. (goto-char (point-max)) @@ -1031,12 +1025,10 @@ is t, hide the part initially and show the button." (defun notmuch-show-insert-body (msg body depth) "Insert the body BODY at depth DEPTH in the current thread." - ;; Register all content IDs for this message. According to RFC ;; 2392, content IDs are *global*, but it's okay if an MUA treats ;; them as only global within a message. - (notmuch-show--register-cids msg (first body)) - + (notmuch-show--register-cids msg (car body)) (mapc (lambda (part) (notmuch-show-insert-bodypart msg part depth)) body)) (defun notmuch-show-make-symbol (type) @@ -1057,18 +1049,13 @@ is t, hide the part initially and show the button." content-start content-end headers-start headers-end (bare-subject (notmuch-show-strip-re (plist-get headers :Subject)))) - (setq message-start (point-marker)) - (notmuch-show-insert-headerline headers - (or (if notmuch-show-relative-dates - (plist-get msg :date_relative) - nil) + (or (and notmuch-show-relative-dates + (plist-get msg :date_relative)) (plist-get headers :Date)) (plist-get msg :tags) depth) - (setq content-start (point-marker)) - ;; Set `headers-start' to point after the 'Subject:' header to be ;; compatible with the existing implementation. This just sets it ;; to after the first header. @@ -1078,14 +1065,11 @@ is t, hide the part initially and show the button." ;; If the subject of this message is the same as that of the ;; previous message, don't display it when this message is ;; collapsed. - (when (not (string= notmuch-show-previous-subject - bare-subject)) + (unless (string= notmuch-show-previous-subject bare-subject) (forward-line 1)) (setq headers-start (point-marker))) (setq headers-end (point-marker)) - (setq notmuch-show-previous-subject bare-subject) - ;; A blank line between the headers and the body. (insert "\n") (notmuch-show-insert-body msg (plist-get msg :body) @@ -1094,32 +1078,28 @@ is t, hide the part initially and show the button." (unless (bolp) (insert "\n")) (setq content-end (point-marker)) - ;; Indent according to the depth in the thread. - (if notmuch-show-indent-content - (indent-rigidly content-start content-end (* notmuch-show-indent-messages-width depth))) - + (when notmuch-show-indent-content + (indent-rigidly content-start + content-end + (* notmuch-show-indent-messages-width depth))) (setq message-end (point-max-marker)) - ;; Save the extents of this message over the whole text of the ;; message. - (put-text-property message-start message-end :notmuch-message-extent (cons message-start message-end)) - + (put-text-property message-start message-end + :notmuch-message-extent + (cons message-start message-end)) ;; Create overlays used to control visibility (plist-put msg :headers-overlay (make-overlay headers-start headers-end)) (plist-put msg :message-overlay (make-overlay headers-start content-end)) - (plist-put msg :depth depth) - ;; Save the properties for this message. Currently this saves the ;; entire message (augmented it with other stuff), which seems ;; like overkill. We might save a reduced subset (for example, not ;; the content). (notmuch-show-set-message-properties msg) - ;; Set header visibility. (notmuch-show-headers-visible msg notmuch-message-headers-visible) - ;; Message visibility depends on whether it matched the search ;; criteria. (notmuch-show-message-visible msg (and (plist-get msg :match) @@ -1137,7 +1117,8 @@ is t, hide the part initially and show the button." (defun notmuch-show-toggle-elide-non-matching () "Toggle the display of non-matching messages." (interactive) - (setq notmuch-show-elide-non-matching-messages (not notmuch-show-elide-non-matching-messages)) + (setq notmuch-show-elide-non-matching-messages + (not notmuch-show-elide-non-matching-messages)) (message (if notmuch-show-elide-non-matching-messages "Showing matching messages only." "Showing all messages.")) @@ -1218,13 +1199,13 @@ buttons for a corresponding notmuch search." (url-unhex-string (match-string 0 mid-cid))))) (push (list (match-beginning 0) (match-end 0) (notmuch-id-to-query mid)) links))) - (dolist (link links) + (pcase-dolist (`(,beg ,end ,link) links) ;; Remove the overlay created by goto-address-mode - (remove-overlays (first link) (second link) 'goto-address t) - (make-text-button (first link) (second link) + (remove-overlays beg end 'goto-address t) + (make-text-button beg end :type 'notmuch-button-type 'action `(lambda (arg) - (notmuch-show ,(third link) current-prefix-arg)) + (notmuch-show ,link current-prefix-arg)) 'follow-link t 'help-echo "Mouse-1, RET: search for this message" 'face goto-address-mail-face))))) @@ -1263,38 +1244,33 @@ matched." (eval (car (get 'mm-inline-override-types 'standard-value)))) (cons "application/*" mm-inline-override-types) mm-inline-override-types))) - (switch-to-buffer (get-buffer-create buffer-name)) + (pop-to-buffer-same-window (get-buffer-create buffer-name)) ;; No need to track undo information for this buffer. (setq buffer-undo-list t) - (notmuch-show-mode) - ;; Set various buffer local variables to their appropriate initial ;; state. Do this after enabling `notmuch-show-mode' so that they ;; aren't wiped out. - (setq notmuch-show-thread-id thread-id - notmuch-show-parent-buffer parent-buffer - notmuch-show-query-context (if (or (string= query-context "") - (string= query-context "*")) - nil query-context) - - notmuch-show-process-crypto notmuch-crypto-process-mime - ;; If `elide-toggle', invert the default value. - notmuch-show-elide-non-matching-messages + (setq notmuch-show-thread-id thread-id) + (setq notmuch-show-parent-buffer parent-buffer) + (setq notmuch-show-query-context + (if (or (string= query-context "") + (string= query-context "*")) + nil + query-context)) + (setq notmuch-show-process-crypto notmuch-crypto-process-mime) + ;; If `elide-toggle', invert the default value. + (setq notmuch-show-elide-non-matching-messages (if elide-toggle (not notmuch-show-only-matching-messages) notmuch-show-only-matching-messages)) - (add-hook 'post-command-hook #'notmuch-show-command-hook nil t) (jit-lock-register #'notmuch-show-buttonise-links) - (notmuch-tag-clear-cache) - (let ((inhibit-read-only t)) (if (notmuch-show--build-buffer) ;; Messages were inserted into the buffer. (current-buffer) - ;; No messages were inserted - presumably none matched the ;; query. (kill-buffer (current-buffer)) @@ -1311,7 +1287,8 @@ and THREAD. The next query is THREAD alone, and serves as a fallback if the prior matches no messages." (let (queries) (push (list thread) queries) - (if context (push (list thread "and (" context ")") queries)) + (when context + (push (list thread "and (" context ")") queries)) queries)) (defun notmuch-show--build-buffer (&optional state) @@ -1322,8 +1299,8 @@ first relevant message. If no messages match the query return NIL." (let* ((cli-args (cons "--exclude=false" - (when notmuch-show-elide-non-matching-messages - (list "--entire-thread=false")))) + (and notmuch-show-elide-non-matching-messages + (list "--entire-thread=false")))) (queries (notmuch-show--build-queries notmuch-show-thread-id notmuch-show-query-context)) (forest nil) @@ -1337,26 +1314,21 @@ If no messages match the query return NIL." (setq queries (cdr queries))) (when forest (notmuch-show-insert-forest forest) - ;; Store the original tags for each message so that we can ;; display changes. (notmuch-show-mapc (lambda () (notmuch-show-set-prop :orig-tags (notmuch-show-get-tags)))) - ;; Set the header line to the subject of the first message. (setq header-line-format (replace-regexp-in-string "%" "%%" (notmuch-sanitize (notmuch-show-strip-re (notmuch-show-get-subject))))) - (run-hooks 'notmuch-show-hook) - (if state (notmuch-show-apply-state state) ;; With no state to apply, just go to the first message. (notmuch-show-goto-first-wanted-message))) - ;; Report back to the caller whether any messages matched. forest)) @@ -1374,7 +1346,7 @@ This includes: (list win-id-combo (notmuch-show-get-message-ids-for-open-messages)))) (defun notmuch-show-get-query () - "Return the current query in this show buffer" + "Return the current query in this show buffer." (if notmuch-show-query-context (concat notmuch-show-thread-id " and (" @@ -1385,9 +1357,9 @@ This includes: (defun notmuch-show-goto-message (msg-id) "Go to message with msg-id." (goto-char (point-min)) - (unless (loop if (string= msg-id (notmuch-show-get-message-id)) - return t - until (not (notmuch-show-goto-message-next))) + (unless (cl-loop if (string= msg-id (notmuch-show-get-message-id)) + return t + until (not (notmuch-show-goto-message-next))) (goto-char (point-min)) (message "Message-id not found.")) (notmuch-show-message-adjust)) @@ -1401,13 +1373,12 @@ This includes: - moving to the correct current message in every displayed window." (let ((win-msg-alist (car state)) (open (cadr state))) - ;; Open those that were open. (goto-char (point-min)) - (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties) - (member (notmuch-show-get-message-id) open)) - until (not (notmuch-show-goto-message-next))) - + (cl-loop do (notmuch-show-message-visible + (notmuch-show-get-message-properties) + (member (notmuch-show-get-message-id) open)) + until (not (notmuch-show-goto-message-next))) (dolist (win-msg-pair win-msg-alist) (with-selected-window (car win-msg-pair) ;; Go to the previously open message in this window @@ -1429,7 +1400,6 @@ reset based on the original query." ;; manually. (remove-overlays) (erase-buffer) - (unless (notmuch-show--build-buffer state) ;; No messages were inserted. (kill-buffer (current-buffer)) @@ -1452,7 +1422,7 @@ reset based on the original query." (define-key map "G" 'notmuch-show-stash-git-send-email) (define-key map "?" 'notmuch-subkeymap-help) map) - "Submap for stash commands") + "Submap for stash commands.") (fset 'notmuch-show-stash-map notmuch-show-stash-map) (defvar notmuch-show-part-map @@ -1464,13 +1434,14 @@ reset based on the original query." (define-key map "m" 'notmuch-show-choose-mime-of-part) (define-key map "?" 'notmuch-subkeymap-help) map) - "Submap for part commands") + "Submap for part commands.") (fset 'notmuch-show-part-map notmuch-show-part-map) (defvar notmuch-show-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map notmuch-common-keymap) (define-key map "Z" 'notmuch-tree-from-show-current-query) + (define-key map "U" 'notmuch-unthreaded-from-show-current-query) (define-key map (kbd "") 'widget-backward) (define-key map (kbd "M-TAB") 'notmuch-show-previous-button) (define-key map (kbd "") 'notmuch-show-previous-button) @@ -1545,20 +1516,27 @@ All currently available key bindings: \\{notmuch-show-mode-map}" (setq notmuch-buffer-refresh-function #'notmuch-show-refresh-view) - (setq buffer-read-only t - truncate-lines t) + (setq buffer-read-only t) + (setq truncate-lines t) (setq imenu-prev-index-position-function - #'notmuch-show-imenu-prev-index-position-function) + #'notmuch-show-imenu-prev-index-position-function) (setq imenu-extract-index-name-function - #'notmuch-show-imenu-extract-index-name-function)) + #'notmuch-show-imenu-extract-index-name-function)) (defun notmuch-tree-from-show-current-query () - "Call notmuch tree with the current query" + "Call notmuch tree with the current query." (interactive) (notmuch-tree notmuch-show-thread-id notmuch-show-query-context (notmuch-show-get-message-id))) +(defun notmuch-unthreaded-from-show-current-query () + "Call notmuch unthreaded with the current query." + (interactive) + (notmuch-unthreaded notmuch-show-thread-id + notmuch-show-query-context + (notmuch-show-get-message-id))) + (defun notmuch-show-move-to-message-top () (goto-char (notmuch-show-message-top))) @@ -1610,8 +1588,8 @@ of the current message." effects." (save-excursion (goto-char (point-min)) - (loop do (funcall function) - while (notmuch-show-goto-message-next)))) + (cl-loop do (funcall function) + while (notmuch-show-goto-message-next)))) ;; Functions relating to the visibility of messages and their ;; components. @@ -1630,7 +1608,8 @@ effects." (defun notmuch-show-set-message-properties (props) (save-excursion (notmuch-show-move-to-message-top) - (put-text-property (point) (+ (point) 1) :notmuch-message-properties props))) + (put-text-property (point) (+ (point) 1) + :notmuch-message-properties props))) (defun notmuch-show-get-message-properties () "Return the properties of the current message as a plist. @@ -1765,8 +1744,8 @@ We only mark it read once: if it is changed back then that is a user decision and we should not override it." (when (and (notmuch-show-message-visible-p) (not (notmuch-show-get-prop :seen))) - (notmuch-show-mark-read) - (notmuch-show-set-prop :seen t))) + (notmuch-show-mark-read) + (notmuch-show-set-prop :seen t))) (defvar notmuch-show--seen-has-errored nil) (make-variable-buffer-local 'notmuch-show--seen-has-errored) @@ -1783,8 +1762,9 @@ user decision and we should not override it." (setq notmuch-show--seen-has-errored 't) (setq header-line-format (concat header-line-format - (propertize " [some mark read tag changes may have failed]" - 'face font-lock-warning-face))))))))) + (propertize + " [some mark read tag changes may have failed]" + 'face font-lock-warning-face))))))))) (defun notmuch-show-filter-thread (query) "Filter or LIMIT the current thread based on a new query string. @@ -1805,12 +1785,11 @@ Reshows the current thread with matches defined by the new query-string." (let (message-ids done) (goto-char (point-min)) (while (not done) - (if (notmuch-show-message-visible-p) - (setq message-ids (append message-ids (list (notmuch-show-get-message-id))))) - (setq done (not (notmuch-show-goto-message-next))) - ) - message-ids - ))) + (when (notmuch-show-message-visible-p) + (setq message-ids + (append message-ids (list (notmuch-show-get-message-id))))) + (setq done (not (notmuch-show-goto-message-next)))) + message-ids))) ;; Commands typically bound to keys. @@ -1839,16 +1818,13 @@ current window), advance to the next open message." (> visible-end-of-this-message (window-end))) ;; The bottom of this message is not visible - scroll. (scroll-up nil)) - ((not (= end-of-this-message (point-max))) ;; This is not the last message - move to the next visible one. (notmuch-show-next-open-message)) - ((not (= (point) (point-max))) ;; This is the last message, but the cursor is not at the end of ;; the buffer. Move it there. (goto-char (point-max))) - (t ;; This is the last message - change the return value (setq ret t))) @@ -1866,11 +1842,12 @@ archives the entire current thread, (apply changes in thread from the search from which this thread was originally shown." (interactive) - (if (notmuch-show-advance) - (notmuch-show-archive-thread-then-next))) + (when (notmuch-show-advance) + (notmuch-show-archive-thread-then-next))) (defun notmuch-show-rewind () - "Backup through the thread (reverse scrolling compared to \\[notmuch-show-advance-and-archive]). + "Backup through the thread (reverse scrolling compared to \ +\\[notmuch-show-advance-and-archive]). Specifically, if the beginning of the previous email is fewer than `window-height' lines from the current point, move to it @@ -1885,9 +1862,9 @@ any effects from previous calls to (let ((start-of-message (notmuch-show-message-top)) (start-of-window (window-start))) (cond - ;; Either this message is properly aligned with the start of the - ;; window or the start of this message is not visible on the - ;; screen - scroll. + ;; Either this message is properly aligned with the start of the + ;; window or the start of this message is not visible on the + ;; screen - scroll. ((or (= start-of-message start-of-window) (< start-of-message start-of-window)) (scroll-down) @@ -1996,7 +1973,7 @@ to show, nil otherwise." (notmuch-show-message-visible props (plist-get props :match)))) (defun notmuch-show-goto-first-wanted-message () - "Move to the first open message and mark it read" + "Move to the first open message and mark it read." (goto-char (point-min)) (unless (notmuch-show-message-visible-p) (notmuch-show-next-open-message)) @@ -2024,7 +2001,7 @@ to show, nil otherwise." (let* ((id (notmuch-show-get-message-id)) (buf (get-buffer-create (concat "*notmuch-raw-" id "*"))) (inhibit-read-only t)) - (switch-to-buffer buf) + (pop-to-buffer-same-window buf) (erase-buffer) (let ((coding-system-for-read 'no-conversion)) (call-process notmuch-command nil t nil "show" "--format=raw" id)) @@ -2062,11 +2039,14 @@ message." (setq shell-command (concat notmuch-command " show --format=mbox --exclude=false " (shell-quote-argument - (mapconcat 'identity (notmuch-show-get-message-ids-for-open-messages) " OR ")) + (mapconcat 'identity + (notmuch-show-get-message-ids-for-open-messages) + " OR ")) " | " command)) (setq shell-command (concat notmuch-command " show --format=raw " - (shell-quote-argument (notmuch-show-get-message-id)) " | " command))) + (shell-quote-argument (notmuch-show-get-message-id)) + " | " command))) (let ((cwd default-directory) (buf (get-buffer-create (concat "*notmuch-pipe*")))) (with-current-buffer buf @@ -2080,7 +2060,7 @@ message." (set-buffer-modified-p nil) (setq buffer-read-only t) (unless (zerop exit-code) - (switch-to-buffer-other-window buf) + (pop-to-buffer buf) (message (format "Command '%s' exited abnormally with code %d" shell-command exit-code)))))))) @@ -2167,9 +2147,10 @@ argument, hide all of the messages." (interactive) (save-excursion (goto-char (point-min)) - (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties) - (not current-prefix-arg)) - until (not (notmuch-show-goto-message-next)))) + (cl-loop do (notmuch-show-message-visible + (notmuch-show-get-message-properties) + (not current-prefix-arg)) + until (not (notmuch-show-goto-message-next)))) (force-window-update)) (defun notmuch-show-next-button () @@ -2427,7 +2408,7 @@ MIME-TYPE is given then set the handle's mime-type to MIME-TYPE." (buf (notmuch-show-generate-part-buffer msg part)) (computed-type (or mime-type (plist-get part :computed-type))) (filename (plist-get part :filename)) - (disposition (if filename `(attachment (filename . ,filename))))) + (disposition (and filename `(attachment (filename . ,filename))))) (mm-make-handle buf (list computed-type) nil nil disposition))) (defun notmuch-show-apply-to-current-part-handle (fn &optional mime-type) @@ -2480,7 +2461,6 @@ part to be treated as if it had that mime-type." (interactive) (notmuch-show-apply-to-current-part-handle #'mm-pipe-part)) - (defun notmuch-show--mm-display-part (handle) "Use mm-display-part to display HANDLE in a new buffer. @@ -2488,7 +2468,7 @@ If the part is displayed in an external application then close the new buffer." (let ((buf (get-buffer-create (generate-new-buffer-name (concat " *notmuch-internal-part*"))))) - (switch-to-buffer buf) + (pop-to-buffer-same-window buf) (if (eq (mm-display-part handle) 'external) (kill-buffer buf) (goto-char (point-min)) @@ -2496,11 +2476,12 @@ the new buffer." (view-buffer buf 'kill-buffer-if-not-modified)))) (defun notmuch-show-choose-mime-of-part (mime-type) - "Choose the mime type to use for displaying part" + "Choose the mime type to use for displaying part." (interactive (list (completing-read "Mime type to use (default text/plain): " (mailcap-mime-types) nil nil nil nil "text/plain"))) - (notmuch-show-apply-to-current-part-handle #'notmuch-show--mm-display-part mime-type)) + (notmuch-show-apply-to-current-part-handle #'notmuch-show--mm-display-part + mime-type)) (defun notmuch-show-imenu-prev-index-position-function () "Move point to previous message in notmuch-show buffer. @@ -2527,9 +2508,9 @@ beginning of the line." message." `(save-excursion (save-restriction - (let ((extent (notmuch-show-message-extent))) - (narrow-to-region (car extent) (cdr extent)) - ,@body)))) + (let ((extent (notmuch-show-message-extent))) + (narrow-to-region (car extent) (cdr extent)) + ,@body)))) (defun notmuch-show--gather-urls () "Gather any URLs in the current message." @@ -2540,12 +2521,16 @@ message." (push (match-string-no-properties 0) urls)) (reverse urls)))) -(defun notmuch-show-browse-urls () - "Offer to browse any URLs in the current message." - (interactive) - (let ((urls (notmuch-show--gather-urls))) +(defun notmuch-show-browse-urls (&optional kill) + "Offer to browse any URLs in the current message. +With a prefix argument, copy the URL to the kill ring rather than +browsing." + (interactive "P") + (let ((urls (notmuch-show--gather-urls)) + (prompt (if kill "Copy URL to kill ring: " "Browse URL: ")) + (fn (if kill #'kill-new #'browse-url))) (if urls - (browse-url (completing-read "Browse URL: " (cdr urls) nil nil (car urls))) + (funcall fn (completing-read prompt urls nil nil nil nil (car urls))) (message "No URLs found.")))) (provide 'notmuch-show) diff --git a/emacs/notmuch-tag.el b/emacs/notmuch-tag.el index 0500927d..5d4a6865 100644 --- a/emacs/notmuch-tag.el +++ b/emacs/notmuch-tag.el @@ -20,19 +20,22 @@ ;; ;; Authors: Carl Worth ;; Damien Cassou -;; + ;;; Code: -;; -(require 'cl) +(require 'cl-lib) +(eval-when-compile + (require 'pcase)) + (require 'crm) -(require 'notmuch-lib) -(declare-function notmuch-search-tag "notmuch" tag-changes) -(declare-function notmuch-show-tag "notmuch-show" tag-changes) -(declare-function notmuch-tree-tag "notmuch-tree" tag-changes) +(require 'notmuch-lib) -(autoload 'notmuch-jump "notmuch-jump") +(declare-function notmuch-search-tag "notmuch" + (tag-changes &optional beg end only-matched)) +(declare-function notmuch-show-tag "notmuch-show" (tag-changes)) +(declare-function notmuch-tree-tag "notmuch-tree" (tag-changes)) +(declare-function notmuch-jump "notmuch-jump" (action-map prompt)) (define-widget 'notmuch-tag-key-type 'list "A single key tagging binding." @@ -40,7 +43,9 @@ :args '((list :inline t :format "%v" (key-sequence :tag "Key") - (radio :tag "Tag operations" (repeat :tag "Tag list" (string :format "%v" :tag "change")) + (radio :tag "Tag operations" + (repeat :tag "Tag list" + (string :format "%v" :tag "change")) (variable :tag "Tag variable")) (string :tag "Name")))) @@ -80,7 +85,7 @@ from TAGGING-OPERATIONS." :group 'notmuch-tag) (define-widget 'notmuch-tag-format-type 'lazy - "Customize widget for notmuch-tag-format and friends" + "Customize widget for notmuch-tag-format and friends." :type '(alist :key-type (regexp :tag "Tag") :extra-offset -3 :value-type @@ -230,7 +235,7 @@ DATA is the content of an SVG picture (e.g., as returned by (defun notmuch-tag-star-icon () "Return SVG data representing a star icon. This can be used with `notmuch-tag-format-image-data'." -" + "