notmuch release 0.31.2-3 for unstable (sid) [dgit]

[dgit distro=debian no-split --quilt=linear]
 -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCAAdFiEEkiyHYXwaY0SiY6fqA0U5G1WqFSEFAl+phO4ACgkQA0U5G1Wq
 FSEy5xAAgah+TYXvXzpf1IeJ7NjABVqtHEmNJprNXe5FwuIS4l30A1vo69Qk5IX4
 //hJ1+/vucZHSvARYPX/aeZOfQ8WknaNgIahnQPdJTOUw93cdF1hZNyQ5X0WAVpH
 OjXoUuBldoRx8xlgKoFPBWYsM/MqNE8sYXcaV858WeLj1HTHOI3CIFOsdXct2oR+
 CuhzaFiiwPLdjNjGKRItSwCYvO8rZyWPV3ZX79GTLGfc4dDOTmU7kD/cpAcuJ7Jp
 LJsiuH4xIJ4oVxc0aandTCkMnFUxhB64EfzqeYvezs4Wf8Iu4fUbSwQNRYzEvCyT
 XSoHJRgpWQMGf/Mk8po9JmKkOGCVAijK8jIT6AD5ks9+Nqeh23Z/k7j/IoWJv9Oy
 aaP2NWu7C76FT5HQoHejdZRVGyxHqh+sXvgCbpI6cTk9HG6ydlHzxZ4sJoeJxf7x
 EhJUIEzORRU9fenX73LLH6RwoSDCbcWZZ4Bta822n9HplgCXscpa3h/rOcaQsKjA
 HNm+5k/QLQoAAfwJUJpYCPz5ZAsxWA9NQWo3/wWsEphpLvcyanBKVbBy4oJacJ7B
 G1iLCydKjh/E2hf3DqC95QkiaJ41D9q6TH9YpdRQHvjx0Qd0rCx/33d/bDl66aYx
 FLPCYpdhL3gsSTtJMvH6xY891YzryU84NOgTStu9T9OjejmP/ts=
 =3fyS
 -----END PGP SIGNATURE-----

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]
This commit is contained in:
David Bremner 2020-12-13 10:38:31 -04:00
commit 7a9c97e8a5
277 changed files with 12790 additions and 4773 deletions

View file

@ -15,7 +15,7 @@
(emacs-lisp-mode (emacs-lisp-mode
(indent-tabs-mode . t) (indent-tabs-mode . t)
(tab-width . 8)) (tab-width . 8))
(shell-mode (sh-mode
(indent-tabs-mode . t) (indent-tabs-mode . t)
(tab-width . 8) (tab-width . 8)
(sh-basic-offset . 4) (sh-basic-offset . 4)

26
.gitignore vendored
View file

@ -1,18 +1,20 @@
*.[ao]
*.stamp
*cscope*
*~
.*.swp
/.deps
/.first-build-message /.first-build-message
/.stamps
/Makefile.config /Makefile.config
/bindings/python-cffi/build/
/lib/libnotmuch*.dylib
/lib/libnotmuch.so*
/notmuch
/notmuch-shared
/releases
/sh.config /sh.config
/sphinx.config
/version.stamp /version.stamp
TAGS TAGS
tags tags
*cscope*
/.deps
/notmuch
/notmuch-shared
/lib/libnotmuch.so*
/lib/libnotmuch*.dylib
*.[ao]
*~
.*.swp
/releases
/.stamps
*.stamp

View file

@ -1,23 +1,25 @@
language: c language: c
dist: xenial dist: bionic
addons: addons:
apt: apt:
sources: sources:
- sourceline: 'ppa:xapian-backports/ppa' - sourceline: 'ppa:xapian-backports/ppa'
- sourceline: 'ppa:notmuch/notmuch'
packages: packages:
- dtach - dtach
- libxapian-dev - libxapian-dev
- libgmime-3.0-dev - libgmime-3.0-dev
- libtalloc-dev - libtalloc-dev
- python3-sphinx - python3-sphinx
- python3-cffi
- python3-pytest
- python3-setuptools
- libpython3-all-dev
- gpgsm - gpgsm
script: script:
- ./configure - ./configure
- make download-test-databases
- make test - make test
notifications: notifications:

109
AUTHORS
View file

@ -1,5 +1,6 @@
Carl Worth <cworth@cworth.org> is the primary author of Notmuch. Carl Worth <cworth@cworth.org> was the original author of Notmuch.
But there's really not much that he's done. There's been a lot of 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: standing on shoulders here:
William Morgan deserves credit for providing the primary inspiration 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 system libraries, compilers, and the kernel that make it all work
(thanks GNU, thanks Linux). Thanks to everyone who has played a part! (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 Here is an incomplete list of other people that have made
contributions to Notmuch (whether by code, bug reporting/fixes, contributions to Notmuch (whether by code, bug reporting/fixes,
ideas, inspiration, testing or feedback): ideas, inspiration, testing or feedback):
Martin Krafft Martin Krafft
Keith Packard Jamey Sharp
Jamey Sharp
The Notmuch project acknowledges the contributions of the following
organizations via their employees
Google LLC

View file

@ -95,7 +95,7 @@ dependencies with a single simple command line. For example:
For Fedora and similar: 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 On other systems, a similar command can be used, but the details of
the package names may be different. the package names may be different.

View file

@ -1,3 +1,4 @@
# -*- makefile-gmake -*-
# Here's the (hopefully simple) versioning scheme. # Here's the (hopefully simple) versioning scheme.
# #
# Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We # Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We
@ -16,7 +17,7 @@ else
DATE:=$(shell date +%F) DATE:=$(shell date +%F)
endif endif
VERSION:=$(shell cat ${srcdir}/version) VERSION:=$(shell cat ${srcdir}/version.txt)
ELPA_VERSION:=$(subst ~,_,$(VERSION)) ELPA_VERSION:=$(subst ~,_,$(VERSION))
ifeq ($(filter release release-message pre-release update-versions,$(MAKECMDGOALS)),) ifeq ($(filter release release-message pre-release update-versions,$(MAKECMDGOALS)),)
ifeq ($(IS_GIT),yes) ifeq ($(IS_GIT),yes)
@ -39,6 +40,7 @@ DEB_TAG=debian/$(UPSTREAM_TAG)-1
RELEASE_HOST=notmuchmail.org RELEASE_HOST=notmuchmail.org
RELEASE_DIR=/srv/notmuchmail.org/www/releases RELEASE_DIR=/srv/notmuchmail.org/www/releases
DOC_DIR=/srv/notmuchmail.org/www/doc/latest
RELEASE_URL=https://notmuchmail.org/releases RELEASE_URL=https://notmuchmail.org/releases
TAR_FILE=$(PACKAGE)-$(VERSION).tar.xz TAR_FILE=$(PACKAGE)-$(VERSION).tar.xz
ELPA_FILE:=$(PACKAGE)-emacs-$(ELPA_VERSION).tar ELPA_FILE:=$(PACKAGE)-emacs-$(ELPA_VERSION).tar
@ -49,8 +51,7 @@ DETACHED_SIG_FILE=$(TAR_FILE).asc
PV_FILE=bindings/python/notmuch/version.py PV_FILE=bindings/python/notmuch/version.py
# Smash together user's values with our extra values # Smash together user's values with our extra values
STD_CFLAGS := -std=gnu99 FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(extra_cflags) $(CONFIGURE_CFLAGS)
FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(STD_CFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(extra_cflags) $(CONFIGURE_CFLAGS)
FINAL_CXXFLAGS = $(CPPFLAGS) $(CXXFLAGS) $(WARN_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) $(CONFIGURE_CXXFLAGS) FINAL_CXXFLAGS = $(CPPFLAGS) $(CXXFLAGS) $(WARN_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) $(CONFIGURE_CXXFLAGS)
FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Lutil -lnotmuch_util -Llib -lnotmuch FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Lutil -lnotmuch_util -Llib -lnotmuch
ifeq ($(LIBDIR_IN_LDCONFIG),0) ifeq ($(LIBDIR_IN_LDCONFIG),0)

View file

@ -1,7 +1,7 @@
# -*- makefile -*- # -*- makefile-gmake -*-
.PHONY: all .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 ($(MAKECMDGOALS),)
ifeq ($(shell cat .first-build-message 2>/dev/null),) ifeq ($(shell cat .first-build-message 2>/dev/null),)
@NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all @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) # Depend (also) on the file 'version'. In case of ifeq ($(IS_GIT),yes)
# this file may already have been updated. # this file may already have been updated.
version.stamp: $(srcdir)/version version.stamp: $(srcdir)/version.txt
echo $(VERSION) > $@ echo $(VERSION) > $@
$(TAR_FILE): $(TAR_FILE):
@ -30,12 +30,12 @@ $(TAR_FILE):
echo "Warning: No signed tag for $(VERSION)"; \ echo "Warning: No signed tag for $(VERSION)"; \
fi ; \ fi ; \
git archive --format=tar --prefix=$(PACKAGE)-$(VERSION)/ $$ref > $(TAR_FILE).tmp 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` ; \ ct=`git --no-pager log -1 --pretty=format:%ct $$ref` ; \
tar --owner root --group root --append -f $(TAR_FILE).tmp \ tar --owner root --group root --append -f $(TAR_FILE).tmp \
--transform s_^_$(PACKAGE)-$(VERSION)/_ \ --transform s_^_$(PACKAGE)-$(VERSION)/_ \
--transform 's_.tmp$$__' --mtime=@$$ct version.tmp --transform 's_.tmp$$__' --mtime=@$$ct version.txt.tmp
rm version.tmp rm version.txt.tmp
xz -C sha256 -9 < $(TAR_FILE).tmp > $(TAR_FILE) xz -C sha256 -9 < $(TAR_FILE).tmp > $(TAR_FILE)
@echo "Source is ready for release in $(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}\'/" \ sed -i -e "s/^__VERSION__[[:blank:]]*=.*$$/__VERSION__ = \'${VERSION}\'/" \
-e "s/^SOVERSION[[:blank:]]*=.*$$/SOVERSION = \'${LIBNOTMUCH_VERSION_MAJOR}\'/" \ -e "s/^SOVERSION[[:blank:]]*=.*$$/SOVERSION = \'${LIBNOTMUCH_VERSION_MAJOR}\'/" \
${PV_FILE} ${PV_FILE}
cp version.txt bindings/python-cffi
# We invoke make recursively only to force ordering of our phony # We invoke make recursively only to force ordering of our phony
# targets in the case of parallel invocation of make (-j). # targets in the case of parallel invocation of make (-j).
@ -66,6 +67,7 @@ update-versions:
release: verify-source-tree-and-version release: verify-source-tree-and-version
$(MAKE) VERSION=$(VERSION) verify-newer $(MAKE) VERSION=$(VERSION) verify-newer
$(MAKE) VERSION=$(VERSION) clean $(MAKE) VERSION=$(VERSION) clean
$(MAKE) VERSION=$(VERSION) sphinx-html
$(MAKE) VERSION=$(VERSION) test $(MAKE) VERSION=$(VERSION) test
git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG) git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG)
$(MAKE) VERSION=$(VERSION) $(SHA256_FILE) $(DETACHED_SIG_FILE) $(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 git push origin $(VERSION) $(DEB_TAG) release pristine-tar
cd releases && scp $(TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) $(RELEASE_HOST):$(RELEASE_DIR) 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)" 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 endif
@echo "Please send a release announcement using $(PACKAGE)-$(VERSION).announce as a template." @echo "Please send a release announcement using $(PACKAGE)-$(VERSION).announce as a template."
@ -88,7 +91,7 @@ pre-release:
$(MAKE) VERSION=$(VERSION) test $(MAKE) VERSION=$(VERSION) test
git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG) git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG)
git tag -s -m "$(PACKAGE) Debian $(VERSION)-1 upload (same as $(VERSION))" $(DEB_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) ln -sf $(TAR_FILE) $(DEB_TAR_FILE)
pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG) pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG)
mkdir -p releases mkdir -p releases
@ -97,14 +100,16 @@ pre-release:
.PHONY: debian-snapshot .PHONY: debian-snapshot
debian-snapshot: debian-snapshot:
make VERSION=$(VERSION) clean make VERSION=$(VERSION) clean
TMPFILE=$$(mktemp /tmp/notmuch.XXXXXX); \ RETVAL=0 && \
cp debian/changelog $${TMPFILE}; \ TMPFILE=$$(mktemp /tmp/notmuch.XXXXXX) && \
EDITOR=/bin/true dch -b -v $(VERSION)+1 \ cp debian/changelog $${TMPFILE} && \
-D UNRELEASED 'test build, not for upload'; \ (EDITOR=/bin/true dch -b -v $(VERSION)+1 \
echo '3.0 (native)' > debian/source/format; \ -D UNRELEASED 'test build, not for upload' && \
debuild -us -uc; \ echo '3.0 (native)' > debian/source/format && \
mv -f $${TMPFILE} debian/changelog; \ debuild -us -uc); RETVAL=$$? \
echo '3.0 (quilt)' > debian/source/format mv -f $${TMPFILE} debian/changelog; \
echo '3.0 (quilt)' > debian/source/format; \
exit $$RETVAL
.PHONY: release-message .PHONY: release-message
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) version.stamp notmuch-*.tar.gz.tmp
CLEAN := $(CLEAN) .deps 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/%) CPPCHECK_STAMPS := $(SRCS:%=.stamps/cppcheck/%)
.PHONY: cppcheck .PHONY: cppcheck

153
NEWS
View file

@ -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) Notmuch 0.29.3 (2019-11-27)
=========================== ===========================
@ -72,7 +223,7 @@ information about cryptographic protections for the Subject header.
Emacs Emacs
----- -----
Optionally check for missing attachements in outgoing messages (see Optionally check for missing attachments in outgoing messages (see
function `notmuch-mua-attachment-check`). function `notmuch-mua-attachment-check`).
Bind `B` to browse URLs in current message. Bind `B` to browse URLs in current message.

View file

@ -1,4 +1,4 @@
# -*- makefile -*- # -*- makefile-gmake -*-
dir := bindings dir := bindings
@ -13,6 +13,13 @@ ifeq ($(HAVE_RUBY_DEV),1)
$(MAKE) -C $(dir)/ruby $(MAKE) -C $(dir)/ruby
endif 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/%, \ CLEAN += $(patsubst %,$(dir)/ruby/%, \
.RUBYARCHDIR.time \ .RUBYARCHDIR.time \
Makefile database.o directory.o filenames.o\ Makefile database.o directory.o filenames.o\
@ -20,3 +27,5 @@ CLEAN += $(patsubst %,$(dir)/ruby/%, \
status.o tags.o thread.o threads.o) status.o tags.o thread.o threads.o)
CLEAN += bindings/ruby/.vendorarchdir.time CLEAN += bindings/ruby/.vendorarchdir.time
CLEAN += bindings/python-cffi/build

View file

@ -0,0 +1,2 @@
include MANIFEST.in
include tox.ini

View file

@ -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

View file

@ -0,0 +1,238 @@
import abc
import collections.abc
from notmuch2 import _capi as capi
from notmuch2 import _errors as errors
__all__ = ['NotmuchObject', 'BinString']
class NotmuchObject(metaclass=abc.ABCMeta):
"""Base notmuch object syntax.
This base class exists to define the memory management handling
required to use the notmuch library. It is meant as an interface
definition rather than a base class, though you can use it as a
base class to ensure you don't forget part of the interface. It
only concerns you if you are implementing this package itself
rather then using it.
libnotmuch uses a hierarchical memory allocator, where freeing the
memory of a parent object also frees the memory of all child
objects. To make this work seamlessly in Python this package
keeps references to parent objects which makes them stay alive
correctly under normal circumstances. When an object finally gets
deleted the :meth:`__del__` method will be called to free the
memory.
However during some peculiar situations, e.g. interpreter
shutdown, it is possible for the :meth:`__del__` method to have
been called, whele there are still references to an object. This
could result in child objects asking their memory to be freed
after the parent has already freed the memory, making things
rather unhappy as double frees are not taken lightly in C. To
handle this case all objects need to follow the same protocol to
destroy themselves, see :meth:`destroy`.
Once an object has been destroyed trying to use it should raise
the :exc:`ObjectDestroyedError` exception. For this see also the
convenience :class:`MemoryPointer` descriptor in this module which
can be used as a pointer to libnotmuch memory.
"""
@abc.abstractmethod
def __init__(self, parent, *args, **kwargs):
"""Create a new object.
Other then for the toplevel :class:`Database` object
constructors are only ever called by internal code and not by
the user. Per convention their signature always takes the
parent object as first argument. Feel free to make the rest
of the signature match the object's requirement. The object
needs to keep a reference to the parent, so it can check the
parent is still alive.
"""
@property
@abc.abstractmethod
def alive(self):
"""Whether the object is still alive.
This indicates whether the object is still alive. The first
thing this needs to check is whether the parent object is
still alive, if it is not then this object can not be alive
either. If the parent is alive then it depends on whether the
memory for this object has been freed yet or not.
"""
def __del__(self):
self._destroy()
@abc.abstractmethod
def _destroy(self):
"""Destroy the object, freeing all memory.
This method needs to destroy the object on the
libnotmuch-level. It must ensure it's not been destroyed by
it's parent object yet before doing so. It also must be
idempotent.
"""
class MemoryPointer:
"""Data Descriptor to handle accessing libnotmuch pointers.
Most :class:`NotmuchObject` instances will have one or more CFFI
pointers to C-objects. Once an object is destroyed this pointer
should no longer be used and a :exc:`ObjectDestroyedError`
exception should be raised on trying to access it. This
descriptor simplifies implementing this, allowing the creation of
an attribute which can be assigned to, but when accessed when the
stored value is *None* it will raise the
:exc:`ObjectDestroyedError` exception::
class SomeOjb:
_ptr = MemoryPointer()
def __init__(self, ptr):
self._ptr = ptr
def destroy(self):
somehow_free(self._ptr)
self._ptr = None
def do_something(self):
return some_libnotmuch_call(self._ptr)
"""
def __get__(self, instance, owner):
try:
val = getattr(instance, self.attr_name, None)
except AttributeError:
# We're not on 3.6+ and self.attr_name does not exist
self.__set_name__(instance, 'dummy')
val = getattr(instance, self.attr_name, None)
if val is None:
raise errors.ObjectDestroyedError()
return val
def __set__(self, instance, value):
try:
setattr(instance, self.attr_name, value)
except AttributeError:
# We're not on 3.6+ and self.attr_name does not exist
self.__set_name__(instance, 'dummy')
setattr(instance, self.attr_name, value)
def __set_name__(self, instance, name):
self.attr_name = '_memptr_{}_{:x}'.format(name, id(instance))
class BinString(str):
"""A str subclass with binary data.
Most data in libnotmuch should be valid ASCII or valid UTF-8.
However since it is a C library these are represented as
bytestrings instead which means on an API level we can not
guarantee that decoding this to UTF-8 will both succeed and be
lossless. This string type converts bytes to unicode in a lossy
way, but also makes the raw bytes available.
This object is a normal unicode string for most intents and
purposes, but you can get the original bytestring back by calling
``bytes()`` on it.
"""
def __new__(cls, data, encoding='utf-8', errors='ignore'):
if not isinstance(data, bytes):
data = bytes(data, encoding=encoding)
strdata = str(data, encoding=encoding, errors=errors)
inst = super().__new__(cls, strdata)
inst._bindata = data
return inst
@classmethod
def from_cffi(cls, cdata):
"""Create a new string from a CFFI cdata pointer."""
return cls(capi.ffi.string(cdata))
def __bytes__(self):
return self._bindata
class NotmuchIter(NotmuchObject, collections.abc.Iterator):
"""An iterator for libnotmuch iterators.
It is tempting to use a generator function instead, but this would
not correctly respect the :class:`NotmuchObject` memory handling
protocol and in some unsuspecting cornercases cause memory
trouble. You probably want to sublcass this in order to wrap the
value returned by :meth:`__next__`.
:param parent: The parent object.
:type parent: NotmuchObject
:param iter_p: The CFFI pointer to the C iterator.
:type iter_p: cffi.cdata
:param fn_destory: The CFFI notmuch_*_destroy function.
:param fn_valid: The CFFI notmuch_*_valid function.
:param fn_get: The CFFI notmuch_*_get function.
:param fn_next: The CFFI notmuch_*_move_to_next function.
"""
_iter_p = MemoryPointer()
def __init__(self, parent, iter_p,
*, fn_destroy, fn_valid, fn_get, fn_next):
self._parent = parent
self._iter_p = iter_p
self._fn_destroy = fn_destroy
self._fn_valid = fn_valid
self._fn_get = fn_get
self._fn_next = fn_next
def __del__(self):
self._destroy()
@property
def alive(self):
if not self._parent.alive:
return False
try:
self._iter_p
except errors.ObjectDestroyedError:
return False
else:
return True
def _destroy(self):
if self.alive:
try:
self._fn_destroy(self._iter_p)
except errors.ObjectDestroyedError:
pass
self._iter_p = None
def __iter__(self):
"""Return the iterator itself.
Note that as this is an iterator and not a container this will
not return a new iterator. Thus any elements already consumed
will not be yielded by the :meth:`__next__` method anymore.
"""
return self
def __next__(self):
if not self._fn_valid(self._iter_p):
self._destroy()
raise StopIteration()
obj_p = self._fn_get(self._iter_p)
self._fn_next(self._iter_p)
return obj_p
def __repr__(self):
try:
self._iter_p
except errors.ObjectDestroyedError:
return '<NotmuchIter (exhausted)>'
else:
return '<NotmuchIter>'

View file

@ -0,0 +1,339 @@
import cffi
ffibuilder = cffi.FFI()
ffibuilder.set_source(
'notmuch2._capi',
r"""
#include <stdlib.h>
#include <time.h>
#include <notmuch.h>
#if LIBNOTMUCH_MAJOR_VERSION < 5
#error libnotmuch version not supported by notmuch2 python bindings
#endif
#if LIBNOTMUCH_MINOR_VERSION < 1
#ERROR libnotmuch version < 5.1 not supported
#endif
""",
include_dirs=['../../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)

View file

@ -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)

View file

@ -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)

View file

@ -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'

View file

@ -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: <cdata>
:param dup: Whether the message was a duplicate on insertion.
:type dup: None or bool
"""
_msg_p = base.MemoryPointer()
def __init__(self, parent, msg_p, *, db):
self._parent = parent
self._msg_p = msg_p
self._db = db
@property
def alive(self):
if not self._parent.alive:
return False
try:
self._msg_p
except errors.ObjectDestroyedError:
return False
else:
return True
def __del__(self):
self._destroy()
def _destroy(self):
if self.alive:
capi.lib.notmuch_message_destroy(self._msg_p)
self._msg_p = None
@property
def messageid(self):
"""The message ID as a string.
The message ID is decoded with the ignore error handler. This
is fine as long as the message ID is well formed. If it is
not valid ASCII then this will be lossy. So if you need to be
able to write the exact same message ID back you should use
:attr:`messageidb`.
Note that notmuch will decode the message ID value and thus
strip off the surrounding ``<`` and ``>`` characters. This is
different from Python's :mod:`email` package behaviour which
leaves these characters in place.
:returns: The message ID.
:rtype: :class:`BinString`, this is a normal str but calling
bytes() on it will return the original bytes used to create
it.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
return base.BinString(capi.ffi.string(ret))
@property
def threadid(self):
"""The thread ID.
The thread ID is decoded with the surrogateescape error
handler so that it is possible to reconstruct the original
thread ID if it is not valid UTF-8.
:returns: The thread ID.
:rtype: :class:`BinString`, this is a normal str but calling
bytes() on it will return the original bytes used to create
it.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
return base.BinString(capi.ffi.string(ret))
@property
def path(self):
"""A pathname of the message as a pathlib.Path instance.
If multiple files in the database contain the same message ID
this will be just one of the files, chosen at random.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_filename(self._msg_p)
return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
@property
def pathb(self):
"""A pathname of the message as a bytes object.
See :attr:`path` for details, this is the same but does return
the path as a bytes object which is faster but less convenient.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_filename(self._msg_p)
return capi.ffi.string(ret)
def filenames(self):
"""Return an iterator of all files for this message.
If multiple files contained the same message ID they will all
be returned here. The files are returned as instances of
:class:`pathlib.Path`.
:returns: Iterator yielding :class:`pathlib.Path` instances.
:rtype: iter
:raises ObjectDestroyedError: if used after destroyed.
"""
fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
return PathIter(self, fnames_p)
def filenamesb(self):
"""Return an iterator of all files for this message.
This is like :meth:`pathnames` but the files are returned as
byte objects instead.
:returns: Iterator yielding :class:`bytes` instances.
:rtype: iter
:raises ObjectDestroyedError: if used after destroyed.
"""
fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
return FilenamesIter(self, fnames_p)
@property
def ghost(self):
"""Indicates whether this message is a ghost message.
A ghost message if a message which we know exists, but it has
no files or content associated with it. This can happen if
it was referenced by some other message. Only the
:attr:`messageid` and :attr:`threadid` attributes are valid
for it.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_flag(
self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
return bool(ret)
@property
def excluded(self):
"""Indicates whether this message was excluded from the query.
When a message is created from a search, sometimes messages
that where excluded by the search query could still be
returned by it, e.g. because they are part of a thread
matching the query. the :meth:`Database.query` method allows
these messages to be flagged, which results in this property
being set to *True*.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_flag(
self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
return bool(ret)
@property
def 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)

View file

@ -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]

View file

@ -0,0 +1,359 @@
import collections.abc
import notmuch2._base as base
import notmuch2._capi as capi
import notmuch2._errors as errors
__all__ = ['ImmutableTagSet', 'MutableTagSet', 'TagsIter']
class ImmutableTagSet(base.NotmuchObject, collections.abc.Set):
"""The tags associated with a message thread or whole database.
Both a thread as well as the database expose the union of all tags
in messages associated with them. This exposes these as a
:class:`collections.abc.Set` object.
Note that due to the underlying notmuch API the performance of the
implementation is not the same as you would expect from normal
sets. E.g. the :meth:`__contains__` and :meth:`__len__` are O(n)
rather then O(1).
Tags are internally stored as bytestrings but normally exposed as
unicode strings using the UTF-8 encoding and the *ignore* decoder
error handler. However the :meth:`iter` method can be used to
return tags as bytestrings or using a different error handler.
Note that when doing arithmetic operations on tags, this class
will return a plain normal set as it is no longer associated with
the message.
:param parent: the parent object
:param ptr_name: the name of the attribute on the parent which will
return the memory pointer. This allows this object to
access the pointer via the parent's descriptor and thus
trigger :class:`MemoryPointer`'s memory safety.
:param cffi_fn: the callable CFFI wrapper to retrieve the tags
iter. This can be one of notmuch_database_get_all_tags,
notmuch_thread_get_tags or notmuch_message_get_tags.
"""
def __init__(self, parent, ptr_name, cffi_fn):
self._parent = parent
self._ptr = lambda: getattr(parent, ptr_name)
self._cffi_fn = cffi_fn
def __del__(self):
self._destroy()
@property
def alive(self):
return self._parent.alive
def _destroy(self):
pass
@classmethod
def _from_iterable(cls, it):
return set(it)
def __iter__(self):
"""Return an iterator over the tags.
Tags are yielded as unicode strings, decoded using the
"ignore" error handler.
:raises NullPointerError: If the iterator can not be created.
"""
return self.iter(encoding='utf-8', errors='ignore')
def iter(self, *, encoding=None, errors='strict'):
"""Aternate iterator constructor controlling string decoding.
Tags are stored as bytes in the notmuch database, in Python
it's easier to work with unicode strings and thus is what the
normal iterator returns. However this method allows you to
specify how you would like to get the tags, defaulting to the
bytestring representation instead of unicode strings.
:param encoding: Which codec to use. The default *None* does not
decode at all and will return the unmodified bytes.
Otherwise this is passed on to :func:`str.decode`.
:param errors: If using a codec, this is the error handler.
See :func:`str.decode` to which this is passed on.
:raises NullPointerError: When things do not go as planned.
"""
# self._cffi_fn should point either to
# notmuch_database_get_all_tags, notmuch_thread_get_tags or
# notmuch_message_get_tags. nothmuch.h suggests these never
# fail, let's handle NULL anyway.
tags_p = self._cffi_fn(self._ptr())
if tags_p == capi.ffi.NULL:
raise errors.NullPointerError()
tags = TagsIter(self, tags_p, encoding=encoding, errors=errors)
return tags
def __len__(self):
return sum(1 for t in self)
def __contains__(self, tag):
if isinstance(tag, str):
tag = tag.encode()
for msg_tag in self.iter():
if tag == msg_tag:
return True
else:
return False
def __eq__(self, other):
return tuple(sorted(self.iter())) == tuple(sorted(other.iter()))
def issubset(self, other):
return self <= other
def issuperset(self, other):
return self >= other
def union(self, other):
return self | other
def intersection(self, other):
return self & other
def difference(self, other):
return self - other
def symmetric_difference(self, other):
return self ^ other
def copy(self):
return set(self)
def __hash__(self):
return hash(tuple(self.iter()))
def __repr__(self):
return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
name=self.__class__.__name__,
addr=id(self),
tags=', '.join(repr(t) for t in self))
class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
"""The tags associated with a message.
This is a :class:`collections.abc.MutableSet` object which can be
used to manipulate the tags of a message.
Note that due to the underlying notmuch API the performance of the
implementation is not the same as you would expect from normal
sets. E.g. the ``in`` operator and variants are O(n) rather then
O(1).
Tags are bytestrings and calling ``iter()`` will return an
iterator yielding bytestrings. However the :meth:`iter` method
can be used to return tags as unicode strings, while all other
operations accept either byestrings or unicode strings. In case
unicode strings are used they will be encoded using utf-8 before
being passed to notmuch.
"""
# Since we subclass ImmutableTagSet we inherit a __hash__. But we
# are mutable, setting it to None will make the Python machinery
# recognise us as unhashable.
__hash__ = None
def add(self, tag):
"""Add a tag to the message.
:param tag: The tag to add.
:type tag: str or bytes. A str will be encoded using UTF-8.
:param sync_flags: Whether to sync the maildir flags with the
new set of tags. Leaving this as *None* respects the
configuration set in the database, while *True* will always
sync and *False* will never sync.
:param sync_flags: NoneType or bool
:raises TypeError: If the tag is not a valid type.
:raises TagTooLongError: If the added tag exceeds the maximum
length, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
:raises ReadOnlyDatabaseError: If the database is opened in
read-only mode.
"""
if isinstance(tag, str):
tag = tag.encode()
if not isinstance(tag, bytes):
raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
ret = capi.lib.notmuch_message_add_tag(self._ptr(), tag)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def discard(self, tag):
"""Remove a tag from the message.
:param tag: The tag to remove.
:type tag: str of bytes. A str will be encoded using UTF-8.
:param sync_flags: Whether to sync the maildir flags with the
new set of tags. Leaving this as *None* respects the
configuration set in the database, while *True* will always
sync and *False* will never sync.
:param sync_flags: NoneType or bool
:raises TypeError: If the tag is not a valid type.
:raises TagTooLongError: If the tag exceeds the maximum
length, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
:raises ReadOnlyDatabaseError: If the database is opened in
read-only mode.
"""
if isinstance(tag, str):
tag = tag.encode()
if not isinstance(tag, bytes):
raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
ret = capi.lib.notmuch_message_remove_tag(self._ptr(), tag)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def clear(self):
"""Remove all tags from the message.
:raises ReadOnlyDatabaseError: If the database is opened in
read-only mode.
"""
ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def from_maildir_flags(self):
"""Update the tags based on the state in the message's maildir flags.
This function examines the filenames of 'message' for maildir
flags, and adds or removes tags on 'message' as follows when
these flags are present:
Flag Action if present
---- -----------------
'D' Adds the "draft" tag to the message
'F' Adds the "flagged" tag to the message
'P' Adds the "passed" tag to the message
'R' Adds the "replied" tag to the message
'S' Removes the "unread" tag from the message
For each flag that is not present, the opposite action
(add/remove) is performed for the corresponding tags.
Flags are identified as trailing components of the filename
after a sequence of ":2,".
If there are multiple filenames associated with this message,
the flag is considered present if it appears in one or more
filenames. (That is, the flags from the multiple filenames are
combined with the logical OR operator.)
"""
ret = capi.lib.notmuch_message_maildir_flags_to_tags(self._ptr())
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def to_maildir_flags(self):
"""Update the message's maildir flags based on the notmuch tags.
If the message's filename is in a maildir directory, that is a
directory named ``new`` or ``cur``, and has a valid maildir
filename then the flags will be added as such:
'D' if the message has the "draft" tag
'F' if the message has the "flagged" tag
'P' if the message has the "passed" tag
'R' if the message has the "replied" tag
'S' if the message does not have the "unread" tag
Any existing flags unmentioned in the list above will be
preserved in the renaming.
Also, if this filename is in a directory named "new", rename it to
be within the neighboring directory named "cur".
In case there are multiple files associated with the message
all filenames will get the same logic applied.
"""
ret = capi.lib.notmuch_message_tags_to_maildir_flags(self._ptr())
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
class TagsIter(base.NotmuchObject, collections.abc.Iterator):
"""Iterator over tags.
This is only an iterator, not a container so calling
:meth:`__iter__` does not return a new, replenished iterator but
only itself.
:param parent: The parent object to keep alive.
:param tags_p: The CFFI pointer to the C-level tags iterator.
:param encoding: Which codec to use. The default *None* does not
decode at all and will return the unmodified bytes.
Otherwise this is passed on to :func:`str.decode`.
:param errors: If using a codec, this is the error handler.
See :func:`str.decode` to which this is passed on.
:raises ObjectDestroyedError: if used after destroyed.
"""
_tags_p = base.MemoryPointer()
def __init__(self, parent, tags_p, *, encoding=None, errors='strict'):
self._parent = parent
self._tags_p = tags_p
self._encoding = encoding
self._errors = errors
def __del__(self):
self._destroy()
@property
def alive(self):
if not self._parent.alive:
return False
try:
self._tags_p
except errors.ObjectDestroyedError:
return False
else:
return True
def _destroy(self):
if self.alive:
try:
capi.lib.notmuch_tags_destroy(self._tags_p)
except errors.ObjectDestroyedError:
pass
self._tags_p = None
def __iter__(self):
"""Return the iterator itself.
Note that as this is an iterator and not a container this will
not return a new iterator. Thus any elements already consumed
will not be yielded by the :meth:`__next__` method anymore.
"""
return self
def __next__(self):
if not capi.lib.notmuch_tags_valid(self._tags_p):
self._destroy()
raise StopIteration()
tag_p = capi.lib.notmuch_tags_get(self._tags_p)
tag = capi.ffi.string(tag_p)
if self._encoding:
tag = tag.decode(encoding=self._encoding, errors=self._errors)
capi.lib.notmuch_tags_move_to_next(self._tags_p)
return tag
def __repr__(self):
try:
self._tags_p
except errors.ObjectDestroyedError:
return '<TagsIter (exhausted)>'
else:
return '<TagsIter>'

View file

@ -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)

View file

@ -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',
],
)

View file

@ -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)

View file

@ -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 == '<EFBFBD>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'

View file

@ -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

View file

@ -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

View file

@ -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')}

View file

@ -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'}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
0.31.2

View file

@ -65,7 +65,7 @@ class Database(object):
.. note:: .. note::
Any function in this class can and will throw an 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. properly.
""" """
_std_db_path = None _std_db_path = None
@ -273,9 +273,9 @@ class Database(object):
return Database._get_version(self._db) return Database._get_version(self._db)
def get_revision (self): 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. and the UUID of the database.
""" """
self._assert_db_is_initialized() self._assert_db_is_initialized()
@ -574,7 +574,7 @@ class Database(object):
in the meantime. In this case, you should close and in the meantime. In this case, you should close and
reopen the database and retry. reopen the database and retry.
:exc:`NotInitializedError` if :exc:`NotInitializedError` if
the database was not intitialized. the database was not initialized.
""" """
self._assert_db_is_initialized() self._assert_db_is_initialized()
msg_p = NotmuchMessageP() msg_p = NotmuchMessageP()
@ -600,7 +600,7 @@ class Database(object):
case, you should close and reopen the database and case, you should close and reopen the database and
retry. retry.
:raises: :exc:`NotInitializedError` if the database was not :raises: :exc:`NotInitializedError` if the database was not
intitialized. initialized.
*Added in notmuch 0.9*""" *Added in notmuch 0.9*"""
self._assert_db_is_initialized() 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` with a list of all tags found in the database
:returns: :class:`Tags` :returns: :class:`Tags`
:execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER :exception: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
on error on error
""" """
self._assert_db_is_initialized() self._assert_db_is_initialized()

View file

@ -46,7 +46,7 @@ import sys
class Message(Python3StringMixIn): class Message(Python3StringMixIn):
"""Represents a single Email message r"""Represents a single Email message
Technically, this wraps the underlying *notmuch_message_t* Technically, this wraps the underlying *notmuch_message_t*
structure. A user will usually not create these objects themselves structure. A user will usually not create these objects themselves

View file

@ -32,7 +32,7 @@ from .tag import Tags
from .message import Message from .message import Message
class Messages(object): 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 This object provides an iterator over a list of notmuch messages
(Technically, it provides a wrapper for the underlying (Technically, it provides a wrapper for the underlying

View file

@ -95,7 +95,7 @@ class Query(object):
:exc:`NullPointerError` if the query creation failed :exc:`NullPointerError` if the query creation failed
(e.g. too little memory). (e.g. too little memory).
:exc:`NotInitializedError` if the underlying db was not :exc:`NotInitializedError` if the underlying db was not
intitialized. initialized.
""" """
db._assert_db_is_initialized() db._assert_db_is_initialized()
# create reference to parent db to keep it alive # create reference to parent db to keep it alive
@ -140,7 +140,7 @@ class Query(object):
_search_threads.restype = c_uint _search_threads.restype = c_uint
def search_threads(self): 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. Execute a query for threads, returning a :class:`Threads` iterator.
The returned threads are owned by the query and as such, will only be The returned threads are owned by the query and as such, will only be

View file

@ -1,3 +1,3 @@
# this file should be kept in sync with ../../../version # this file should be kept in sync with ../../../version
__VERSION__ = '0.29.3' __VERSION__ = '0.31.2'
SOVERSION = '5' SOVERSION = '5'

View file

@ -137,13 +137,18 @@ VALUE
notmuch_rb_message_get_flag (VALUE self, VALUE flagv) notmuch_rb_message_get_flag (VALUE self, VALUE flagv)
{ {
notmuch_message_t *message; notmuch_message_t *message;
notmuch_bool_t is_set;
notmuch_status_t status;
Data_Get_Notmuch_Message (self, message); Data_Get_Notmuch_Message (self, message);
if (!FIXNUM_P (flagv)) if (!FIXNUM_P (flagv))
rb_raise (rb_eTypeError, "Flag not a Fixnum"); 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;
} }
/* /*

View file

@ -5,16 +5,16 @@
#include "command-line-arguments.h" #include "command-line-arguments.h"
typedef enum { typedef enum {
OPT_FAILED, /* false */ OPT_FAILED, /* false */
OPT_OK, /* good */ OPT_OK, /* good */
OPT_GIVEBACK, /* pop one of the arguments you thought you were getting off the stack */ OPT_GIVEBACK, /* pop one of the arguments you thought you were getting off the stack */
} opt_handled; } opt_handled;
/* /*
Search the array of keywords for a given argument, assigning the * Search the array of keywords for a given argument, assigning the
output variable to the corresponding value. Return false if nothing * output variable to the corresponding value. Return false if nothing
matches. * matches.
*/ */
static opt_handled static opt_handled
_process_keyword_arg (const notmuch_opt_desc_t *arg_desc, char next, _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; return OPT_FAILED;
} }
*arg_desc->opt_bool = negate ? !value : value; *arg_desc->opt_bool = negate ? (! value) : value;
return OPT_OK; return OPT_OK;
} }
static opt_handled 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; char *endptr;
if (next == '\0' || arg_str[0] == '\0') { if (next == '\0' || arg_str[0] == '\0') {
fprintf (stderr, "Option \"%s\" needs an integer argument.\n", arg_desc->name); fprintf (stderr, "Option \"%s\" needs an integer argument.\n", arg_desc->name);
return OPT_FAILED; 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 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') { if (next == '\0') {
fprintf (stderr, "Option \"%s\" needs a string argument.\n", arg_desc->name); 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. */ /* 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 return
!!opt_desc->opt_inherit + (bool) opt_desc->opt_inherit +
!!opt_desc->opt_bool + (bool) opt_desc->opt_bool +
!!opt_desc->opt_int + (bool) opt_desc->opt_int +
!!opt_desc->opt_keyword + (bool) opt_desc->opt_keyword +
!!opt_desc->opt_flags + (bool) opt_desc->opt_flags +
!!opt_desc->opt_string + (bool) opt_desc->opt_string +
!!opt_desc->opt_position; (bool) opt_desc->opt_position;
} }
/* Return true if opt_desc is valid. */ /* 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); 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 * Search for the {pos_arg_index}th position argument, return false if
that does not exist. * that does not exist.
*/ */
bool bool
parse_position_arg (const char *arg_str, int pos_arg_index, 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; int pos_arg_counter = 0;
while (_opt_valid (arg_desc)) { while (_opt_valid (arg_desc)) {
if (arg_desc->opt_position) { if (arg_desc->opt_position) {
if (pos_arg_counter == pos_arg_index) { if (pos_arg_counter == pos_arg_index) {
@ -176,12 +183,12 @@ parse_position_arg (const char *arg_str, int pos_arg_index,
int 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)
{ {
assert(argv); assert (argv);
const char *_arg = argv[opt_index]; const char *_arg = argv[opt_index];
assert(_arg); assert (_arg);
assert(options); assert (options);
const char *arg = _arg + 2; /* _arg starts with -- */ const char *arg = _arg + 2; /* _arg starts with -- */
const char *negative_arg = NULL; 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) { if (lookahead) {
next = ' '; next = ' ';
value = next_arg; value = next_arg;
opt_index ++; opt_index++;
} }
opt_handled opt_status = OPT_FAILED; 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; return -1;
if (lookahead && opt_status == OPT_GIVEBACK) if (lookahead && opt_status == OPT_GIVEBACK)
opt_index --; opt_index--;
if (try->present) if (try->present)
*try->present = true; *try->present = true;
return opt_index+1; return opt_index + 1;
} }
return -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 */ /* See command-line-arguments.h for description */
int int
parse_arguments (int argc, char **argv, 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; int pos_arg_index = 0;
bool more_args = true; bool more_args = true;
while (more_args && opt_index < argc) { 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); 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; int prev_opt_index = opt_index;
if (strlen (argv[opt_index]) == 2) if (strlen (argv[opt_index]) == 2)
return opt_index+1; return opt_index + 1;
opt_index = parse_option (argc, argv, options, opt_index); opt_index = parse_option (argc, argv, options, opt_index);
if (opt_index < 0) { if (opt_index < 0) {

View file

@ -45,20 +45,20 @@ typedef struct notmuch_opt_desc {
/* /*
This is the main entry point for command line argument parsing. * This is the main entry point for command line argument parsing.
*
Parse command line arguments according to structure options, * Parse command line arguments according to structure options,
starting at position opt_index. * starting at position opt_index.
*
All output of parsed values is via pointers in options. * All output of parsed values is via pointers in options.
*
Parsing stops at -- (consumed) or at the (k+1)st argument * Parsing stops at -- (consumed) or at the (k+1)st argument
not starting with -- (a "positional argument") if options contains * not starting with -- (a "positional argument") if options contains
k positional argument descriptors. * k positional argument descriptors.
*
Returns the index of first non-parsed argument, or -1 in case of error. * Returns the index of first non-parsed argument, or -1 in case of error.
*
*/ */
int int
parse_arguments (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index); 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 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 bool
parse_position_arg (const char *arg, parse_position_arg (const char *arg,
int position_arg_index, int position_arg_index,
const notmuch_opt_desc_t* options); const notmuch_opt_desc_t *options);
#endif #endif

View file

@ -1,4 +1,4 @@
# -*- makefile -*- # -*- makefile-gmake -*-
dir := compat dir := compat
extra_cflags += -I$(srcdir)/$(dir) extra_cflags += -I$(srcdir)/$(dir)

View file

@ -4,10 +4,10 @@
#include <stdlib.h> #include <stdlib.h>
char * char *
canonicalize_file_name (const char * path) canonicalize_file_name (const char *path)
{ {
#ifdef PATH_MAX #ifdef PATH_MAX
char *resolved_path = malloc (PATH_MAX+1); char *resolved_path = malloc (PATH_MAX + 1);
if (resolved_path == NULL) if (resolved_path == NULL)
return NULL; return NULL;

View file

@ -1,7 +1,8 @@
#include <time.h> #include <time.h>
#include <stdio.h> #include <stdio.h>
int main() int
main ()
{ {
struct tm tm; struct tm tm;

View file

@ -1,7 +1,8 @@
#include <stdio.h> #include <stdio.h>
#include <pwd.h> #include <pwd.h>
int main() int
main ()
{ {
struct passwd passwd, *ignored; struct passwd passwd, *ignored;

View file

@ -30,14 +30,14 @@
extern "C" { extern "C" {
#endif #endif
#if !STD_GETPWUID #if ! STD_GETPWUID
#define _POSIX_PTHREAD_SEMANTICS 1 #define _POSIX_PTHREAD_SEMANTICS 1
#endif #endif
#if !STD_ASCTIME #if ! STD_ASCTIME
#define _POSIX_PTHREAD_SEMANTICS 1 #define _POSIX_PTHREAD_SEMANTICS 1
#endif #endif
#if !HAVE_CANONICALIZE_FILE_NAME #if ! HAVE_CANONICALIZE_FILE_NAME
/* we only call this function from C, and this makes testing easier */ /* we only call this function from C, and this makes testing easier */
#ifndef __cplusplus #ifndef __cplusplus
char * char *
@ -45,7 +45,7 @@ canonicalize_file_name (const char *path);
#endif #endif
#endif #endif
#if !HAVE_GETLINE #if ! HAVE_GETLINE
#include <stdio.h> #include <stdio.h>
#include <unistd.h> #include <unistd.h>
@ -55,31 +55,31 @@ getline (char **lineptr, size_t *n, FILE *stream);
ssize_t ssize_t
getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp); getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp);
#endif /* !HAVE_GETLINE */ #endif /* !HAVE_GETLINE */
#if !HAVE_STRCASESTR #if ! HAVE_STRCASESTR
char* strcasestr(const char *haystack, const char *needle); char *strcasestr (const char *haystack, const char *needle);
#endif /* !HAVE_STRCASESTR */ #endif /* !HAVE_STRCASESTR */
#if !HAVE_STRSEP #if ! HAVE_STRSEP
char *strsep(char **stringp, const char *delim); char *strsep (char **stringp, const char *delim);
#endif /* !HAVE_STRSEP */ #endif /* !HAVE_STRSEP */
#if !HAVE_TIMEGM #if ! HAVE_TIMEGM
#include <time.h> #include <time.h>
time_t timegm (struct tm *tm); time_t timegm (struct tm *tm);
#endif /* !HAVE_TIMEGM */ #endif /* !HAVE_TIMEGM */
/* Silence gcc warnings about unused results. These warnings exist /* Silence gcc warnings about unused results. These warnings exist
* for a reason; any use of this needs to be justified. */ * for a reason; any use of this needs to be justified. */
#ifdef __GNUC__ #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__ */ #else /* !__GNUC__ */
#define IGNORE_RESULT(x) x #define IGNORE_RESULT(x) x
#endif /* __GNUC__ */ #endif /* __GNUC__ */
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
#endif /* NOTMUCH_COMPAT_H */ #endif /* NOTMUCH_COMPAT_H */

View file

@ -35,9 +35,9 @@
* provides support for testing for function attributes. * provides support for testing for function attributes.
*/ */
#ifndef NORETURN_ATTRIBUTE #ifndef NORETURN_ATTRIBUTE
#if (__GNUC__ >= 3 || \ #if (__GNUC__ >= 3 || \
(__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || \ (__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || \
__has_attribute (noreturn)) __has_attribute (noreturn))
#define NORETURN_ATTRIBUTE __attribute__ ((noreturn)) #define NORETURN_ATTRIBUTE __attribute__ ((noreturn))
#else #else
#define NORETURN_ATTRIBUTE #define NORETURN_ATTRIBUTE

View file

@ -2,17 +2,18 @@
#include <zlib.h> #include <zlib.h>
static const char *template = static const char *template =
"prefix=/usr\n" "prefix=/usr\n"
"exec_prefix=${prefix}\n" "exec_prefix=${prefix}\n"
"libdir=${exec_prefix}/lib\n" "libdir=${exec_prefix}/lib\n"
"\n" "\n"
"Name: zlib\n" "Name: zlib\n"
"Description: zlib compression library\n" "Description: zlib compression library\n"
"Version: %s\n" "Version: %s\n"
"Libs: -lz\n"; "Libs: -lz\n";
int main(void) int
main (void)
{ {
printf(template, ZLIB_VERSION); printf (template, ZLIB_VERSION);
return 0; return 0;
} }

View file

@ -1,21 +1,21 @@
/* getdelim.c --- Implementation of replacement getdelim function. /* getdelim.c --- Implementation of replacement getdelim function.
Copyright (C) 1994, 1996, 1997, 1998, 2001, 2003, 2005, 2006, 2007, * Copyright (C) 1994, 1996, 1997, 1998, 2001, 2003, 2005, 2006, 2007,
2008, 2009 Free Software Foundation, Inc. * 2008, 2009 Free Software Foundation, Inc.
*
This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as * modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3, or (at * published by the Free Software Foundation; either version 3, or (at
your option) any later version. * your option) any later version.
*
This program is distributed in the hope that it will be useful, but * This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of * WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details. * General Public License for more details.
*
You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software * along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA. */ * 02110-1301, USA. */
/* Ported from glibc by Simon Josefsson. */ /* Ported from glibc by Simon Josefsson. */
@ -34,100 +34,92 @@
#if USE_UNLOCKED_IO #if USE_UNLOCKED_IO
# include "unlocked-io.h" # include "unlocked-io.h"
# define getc_maybe_unlocked(fp) getc(fp) # define getc_maybe_unlocked(fp) getc (fp)
#elif !HAVE_FLOCKFILE || !HAVE_FUNLOCKFILE || !HAVE_DECL_GETC_UNLOCKED #elif ! HAVE_FLOCKFILE || ! HAVE_FUNLOCKFILE || ! HAVE_DECL_GETC_UNLOCKED
# undef flockfile # undef flockfile
# undef funlockfile # undef funlockfile
# define flockfile(x) ((void) 0) # define flockfile(x) ((void) 0)
# define funlockfile(x) ((void) 0) # define funlockfile(x) ((void) 0)
# define getc_maybe_unlocked(fp) getc(fp) # define getc_maybe_unlocked(fp) getc (fp)
#else #else
# define getc_maybe_unlocked(fp) getc_unlocked(fp) # define getc_maybe_unlocked(fp) getc_unlocked (fp)
#endif #endif
/* Read up to (and including) a DELIMITER from FP into *LINEPTR (and /* Read up to (and including) a DELIMITER from FP into *LINEPTR (and
NUL-terminate it). *LINEPTR is a pointer returned from malloc (or * NUL-terminate it). *LINEPTR is a pointer returned from malloc (or
NULL), pointing to *N characters of space. It is realloc'ed as * NULL), pointing to *N characters of space. It is realloc'ed as
necessary. Returns the number of characters read (not including * necessary. Returns the number of characters read (not including
the null terminator), or -1 on error or EOF. */ * the null terminator), or -1 on error or EOF. */
ssize_t ssize_t
getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp) getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp)
{ {
ssize_t result = -1; ssize_t result = -1;
size_t cur_len = 0; size_t cur_len = 0;
if (lineptr == NULL || n == NULL || fp == NULL) if (lineptr == NULL || n == NULL || fp == NULL) {
{ errno = EINVAL;
errno = EINVAL; return -1;
return -1;
} }
flockfile (fp); flockfile (fp);
if (*lineptr == NULL || *n == 0) if (*lineptr == NULL || *n == 0) {
{ char *new_lineptr;
char *new_lineptr; *n = 120;
*n = 120; new_lineptr = (char *) realloc (*lineptr, *n);
new_lineptr = (char *) realloc (*lineptr, *n); if (new_lineptr == NULL) {
if (new_lineptr == NULL) result = -1;
{ goto unlock_return;
result = -1;
goto unlock_return;
} }
*lineptr = new_lineptr; *lineptr = new_lineptr;
} }
for (;;) for (;;) {
{ int i;
int i;
i = getc_maybe_unlocked (fp); i = getc_maybe_unlocked (fp);
if (i == EOF) if (i == EOF) {
{ result = -1;
result = -1; break;
break;
} }
/* Make enough space for len+1 (for final NUL) bytes. */ /* Make enough space for len+1 (for final NUL) bytes. */
if (cur_len + 1 >= *n) if (cur_len + 1 >= *n) {
{ size_t needed_max =
size_t needed_max = SSIZE_MAX < SIZE_MAX ? (size_t) SSIZE_MAX + 1 : SIZE_MAX;
SSIZE_MAX < SIZE_MAX ? (size_t) SSIZE_MAX + 1 : SIZE_MAX; size_t needed = 2 * *n + 1; /* Be generous. */
size_t needed = 2 * *n + 1; /* Be generous. */ char *new_lineptr;
char *new_lineptr;
if (needed_max < needed) if (needed_max < needed)
needed = needed_max; needed = needed_max;
if (cur_len + 1 >= needed) if (cur_len + 1 >= needed) {
{ result = -1;
result = -1; errno = EOVERFLOW;
errno = EOVERFLOW; goto unlock_return;
goto unlock_return;
} }
new_lineptr = (char *) realloc (*lineptr, needed); new_lineptr = (char *) realloc (*lineptr, needed);
if (new_lineptr == NULL) if (new_lineptr == NULL) {
{ result = -1;
result = -1; goto unlock_return;
goto unlock_return;
} }
*lineptr = new_lineptr; *lineptr = new_lineptr;
*n = needed; *n = needed;
} }
(*lineptr)[cur_len] = i; (*lineptr)[cur_len] = i;
cur_len++; cur_len++;
if (i == delimiter) if (i == delimiter)
break; break;
} }
(*lineptr)[cur_len] = '\0'; (*lineptr)[cur_len] = '\0';
result = cur_len ? (ssize_t) cur_len : result; result = cur_len ? (ssize_t) cur_len : result;
unlock_return: unlock_return:
funlockfile (fp); /* doesn't set errno */ funlockfile (fp); /* doesn't set errno */
return result; return result;
} }

View file

@ -1,20 +1,20 @@
/* getline.c --- Implementation of replacement getline function. /* getline.c --- Implementation of replacement getline function.
Copyright (C) 2005, 2006, 2007 Free Software Foundation, Inc. * Copyright (C) 2005, 2006, 2007 Free Software Foundation, Inc.
*
This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as * modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3, or (at * published by the Free Software Foundation; either version 3, or (at
your option) any later version. * your option) any later version.
*
This program is distributed in the hope that it will be useful, but * This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of * WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details. * General Public License for more details.
*
You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software * along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA. */ * 02110-1301, USA. */
/* Written by Simon Josefsson. */ /* Written by Simon Josefsson. */
@ -25,5 +25,5 @@
ssize_t ssize_t
getline (char **lineptr, size_t *n, FILE *stream) getline (char **lineptr, size_t *n, FILE *stream)
{ {
return getdelim (lineptr, n, '\n', stream); return getdelim (lineptr, n, '\n', stream);
} }

View file

@ -1,7 +1,8 @@
#define _GNU_SOURCE #define _GNU_SOURCE
#include <stdlib.h> #include <stdlib.h>
int main() int
main ()
{ {
char *found; char *found;
char *string; char *string;

View file

@ -1,6 +1,7 @@
#include <dirent.h> #include <dirent.h>
int main() int
main ()
{ {
struct dirent ent; struct dirent ent;

View file

@ -2,12 +2,13 @@
#include <stdio.h> #include <stdio.h>
#include <sys/types.h> #include <sys/types.h>
int main() int
main ()
{ {
ssize_t count = 0; ssize_t count = 0;
size_t n = 0; size_t n = 0;
char **lineptr = NULL; char **lineptr = NULL;
FILE *stream = NULL; FILE *stream = NULL;
count = getline(lineptr, &n, stream); count = getline (lineptr, &n, stream);
} }

View file

@ -1,10 +1,11 @@
#define _GNU_SOURCE #define _GNU_SOURCE
#include <strings.h> #include <strings.h>
int main() int
main ()
{ {
char *found; char *found;
const char *haystack, *needle; const char *haystack, *needle;
found = strcasestr(haystack, needle); found = strcasestr (haystack, needle);
} }

View file

@ -1,11 +1,12 @@
#define _GNU_SOURCE #define _GNU_SOURCE
#include <string.h> #include <string.h>
int main() int
main ()
{ {
char *found; char *found;
char **stringp; char **stringp;
const char *delim; const char *delim;
found = strsep(stringp, delim); found = strsep (stringp, delim);
} }

View file

@ -1,6 +1,7 @@
#include <time.h> #include <time.h>
int main() int
main ()
{ {
return (int) timegm((struct tm *)0); return (int) timegm ((struct tm *) 0);
} }

View file

@ -3,20 +3,20 @@
* don't include it in their library * don't include it in their library
* *
* based on a GPL implementation in OpenTTD found under GPL v2 * based on a GPL implementation in OpenTTD found under GPL v2
*
This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as * modify it under the terms of the GNU General Public License as
published by the Free Software Foundation, version 2. * published by the Free Software Foundation, version 2.
*
This program is distributed in the hope that it will be useful, but * This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of * WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details. * General Public License for more details.
*
You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software * along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA. */ * 02110-1301, USA. */
/* Imported into notmuch by Dirk Hohndel - original author unknown. */ /* Imported into notmuch by Dirk Hohndel - original author unknown. */
@ -24,17 +24,19 @@
#include "compat.h" #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 hay_len = strlen (haystack);
size_t needle_len = strlen(needle); size_t needle_len = strlen (needle);
while (hay_len >= needle_len) {
if (strncasecmp(haystack, needle, needle_len) == 0)
return (char *) haystack;
haystack++; while (hay_len >= needle_len) {
hay_len--; if (strncasecmp (haystack, needle, needle_len) == 0)
} return (char *) haystack;
return NULL; haystack++;
hay_len--;
}
return NULL;
} }

View file

@ -1,65 +1,61 @@
/* Copyright (C) 1992, 93, 96, 97, 98, 99, 2004 Free Software Foundation, Inc. /* Copyright (C) 1992, 93, 96, 97, 98, 99, 2004 Free Software Foundation, Inc.
This file is part of the GNU C Library. * This file is part of the GNU C Library.
*
The GNU C Library is free software; you can redistribute it and/or * The GNU C Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public * modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either * License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. * 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, * The GNU C Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details. * Lesser General Public License for more details.
*
You should have received a copy of the GNU Lesser General Public * 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 * License along with the GNU C Library; if not, write to the Free
Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA. */ * 02111-1307 USA. */
#include <string.h> #include <string.h>
/* Taken from glibc 2.6.1 */ /* 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; begin = *stringp;
if (begin == NULL) if (begin == NULL)
return NULL; return NULL;
/* A frequent case is when the delimiter string contains only one /* A frequent case is when the delimiter string contains only one
character. Here we don't need to call the expensive `strpbrk' * character. Here we don't need to call the expensive `strpbrk'
function and instead work using `strchr'. */ * function and instead work using `strchr'. */
if (delim[0] == '\0' || delim[1] == '\0') if (delim[0] == '\0' || delim[1] == '\0') {
{ char ch = delim[0];
char ch = delim[0];
if (ch == '\0') if (ch == '\0')
end = NULL;
else
{
if (*begin == ch)
end = begin;
else if (*begin == '\0')
end = NULL; end = NULL;
else else {
end = strchr (begin + 1, ch); if (*begin == ch)
end = begin;
else if (*begin == '\0')
end = NULL;
else
end = strchr (begin + 1, ch);
} }
} } else
else /* Find the end of the token. */
/* Find the end of the token. */ end = strpbrk (begin, delim);
end = strpbrk (begin, delim);
if (end) if (end) {
{ /* Terminate the token and set *STRINGP past NUL character. */
/* Terminate the token and set *STRINGP past NUL character. */ *end++ = '\0';
*end++ = '\0'; *stringp = end;
*stringp = end; } else
} /* No more delimiters; this is the last token. */
else *stringp = NULL;
/* No more delimiters; this is the last token. */
*stringp = NULL;
return begin; return begin;
} }

View file

@ -1,19 +1,19 @@
/* timegm.c --- Implementation of replacement timegm function. /* timegm.c --- Implementation of replacement timegm function.
*
This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as * modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3, or (at * published by the Free Software Foundation; either version 3, or (at
your option) any later version. * your option) any later version.
*
This program is distributed in the hope that it will be useful, but * This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of * WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details. * General Public License for more details.
*
You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software * along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA. */ * 02110-1301, USA. */
/* Copyright 2013 Blake Jones. */ /* Copyright 2013 Blake Jones. */
@ -35,20 +35,20 @@ leapyear (int year)
time_t time_t
timegm (struct tm *tm) 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, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
{ 31, 29, 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); days = 365 * (tm->tm_year - 70);
for (year = 70; year < tm->tm_year; year++) { for (year = 70; year < tm->tm_year; year++) {
if (leapyear(1900 + year)) { if (leapyear (1900 + year)) {
days++; days++;
} }
} }
for (month = 0; month < tm->tm_mon; month++) { 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; days += tm->tm_mday - 1;

View file

@ -1,4 +1,4 @@
# -*- makefile -*- # -*- makefile-gmake -*-
dir := completion dir := completion

426
configure vendored
View file

@ -26,8 +26,29 @@ readonly DEFAULT_IFS="$IFS"
srcdir=$(dirname "$0") srcdir=$(dirname "$0")
NOTMUCH_SRCDIR=$(cd "$srcdir" && pwd) 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="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" subdirs="${subdirs} bindings"
# For a non-srcdir configure invocation (such as ../configure), create # For a non-srcdir configure invocation (such as ../configure), create
@ -49,6 +70,14 @@ if [ "$srcdir" != "." ]; then
mkdir bindings/ruby mkdir bindings/ruby
cp -a "$srcdir"/bindings/ruby/*.[ch] bindings/ruby cp -a "$srcdir"/bindings/ruby/*.[ch] bindings/ruby
cp -a "$srcdir"/bindings/ruby/extconf.rb 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 fi
# Set several defaults (optionally specified by the user in # Set several defaults (optionally specified by the user in
@ -79,6 +108,7 @@ PREFIX=/usr/local
LIBDIR= LIBDIR=
WITH_DOCS=1 WITH_DOCS=1
WITH_API_DOCS=1 WITH_API_DOCS=1
WITH_PYTHON_DOCS=1
WITH_EMACS=1 WITH_EMACS=1
WITH_DESKTOP=1 WITH_DESKTOP=1
WITH_BASH=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] --emacslispdir=DIR Emacs code [PREFIX/share/emacs/site-lisp]
--emacsetcdir=DIR Emacs miscellaneous files [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] --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 Some features can be disabled (--with-feature=no is equivalent to
--without-feature) : --without-feature) :
@ -401,15 +431,22 @@ else
have_pkg_config=0 have_pkg_config=0
fi fi
printf "Checking for Xapian development files... "
printf "Checking for Xapian development files (>= 1.4.0)... "
have_xapian=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 if ${xapian_config} --version > /dev/null 2>&1; then
xapian_version=$(${xapian_config} --version | sed -e 's/.* //') xapian_version=$(${xapian_config} --version | sed -e 's/.* //')
printf "Yes (%s).\n" ${xapian_version} case $xapian_version in
have_xapian=1 1.[4-9]* | 1.[1-9][0-9]* | [2-9]* | [1-9][0-9]*)
xapian_cxxflags=$(${xapian_config} --cxxflags) printf "Yes (%s).\n" ${xapian_version}
xapian_ldflags=$(${xapian_config} --libs) have_xapian=1
xapian_cxxflags=$(${xapian_config} --cxxflags)
xapian_ldflags=$(${xapian_config} --libs)
;;
*) printf "Xapian $xapian_version not supported... "
esac
break break
fi fi
done done
@ -418,81 +455,10 @@ if [ ${have_xapian} = "0" ]; then
errors=$((errors + 1)) errors=$((errors + 1))
fi fi
have_xapian_compact=0
have_xapian_field_processor=0
if [ ${have_xapian} = "1" ]; then
printf "Checking for Xapian compaction support... "
cat>_compact.cc<<EOF
#include <xapian.h>
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<<EOF
#include <xapian.h>
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<<EOF
#include <xapian.h>
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 <<EOF
#include <xapian.h>
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 GMIME_MINVER=3.0.3
printf "Checking for GMime development files... " printf "Checking for GMime development files (>= $GMIME_MINVER)... "
if pkg-config --exists "gmime-3.0 > $GMIME_MINVER"; then if pkg-config --exists "gmime-3.0 >= $GMIME_MINVER"; then
printf "Yes.\n" printf "Yes.\n"
have_gmime=1 have_gmime=1
gmime_cflags=$(pkg-config --cflags gmime-3.0) gmime_cflags=$(pkg-config --cflags gmime-3.0)
@ -513,11 +479,11 @@ int main () {
g_mime_init (); g_mime_init ();
parser = g_mime_parser_new (); 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"); 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))); 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); output = g_mime_multipart_encrypted_decrypt (body, GMIME_DECRYPT_EXPORT_SESSION_KEY, NULL, &decrypt_result, &error);
if (error || output == NULL) return !! fprintf (stderr, "decryption failed\n"); if (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' printf 'No.\nCould not make tempdir for testing session-key support.\n'
errors=$((errors + 1)) errors=$((errors + 1))
elif ${CC} ${CFLAGS} ${gmime_cflags} _check_session_keys.c ${gmime_ldflags} -o _check_session_keys \ 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=$(GNUPGHOME=${TEMP_GPG} ./_check_session_keys) \
&& [ $SESSION_KEY = 9:0BACD64099D1468AB07C796F0C0AC4851948A658A15B34E803865E9FC635F2F5 ] && [ $SESSION_KEY = 9:0BACD64099D1468AB07C796F0C0AC4851948A658A15B34E803865E9FC635F2F5 ]
then then
@ -559,6 +525,154 @@ EOF
if [ -n "$TEMP_GPG" -a -d "$TEMP_GPG" ]; then if [ -n "$TEMP_GPG" -a -d "$TEMP_GPG" ]; then
rm -rf "$TEMP_GPG" rm -rf "$TEMP_GPG"
fi fi
# see https://github.com/jstedfast/gmime/pull/90
# should be fixed in GMime in 3.2.7, but some distros might patch
printf "Checking for GMime X.509 certificate validity... "
cat > _check_x509_validity.c <<EOF
#include <stdio.h>
#include <gmime/gmime.h>
int main () {
GError *error = NULL;
GMimeParser *parser = NULL;
GMimeApplicationPkcs7Mime *body = NULL;
GMimeSignatureList *sig_list = NULL;
GMimeSignature *sig = NULL;
GMimeCertificate *cert = NULL;
GMimeObject *output = NULL;
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 <<EOF
*** Error: GMime fails to calculate X.509 certificate validity, and
is later than 3.2.7, which should have fixed this issue.
Please follow up on https://github.com/jstedfast/gmime/pull/90 with
more details.
EOF
errors=$((errors + 1))
fi
fi
else
printf 'No.\nFailed to set up gpgsm for testing X.509 certificate validity support.\n'
errors=$((errors + 1))
fi
if [ -n "$TEMP_GPG" -a -d "$TEMP_GPG" ]; then
rm -rf "$TEMP_GPG"
fi
# see https://dev.gnupg.org/T3464
# there are problems verifying signatures when decrypting with session keys with GPGME 1.13.0 and 1.13.1
printf "Checking signature verification when decrypting using session keys... "
cat > _verify_sig_with_session_key.c <<EOF
#include <stdio.h>
#include <gmime/gmime.h>
int main () {
GError *error = NULL;
GMimeParser *parser = NULL;
GMimeMultipartEncrypted *body = NULL;
GMimeDecryptResult *result = NULL;
GMimeSignatureList *sig_list = NULL;
GMimeSignature *sig = NULL;
GMimeObject *output = NULL;
GMimeSignatureStatus status;
int len;
g_mime_init ();
parser = g_mime_parser_new ();
g_mime_parser_init_with_stream (parser, g_mime_stream_file_open("$srcdir/test/corpora/crypto/encrypted-signed.eml", "r", &error));
if (error) return !! fprintf (stderr, "failed to instantiate parser with test/corpora/pkcs7/smime-onepart-signed.eml\n");
body = GMIME_MULTIPART_ENCRYPTED(g_mime_message_get_mime_part (g_mime_parser_construct_message (parser, NULL)));
if (body == NULL) return !! fprintf (stderr, "did not find a multipart/encrypted message\n");
output = g_mime_multipart_encrypted_decrypt (body, GMIME_DECRYPT_NONE, "9: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 <<EOF
*** Error: GMime fails to verify signatures when decrypting with a session key.
This is most likely due to a buggy version of GPGME, which should be fixed in 1.13.2 or later.
See https://dev.gnupg.org/T3464 for more details.
EOF
fi
else
printf 'No.\nFailed to set up gpg for testing signature verification while decrypting with a session key.\n'
errors=$((errors + 1))
fi
if [ -n "$TEMP_GPG" -a -d "$TEMP_GPG" ]; then
rm -rf "$TEMP_GPG"
fi
else else
have_gmime=0 have_gmime=0
printf "No.\n" printf "No.\n"
@ -583,7 +697,7 @@ fi
if ! pkg-config --exists zlib; then if ! pkg-config --exists zlib; then
${CC} -o compat/gen_zlib_pc "$srcdir"/compat/gen_zlib_pc.c >/dev/null 2>&1 && ${CC} -o compat/gen_zlib_pc "$srcdir"/compat/gen_zlib_pc.c >/dev/null 2>&1 &&
compat/gen_zlib_pc > compat/zlib.pc && 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 export PKG_CONFIG_PATH
rm -f compat/gen_zlib_pc rm -f compat/gen_zlib_pc
fi fi
@ -650,6 +764,43 @@ if [ $have_python -eq 0 ]; then
errors=$((errors + 1)) errors=$((errors + 1))
fi 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... " printf "Checking for valgrind development files... "
if pkg-config --exists valgrind; then if pkg-config --exists valgrind; then
printf "Yes.\n" printf "Yes.\n"
@ -677,13 +828,14 @@ if [ -z "${EMACSETCDIR-}" ]; then
EMACSETCDIR="\$(prefix)/share/emacs/site-lisp" EMACSETCDIR="\$(prefix)/share/emacs/site-lisp"
fi fi
printf "Checking if emacs (>= 24) is available... " if [ $WITH_EMACS = "1" ]; then
if emacs --quick --batch --eval '(if (< emacs-major-version 24) (kill-emacs 1))' > /dev/null 2>&1; then printf "Checking if emacs (>= 25) is available... "
printf "Yes.\n" if emacs --quick --batch --eval '(if (< emacs-major-version 25) (kill-emacs 1))' > /dev/null 2>&1; then
have_emacs=1 printf "Yes.\n"
else else
printf "No (so will not byte-compile emacs code)\n" printf "No (disabling emacs related parts of build)\n"
have_emacs=0 WITH_EMACS=0
fi
fi fi
have_doxygen=0 have_doxygen=0
@ -823,8 +975,8 @@ EOF
if [ $have_python -eq 0 ]; then if [ $have_python -eq 0 ]; then
echo " python interpreter" echo " python interpreter"
fi fi
if [ $have_xapian -eq 0 -o $have_xapian_compact -eq 0 ]; then if [ $have_xapian -eq 0 ]; then
echo " Xapian library (>= version 1.2.6, including development files such as headers)" echo " Xapian library (>= version 1.4.0, including development files such as headers)"
echo " https://xapian.org/" echo " https://xapian.org/"
fi fi
if [ $have_zlib -eq 0 ]; then if [ $have_zlib -eq 0 ]; then
@ -859,7 +1011,7 @@ On Debian and similar systems:
Or on Fedora 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 On other systems, similar commands can be used, but the details of the
package names may be different. package names may be different.
@ -874,7 +1026,7 @@ to install pkg-config with a command such as:
sudo apt-get install pkg-config sudo apt-get install pkg-config
Or: Or:
sudo yum install pkgconfig sudo dnf install pkgconfig
But if pkg-config is not available for your system, then you will need 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 to modify the configure script to manually set the cflags and ldflags
@ -948,6 +1100,22 @@ else
fi fi
rm -f compat/have_timegm rm -f compat/have_timegm
cat <<EOF > _time_t.c
#include <time.h>
#include <assert.h>
static_assert(sizeof(time_t) >= 8, "sizeof(time_t) < 8");
EOF
printf "Checking for 64 bit time_t... "
if ${CC} -c _time_t.c -o /dev/null
then
printf "Yes.\n"
have_64bit_time_t=1
else
printf "No.\n"
have_64bit_time_t=0
fi
printf "Checking for dirent.d_type... " printf "Checking for dirent.d_type... "
if ${CC} -o compat/have_d_type "$srcdir"/compat/have_d_type.c > /dev/null 2>&1 if ${CC} -o compat/have_d_type "$srcdir"/compat/have_d_type.c > /dev/null 2>&1
then then
@ -1031,7 +1199,8 @@ for flag in -Wmissing-declarations; do
done done
printf "\n\t%s\n" "${WARN_CFLAGS}" 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 # construct the Makefile.config
cat > Makefile.config <<EOF cat > Makefile.config <<EOF
@ -1165,9 +1334,6 @@ BASH_ABSOLUTE = ${bash_absolute}
HAVE_PERL = ${have_perl} HAVE_PERL = ${have_perl}
PERL_ABSOLUTE = ${perl_absolute} PERL_ABSOLUTE = ${perl_absolute}
# Whether there's an emacs binary available for byte-compiling
HAVE_EMACS = ${have_emacs}
# Whether there's a sphinx-build binary available for building documentation # Whether there's a sphinx-build binary available for building documentation
HAVE_SPHINX=${have_sphinx} HAVE_SPHINX=${have_sphinx}
@ -1187,7 +1353,7 @@ desktop_dir = \$(prefix)/share/applications
bash_completion_dir = ${BASHCOMPLETIONDIR:=\$(prefix)/share/bash-completion/completions} bash_completion_dir = ${BASHCOMPLETIONDIR:=\$(prefix)/share/bash-completion/completions}
# The directory to which zsh completions files should be installed # The directory to which zsh completions files should be installed
zsh_completion_dir = ${ZSHCOMLETIONDIR:=\$(prefix)/share/zsh/functions/Completion/Unix} zsh_completion_dir = ${ZSHCOMLETIONDIR:=\$(prefix)/share/zsh/site-functions}
# Whether the canonicalize_file_name function is available (if not, then notmuch will # Whether the canonicalize_file_name function is available (if not, then notmuch will
# build its own version) # build its own version)
@ -1204,6 +1370,12 @@ HAVE_GETLINE = ${have_getline}
# building/testing ruby bindings. # building/testing ruby bindings.
HAVE_RUBY_DEV = ${have_ruby_dev} HAVE_RUBY_DEV = ${have_ruby_dev}
# Is the python cffi package available?
HAVE_PYTHON3_CFFI = ${have_python3_cffi}
# Is the python pytest package available?
HAVE_PYTHON3_PYTEST = ${have_python3_pytest}
# Whether the strcasestr function is available (if not, then notmuch will # Whether the strcasestr function is available (if not, then notmuch will
# build its own version) # build its own version)
HAVE_STRCASESTR = ${have_strcasestr} HAVE_STRCASESTR = ${have_strcasestr}
@ -1219,14 +1391,8 @@ HAVE_TIMEGM = ${have_timegm}
# Whether struct dirent has d_type (if not, then notmuch will use stat) # Whether struct dirent has d_type (if not, then notmuch will use stat)
HAVE_D_TYPE = ${have_d_type} HAVE_D_TYPE = ${have_d_type}
# Whether the Xapian version in use supports compaction # Whether to have Xapian retry lock
HAVE_XAPIAN_COMPACT = ${have_xapian_compact} HAVE_XAPIAN_DB_RETRY_LOCK = ${WITH_RETRY_LOCK}
# Whether the Xapian version in use supports field processors
HAVE_XAPIAN_FIELD_PROCESSOR = ${have_xapian_field_processor}
# Whether the Xapian version in use supports DB_RETRY_LOCK
HAVE_XAPIAN_DB_RETRY_LOCK = ${have_xapian_db_retry_lock}
# Whether the getpwuid_r function is standards-compliant # Whether the getpwuid_r function is standards-compliant
# (if not, then notmuch will #define _POSIX_PTHREAD_SEMANTICS # (if not, then notmuch will #define _POSIX_PTHREAD_SEMANTICS
@ -1250,9 +1416,6 @@ LINKER_RESOLVES_LIBRARY_DEPENDENCIES = ${linker_resolves_library_dependencies}
XAPIAN_CXXFLAGS = ${xapian_cxxflags} XAPIAN_CXXFLAGS = ${xapian_cxxflags}
XAPIAN_LDFLAGS = ${xapian_ldflags} XAPIAN_LDFLAGS = ${xapian_ldflags}
# Which backend will Xapian use by default?
DEFAULT_XAPIAN_BACKEND = ${default_xapian_backend}
# Flags needed to compile and link against GMime # Flags needed to compile and link against GMime
GMIME_CFLAGS = ${gmime_cflags} GMIME_CFLAGS = ${gmime_cflags}
GMIME_LDFLAGS = ${gmime_ldflags} GMIME_LDFLAGS = ${gmime_ldflags}
@ -1305,16 +1468,14 @@ COMMON_CONFIGURE_CFLAGS = \\
-DHAVE_D_TYPE=\$(HAVE_D_TYPE) \\ -DHAVE_D_TYPE=\$(HAVE_D_TYPE) \\
-DSTD_GETPWUID=\$(STD_GETPWUID) \\ -DSTD_GETPWUID=\$(STD_GETPWUID) \\
-DSTD_ASCTIME=\$(STD_ASCTIME) \\ -DSTD_ASCTIME=\$(STD_ASCTIME) \\
-DHAVE_XAPIAN_COMPACT=\$(HAVE_XAPIAN_COMPACT) \\
-DSILENCE_XAPIAN_DEPRECATION_WARNINGS \\ -DSILENCE_XAPIAN_DEPRECATION_WARNINGS \\
-DHAVE_XAPIAN_FIELD_PROCESSOR=\$(HAVE_XAPIAN_FIELD_PROCESSOR) \\
-DHAVE_XAPIAN_DB_RETRY_LOCK=\$(HAVE_XAPIAN_DB_RETRY_LOCK) -DHAVE_XAPIAN_DB_RETRY_LOCK=\$(HAVE_XAPIAN_DB_RETRY_LOCK)
CONFIGURE_CFLAGS = \$(COMMON_CONFIGURE_CFLAGS) CONFIGURE_CFLAGS = \$(COMMON_CONFIGURE_CFLAGS)
CONFIGURE_CXXFLAGS = \$(COMMON_CONFIGURE_CFLAGS) \$(XAPIAN_CXXFLAGS) CONFIGURE_CXXFLAGS = \$(COMMON_CONFIGURE_CFLAGS) \$(XAPIAN_CXXFLAGS)
CONFIGURE_LDFLAGS = \$(GMIME_LDFLAGS) \$(TALLOC_LDFLAGS) \$(ZLIB_LDFLAGS) \$(XAPIAN_LDFLAGS) CONFIGURE_LDFLAGS = \$(GMIME_LDFLAGS) \$(TALLOC_LDFLAGS) \$(ZLIB_LDFLAGS) \$(XAPIAN_LDFLAGS)
EOF EOF
# construct the sh.config # construct the sh.config
@ -1324,17 +1485,14 @@ cat > sh.config <<EOF
NOTMUCH_SRCDIR='${NOTMUCH_SRCDIR}' NOTMUCH_SRCDIR='${NOTMUCH_SRCDIR}'
# Whether the Xapian version in use supports compaction # Whether to have Xapian retry lock
NOTMUCH_HAVE_XAPIAN_COMPACT=${have_xapian_compact} NOTMUCH_HAVE_XAPIAN_DB_RETRY_LOCK=${WITH_RETRY_LOCK}
# Whether the Xapian version in use supports field processors # Whether GMime can verify X.509 certificate validity
NOTMUCH_HAVE_XAPIAN_FIELD_PROCESSOR=${have_xapian_field_processor} NOTMUCH_GMIME_X509_CERT_VALIDITY=${gmime_x509_cert_validity}
# Whether the Xapian version in use supports lock retry # Whether GMime can verify signatures when decrypting with a session key:
NOTMUCH_HAVE_XAPIAN_DB_RETRY_LOCK=${have_xapian_db_retry_lock} NOTMUCH_GMIME_VERIFY_WITH_SESSION_KEY=${gmime_verify_with_session_key}
# Which backend will Xapian use by default?
NOTMUCH_DEFAULT_XAPIAN_BACKEND=${default_xapian_backend}
# do we have man pages? # do we have man pages?
NOTMUCH_HAVE_MAN=$((have_sphinx)) NOTMUCH_HAVE_MAN=$((have_sphinx))
@ -1343,6 +1501,9 @@ NOTMUCH_HAVE_MAN=$((have_sphinx))
NOTMUCH_HAVE_BASH=${have_bash} NOTMUCH_HAVE_BASH=${have_bash}
NOTMUCH_BASH_ABSOLUTE=${bash_absolute} NOTMUCH_BASH_ABSOLUTE=${bash_absolute}
# Whether time_t is 64 bits (or more)
NOTMUCH_HAVE_64BIT_TIME_T=${have_64bit_time_t}
# Whether perl exists, and if so where # Whether perl exists, and if so where
NOTMUCH_HAVE_PERL=${have_perl} NOTMUCH_HAVE_PERL=${have_perl}
NOTMUCH_PERL_ABSOLUTE=${perl_absolute} NOTMUCH_PERL_ABSOLUTE=${perl_absolute}
@ -1357,10 +1518,27 @@ NOTMUCH_RUBY=${RUBY}
# building/testing ruby bindings. # building/testing ruby bindings.
NOTMUCH_HAVE_RUBY_DEV=${have_ruby_dev} NOTMUCH_HAVE_RUBY_DEV=${have_ruby_dev}
# Is the python cffi package available?
NOTMUCH_HAVE_PYTHON3_CFFI=${have_python3_cffi}
# Is the python pytest package available?
NOTMUCH_HAVE_PYTHON3_PYTEST=${have_python3_pytest}
# Platform we are run on # Platform we are run on
PLATFORM=${platform} PLATFORM=${platform}
EOF EOF
{
echo "# Generated by configure, run from doc/conf.py"
if [ $WITH_EMACS = "1" ]; then
echo "tags.add('WITH_EMACS')"
fi
if [ $WITH_PYTHON_DOCS = "1" ]; then
echo "tags.add('WITH_PYTHON')"
fi
printf "rsti_dir = '%s'\n" "$(cd emacs && pwd -P)"
} > sphinx.config
# Finally, after everything configured, inform the user how to continue. # Finally, after everything configured, inform the user how to continue.
cat <<EOF cat <<EOF

View file

@ -15,11 +15,11 @@ README.html: README
markdown $< > $@ markdown $< > $@
install: all 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) sed "1s|^#!.*|#! $(PERL_ABSOLUTE)|" < $(NAME) > $(DESTDIR)$(prefix)/bin/$(NAME)
chmod 755 $(DESTDIR)$(prefix)/bin/$(NAME) chmod 755 $(DESTDIR)$(prefix)/bin/$(NAME)
install -D -m 644 $(NAME).1 $(DESTDIR)$(mandir)/man1/$(NAME).1 install -m 644 $(NAME).1 $(DESTDIR)$(mandir)/man1/
install -D -m 644 $(NAME).rc $(DESTDIR)$(sysconfdir)/Muttrc.d/$(NAME).rc install -m 644 $(NAME).rc $(DESTDIR)$(sysconfdir)/Muttrc.d/
clean: clean:
rm -f notmuch-mutt.1 README.html rm -f notmuch-mutt.1 README.html

View file

@ -12,6 +12,7 @@ use strict;
use warnings; use warnings;
use File::Path; use File::Path;
use File::Basename;
use Getopt::Long qw(:config no_getopt_compat); use Getopt::Long qw(:config no_getopt_compat);
use Mail::Header; use Mail::Header;
use Mail::Box::Maildir; use Mail::Box::Maildir;
@ -41,16 +42,17 @@ sub search($$$) {
my ($maildir, $remove_dups, $query) = @_; my ($maildir, $remove_dups, $query) = @_;
my $dup_option = ""; my $dup_option = "";
$query = shell_quote($query); my @args = qw/notmuch search --output=files/;
push @args, "--duplicate=1" if $remove_dups;
if ($remove_dups) { push @args, $query;
$dup_option = "--duplicate=1";
}
empty_maildir($maildir); empty_maildir($maildir);
system("notmuch search --output=files $dup_option $query" open my $pipe, '-|', @args or die "Running @args failed: $!\n";
. " | sed -e 's: :\\\\ :g'" while (<$pipe>) {
. " | xargs -r -I searchoutput ln -s searchoutput $maildir/cur/"); chomp;
my $ln = "$maildir/cur/" . basename $_;
symlink $_, "$ln" or warn "Failed to symlink '$_', '$ln': $!\n";
}
} }
sub prompt($$) { sub prompt($$) {

97
debian/changelog vendored
View file

@ -1,3 +1,100 @@
notmuch (0.31.2-3) unstable; urgency=medium
* Switch to debhelper compat level 13
-- David Bremner <bremner@debian.org> Mon, 09 Nov 2020 13:59:47 -0400
notmuch (0.31.2-2) unstable; urgency=medium
* Run tests in verbose mode
-- David Bremner <bremner@debian.org> Mon, 09 Nov 2020 08:45:38 -0400
notmuch (0.31.2-1) unstable; urgency=medium
* Delete stray "version" file in upstream source
-- David Bremner <bremner@debian.org> Sun, 08 Nov 2020 11:32:45 -0400
notmuch (0.31.1-1) unstable; urgency=medium
* New upstream bugfix release.
- Portability / C++20 fixes
- Fix initialization bug in library config handling.
-- David Bremner <bremner@debian.org> Sun, 08 Nov 2020 07:48:22 -0400
notmuch (0.31-1) unstable; urgency=medium
* New upstream release
* Compatibility fixes for Emacs 27.1
-- David Bremner <bremner@debian.org> Sat, 05 Sep 2020 21:47:42 -0300
notmuch (0.31~rc2-1) experimental; urgency=medium
* New upstream release candidate
* Bug fix: "suggest elpa-mailscripts", thanks to Sean Whitton (Closes:
#944269).
* Bug fix: "suggest mailscripts", thanks to Sean Whitton (Closes:
#944270).
* Bug fix: "please drop transitional package notmuch-emacs from
src:notmuch", thanks to Holger Levsen (Closes: #940738).
-- David Bremner <bremner@debian.org> Tue, 25 Aug 2020 07:51:33 -0300
notmuch (0.31~rc1-1) experimental; urgency=medium
* Fix buggy test in T562-lib-database
* Clean up generated file in source package.
-- David Bremner <bremner@debian.org> Mon, 17 Aug 2020 21:05:46 -0300
notmuch (0.31~rc0-1) experimental; urgency=medium
* New upstream release candidate.
* Update notmuch-emacs for compatibility with GNU Emacs 27.1.
-- David Bremner <bremner@debian.org> Sun, 16 Aug 2020 11:08:14 -0300
notmuch (0.30-1) unstable; urgency=medium
* New upstream release
* Improvements to S/MIME handling
* Repairs to some mangled MIME messages
* New python bindings (notmuch2) compatible with current python 3
-- David Bremner <bremner@debian.org> Fri, 10 Jul 2020 22:24:14 -0300
notmuch (0.30~rc3-1) experimental; urgency=medium
* New upstream release candidate
* Mark two tests broken on legacy (32 bit time_t) architectures.
* Drop -std=c99
-- David Bremner <bremner@debian.org> Fri, 03 Jul 2020 06:48:51 -0300
notmuch (0.30~rc2-1) experimental; urgency=medium
* New upstream release candidate.
* Upstream fixes for new python bindings (python3-notmuch2).
* Update debian/copyright (one new author).
-- David Bremner <bremner@debian.org> Tue, 16 Jun 2020 08:32:16 -0300
notmuch (0.30~rc1-1) experimental; urgency=medium
* New upstream release candidate
* Update debian/changelog (new copyright holders)
-- David Bremner <bremner@debian.org> Sat, 06 Jun 2020 08:06:56 -0300
notmuch (0.30~rc0-2) experimental; urgency=medium
* New upstream release candidate
-- David Bremner <bremner@debian.org> Mon, 01 Jun 2020 21:01:27 -0300
notmuch (0.29.3-1~bpo10+1) buster-backports; urgency=medium notmuch (0.29.3-1~bpo10+1) buster-backports; urgency=medium
* Rebuild for buster-backports. * Rebuild for buster-backports.

1
debian/compat vendored
View file

@ -1 +0,0 @@
11

156
debian/control vendored
View file

@ -4,39 +4,57 @@ Priority: optional
Maintainer: Carl Worth <cworth@debian.org> Maintainer: Carl Worth <cworth@debian.org>
Uploaders: Uploaders:
Jameson Graef Rollins <jrollins@finestructure.net>, Jameson Graef Rollins <jrollins@finestructure.net>,
David Bremner <bremner@debian.org> David Bremner <bremner@debian.org>,
Build-Conflicts: ruby1.8, gdb-minimal, gdb [ia64 mips mips64el] Build-Conflicts:
gdb [ia64 mips mips64el],
gdb-minimal,
ruby1.8,
Build-Depends: Build-Depends:
dpkg-dev (>= 1.17.14),
debhelper (>= 11~),
pkg-config,
libxapian-dev,
libgmime-3.0-dev (>= 3.0.3~),
libtalloc-dev,
libz-dev,
python3-all (>= 3.1.2-7~),
dh-python,
dh-elpa (>= 1.3),
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] <!nocheck>,
dtach (>= 0.8) <!nocheck>,
gpgsm <!nocheck>,
gnupg <!nocheck>,
bash-completion (>=1.9.0~), bash-completion (>=1.9.0~),
texinfo debhelper-compat (= 13),
Standards-Version: 4.1.3 dh-elpa (>= 1.3),
dh-python,
desktop-file-utils,
doxygen,
dpkg-dev (>= 1.17.14),
dtach (>= 0.8) <!nocheck>,
emacs-nox | emacs-gtk | emacs-lucid | emacs25-nox | emacs25 (>=25~) | emacs25-lucid (>=25~) | emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~),
gdb [!ia64 !mips !mips64el !kfreebsd-any !alpha] <!nocheck>,
gnupg <!nocheck>,
gpgsm <!nocheck>,
libgmime-3.0-dev (>= 3.0.3~),
libpython3-all-dev,
libtalloc-dev,
libxapian-dev,
libz-dev,
pkg-config,
python3-all (>= 3.1.2-7~),
python3-cffi,
python3-pytest,
python3-pytest-cov,
python3-setuptools,
python3-sphinx,
ruby,
ruby-dev (>>1:1.9.3~),
texinfo,
Standards-Version: 4.4.1
Homepage: https://notmuchmail.org/ Homepage: https://notmuchmail.org/
Vcs-Git: https://git.notmuchmail.org/git/notmuch -b release Vcs-Git: https://git.notmuchmail.org/git/notmuch -b release
Vcs-Browser: https://git.notmuchmail.org/git/notmuch Vcs-Browser: https://git.notmuchmail.org/git/notmuch
Rules-Requires-Root: no
Package: notmuch Package: notmuch
Architecture: any Architecture: any
Depends: libnotmuch5 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends} Depends:
Recommends: elpa-notmuch | notmuch-vim | notmuch-mutt | alot, gnupg-agent, gpgsm 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 Description: thread-based email index, search and tagging
Notmuch is a system for indexing, searching, reading, and tagging Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses 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 Package: libnotmuch5
Section: libs Section: libs
Architecture: any Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends} Depends:
Pre-Depends: ${misc:Pre-Depends} ${misc:Depends},
${shlibs:Depends},
Pre-Depends:
${misc:Pre-Depends},
Description: thread-based email index, search and tagging (runtime) Description: thread-based email index, search and tagging (runtime)
Notmuch is a system for indexing, searching, reading, and tagging Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses 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 Package: libnotmuch-dev
Section: libdevel Section: libdevel
Architecture: any Architecture: any
Depends: ${misc:Depends}, libnotmuch5 (= ${binary:Version}) Depends:
libnotmuch5 (= ${binary:Version}),
${misc:Depends},
Description: thread-based email index, search and tagging (development) Description: thread-based email index, search and tagging (development)
Notmuch is a system for indexing, searching, reading, and tagging Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses 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 Package: python3-notmuch
Architecture: all Architecture: all
Section: python 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 Description: Python 3 interface to the notmuch mail search and index library
Notmuch is a system for indexing, searching, reading, and tagging Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses 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. convenient search syntax.
. .
This package provides a Python 3 interface to the notmuch 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 Package: ruby-notmuch
Architecture: any Architecture: any
Section: ruby Section: ruby
Depends: ${shlibs:Depends}, ${misc:Depends} Depends:
${misc:Depends},
${shlibs:Depends},
Description: Ruby interface to the notmuch mail search and index library Description: Ruby interface to the notmuch mail search and index library
Notmuch is a system for indexing, searching, reading, and tagging Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses 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 This package provides a Ruby interface to the notmuch
functionality, directly interfacing with a shared notmuch library. 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 Package: elpa-notmuch
Architecture: all 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) Description: thread-based email index, search and tagging (emacs interface)
Notmuch is a system for indexing, searching, reading, and tagging Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses 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 Package: notmuch-vim
Architecture: all Architecture: all
Breaks: notmuch (<<0.6~254~) Breaks:
Replaces: notmuch (<<0.6~254~) notmuch (<<0.6~254~),
Depends: ${misc:Depends}, notmuch, vim-addon-manager, vim-ruby, ruby-notmuch Replaces:
Recommends: ruby-mail 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) Description: thread-based email index, search and tagging (vim interface)
Notmuch is a system for indexing, searching, reading, and tagging Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses 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 Package: notmuch-mutt
Architecture: all Architecture: all
Depends: Depends:
libmail-box-perl,
libmailtools-perl,
libstring-shellquote-perl,
libterm-readline-gnu-perl,
notmuch (>= 0.4), notmuch (>= 0.4),
libmail-box-perl, libmailtools-perl, ${misc:Depends},
libstring-shellquote-perl, libterm-readline-gnu-perl, ${perl:Depends},
${misc:Depends} Recommends:
Recommends: mutt mutt,
Enhances: notmuch, mutt Enhances:
mutt,
notmuch,
Description: thread-based email index, search and tagging (Mutt interface) Description: thread-based email index, search and tagging (Mutt interface)
notmuch-mutt provides integration among the Mutt mail user agent and notmuch-mutt provides integration among the Mutt mail user agent and
the Notmuch mail indexer. the Notmuch mail indexer.

120
debian/copyright vendored
View file

@ -1,36 +1,100 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: notmuch Upstream-Name: notmuch
Source: git://notmuchmail.org/git/notmuch Source: https://git.notmuchmail.org/git/notmuch
Upstream-Contact: Notmuch Mailing List <notmuch@notmuchmail.org> Upstream-Contact: Notmuch Mailing List <notmuch@notmuchmail.org>
Files: * Files: *
Copyright: Copyright 2009 Carl Worth <cworth@cworth.org> Copyright: Copyright 2009-2020
Bart Trojanowski <bart@jukie.net> David Bremner
Keith Packard <keithp@keithp.com> Carl Worth
Alexander Botero-Lowry <alex.boterolowry@gmail.com> Jani Nikula
Ingmar Vanhassel <ingmar@exherbo.org> Austin Clements
Jed Brown <jed@59A2.org> Daniel Kahn Gillmor
Jan Janak <jan@ryngle.com> Mark Walters
Chris Wilson <chris@chris-wilson.co.uk> Floris Bruynooghe
Keith Amidon <keith@nicira.com> David Edmondson
Aneesh Kumar K.V <aneesh.kumar@linux.vnet.ibm.com> Tomi Ollila
Mikhail Gusarov <dottedmag@dottedmag.net> Sebastian Spaeth
Jeffrey C. Ollie <jeff@ocjtech.us> Ali Polatel
Jameson Graef Rollins <jrollins@finestructure.net> Michal Sojka
Stewart Smith <stewart@flamingspork.com> Justus Winter
Adrian Perez <aperez@igalia.com> Sebastien Binet
Kan-Ru Chen <kanru@kanru.info> W. Trevor King
James Rowe <jnrowe@gmail.com> Jameson Graef Rollins
Eric Anholt <eric@anholt.net> Felipe Contreras
Alec Berryman <alec@thened.net> Pieter Praet
Tassilo Horn <tassilo@member.fsf.org> Peter Feigl
Stefan Schmidt <stefan@datenfreihafen.org> Dmitry Kurochkin
Rolland Santimano <rollandsantimano@yahoo.com> Peter Wang
Peter Wang <novalazy@gmail.com> Daniel Schoepe
Lars Kellogg-Stedman <lars@seas.harvard.edu> Gregor Zattler
Holger Freyther <zecke@selfish.org> Keith Packard
David Bremner <bremner@unb.ca> Adam Wolfe Gordon
Alexander Botero-Lowry <alexbl@fortitudo.(none)> 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+ License: GPL-3+
Files: debian/* Files: debian/*

View file

@ -1,3 +1,3 @@
emacs/*.el debian/tmp/usr/share/emacs/site-lisp/*.el
emacs/notmuch-logo.png debian/tmp/usr/share/emacs/site-lisp/notmuch-logo.png
debian/tmp/usr/share/info/* emacs/notmuch-pkg.el

1
debian/elpa-notmuch.info vendored Normal file
View file

@ -0,0 +1 @@
usr/share/info/*.info

4
debian/elpa-notmuch.lintian-overrides vendored Normal file
View file

@ -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

1
debian/libnotmuch-dev.manpages vendored Normal file
View file

@ -0,0 +1 @@
usr/share/man/man3/notmuch.3.gz

View file

@ -1,4 +1,5 @@
libnotmuch.so.5 libnotmuch5 #MINVER# libnotmuch.so.5 libnotmuch5 #MINVER#
* Build-Depends-Package: libnotmuch-dev
notmuch_built_with@Base 0.23~rc0 notmuch_built_with@Base 0.23~rc0
notmuch_config_list_destroy@Base 0.23~rc0 notmuch_config_list_destroy@Base 0.23~rc0
notmuch_config_list_key@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_filename@Base 0.3
notmuch_message_get_filenames@Base 0.5 notmuch_message_get_filenames@Base 0.5
notmuch_message_get_flag@Base 0.3 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_header@Base 0.3
notmuch_message_get_message_id@Base 0.3 notmuch_message_get_message_id@Base 0.3
notmuch_message_get_properties@Base 0.23~rc0 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_tags@Base 0.3
notmuch_message_get_thread_id@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@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_maildir_flags_to_tags@Base 0.5
notmuch_message_properties_destroy@Base 0.23~rc0 notmuch_message_properties_destroy@Base 0.23~rc0
notmuch_message_properties_key@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::DatabaseError@Base" 0.24~rc0
(c++)"typeinfo for Xapian::DatabaseModifiedError@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++|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::LogicError@Base" 0.6.1
(c++)"typeinfo name for Xapian::RuntimeError@Base" 0.6.1 (c++)"typeinfo name for Xapian::RuntimeError@Base" 0.6.1
(c++)"typeinfo name for Xapian::DocNotFoundError@Base" 0.6.1 (c++)"typeinfo name for Xapian::DocNotFoundError@Base" 0.6.1

3
debian/not-installed vendored Normal file
View file

@ -0,0 +1,3 @@
usr/share/applications/mimeinfo.cache
usr/share/info/dir
usr/share/emacs/site-lisp/*.elc

View file

@ -1,2 +1,2 @@
usr/bin/notmuch-mutt
etc/Muttrc.d/notmuch-mutt.rc etc/Muttrc.d/notmuch-mutt.rc
usr/bin/notmuch-mutt

View file

@ -1,4 +1,4 @@
usr/share/vim/registry
usr/share/vim/addons/plugin
usr/share/vim/addons/doc usr/share/vim/addons/doc
usr/share/vim/addons/plugin
usr/share/vim/addons/syntax usr/share/vim/addons/syntax
usr/share/vim/registry

View file

@ -1,4 +1,4 @@
vim/notmuch.vim usr/share/vim/addons/plugin
vim/notmuch.txt usr/share/vim/addons/doc 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/notmuch.yaml usr/share/vim/registry
vim/syntax/notmuch-*.vim usr/share/vim/addons/syntax

View file

@ -1,5 +1,5 @@
usr/bin/notmuch usr/bin/notmuch
usr/bin/notmuch-emacs-mua usr/bin/notmuch-emacs-mua
usr/share/applications/notmuch-emacs-mua.desktop
usr/share/bash-completion usr/share/bash-completion
usr/share/zsh/vendor-completions usr/share/zsh/vendor-completions
emacs/notmuch-emacs-mua.desktop usr/share/applications

View file

@ -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-compact.1.gz
usr/share/man/man1/notmuch-emacs-mua.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-address.1.gz
usr/share/man/man1/notmuch-tag.1.gz usr/share/man/man1/notmuch-compact.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-show.1.gz
usr/share/man/man1/notmuch-config.1.gz usr/share/man/man1/notmuch-config.1.gz
usr/share/man/man1/notmuch-count.1.gz
usr/share/man/man1/notmuch-dump.1.gz
usr/share/man/man1/notmuch-emacs-mua.1.gz
usr/share/man/man1/notmuch-insert.1.gz
usr/share/man/man1/notmuch-new.1.gz
usr/share/man/man1/notmuch-reindex.1.gz
usr/share/man/man1/notmuch-reply.1.gz
usr/share/man/man1/notmuch-restore.1.gz
usr/share/man/man1/notmuch-search.1.gz
usr/share/man/man1/notmuch-setup.1.gz
usr/share/man/man1/notmuch-show.1.gz
usr/share/man/man1/notmuch-tag.1.gz
usr/share/man/man1/notmuch.1.gz
usr/share/man/man5/notmuch-hooks.5.gz
usr/share/man/man7/notmuch-properties.7.gz usr/share/man/man7/notmuch-properties.7.gz
usr/share/man/man7/notmuch-search-terms.7.gz usr/share/man/man7/notmuch-search-terms.7.gz

View file

@ -1 +0,0 @@
usr/lib/python2*

14
debian/rules vendored
View file

@ -1,6 +1,6 @@
#!/usr/bin/make -f #!/usr/bin/make -f
export PYBUILD_NAME=notmuch export DEB_BUILD_MAINT_OPTIONS = hardening=+all
%: %:
dh $@ --with python3,elpa dh $@ --with python3,elpa
@ -15,19 +15,25 @@ override_dh_auto_configure:
--zshcompletiondir=/usr/share/zsh/vendor-completions \ --zshcompletiondir=/usr/share/zsh/vendor-completions \
--localstatedir=/var --localstatedir=/var
override_dh_auto_test:
dh_auto_test -- V=1
override_dh_auto_build: override_dh_auto_build:
dh_auto_build -- V=1 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 $(MAKE) -C contrib/notmuch-mutt
override_dh_auto_clean: override_dh_auto_clean:
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 dh_auto_clean --sourcedirectory bindings/ruby
$(MAKE) -C contrib/notmuch-mutt clean $(MAKE) -C contrib/notmuch-mutt clean
override_dh_auto_install: override_dh_auto_install:
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 $(MAKE) -C contrib/notmuch-mutt DESTDIR=$(CURDIR)/debian/tmp install
dh_auto_install --sourcedirectory bindings/ruby dh_auto_install --sourcedirectory bindings/ruby

6
debian/upstream/metadata vendored Normal file
View file

@ -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/

View file

@ -39,8 +39,7 @@ debugger_is_active (void)
sprintf (buf, "/proc/%d/exe", getppid ()); sprintf (buf, "/proc/%d/exe", getppid ());
if (readlink (buf, buf2, sizeof (buf2)) != -1 && if (readlink (buf, buf2, sizeof (buf2)) != -1 &&
strncmp (basename (buf2), "gdb", 3) == 0) strncmp (basename (buf2), "gdb", 3) == 0) {
{
return true; return true;
} }

View file

@ -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 if/for/while test) and are preceded by a space. The opening brace of
functions is the exception, and starts on a new line. functions is the exception, and starts on a new line.
* Comments are always C-style /* */ block comments. They should start * Opening parens also cuddle, even if the first argument does not fit
with a capital letter and generally be written in complete on the same line.
sentences. Public library functions are documented immediately
before their prototype in lib/notmuch.h. Internal functions are * Ternary operators that span a line should be parenthesized like as
typically documented immediately before their definition. "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 * Code lines should be less than 80 columns and comments should be
wrapped at 70 columns. wrapped at 70 columns.

11
devel/author-scan.sh Normal file
View file

@ -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

View file

@ -20,7 +20,7 @@
| q | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | | 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 | | r | notmuch-search-reply-to-thread-sender | notmuch-show-reply-sender | notmuch-show-reply-sender |
| s | notmuch-search | notmuch-search | notmuch-search | | 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 | | | | | u | | | |
| v | | | notmuch-show-view-all-mime-parts | | v | | | notmuch-show-view-all-mime-parts |
| w | | notmuch-show-save-attachments | notmuch-show-save-attachments | | w | | notmuch-show-save-attachments | notmuch-show-save-attachments |

View file

@ -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-SUFFIX>.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 <jani@nikula.org>
#
# 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

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# Copyright (c) 2011-2014 David Bremner <david@tethera.net> # Copyright (c) 2011-2014 David Bremner <david@tethera.net>
# W. Trevor King <wking@tremily.us> # W. Trevor King <wking@tremily.us>

View file

@ -1,10 +1,9 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# Copyright (c) 2011-2012 David Bremner <david@tethera.net> # Copyright (c) 2011-2012 David Bremner <david@tethera.net>
# #
# dependencies # dependencies
# - python 2.6 for json # - python3 or python2.7
# - argparse; either python 2.7, or install separately
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by

View file

@ -1,69 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
# 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), '└')

View file

@ -29,7 +29,7 @@ append_emsg ()
emsgs="${emsgs:+$emsgs\n} $1" 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 do
if [ ! -f "$f" ]; then append_emsg "File '$f' is missing" if [ ! -f "$f" ]; then append_emsg "File '$f' is missing"
elif [ ! -r "$f" ]; then append_emsg "File '$f' is unreadable" elif [ ! -r "$f" ]; then append_emsg "File '$f' is unreadable"
@ -53,7 +53,7 @@ then
else else
echo "Reading './version' file failed (surprisingly!)" echo "Reading './version' file failed (surprisingly!)"
exit 1 exit 1
fi < ./version fi < ./version.txt
readonly VERSION readonly VERSION
@ -109,7 +109,7 @@ else
fi fi
echo -n "Checking that python bindings version is $VERSION... " 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" ] if [ "$py_version" = "$VERSION" ]
then then
echo Yes. echo Yes.
@ -178,10 +178,7 @@ esac
year=`exec date +%Y` year=`exec date +%Y`
echo -n "Checking that copyright in documentation contains 2009-$year... " echo -n "Checking that copyright in documentation contains 2009-$year... "
# Read the value of variable `copyright' defined in 'doc/conf.py'. # Read the value of variable `copyright' defined in 'doc/conf.py'.
# As __file__ is not defined when python command is given from command line, copyrightline=$(grep ^copyright doc/conf.py)
# 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)"`
case $copyrightline in case $copyrightline in
*2009-$year*) *2009-$year*)
echo Yes. ;; echo Yes. ;;

View file

@ -44,28 +44,10 @@ while looking at: " pdir "emacs\n\nexit emacs (y or n)? ")
try-notmuch-emacs-directory (concat pdir "emacs/") try-notmuch-emacs-directory (concat pdir "emacs/")
load-path (cons try-notmuch-emacs-directory load-path))) load-path (cons try-notmuch-emacs-directory load-path)))
;; they say advice doesn't work for primitives (functions from c source) (define-advice require
;; well, these 'before' advice works for emacs 23.1 - 24.5 (at least) (:before (feature &optional _filename _noerror) notmuch)
;; ...and for our purposes 24.3 is enough (there is no load-prefer-newer there) (unless (featurep feature)
;; note also that the old, "obsolete" defadvice mechanism was used, but that (message "require: %s" feature)))
;; 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)
)))))
(insert "Found notmuch emacs client in " try-notmuch-emacs-directory "\n") (insert "Found notmuch emacs client in " try-notmuch-emacs-directory "\n")

View file

@ -117,3 +117,5 @@ align_right_cmt_span = 8 # align comments span this much in func
cmt_star_cont = true cmt_star_cont = true
# indent_brace = 0 # indent_brace = 0
indent_class = true

View file

@ -1,10 +1,10 @@
# -*- makefile -*- # -*- makefile-gmake -*-
dir := doc dir := doc
# You can set these variables from the command line. # You can set these variables from the command line.
SPHINXOPTS := -q SPHINXOPTS := -q
SPHINXBUILD = HAVE_EMACS=${HAVE_EMACS} WITH_EMACS=${WITH_EMACS} sphinx-build SPHINXBUILD = sphinx-build
DOCBUILDDIR := $(dir)/_build DOCBUILDDIR := $(dir)/_build
# Internal variables. # 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)) MAN5_TEXI := $(patsubst $(srcdir)/doc/man5/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$(MAN5_RST))
MAN7_TEXI := $(patsubst $(srcdir)/doc/man7/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$(MAN7_RST)) MAN7_TEXI := $(patsubst $(srcdir)/doc/man7/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$(MAN7_RST))
INFO_TEXI_FILES := $(MAN1_TEXI) $(MAN5_TEXI) $(MAN7_TEXI) INFO_TEXI_FILES := $(MAN1_TEXI) $(MAN5_TEXI) $(MAN7_TEXI)
ifeq ($(HAVE_EMACS)$(WITH_EMACS),11) ifeq ($(WITH_EMACS),1)
INFO_TEXI_FILES := $(INFO_TEXI_FILES) $(DOCBUILDDIR)/texinfo/notmuch-emacs.texi INFO_TEXI_FILES += $(DOCBUILDDIR)/texinfo/notmuch-emacs.texi
endif endif
INFO_INFO_FILES := $(INFO_TEXI_FILES:.texi=.info) 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 .PHONY: install-man build-man apidocs install-apidocs
%.gz: % %.gz: %
rm -f $@ && gzip --stdout $^ > $@ rm -f $@ && gzip --no-name --stdout $^ > $@
ifeq ($(WITH_EMACS),1) ifeq ($(WITH_EMACS),1)
$(DOCBUILDDIR)/.roff.stamp sphinx-html sphinx-texinfo: docstring.stamp $(DOCBUILDDIR)/.roff.stamp sphinx-html sphinx-texinfo: docstring.stamp

View file

@ -4,6 +4,8 @@
import sys import sys
import os import os
extensions = [ 'sphinx.ext.autodoc' ]
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = '.rst' source_suffix = '.rst'
@ -12,16 +14,26 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'notmuch' 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__) location = os.path.dirname(__file__)
for pathdir in ['.', '..']: 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): if os.path.exists(version_file):
with open(version_file,'r') as infile: with open(version_file,'r') as infile:
version=infile.read().replace('\n','') 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. # The full version, including alpha/beta/rc tags.
release = version release = version
@ -29,12 +41,23 @@ release = version
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = ['_build'] exclude_patterns = ['_build']
# If we don't have emacs (or the user configured --without-emacs), if tags.has('WITH_EMACS'):
# don't build the notmuch-emacs docs, as they need emacs to generate # Hacky reimplementation of include to workaround limitations of
# the docstring include files # sphinx-doc
if os.environ.get('HAVE_EMACS') != '1' or os.environ.get('WITH_EMACS') != '1': 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') 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. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx' pygments_style = 'sphinx'

View file

@ -264,12 +264,10 @@ GENERATE_TAGFILE =
ALLEXTERNALS = NO ALLEXTERNALS = NO
EXTERNAL_GROUPS = NO EXTERNAL_GROUPS = NO
EXTERNAL_PAGES = NO EXTERNAL_PAGES = NO
PERL_PATH = /usr/bin/perl
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Configuration options related to the dot tool # Configuration options related to the dot tool
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
CLASS_DIAGRAMS = NO CLASS_DIAGRAMS = NO
MSCGEN_PATH =
HIDE_UNDOC_RELATIONS = YES HIDE_UNDOC_RELATIONS = YES
HAVE_DOT = NO HAVE_DOT = NO
DOT_NUM_THREADS = 0 DOT_NUM_THREADS = 0

View file

@ -26,6 +26,7 @@ Contents:
man7/notmuch-search-terms man7/notmuch-search-terms
man1/notmuch-show man1/notmuch-show
man1/notmuch-tag man1/notmuch-tag
python-bindings
Indices and tables Indices and tables
================== ==================

View file

@ -38,7 +38,7 @@ programmatically as described in the SYNOPSIS above.
Every configuration item is printed to stdout, each on a separate Every configuration item is printed to stdout, each on a separate
line of the form:: line of the form::
*section*.\ *item*\ =\ *value* section.item=value
No additional whitespace surrounds the dot or equals sign No additional whitespace surrounds the dot or equals sign
characters. In a multiple-value item (a list), the values are characters. In a multiple-value item (a list), the values are
@ -134,14 +134,6 @@ The available configuration items are described below.
Default: ``true``. 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]** **index.decrypt** **[STORED IN DATABASE]**
Policy for decrypting encrypted messages during indexing. Must be Policy for decrypting encrypted messages during indexing. Must be
one of: ``false``, ``auto``, ``nostash``, or ``true``. one of: ``false``, ``auto``, ``nostash``, or ``true``.
@ -206,8 +198,9 @@ The available configuration items are described below.
**built_with.<name>** **built_with.<name>**
Compile time feature <name>. Current possibilities include Compile time feature <name>. Current possibilities include
"compact" (see **notmuch-compact(1)**) and "field_processor" (see "retry_lock" (configure option, included by default).
**notmuch-search-terms(7)**). (since notmuch 0.30, "compact" and "field_processor" are
always included.)
**query.<name>** **[STORED IN DATABASE]** **query.<name>** **[STORED IN DATABASE]**
Expansion for named query called <name>. See Expansion for named query called <name>. See

View file

@ -128,9 +128,9 @@ OPTION SYNTAX
------------- -------------
All options accepting an argument can be used with '=' or ':' as a All options accepting an argument can be used with '=' or ':' as a
separator. For the cases where it's not ambiguous (in particular separator. Except for boolean options (which would be ambiguous), a
excluding boolean options), a space can also be used. The following space can also be used as a separator. The following are all
are all equivalent: equivalent:
:: ::

View file

@ -109,6 +109,30 @@ of its normal activity.
example, an AES-128 key might be stashed in a notmuch property as: example, an AES-128 key might be stashed in a notmuch property as:
``session-key=7:14B16AF65536C28AF209828DFE34C9E0``. ``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 SEE ALSO
======== ========

View file

@ -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 <brackets> terms to match against specific portions of an email, (where <brackets>
indicate user-supplied values). indicate user-supplied values).
If notmuch is built with **Xapian Field Processors** (see below) some Some of the prefixes with <regex> forms can be also used to restrict
of the prefixes with <regex> forms can be also used to restrict the the results to those whose value matches a regular expression (see
results to those whose value matches a regular expression (see
**regex(7)**) delimited with //, for example:: **regex(7)**) delimited with //, for example::
notmuch search 'from:"/bob@.*[.]example[.]com/"' notmuch search 'from:"/bob@.*[.]example[.]com/"'
@ -87,8 +86,7 @@ thread:<thread-id>
of output from **notmuch search** of output from **notmuch search**
thread:{<notmuch query>} thread:{<notmuch query>}
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 notmuch query in **{}**. For example, the following returns
threads containing a message from mallory and one (not necessarily threads containing a message from mallory and one (not necessarily
the same message) with Subject containing the word "crypto". the same message) with Subject containing the word "crypto".
@ -158,9 +156,7 @@ lastmod:<initial-revision>..<final-revision>
query:<name> query:<name>
The **query:** prefix allows queries to refer to previously saved The **query:** prefix allows queries to refer to previously saved
queries added with **notmuch-config(1)**. Named queries are only queries added with **notmuch-config(1)**.
available if notmuch is built with **Xapian Field Processors**
(see below).
property:<key>=<value> property:<key>=<value>
The **property:** prefix searches for messages with a particular The **property:** prefix searches for messages with a particular
@ -353,23 +349,21 @@ since 1970-01-01 00:00:00 UTC. For example:
date:@<initial-timestamp>..@<final-timestamp> date:@<initial-timestamp>..@<final-timestamp>
date:<expr>..! can be used as a shorthand for date:<expr>..<expr>. The Currently, spaces in range expressions are not supported. You can
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
replace the spaces with '\_', or (in most cases) '-', or (in some cases) replace the spaces with '\_', or (in most cases) '-', or (in some cases)
leave the spaces out altogether. Examples in this man page use spaces leave the spaces out altogether. Examples in this man page use spaces
for clarity. for clarity.
Open-ended ranges are supported (since Xapian 1.2.1), i.e. it's possible Open-ended ranges are supported. I.e. it's possible to specify
to specify date:..<until> or date:<since>.. to not limit the start or date:..<until> or date:<since>.. to not limit the start or
end time, respectively. Pre-1.2.1 Xapian does not report an error on end time, respectively.
open ended ranges, but it does not work as expected either.
Single expression
-----------------
date:<expr> works as a shorthand for date:<expr>..<expr>.
For example, date:monday matches from the beginning of Monday until
the end of Monday.
Relative date and time Relative date and time
---------------------- ----------------------
@ -446,24 +440,6 @@ Time zones
Some time zone codes, e.g. UTC, EET. 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 SEE ALSO
======== ========

View file

@ -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 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`` work with it. If Emacs was invoked with the ``-q`` or ``--no-init-file``
options, ``notmuch-init-file`` is not read. 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

5
doc/python-bindings.rst Normal file
View file

@ -0,0 +1,5 @@
Python Bindings
===============
.. automodule:: notmuch2
:members:

View file

@ -1,4 +1,4 @@
# -*- makefile -*- # -*- makefile-gmake -*-
dir := emacs dir := emacs
emacs_sources := \ emacs_sources := \
@ -47,7 +47,7 @@ emacs_images := \
emacs_bytecode = $(emacs_sources:.el=.elc) emacs_bytecode = $(emacs_sources:.el=.elc)
emacs_docstrings = $(emacs_sources:.el=.rsti) emacs_docstrings = $(emacs_sources:.el=.rsti)
ifneq ($(HAVE_SPHINX)$(HAVE_EMACS),11) ifneq ($(HAVE_SPHINX)$(WITH_EMACS),11)
docstring.stamp: docstring.stamp:
@echo "Missing prerequisites, not collecting docstrings" @echo "Missing prerequisites, not collecting docstrings"
else else
@ -60,7 +60,7 @@ endif
# the byte compiler may load an old .elc file when processing a # 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 # "require" or we may fail to rebuild a .elc that depended on a macro
# from an updated file. # from an updated file.
ifeq ($(HAVE_EMACS),1) ifeq ($(WITH_EMACS),1)
$(dir)/.eldeps: $(dir)/Makefile.local $(dir)/make-deps.el $(emacs_sources) $(dir)/.eldeps: $(dir)/Makefile.local $(dir)/make-deps.el $(emacs_sources)
$(call quiet,EMACS) --directory emacs -batch -l make-deps.el \ $(call quiet,EMACS) --directory emacs -batch -l make-deps.el \
-f batch-make-deps $(emacs_sources) > $@.tmp && \ -f batch-make-deps $(emacs_sources) > $@.tmp && \
@ -82,7 +82,7 @@ $(dir)/notmuch-lib.elc: $(dir)/notmuch-version.elc
endif endif
CLEAN+=$(dir)/.eldeps $(dir)/.eldeps.tmp $(dir)/.eldeps.x CLEAN+=$(dir)/.eldeps $(dir)/.eldeps.tmp $(dir)/.eldeps.x
ifeq ($(HAVE_EMACS),1) ifeq ($(WITH_EMACS),1)
%.elc: %.el $(global_deps) %.elc: %.el $(global_deps)
$(call quiet,EMACS) --directory emacs -batch -f batch-byte-compile $< $(call quiet,EMACS) --directory emacs -batch -f batch-byte-compile $<
%.rsti: %.el %.rsti: %.el
@ -103,10 +103,8 @@ endif
rm -r .elpa-build rm -r .elpa-build
ifeq ($(WITH_EMACS),1) ifeq ($(WITH_EMACS),1)
ifeq ($(HAVE_EMACS),1)
all: $(emacs_bytecode) $(emacs_docstrings) all: $(emacs_bytecode) $(emacs_docstrings)
install-emacs: $(emacs_bytecode) install-emacs: $(emacs_bytecode)
endif
install: install-emacs install: install-emacs
endif endif
@ -115,7 +113,7 @@ endif
install-emacs: $(emacs_sources) $(emacs_images) install-emacs: $(emacs_sources) $(emacs_images)
mkdir -p "$(DESTDIR)$(emacslispdir)" mkdir -p "$(DESTDIR)$(emacslispdir)"
install -m0644 $(emacs_sources) "$(DESTDIR)$(emacslispdir)" install -m0644 $(emacs_sources) "$(DESTDIR)$(emacslispdir)"
ifeq ($(HAVE_EMACS),1) ifeq ($(WITH_EMACS),1)
install -m0644 $(emacs_bytecode) "$(DESTDIR)$(emacslispdir)" install -m0644 $(emacs_bytecode) "$(DESTDIR)$(emacslispdir)"
endif endif
mkdir -p "$(DESTDIR)$(emacsetcdir)" mkdir -p "$(DESTDIR)$(emacsetcdir)"

Some files were not shown because too many files have changed in this diff Show more