mirror of
https://git.notmuchmail.org/git/notmuch
synced 2024-11-23 11:28:13 +01:00
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:
commit
7a9c97e8a5
277 changed files with 12790 additions and 4773 deletions
|
@ -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
26
.gitignore
vendored
|
@ -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
|
|
||||||
|
|
|
@ -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
109
AUTHORS
|
@ -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
|
||||||
|
|
2
INSTALL
2
INSTALL
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
153
NEWS
|
@ -1,3 +1,154 @@
|
||||||
|
Notmuch 0.31.2 (2020-11-08)
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Build
|
||||||
|
-----
|
||||||
|
|
||||||
|
Catch one more occurence of "version" in the build system, which
|
||||||
|
caused the file to be regenerated in the release tarball.
|
||||||
|
|
||||||
|
Notmuch 0.31.1 (2020-11-08)
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Library
|
||||||
|
-------
|
||||||
|
|
||||||
|
Fix a memory initialization bug in notmuch_database_get_config_list.
|
||||||
|
|
||||||
|
Build
|
||||||
|
-----
|
||||||
|
|
||||||
|
Rename file 'version' to 'version.txt'. The old file name conflicted
|
||||||
|
with a C++ header for some compilers.
|
||||||
|
|
||||||
|
Replace use of coreutils `realpath` in configure.
|
||||||
|
|
||||||
|
Notmuch 0.31 (2020-09-05)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Emacs
|
||||||
|
-----
|
||||||
|
|
||||||
|
Notmuch now supports Emacs 27.1. You may need to set
|
||||||
|
`mml-secure-openpgp-sign-with-sender` and/or
|
||||||
|
`mml-secure-smime-sign-with-sender` to continue signing messages.
|
||||||
|
|
||||||
|
The minimum supported major version of GNU Emacs is now 25.1.
|
||||||
|
|
||||||
|
Add support for moving between threads after notmuch-tree-from-search-thread.
|
||||||
|
|
||||||
|
New `notmuch-unthreaded` mode (added in Notmuch 0.30)
|
||||||
|
|
||||||
|
Unthreaded view is a mode where each matching message is shown on a
|
||||||
|
separate line.
|
||||||
|
|
||||||
|
The main key entries to unthreaded view are
|
||||||
|
|
||||||
|
'u' enter a query to view in unthreaded mode (works in hello,
|
||||||
|
search, show and tree mode)
|
||||||
|
|
||||||
|
'U' view the current query in unthreaded mode (works from search,
|
||||||
|
show and tree)
|
||||||
|
|
||||||
|
Saved searches can also specify that they should open in unthreaded
|
||||||
|
view.
|
||||||
|
|
||||||
|
Currently it is not possible to specify the sort order: it will
|
||||||
|
always be newest first.
|
||||||
|
|
||||||
|
Notmuch-Mutt
|
||||||
|
------------
|
||||||
|
|
||||||
|
The shell pipeline executed by notmuch-mutt, which symlinked matched
|
||||||
|
files to a maildir for mutt to access is replaced with internal perl
|
||||||
|
processing. This search operation is now more portable, and somewhat
|
||||||
|
faster.
|
||||||
|
|
||||||
|
Library
|
||||||
|
-------
|
||||||
|
|
||||||
|
Improve exception handling in the library. This should
|
||||||
|
largely eliminate terminations inside the library due to uncaught
|
||||||
|
exceptions or internal errors. No doubt there are a few uncovered
|
||||||
|
code paths still; please report them as bugs.
|
||||||
|
|
||||||
|
Add `notmuch_message_get_flag_st` and
|
||||||
|
`notmuch_message_has_maildir_flag_st`, and deprecate the existing
|
||||||
|
non-status providing versions.
|
||||||
|
|
||||||
|
Move memory de-allocation from `notmuch_database_close` to
|
||||||
|
`notmuch_database_destroy`.
|
||||||
|
|
||||||
|
Handle relative filenames in `notmuch_database_index_file`, as
|
||||||
|
promised in the documentation.
|
||||||
|
|
||||||
|
Python Bindings
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Documentation for the python bindings is merged into the main
|
||||||
|
sphinx-doc documentation tree. The merged documentation can be built
|
||||||
|
with e.g. `make sphinx-html`
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
We now support building notmuch against Xapian 1.5 (the current
|
||||||
|
development version).
|
||||||
|
|
||||||
|
Test Suite
|
||||||
|
----------
|
||||||
|
|
||||||
|
Test suite fixes for compatibility with Emacs 27.1.
|
||||||
|
|
||||||
|
Build System
|
||||||
|
------------
|
||||||
|
|
||||||
|
Man pages are now compressed reproducibly.
|
||||||
|
|
||||||
|
Notmuch 0.30 (2020-07-10)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
S/MIME
|
||||||
|
------
|
||||||
|
|
||||||
|
Handle S/MIME (PKCS#7) messages -- one-part signed messages, encrypted
|
||||||
|
messages, and multilayer messages. Treat them symmetrically to
|
||||||
|
OpenPGP messages. This includes handling protected headers
|
||||||
|
gracefully.
|
||||||
|
|
||||||
|
If you're using Notmuch with S/MIME, you currently need to configure
|
||||||
|
gpgsm appropriately.
|
||||||
|
|
||||||
|
Mixed-up MIME Repair
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Detect and automatically repair a common form of message mangling
|
||||||
|
created by Microsoft Exchange (see index.repaired=mixedup in
|
||||||
|
notmuch-properties(7)).
|
||||||
|
|
||||||
|
Protected Headers
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Avoid indexing the legacy-display part of an encrypted message that
|
||||||
|
has protected headers (see
|
||||||
|
index.repaired=skip-protected-headers-legacy-display in
|
||||||
|
notmuch-properties(7)).
|
||||||
|
|
||||||
|
Python
|
||||||
|
------
|
||||||
|
|
||||||
|
Drop support for python2, focus on python3.
|
||||||
|
|
||||||
|
Introduce new CFFI-based python bindings in the python module named
|
||||||
|
"notmuch2". Officially deprecate (but still support) the older
|
||||||
|
"notmuch" module.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
Support for Xapian 1.2 is removed. The minimum supported version of
|
||||||
|
Xapian is now 1.4.0.
|
||||||
|
|
||||||
Notmuch 0.29.3 (2019-11-27)
|
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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
bindings/python-cffi/MANIFEST.in
Normal file
2
bindings/python-cffi/MANIFEST.in
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
include MANIFEST.in
|
||||||
|
include tox.ini
|
62
bindings/python-cffi/notmuch2/__init__.py
Normal file
62
bindings/python-cffi/notmuch2/__init__.py
Normal 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
|
238
bindings/python-cffi/notmuch2/_base.py
Normal file
238
bindings/python-cffi/notmuch2/_base.py
Normal 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>'
|
339
bindings/python-cffi/notmuch2/_build.py
Normal file
339
bindings/python-cffi/notmuch2/_build.py
Normal 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)
|
87
bindings/python-cffi/notmuch2/_config.py
Normal file
87
bindings/python-cffi/notmuch2/_config.py
Normal 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)
|
822
bindings/python-cffi/notmuch2/_database.py
Normal file
822
bindings/python-cffi/notmuch2/_database.py
Normal 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)
|
112
bindings/python-cffi/notmuch2/_errors.py
Normal file
112
bindings/python-cffi/notmuch2/_errors.py
Normal 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'
|
710
bindings/python-cffi/notmuch2/_message.py
Normal file
710
bindings/python-cffi/notmuch2/_message.py
Normal 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)
|
83
bindings/python-cffi/notmuch2/_query.py
Normal file
83
bindings/python-cffi/notmuch2/_query.py
Normal 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]
|
359
bindings/python-cffi/notmuch2/_tags.py
Normal file
359
bindings/python-cffi/notmuch2/_tags.py
Normal 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>'
|
194
bindings/python-cffi/notmuch2/_thread.py
Normal file
194
bindings/python-cffi/notmuch2/_thread.py
Normal 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)
|
24
bindings/python-cffi/setup.py
Normal file
24
bindings/python-cffi/setup.py
Normal 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',
|
||||||
|
],
|
||||||
|
)
|
149
bindings/python-cffi/tests/conftest.py
Normal file
149
bindings/python-cffi/tests/conftest.py
Normal 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)
|
116
bindings/python-cffi/tests/test_base.py
Normal file
116
bindings/python-cffi/tests/test_base.py
Normal 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'
|
56
bindings/python-cffi/tests/test_config.py
Normal file
56
bindings/python-cffi/tests/test_config.py
Normal 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
|
342
bindings/python-cffi/tests/test_database.py
Normal file
342
bindings/python-cffi/tests/test_database.py
Normal 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
|
226
bindings/python-cffi/tests/test_message.py
Normal file
226
bindings/python-cffi/tests/test_message.py
Normal 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')}
|
239
bindings/python-cffi/tests/test_tags.py
Normal file
239
bindings/python-cffi/tests/test_tags.py
Normal 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'}
|
102
bindings/python-cffi/tests/test_thread.py
Normal file
102
bindings/python-cffi/tests/test_thread.py
Normal 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
|
19
bindings/python-cffi/tox.ini
Normal file
19
bindings/python-cffi/tox.ini
Normal 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
|
1
bindings/python-cffi/version.txt
Normal file
1
bindings/python-cffi/version.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
0.31.2
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- makefile -*-
|
# -*- makefile-gmake -*-
|
||||||
|
|
||||||
dir := compat
|
dir := compat
|
||||||
extra_cflags += -I$(srcdir)/$(dir)
|
extra_cflags += -I$(srcdir)/$(dir)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
|
|
||||||
int main()
|
int
|
||||||
|
main ()
|
||||||
{
|
{
|
||||||
struct dirent ent;
|
struct dirent ent;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- makefile -*-
|
# -*- makefile-gmake -*-
|
||||||
|
|
||||||
dir := completion
|
dir := completion
|
||||||
|
|
||||||
|
|
426
configure
vendored
426
configure
vendored
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
97
debian/changelog
vendored
|
@ -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
1
debian/compat
vendored
|
@ -1 +0,0 @@
|
||||||
11
|
|
156
debian/control
vendored
156
debian/control
vendored
|
@ -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
120
debian/copyright
vendored
|
@ -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/*
|
||||||
|
|
6
debian/elpa-notmuch.elpa
vendored
6
debian/elpa-notmuch.elpa
vendored
|
@ -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
1
debian/elpa-notmuch.info
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
usr/share/info/*.info
|
4
debian/elpa-notmuch.lintian-overrides
vendored
Normal file
4
debian/elpa-notmuch.lintian-overrides
vendored
Normal 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
1
debian/libnotmuch-dev.manpages
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
usr/share/man/man3/notmuch.3.gz
|
5
debian/libnotmuch5.symbols
vendored
5
debian/libnotmuch5.symbols
vendored
|
@ -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
3
debian/not-installed
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
usr/share/applications/mimeinfo.cache
|
||||||
|
usr/share/info/dir
|
||||||
|
usr/share/emacs/site-lisp/*.elc
|
2
debian/notmuch-mutt.install
vendored
2
debian/notmuch-mutt.install
vendored
|
@ -1,2 +1,2 @@
|
||||||
usr/bin/notmuch-mutt
|
|
||||||
etc/Muttrc.d/notmuch-mutt.rc
|
etc/Muttrc.d/notmuch-mutt.rc
|
||||||
|
usr/bin/notmuch-mutt
|
||||||
|
|
4
debian/notmuch-vim.dirs
vendored
4
debian/notmuch-vim.dirs
vendored
|
@ -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
|
||||||
|
|
4
debian/notmuch-vim.install
vendored
4
debian/notmuch-vim.install
vendored
|
@ -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
|
||||||
|
|
2
debian/notmuch.install
vendored
2
debian/notmuch.install
vendored
|
@ -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
|
|
||||||
|
|
29
debian/notmuch.manpages
vendored
29
debian/notmuch.manpages
vendored
|
@ -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
|
||||||
|
|
1
debian/python-notmuch.install
vendored
1
debian/python-notmuch.install
vendored
|
@ -1 +0,0 @@
|
||||||
usr/lib/python2*
|
|
14
debian/rules
vendored
14
debian/rules
vendored
|
@ -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
6
debian/upstream/metadata
vendored
Normal 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/
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
devel/STYLE
18
devel/STYLE
|
@ -53,11 +53,19 @@ function (param_type param, param_type param)
|
||||||
if/for/while test) and are preceded by a space. The opening brace of
|
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
11
devel/author-scan.sh
Normal 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
|
|
@ -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 |
|
||||||
|
|
|
@ -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
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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), '└')
|
|
|
@ -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. ;;
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
35
doc/conf.py
35
doc/conf.py
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
==================
|
==================
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|
|
@ -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
5
doc/python-bindings.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
Python Bindings
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. automodule:: notmuch2
|
||||||
|
:members:
|
|
@ -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
Loading…
Reference in a new issue