mirror of
https://git.notmuchmail.org/git/notmuch
synced 2024-11-26 20:57:58 +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
|
||||
(indent-tabs-mode . t)
|
||||
(tab-width . 8))
|
||||
(shell-mode
|
||||
(sh-mode
|
||||
(indent-tabs-mode . t)
|
||||
(tab-width . 8)
|
||||
(sh-basic-offset . 4)
|
||||
|
|
26
.gitignore
vendored
26
.gitignore
vendored
|
@ -1,18 +1,20 @@
|
|||
*.[ao]
|
||||
*.stamp
|
||||
*cscope*
|
||||
*~
|
||||
.*.swp
|
||||
/.deps
|
||||
/.first-build-message
|
||||
/.stamps
|
||||
/Makefile.config
|
||||
/bindings/python-cffi/build/
|
||||
/lib/libnotmuch*.dylib
|
||||
/lib/libnotmuch.so*
|
||||
/notmuch
|
||||
/notmuch-shared
|
||||
/releases
|
||||
/sh.config
|
||||
/sphinx.config
|
||||
/version.stamp
|
||||
TAGS
|
||||
tags
|
||||
*cscope*
|
||||
/.deps
|
||||
/notmuch
|
||||
/notmuch-shared
|
||||
/lib/libnotmuch.so*
|
||||
/lib/libnotmuch*.dylib
|
||||
*.[ao]
|
||||
*~
|
||||
.*.swp
|
||||
/releases
|
||||
/.stamps
|
||||
*.stamp
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
language: c
|
||||
|
||||
dist: xenial
|
||||
dist: bionic
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:xapian-backports/ppa'
|
||||
- sourceline: 'ppa:notmuch/notmuch'
|
||||
packages:
|
||||
- dtach
|
||||
- libxapian-dev
|
||||
- libgmime-3.0-dev
|
||||
- libtalloc-dev
|
||||
- python3-sphinx
|
||||
- python3-cffi
|
||||
- python3-pytest
|
||||
- python3-setuptools
|
||||
- libpython3-all-dev
|
||||
- gpgsm
|
||||
|
||||
script:
|
||||
- ./configure
|
||||
- make download-test-databases
|
||||
- make test
|
||||
|
||||
notifications:
|
||||
|
|
109
AUTHORS
109
AUTHORS
|
@ -1,5 +1,6 @@
|
|||
Carl Worth <cworth@cworth.org> is the primary author of Notmuch.
|
||||
But there's really not much that he's done. There's been a lot of
|
||||
Carl Worth <cworth@cworth.org> was the original author of Notmuch.
|
||||
David Bremner has maintained Notmuch since release 0.6 (2011). But
|
||||
there's really not much that they've done. There's been a lot of
|
||||
standing on shoulders here:
|
||||
|
||||
William Morgan deserves credit for providing the primary inspiration
|
||||
|
@ -21,10 +22,108 @@ engine that does the really heavy lifting, as well as the various
|
|||
system libraries, compilers, and the kernel that make it all work
|
||||
(thanks GNU, thanks Linux). Thanks to everyone who has played a part!
|
||||
|
||||
The following list of people have at least 15 lines of code in the
|
||||
Notmuch 0.31 release (calculated by devel/author-scan.sh).
|
||||
|
||||
David Bremner
|
||||
Carl Worth
|
||||
Jani Nikula
|
||||
Austin Clements
|
||||
Daniel Kahn Gillmor
|
||||
Mark Walters
|
||||
Floris Bruynooghe
|
||||
David Edmondson
|
||||
Tomi Ollila
|
||||
Sebastian Spaeth
|
||||
Ali Polatel
|
||||
Michal Sojka
|
||||
Justus Winter
|
||||
Sebastien Binet
|
||||
W. Trevor King
|
||||
Jameson Graef Rollins
|
||||
Felipe Contreras
|
||||
Jonas Bernoulli
|
||||
Pieter Praet
|
||||
Peter Feigl
|
||||
Dmitry Kurochkin
|
||||
Peter Wang
|
||||
Gregor Zattler
|
||||
Daniel Schoepe
|
||||
Keith Packard
|
||||
Adam Wolfe Gordon
|
||||
Stefano Zacchiroli
|
||||
Vincent Breitmoser
|
||||
laochailan
|
||||
Ben Gamari
|
||||
Aaron Ecay
|
||||
l-m-h@web.de
|
||||
Thomas Jost
|
||||
Jesse Rosenthal
|
||||
Dirk Hohndel
|
||||
Blake Jones
|
||||
Damien Cassou
|
||||
Anton Khirnov
|
||||
Matt Armstrong
|
||||
Vladimir Panteleev
|
||||
William Casarin
|
||||
Örjan Ekeberg
|
||||
Jan Janak
|
||||
Patrick Totzke
|
||||
Ruben Pollan
|
||||
rhn
|
||||
Ioan-Adrian Ratiu
|
||||
Ethan Glasser-Camp
|
||||
Chunyang Xu
|
||||
Todd
|
||||
Chris Wilson
|
||||
Yuri Volchkov
|
||||
Cédric Cabessa
|
||||
Mark Anderson
|
||||
Jed Brown
|
||||
Maxime Coste
|
||||
Ludovic LANGE
|
||||
Sebastian Poeplau
|
||||
Mikhail
|
||||
Keith Amidon
|
||||
Gaute Hope
|
||||
martin f. krafft
|
||||
Jeffrey C. Ollie
|
||||
Jameson Rollins
|
||||
Scott Henson
|
||||
Bart Trojanowski
|
||||
Vladimir Marek
|
||||
Servilio Afre Puentes
|
||||
Tomas Carnecky
|
||||
Kevin McCarthy
|
||||
Kevin J. McCarthy
|
||||
Scott Robinson
|
||||
Wael M. Nasreddine
|
||||
Charles Celerier
|
||||
Olly Betts
|
||||
Istvan Marko
|
||||
Florian Klink
|
||||
Thibaut Horel
|
||||
Joel Borggrén-Franck
|
||||
Ingmar Vanhassel
|
||||
Olivier Taïbi
|
||||
Ian Main
|
||||
Alexander Botero-Lowry
|
||||
Luis Ressel
|
||||
Sergei Shilovsky
|
||||
Trevor Jim
|
||||
Uli Scholler
|
||||
Matthew Lear
|
||||
Jinwoo Lee
|
||||
Amadeusz Żołnowski
|
||||
|
||||
Here is an incomplete list of other people that have made
|
||||
contributions to Notmuch (whether by code, bug reporting/fixes,
|
||||
ideas, inspiration, testing or feedback):
|
||||
|
||||
Martin Krafft
|
||||
Keith Packard
|
||||
Jamey Sharp
|
||||
Martin Krafft
|
||||
Jamey Sharp
|
||||
|
||||
The Notmuch project acknowledges the contributions of the following
|
||||
organizations via their employees
|
||||
|
||||
Google LLC
|
||||
|
|
2
INSTALL
2
INSTALL
|
@ -95,7 +95,7 @@ dependencies with a single simple command line. For example:
|
|||
|
||||
For Fedora and similar:
|
||||
|
||||
sudo yum install xapian-core-devel gmime-devel libtalloc-devel zlib-devel python3-sphinx texinfo info
|
||||
sudo dnf install xapian-core-devel gmime30-devel libtalloc-devel zlib-devel python3-sphinx texinfo info
|
||||
|
||||
On other systems, a similar command can be used, but the details of
|
||||
the package names may be different.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- makefile-gmake -*-
|
||||
# Here's the (hopefully simple) versioning scheme.
|
||||
#
|
||||
# Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We
|
||||
|
@ -16,7 +17,7 @@ else
|
|||
DATE:=$(shell date +%F)
|
||||
endif
|
||||
|
||||
VERSION:=$(shell cat ${srcdir}/version)
|
||||
VERSION:=$(shell cat ${srcdir}/version.txt)
|
||||
ELPA_VERSION:=$(subst ~,_,$(VERSION))
|
||||
ifeq ($(filter release release-message pre-release update-versions,$(MAKECMDGOALS)),)
|
||||
ifeq ($(IS_GIT),yes)
|
||||
|
@ -39,6 +40,7 @@ DEB_TAG=debian/$(UPSTREAM_TAG)-1
|
|||
|
||||
RELEASE_HOST=notmuchmail.org
|
||||
RELEASE_DIR=/srv/notmuchmail.org/www/releases
|
||||
DOC_DIR=/srv/notmuchmail.org/www/doc/latest
|
||||
RELEASE_URL=https://notmuchmail.org/releases
|
||||
TAR_FILE=$(PACKAGE)-$(VERSION).tar.xz
|
||||
ELPA_FILE:=$(PACKAGE)-emacs-$(ELPA_VERSION).tar
|
||||
|
@ -49,8 +51,7 @@ DETACHED_SIG_FILE=$(TAR_FILE).asc
|
|||
PV_FILE=bindings/python/notmuch/version.py
|
||||
|
||||
# Smash together user's values with our extra values
|
||||
STD_CFLAGS := -std=gnu99
|
||||
FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(STD_CFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(extra_cflags) $(CONFIGURE_CFLAGS)
|
||||
FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(extra_cflags) $(CONFIGURE_CFLAGS)
|
||||
FINAL_CXXFLAGS = $(CPPFLAGS) $(CXXFLAGS) $(WARN_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) $(CONFIGURE_CXXFLAGS)
|
||||
FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Lutil -lnotmuch_util -Llib -lnotmuch
|
||||
ifeq ($(LIBDIR_IN_LDCONFIG),0)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- makefile -*-
|
||||
# -*- makefile-gmake -*-
|
||||
|
||||
.PHONY: all
|
||||
all: notmuch notmuch-shared build-man build-info ruby-bindings
|
||||
all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings
|
||||
ifeq ($(MAKECMDGOALS),)
|
||||
ifeq ($(shell cat .first-build-message 2>/dev/null),)
|
||||
@NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all
|
||||
|
@ -19,7 +19,7 @@ endif
|
|||
|
||||
# Depend (also) on the file 'version'. In case of ifeq ($(IS_GIT),yes)
|
||||
# this file may already have been updated.
|
||||
version.stamp: $(srcdir)/version
|
||||
version.stamp: $(srcdir)/version.txt
|
||||
echo $(VERSION) > $@
|
||||
|
||||
$(TAR_FILE):
|
||||
|
@ -30,12 +30,12 @@ $(TAR_FILE):
|
|||
echo "Warning: No signed tag for $(VERSION)"; \
|
||||
fi ; \
|
||||
git archive --format=tar --prefix=$(PACKAGE)-$(VERSION)/ $$ref > $(TAR_FILE).tmp
|
||||
echo $(VERSION) > version.tmp
|
||||
echo $(VERSION) > version.txt.tmp
|
||||
ct=`git --no-pager log -1 --pretty=format:%ct $$ref` ; \
|
||||
tar --owner root --group root --append -f $(TAR_FILE).tmp \
|
||||
--transform s_^_$(PACKAGE)-$(VERSION)/_ \
|
||||
--transform 's_.tmp$$__' --mtime=@$$ct version.tmp
|
||||
rm version.tmp
|
||||
--transform 's_.tmp$$__' --mtime=@$$ct version.txt.tmp
|
||||
rm version.txt.tmp
|
||||
xz -C sha256 -9 < $(TAR_FILE).tmp > $(TAR_FILE)
|
||||
@echo "Source is ready for release in $(TAR_FILE)"
|
||||
|
||||
|
@ -54,6 +54,7 @@ update-versions:
|
|||
sed -i -e "s/^__VERSION__[[:blank:]]*=.*$$/__VERSION__ = \'${VERSION}\'/" \
|
||||
-e "s/^SOVERSION[[:blank:]]*=.*$$/SOVERSION = \'${LIBNOTMUCH_VERSION_MAJOR}\'/" \
|
||||
${PV_FILE}
|
||||
cp version.txt bindings/python-cffi
|
||||
|
||||
# We invoke make recursively only to force ordering of our phony
|
||||
# targets in the case of parallel invocation of make (-j).
|
||||
|
@ -66,6 +67,7 @@ update-versions:
|
|||
release: verify-source-tree-and-version
|
||||
$(MAKE) VERSION=$(VERSION) verify-newer
|
||||
$(MAKE) VERSION=$(VERSION) clean
|
||||
$(MAKE) VERSION=$(VERSION) sphinx-html
|
||||
$(MAKE) VERSION=$(VERSION) test
|
||||
git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG)
|
||||
$(MAKE) VERSION=$(VERSION) $(SHA256_FILE) $(DETACHED_SIG_FILE)
|
||||
|
@ -79,6 +81,7 @@ ifeq ($(REALLY_UPLOAD),yes)
|
|||
git push origin $(VERSION) $(DEB_TAG) release pristine-tar
|
||||
cd releases && scp $(TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) $(RELEASE_HOST):$(RELEASE_DIR)
|
||||
ssh $(RELEASE_HOST) "rm -f $(RELEASE_DIR)/LATEST-$(PACKAGE)-* ; ln -s $(TAR_FILE) $(RELEASE_DIR)/LATEST-$(TAR_FILE)"
|
||||
rsync --verbose --delete --recursive doc/_build/html/ $(RELEASE_HOST):$(DOC_DIR)
|
||||
endif
|
||||
@echo "Please send a release announcement using $(PACKAGE)-$(VERSION).announce as a template."
|
||||
|
||||
|
@ -88,7 +91,7 @@ pre-release:
|
|||
$(MAKE) VERSION=$(VERSION) test
|
||||
git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG)
|
||||
git tag -s -m "$(PACKAGE) Debian $(VERSION)-1 upload (same as $(VERSION))" $(DEB_TAG)
|
||||
$(MAKE) VERSION=$(VERSION) $(TAR_FILE)
|
||||
$(MAKE) VERSION=$(VERSION) $(SHA256_FILE) $(DETACHED_SIG_FILE)
|
||||
ln -sf $(TAR_FILE) $(DEB_TAR_FILE)
|
||||
pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG)
|
||||
mkdir -p releases
|
||||
|
@ -97,14 +100,16 @@ pre-release:
|
|||
.PHONY: debian-snapshot
|
||||
debian-snapshot:
|
||||
make VERSION=$(VERSION) clean
|
||||
TMPFILE=$$(mktemp /tmp/notmuch.XXXXXX); \
|
||||
cp debian/changelog $${TMPFILE}; \
|
||||
EDITOR=/bin/true dch -b -v $(VERSION)+1 \
|
||||
-D UNRELEASED 'test build, not for upload'; \
|
||||
echo '3.0 (native)' > debian/source/format; \
|
||||
debuild -us -uc; \
|
||||
RETVAL=0 && \
|
||||
TMPFILE=$$(mktemp /tmp/notmuch.XXXXXX) && \
|
||||
cp debian/changelog $${TMPFILE} && \
|
||||
(EDITOR=/bin/true dch -b -v $(VERSION)+1 \
|
||||
-D UNRELEASED 'test build, not for upload' && \
|
||||
echo '3.0 (native)' > debian/source/format && \
|
||||
debuild -us -uc); RETVAL=$$? \
|
||||
mv -f $${TMPFILE} debian/changelog; \
|
||||
echo '3.0 (quilt)' > debian/source/format
|
||||
echo '3.0 (quilt)' > debian/source/format; \
|
||||
exit $$RETVAL
|
||||
|
||||
.PHONY: release-message
|
||||
release-message:
|
||||
|
@ -290,7 +295,7 @@ CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules)
|
|||
CLEAN := $(CLEAN) version.stamp notmuch-*.tar.gz.tmp
|
||||
CLEAN := $(CLEAN) .deps
|
||||
|
||||
DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config sh.config
|
||||
DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config sh.config sphinx.config
|
||||
|
||||
CPPCHECK_STAMPS := $(SRCS:%=.stamps/cppcheck/%)
|
||||
.PHONY: cppcheck
|
||||
|
|
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)
|
||||
===========================
|
||||
|
||||
|
@ -72,7 +223,7 @@ information about cryptographic protections for the Subject header.
|
|||
Emacs
|
||||
-----
|
||||
|
||||
Optionally check for missing attachements in outgoing messages (see
|
||||
Optionally check for missing attachments in outgoing messages (see
|
||||
function `notmuch-mua-attachment-check`).
|
||||
|
||||
Bind `B` to browse URLs in current message.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- makefile -*-
|
||||
# -*- makefile-gmake -*-
|
||||
|
||||
dir := bindings
|
||||
|
||||
|
@ -13,6 +13,13 @@ ifeq ($(HAVE_RUBY_DEV),1)
|
|||
$(MAKE) -C $(dir)/ruby
|
||||
endif
|
||||
|
||||
python-cffi-bindings: lib/$(LINKER_NAME)
|
||||
ifeq ($(HAVE_PYTHON3_CFFI),1)
|
||||
cd $(dir)/python-cffi && \
|
||||
${PYTHON} setup.py build --build-lib build/stage && \
|
||||
mkdir -p build/stage/tests && cp tests/*.py build/stage/tests
|
||||
endif
|
||||
|
||||
CLEAN += $(patsubst %,$(dir)/ruby/%, \
|
||||
.RUBYARCHDIR.time \
|
||||
Makefile database.o directory.o filenames.o\
|
||||
|
@ -20,3 +27,5 @@ CLEAN += $(patsubst %,$(dir)/ruby/%, \
|
|||
status.o tags.o thread.o threads.o)
|
||||
|
||||
CLEAN += bindings/ruby/.vendorarchdir.time
|
||||
|
||||
CLEAN += bindings/python-cffi/build
|
||||
|
|
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::
|
||||
|
||||
Any function in this class can and will throw an
|
||||
:exc:`NotInitializedError` if the database was not intitialized
|
||||
:exc:`NotInitializedError` if the database was not initialized
|
||||
properly.
|
||||
"""
|
||||
_std_db_path = None
|
||||
|
@ -273,9 +273,9 @@ class Database(object):
|
|||
return Database._get_version(self._db)
|
||||
|
||||
def get_revision (self):
|
||||
"""Returns the committed database revison and UUID
|
||||
"""Returns the committed database revision and UUID
|
||||
|
||||
:returns: (revison, uuid) The database revision as a positive integer
|
||||
:returns: (revision, uuid) The database revision as a positive integer
|
||||
and the UUID of the database.
|
||||
"""
|
||||
self._assert_db_is_initialized()
|
||||
|
@ -574,7 +574,7 @@ class Database(object):
|
|||
in the meantime. In this case, you should close and
|
||||
reopen the database and retry.
|
||||
:exc:`NotInitializedError` if
|
||||
the database was not intitialized.
|
||||
the database was not initialized.
|
||||
"""
|
||||
self._assert_db_is_initialized()
|
||||
msg_p = NotmuchMessageP()
|
||||
|
@ -600,7 +600,7 @@ class Database(object):
|
|||
case, you should close and reopen the database and
|
||||
retry.
|
||||
:raises: :exc:`NotInitializedError` if the database was not
|
||||
intitialized.
|
||||
initialized.
|
||||
|
||||
*Added in notmuch 0.9*"""
|
||||
self._assert_db_is_initialized()
|
||||
|
@ -616,7 +616,7 @@ class Database(object):
|
|||
"""Returns :class:`Tags` with a list of all tags found in the database
|
||||
|
||||
:returns: :class:`Tags`
|
||||
:execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
|
||||
:exception: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
|
||||
on error
|
||||
"""
|
||||
self._assert_db_is_initialized()
|
||||
|
|
|
@ -46,7 +46,7 @@ import sys
|
|||
|
||||
|
||||
class Message(Python3StringMixIn):
|
||||
"""Represents a single Email message
|
||||
r"""Represents a single Email message
|
||||
|
||||
Technically, this wraps the underlying *notmuch_message_t*
|
||||
structure. A user will usually not create these objects themselves
|
||||
|
|
|
@ -32,7 +32,7 @@ from .tag import Tags
|
|||
from .message import Message
|
||||
|
||||
class Messages(object):
|
||||
"""Represents a list of notmuch messages
|
||||
r"""Represents a list of notmuch messages
|
||||
|
||||
This object provides an iterator over a list of notmuch messages
|
||||
(Technically, it provides a wrapper for the underlying
|
||||
|
|
|
@ -95,7 +95,7 @@ class Query(object):
|
|||
:exc:`NullPointerError` if the query creation failed
|
||||
(e.g. too little memory).
|
||||
:exc:`NotInitializedError` if the underlying db was not
|
||||
intitialized.
|
||||
initialized.
|
||||
"""
|
||||
db._assert_db_is_initialized()
|
||||
# create reference to parent db to keep it alive
|
||||
|
@ -140,7 +140,7 @@ class Query(object):
|
|||
_search_threads.restype = c_uint
|
||||
|
||||
def search_threads(self):
|
||||
"""Execute a query for threads
|
||||
r"""Execute a query for threads
|
||||
|
||||
Execute a query for threads, returning a :class:`Threads` iterator.
|
||||
The returned threads are owned by the query and as such, will only be
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# this file should be kept in sync with ../../../version
|
||||
__VERSION__ = '0.29.3'
|
||||
__VERSION__ = '0.31.2'
|
||||
SOVERSION = '5'
|
||||
|
|
|
@ -137,13 +137,18 @@ VALUE
|
|||
notmuch_rb_message_get_flag (VALUE self, VALUE flagv)
|
||||
{
|
||||
notmuch_message_t *message;
|
||||
notmuch_bool_t is_set;
|
||||
notmuch_status_t status;
|
||||
|
||||
Data_Get_Notmuch_Message (self, message);
|
||||
|
||||
if (!FIXNUM_P (flagv))
|
||||
rb_raise (rb_eTypeError, "Flag not a Fixnum");
|
||||
|
||||
return notmuch_message_get_flag (message, FIX2INT (flagv)) ? Qtrue : Qfalse;
|
||||
status = notmuch_message_get_flag_st (message, FIX2INT (flagv), &is_set);
|
||||
notmuch_rb_status_raise (status);
|
||||
|
||||
return is_set ? Qtrue : Qfalse;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -11,10 +11,10 @@ typedef enum {
|
|||
} opt_handled;
|
||||
|
||||
/*
|
||||
Search the array of keywords for a given argument, assigning the
|
||||
output variable to the corresponding value. Return false if nothing
|
||||
matches.
|
||||
*/
|
||||
* Search the array of keywords for a given argument, assigning the
|
||||
* output variable to the corresponding value. Return false if nothing
|
||||
* matches.
|
||||
*/
|
||||
|
||||
static opt_handled
|
||||
_process_keyword_arg (const notmuch_opt_desc_t *arg_desc, char next,
|
||||
|
@ -78,15 +78,17 @@ _process_boolean_arg (const notmuch_opt_desc_t *arg_desc, char next,
|
|||
return OPT_FAILED;
|
||||
}
|
||||
|
||||
*arg_desc->opt_bool = negate ? !value : value;
|
||||
*arg_desc->opt_bool = negate ? (! value) : value;
|
||||
|
||||
return OPT_OK;
|
||||
}
|
||||
|
||||
static opt_handled
|
||||
_process_int_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str) {
|
||||
_process_int_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str)
|
||||
{
|
||||
|
||||
char *endptr;
|
||||
|
||||
if (next == '\0' || arg_str[0] == '\0') {
|
||||
fprintf (stderr, "Option \"%s\" needs an integer argument.\n", arg_desc->name);
|
||||
return OPT_FAILED;
|
||||
|
@ -102,7 +104,8 @@ _process_int_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg
|
|||
}
|
||||
|
||||
static opt_handled
|
||||
_process_string_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str) {
|
||||
_process_string_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str)
|
||||
{
|
||||
|
||||
if (next == '\0') {
|
||||
fprintf (stderr, "Option \"%s\" needs a string argument.\n", arg_desc->name);
|
||||
|
@ -117,20 +120,22 @@ _process_string_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *
|
|||
}
|
||||
|
||||
/* Return number of non-NULL opt_* fields in opt_desc. */
|
||||
static int _opt_set_count (const notmuch_opt_desc_t *opt_desc)
|
||||
static int
|
||||
_opt_set_count (const notmuch_opt_desc_t *opt_desc)
|
||||
{
|
||||
return
|
||||
!!opt_desc->opt_inherit +
|
||||
!!opt_desc->opt_bool +
|
||||
!!opt_desc->opt_int +
|
||||
!!opt_desc->opt_keyword +
|
||||
!!opt_desc->opt_flags +
|
||||
!!opt_desc->opt_string +
|
||||
!!opt_desc->opt_position;
|
||||
(bool) opt_desc->opt_inherit +
|
||||
(bool) opt_desc->opt_bool +
|
||||
(bool) opt_desc->opt_int +
|
||||
(bool) opt_desc->opt_keyword +
|
||||
(bool) opt_desc->opt_flags +
|
||||
(bool) opt_desc->opt_string +
|
||||
(bool) opt_desc->opt_position;
|
||||
}
|
||||
|
||||
/* Return true if opt_desc is valid. */
|
||||
static bool _opt_valid (const notmuch_opt_desc_t *opt_desc)
|
||||
static bool
|
||||
_opt_valid (const notmuch_opt_desc_t *opt_desc)
|
||||
{
|
||||
int n = _opt_set_count (opt_desc);
|
||||
|
||||
|
@ -142,15 +147,17 @@ static bool _opt_valid (const notmuch_opt_desc_t *opt_desc)
|
|||
}
|
||||
|
||||
/*
|
||||
Search for the {pos_arg_index}th position argument, return false if
|
||||
that does not exist.
|
||||
*/
|
||||
* Search for the {pos_arg_index}th position argument, return false if
|
||||
* that does not exist.
|
||||
*/
|
||||
|
||||
bool
|
||||
parse_position_arg (const char *arg_str, int pos_arg_index,
|
||||
const notmuch_opt_desc_t *arg_desc) {
|
||||
const notmuch_opt_desc_t *arg_desc)
|
||||
{
|
||||
|
||||
int pos_arg_counter = 0;
|
||||
|
||||
while (_opt_valid (arg_desc)) {
|
||||
if (arg_desc->opt_position) {
|
||||
if (pos_arg_counter == pos_arg_index) {
|
||||
|
@ -176,12 +183,12 @@ parse_position_arg (const char *arg_str, int pos_arg_index,
|
|||
int
|
||||
parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index)
|
||||
{
|
||||
assert(argv);
|
||||
assert (argv);
|
||||
|
||||
const char *_arg = argv[opt_index];
|
||||
|
||||
assert(_arg);
|
||||
assert(options);
|
||||
assert (_arg);
|
||||
assert (options);
|
||||
|
||||
const char *arg = _arg + 2; /* _arg starts with -- */
|
||||
const char *negative_arg = NULL;
|
||||
|
@ -239,7 +246,7 @@ parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_
|
|||
if (lookahead) {
|
||||
next = ' ';
|
||||
value = next_arg;
|
||||
opt_index ++;
|
||||
opt_index++;
|
||||
}
|
||||
|
||||
opt_handled opt_status = OPT_FAILED;
|
||||
|
@ -258,12 +265,12 @@ parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_
|
|||
return -1;
|
||||
|
||||
if (lookahead && opt_status == OPT_GIVEBACK)
|
||||
opt_index --;
|
||||
opt_index--;
|
||||
|
||||
if (try->present)
|
||||
*try->present = true;
|
||||
|
||||
return opt_index+1;
|
||||
return opt_index + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
@ -271,13 +278,14 @@ parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_
|
|||
/* See command-line-arguments.h for description */
|
||||
int
|
||||
parse_arguments (int argc, char **argv,
|
||||
const notmuch_opt_desc_t *options, int opt_index) {
|
||||
const notmuch_opt_desc_t *options, int opt_index)
|
||||
{
|
||||
|
||||
int pos_arg_index = 0;
|
||||
bool more_args = true;
|
||||
|
||||
while (more_args && opt_index < argc) {
|
||||
if (strncmp (argv[opt_index],"--",2) != 0) {
|
||||
if (strncmp (argv[opt_index], "--", 2) != 0) {
|
||||
|
||||
more_args = parse_position_arg (argv[opt_index], pos_arg_index, options);
|
||||
|
||||
|
@ -290,7 +298,7 @@ parse_arguments (int argc, char **argv,
|
|||
int prev_opt_index = opt_index;
|
||||
|
||||
if (strlen (argv[opt_index]) == 2)
|
||||
return opt_index+1;
|
||||
return opt_index + 1;
|
||||
|
||||
opt_index = parse_option (argc, argv, options, opt_index);
|
||||
if (opt_index < 0) {
|
||||
|
|
|
@ -45,20 +45,20 @@ typedef struct notmuch_opt_desc {
|
|||
|
||||
|
||||
/*
|
||||
This is the main entry point for command line argument parsing.
|
||||
|
||||
Parse command line arguments according to structure options,
|
||||
starting at position opt_index.
|
||||
|
||||
All output of parsed values is via pointers in options.
|
||||
|
||||
Parsing stops at -- (consumed) or at the (k+1)st argument
|
||||
not starting with -- (a "positional argument") if options contains
|
||||
k positional argument descriptors.
|
||||
|
||||
Returns the index of first non-parsed argument, or -1 in case of error.
|
||||
|
||||
*/
|
||||
* This is the main entry point for command line argument parsing.
|
||||
*
|
||||
* Parse command line arguments according to structure options,
|
||||
* starting at position opt_index.
|
||||
*
|
||||
* All output of parsed values is via pointers in options.
|
||||
*
|
||||
* Parsing stops at -- (consumed) or at the (k+1)st argument
|
||||
* not starting with -- (a "positional argument") if options contains
|
||||
* k positional argument descriptors.
|
||||
*
|
||||
* Returns the index of first non-parsed argument, or -1 in case of error.
|
||||
*
|
||||
*/
|
||||
int
|
||||
parse_arguments (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index);
|
||||
|
||||
|
@ -71,12 +71,12 @@ parse_arguments (int argc, char **argv, const notmuch_opt_desc_t *options, int o
|
|||
*/
|
||||
|
||||
int
|
||||
parse_option (int argc, char **argv, const notmuch_opt_desc_t* options, int opt_index);
|
||||
parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index);
|
||||
|
||||
bool
|
||||
parse_position_arg (const char *arg,
|
||||
int position_arg_index,
|
||||
const notmuch_opt_desc_t* options);
|
||||
const notmuch_opt_desc_t *options);
|
||||
|
||||
|
||||
#endif
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- makefile -*-
|
||||
# -*- makefile-gmake -*-
|
||||
|
||||
dir := compat
|
||||
extra_cflags += -I$(srcdir)/$(dir)
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
#include <stdlib.h>
|
||||
|
||||
char *
|
||||
canonicalize_file_name (const char * path)
|
||||
canonicalize_file_name (const char *path)
|
||||
{
|
||||
#ifdef PATH_MAX
|
||||
char *resolved_path = malloc (PATH_MAX+1);
|
||||
char *resolved_path = malloc (PATH_MAX + 1);
|
||||
if (resolved_path == NULL)
|
||||
return NULL;
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#include <time.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main()
|
||||
int
|
||||
main ()
|
||||
{
|
||||
struct tm tm;
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#include <stdio.h>
|
||||
#include <pwd.h>
|
||||
|
||||
int main()
|
||||
int
|
||||
main ()
|
||||
{
|
||||
struct passwd passwd, *ignored;
|
||||
|
||||
|
|
|
@ -30,14 +30,14 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if !STD_GETPWUID
|
||||
#if ! STD_GETPWUID
|
||||
#define _POSIX_PTHREAD_SEMANTICS 1
|
||||
#endif
|
||||
#if !STD_ASCTIME
|
||||
#if ! STD_ASCTIME
|
||||
#define _POSIX_PTHREAD_SEMANTICS 1
|
||||
#endif
|
||||
|
||||
#if !HAVE_CANONICALIZE_FILE_NAME
|
||||
#if ! HAVE_CANONICALIZE_FILE_NAME
|
||||
/* we only call this function from C, and this makes testing easier */
|
||||
#ifndef __cplusplus
|
||||
char *
|
||||
|
@ -45,7 +45,7 @@ canonicalize_file_name (const char *path);
|
|||
#endif
|
||||
#endif
|
||||
|
||||
#if !HAVE_GETLINE
|
||||
#if ! HAVE_GETLINE
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
|
@ -57,15 +57,15 @@ getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp);
|
|||
|
||||
#endif /* !HAVE_GETLINE */
|
||||
|
||||
#if !HAVE_STRCASESTR
|
||||
char* strcasestr(const char *haystack, const char *needle);
|
||||
#if ! HAVE_STRCASESTR
|
||||
char *strcasestr (const char *haystack, const char *needle);
|
||||
#endif /* !HAVE_STRCASESTR */
|
||||
|
||||
#if !HAVE_STRSEP
|
||||
char *strsep(char **stringp, const char *delim);
|
||||
#if ! HAVE_STRSEP
|
||||
char *strsep (char **stringp, const char *delim);
|
||||
#endif /* !HAVE_STRSEP */
|
||||
|
||||
#if !HAVE_TIMEGM
|
||||
#if ! HAVE_TIMEGM
|
||||
#include <time.h>
|
||||
time_t timegm (struct tm *tm);
|
||||
#endif /* !HAVE_TIMEGM */
|
||||
|
@ -73,7 +73,7 @@ time_t timegm (struct tm *tm);
|
|||
/* Silence gcc warnings about unused results. These warnings exist
|
||||
* for a reason; any use of this needs to be justified. */
|
||||
#ifdef __GNUC__
|
||||
#define IGNORE_RESULT(x) ({ __typeof__(x) __z = (x); (void)(__z = __z); })
|
||||
#define IGNORE_RESULT(x) ({ __typeof__(x) __z = (x); (void) (__z = __z); })
|
||||
#else /* !__GNUC__ */
|
||||
#define IGNORE_RESULT(x) x
|
||||
#endif /* __GNUC__ */
|
||||
|
|
|
@ -11,8 +11,9 @@ static const char *template =
|
|||
"Version: %s\n"
|
||||
"Libs: -lz\n";
|
||||
|
||||
int main(void)
|
||||
int
|
||||
main (void)
|
||||
{
|
||||
printf(template, ZLIB_VERSION);
|
||||
printf (template, ZLIB_VERSION);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
/* getdelim.c --- Implementation of replacement getdelim function.
|
||||
Copyright (C) 1994, 1996, 1997, 1998, 2001, 2003, 2005, 2006, 2007,
|
||||
2008, 2009 Free Software Foundation, Inc.
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as
|
||||
published by the Free Software Foundation; either version 3, or (at
|
||||
your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
02110-1301, USA. */
|
||||
* Copyright (C) 1994, 1996, 1997, 1998, 2001, 2003, 2005, 2006, 2007,
|
||||
* 2008, 2009 Free Software Foundation, Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 3, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA. */
|
||||
|
||||
/* Ported from glibc by Simon Josefsson. */
|
||||
|
||||
|
@ -34,22 +34,22 @@
|
|||
|
||||
#if USE_UNLOCKED_IO
|
||||
# include "unlocked-io.h"
|
||||
# define getc_maybe_unlocked(fp) getc(fp)
|
||||
#elif !HAVE_FLOCKFILE || !HAVE_FUNLOCKFILE || !HAVE_DECL_GETC_UNLOCKED
|
||||
# define getc_maybe_unlocked(fp) getc (fp)
|
||||
#elif ! HAVE_FLOCKFILE || ! HAVE_FUNLOCKFILE || ! HAVE_DECL_GETC_UNLOCKED
|
||||
# undef flockfile
|
||||
# undef funlockfile
|
||||
# define flockfile(x) ((void) 0)
|
||||
# define funlockfile(x) ((void) 0)
|
||||
# define getc_maybe_unlocked(fp) getc(fp)
|
||||
# define getc_maybe_unlocked(fp) getc (fp)
|
||||
#else
|
||||
# define getc_maybe_unlocked(fp) getc_unlocked(fp)
|
||||
# define getc_maybe_unlocked(fp) getc_unlocked (fp)
|
||||
#endif
|
||||
|
||||
/* Read up to (and including) a DELIMITER from FP into *LINEPTR (and
|
||||
NUL-terminate it). *LINEPTR is a pointer returned from malloc (or
|
||||
NULL), pointing to *N characters of space. It is realloc'ed as
|
||||
necessary. Returns the number of characters read (not including
|
||||
the null terminator), or -1 on error or EOF. */
|
||||
* NUL-terminate it). *LINEPTR is a pointer returned from malloc (or
|
||||
* NULL), pointing to *N characters of space. It is realloc'ed as
|
||||
* necessary. Returns the number of characters read (not including
|
||||
* the null terminator), or -1 on error or EOF. */
|
||||
|
||||
ssize_t
|
||||
getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp)
|
||||
|
@ -57,41 +57,35 @@ getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp)
|
|||
ssize_t result = -1;
|
||||
size_t cur_len = 0;
|
||||
|
||||
if (lineptr == NULL || n == NULL || fp == NULL)
|
||||
{
|
||||
if (lineptr == NULL || n == NULL || fp == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
flockfile (fp);
|
||||
|
||||
if (*lineptr == NULL || *n == 0)
|
||||
{
|
||||
if (*lineptr == NULL || *n == 0) {
|
||||
char *new_lineptr;
|
||||
*n = 120;
|
||||
new_lineptr = (char *) realloc (*lineptr, *n);
|
||||
if (new_lineptr == NULL)
|
||||
{
|
||||
if (new_lineptr == NULL) {
|
||||
result = -1;
|
||||
goto unlock_return;
|
||||
}
|
||||
*lineptr = new_lineptr;
|
||||
}
|
||||
|
||||
for (;;)
|
||||
{
|
||||
for (;;) {
|
||||
int i;
|
||||
|
||||
i = getc_maybe_unlocked (fp);
|
||||
if (i == EOF)
|
||||
{
|
||||
if (i == EOF) {
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Make enough space for len+1 (for final NUL) bytes. */
|
||||
if (cur_len + 1 >= *n)
|
||||
{
|
||||
if (cur_len + 1 >= *n) {
|
||||
size_t needed_max =
|
||||
SSIZE_MAX < SIZE_MAX ? (size_t) SSIZE_MAX + 1 : SIZE_MAX;
|
||||
size_t needed = 2 * *n + 1; /* Be generous. */
|
||||
|
@ -99,16 +93,14 @@ getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp)
|
|||
|
||||
if (needed_max < needed)
|
||||
needed = needed_max;
|
||||
if (cur_len + 1 >= needed)
|
||||
{
|
||||
if (cur_len + 1 >= needed) {
|
||||
result = -1;
|
||||
errno = EOVERFLOW;
|
||||
goto unlock_return;
|
||||
}
|
||||
|
||||
new_lineptr = (char *) realloc (*lineptr, needed);
|
||||
if (new_lineptr == NULL)
|
||||
{
|
||||
if (new_lineptr == NULL) {
|
||||
result = -1;
|
||||
goto unlock_return;
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
/* getline.c --- Implementation of replacement getline function.
|
||||
Copyright (C) 2005, 2006, 2007 Free Software Foundation, Inc.
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as
|
||||
published by the Free Software Foundation; either version 3, or (at
|
||||
your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
02110-1301, USA. */
|
||||
* Copyright (C) 2005, 2006, 2007 Free Software Foundation, Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 3, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA. */
|
||||
|
||||
/* Written by Simon Josefsson. */
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <stdlib.h>
|
||||
|
||||
int main()
|
||||
int
|
||||
main ()
|
||||
{
|
||||
char *found;
|
||||
char *string;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include <dirent.h>
|
||||
|
||||
int main()
|
||||
int
|
||||
main ()
|
||||
{
|
||||
struct dirent ent;
|
||||
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
int main()
|
||||
int
|
||||
main ()
|
||||
{
|
||||
ssize_t count = 0;
|
||||
size_t n = 0;
|
||||
char **lineptr = NULL;
|
||||
FILE *stream = NULL;
|
||||
|
||||
count = getline(lineptr, &n, stream);
|
||||
count = getline (lineptr, &n, stream);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <strings.h>
|
||||
|
||||
int main()
|
||||
int
|
||||
main ()
|
||||
{
|
||||
char *found;
|
||||
const char *haystack, *needle;
|
||||
|
||||
found = strcasestr(haystack, needle);
|
||||
found = strcasestr (haystack, needle);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <string.h>
|
||||
|
||||
int main()
|
||||
int
|
||||
main ()
|
||||
{
|
||||
char *found;
|
||||
char **stringp;
|
||||
const char *delim;
|
||||
|
||||
found = strsep(stringp, delim);
|
||||
found = strsep (stringp, delim);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#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
|
||||
*
|
||||
* based on a GPL implementation in OpenTTD found under GPL v2
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as
|
||||
published by the Free Software Foundation, version 2.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
02110-1301, USA. */
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, version 2.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA. */
|
||||
|
||||
/* Imported into notmuch by Dirk Hohndel - original author unknown. */
|
||||
|
||||
|
@ -24,12 +24,14 @@
|
|||
|
||||
#include "compat.h"
|
||||
|
||||
char *strcasestr(const char *haystack, const char *needle)
|
||||
char *
|
||||
strcasestr (const char *haystack, const char *needle)
|
||||
{
|
||||
size_t hay_len = strlen(haystack);
|
||||
size_t needle_len = strlen(needle);
|
||||
size_t hay_len = strlen (haystack);
|
||||
size_t needle_len = strlen (needle);
|
||||
|
||||
while (hay_len >= needle_len) {
|
||||
if (strncasecmp(haystack, needle, needle_len) == 0)
|
||||
if (strncasecmp (haystack, needle, needle_len) == 0)
|
||||
return (char *) haystack;
|
||||
|
||||
haystack++;
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
/* Copyright (C) 1992, 93, 96, 97, 98, 99, 2004 Free Software Foundation, Inc.
|
||||
This file is part of the GNU C Library.
|
||||
|
||||
The GNU C Library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
The GNU C Library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with the GNU C Library; if not, write to the Free
|
||||
Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
02111-1307 USA. */
|
||||
* This file is part of the GNU C Library.
|
||||
*
|
||||
* The GNU C Library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* The GNU C Library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with the GNU C Library; if not, write to the Free
|
||||
* Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
||||
* 02111-1307 USA. */
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* Taken from glibc 2.6.1 */
|
||||
|
||||
char *strsep (char **stringp, const char *delim)
|
||||
char *
|
||||
strsep (char **stringp, const char *delim)
|
||||
{
|
||||
char *begin, *end;
|
||||
|
||||
|
@ -29,16 +30,14 @@ char *strsep (char **stringp, const char *delim)
|
|||
return NULL;
|
||||
|
||||
/* A frequent case is when the delimiter string contains only one
|
||||
character. Here we don't need to call the expensive `strpbrk'
|
||||
function and instead work using `strchr'. */
|
||||
if (delim[0] == '\0' || delim[1] == '\0')
|
||||
{
|
||||
* character. Here we don't need to call the expensive `strpbrk'
|
||||
* function and instead work using `strchr'. */
|
||||
if (delim[0] == '\0' || delim[1] == '\0') {
|
||||
char ch = delim[0];
|
||||
|
||||
if (ch == '\0')
|
||||
end = NULL;
|
||||
else
|
||||
{
|
||||
else {
|
||||
if (*begin == ch)
|
||||
end = begin;
|
||||
else if (*begin == '\0')
|
||||
|
@ -46,18 +45,15 @@ char *strsep (char **stringp, const char *delim)
|
|||
else
|
||||
end = strchr (begin + 1, ch);
|
||||
}
|
||||
}
|
||||
else
|
||||
} else
|
||||
/* Find the end of the token. */
|
||||
end = strpbrk (begin, delim);
|
||||
|
||||
if (end)
|
||||
{
|
||||
if (end) {
|
||||
/* Terminate the token and set *STRINGP past NUL character. */
|
||||
*end++ = '\0';
|
||||
*stringp = end;
|
||||
}
|
||||
else
|
||||
} else
|
||||
/* No more delimiters; this is the last token. */
|
||||
*stringp = NULL;
|
||||
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
/* timegm.c --- Implementation of replacement timegm function.
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as
|
||||
published by the Free Software Foundation; either version 3, or (at
|
||||
your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
02110-1301, USA. */
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 3, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA. */
|
||||
|
||||
/* Copyright 2013 Blake Jones. */
|
||||
|
||||
|
@ -43,12 +43,12 @@ timegm (struct tm *tm)
|
|||
|
||||
days = 365 * (tm->tm_year - 70);
|
||||
for (year = 70; year < tm->tm_year; year++) {
|
||||
if (leapyear(1900 + year)) {
|
||||
if (leapyear (1900 + year)) {
|
||||
days++;
|
||||
}
|
||||
}
|
||||
for (month = 0; month < tm->tm_mon; month++) {
|
||||
days += monthlen[leapyear(1900 + year)][month];
|
||||
days += monthlen[leapyear (1900 + year)][month];
|
||||
}
|
||||
days += tm->tm_mday - 1;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- makefile -*-
|
||||
# -*- makefile-gmake -*-
|
||||
|
||||
dir := completion
|
||||
|
||||
|
|
412
configure
vendored
412
configure
vendored
|
@ -26,8 +26,29 @@ readonly DEFAULT_IFS="$IFS"
|
|||
srcdir=$(dirname "$0")
|
||||
NOTMUCH_SRCDIR=$(cd "$srcdir" && pwd)
|
||||
|
||||
case $NOTMUCH_SRCDIR in ( *\'* | *['\"`$']* )
|
||||
echo "Definitely unsafe characters in source path '$NOTMUCH_SRCDIR'".
|
||||
exit 1
|
||||
esac
|
||||
|
||||
case $PWD in ( *\'* | *['\"`$']* )
|
||||
echo "Definitely unsafe characters in current directory '$PWD'".
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# In case of whitespace, builds may work, tests definitely will not.
|
||||
case $NOTMUCH_SRCDIR in ( *["$IFS"]* )
|
||||
echo "Whitespace in source path '$NOTMUCH_SRCDIR' not supported".
|
||||
exit 1
|
||||
esac
|
||||
|
||||
case $PWD in ( *["$IFS"]* )
|
||||
echo "Whitespace in current directory '$PWD' not supported".
|
||||
exit 1
|
||||
esac
|
||||
|
||||
subdirs="util compat lib parse-time-string completion doc emacs"
|
||||
subdirs="${subdirs} performance-test test test/test-databases"
|
||||
subdirs="${subdirs} performance-test test"
|
||||
subdirs="${subdirs} bindings"
|
||||
|
||||
# For a non-srcdir configure invocation (such as ../configure), create
|
||||
|
@ -49,6 +70,14 @@ if [ "$srcdir" != "." ]; then
|
|||
mkdir bindings/ruby
|
||||
cp -a "$srcdir"/bindings/ruby/*.[ch] bindings/ruby
|
||||
cp -a "$srcdir"/bindings/ruby/extconf.rb bindings/ruby
|
||||
|
||||
# Use the same hack to replicate python-cffi source for
|
||||
# out-of-tree builds (again, not ideal).
|
||||
mkdir bindings/python-cffi
|
||||
cp -a "$srcdir"/bindings/python-cffi/tests \
|
||||
"$srcdir"/bindings/python-cffi/notmuch2 \
|
||||
"$srcdir"/bindings/python-cffi/setup.py \
|
||||
bindings/python-cffi/
|
||||
fi
|
||||
|
||||
# Set several defaults (optionally specified by the user in
|
||||
|
@ -79,6 +108,7 @@ PREFIX=/usr/local
|
|||
LIBDIR=
|
||||
WITH_DOCS=1
|
||||
WITH_API_DOCS=1
|
||||
WITH_PYTHON_DOCS=1
|
||||
WITH_EMACS=1
|
||||
WITH_DESKTOP=1
|
||||
WITH_BASH=1
|
||||
|
@ -147,7 +177,7 @@ Fine tuning of some installation directories is available:
|
|||
--emacslispdir=DIR Emacs code [PREFIX/share/emacs/site-lisp]
|
||||
--emacsetcdir=DIR Emacs miscellaneous files [PREFIX/share/emacs/site-lisp]
|
||||
--bashcompletiondir=DIR Bash completions files [PREFIX/share/bash-completion/completions]
|
||||
--zshcompletiondir=DIR Zsh completions files [PREFIX/share/zsh/functions/Completion/Unix]
|
||||
--zshcompletiondir=DIR Zsh completions files [PREFIX/share/zsh/site-functions]
|
||||
|
||||
Some features can be disabled (--with-feature=no is equivalent to
|
||||
--without-feature) :
|
||||
|
@ -401,15 +431,22 @@ else
|
|||
have_pkg_config=0
|
||||
fi
|
||||
|
||||
printf "Checking for Xapian development files... "
|
||||
|
||||
|
||||
printf "Checking for Xapian development files (>= 1.4.0)... "
|
||||
have_xapian=0
|
||||
for xapian_config in ${XAPIAN_CONFIG} xapian-config xapian-config-1.3; do
|
||||
for xapian_config in ${XAPIAN_CONFIG} xapian-config; do
|
||||
if ${xapian_config} --version > /dev/null 2>&1; then
|
||||
xapian_version=$(${xapian_config} --version | sed -e 's/.* //')
|
||||
case $xapian_version in
|
||||
1.[4-9]* | 1.[1-9][0-9]* | [2-9]* | [1-9][0-9]*)
|
||||
printf "Yes (%s).\n" ${xapian_version}
|
||||
have_xapian=1
|
||||
xapian_cxxflags=$(${xapian_config} --cxxflags)
|
||||
xapian_ldflags=$(${xapian_config} --libs)
|
||||
;;
|
||||
*) printf "Xapian $xapian_version not supported... "
|
||||
esac
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
@ -418,81 +455,10 @@ if [ ${have_xapian} = "0" ]; then
|
|||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
have_xapian_compact=0
|
||||
have_xapian_field_processor=0
|
||||
if [ ${have_xapian} = "1" ]; then
|
||||
printf "Checking for Xapian compaction support... "
|
||||
cat>_compact.cc<<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
|
||||
|
||||
printf "Checking for GMime development files... "
|
||||
if pkg-config --exists "gmime-3.0 > $GMIME_MINVER"; then
|
||||
printf "Checking for GMime development files (>= $GMIME_MINVER)... "
|
||||
if pkg-config --exists "gmime-3.0 >= $GMIME_MINVER"; then
|
||||
printf "Yes.\n"
|
||||
have_gmime=1
|
||||
gmime_cflags=$(pkg-config --cflags gmime-3.0)
|
||||
|
@ -513,7 +479,7 @@ int main () {
|
|||
|
||||
g_mime_init ();
|
||||
parser = g_mime_parser_new ();
|
||||
g_mime_parser_init_with_stream (parser, g_mime_stream_file_open("test/corpora/crypto/basic-encrypted.eml", "r", &error));
|
||||
g_mime_parser_init_with_stream (parser, g_mime_stream_file_open("$srcdir/test/corpora/crypto/basic-encrypted.eml", "r", &error));
|
||||
if (error) return !! fprintf (stderr, "failed to instantiate parser with test/corpora/crypto/basic-encrypted.eml\n");
|
||||
|
||||
body = GMIME_MULTIPART_ENCRYPTED(g_mime_message_get_mime_part (g_mime_parser_construct_message (parser, NULL)));
|
||||
|
@ -533,7 +499,7 @@ EOF
|
|||
printf 'No.\nCould not make tempdir for testing session-key support.\n'
|
||||
errors=$((errors + 1))
|
||||
elif ${CC} ${CFLAGS} ${gmime_cflags} _check_session_keys.c ${gmime_ldflags} -o _check_session_keys \
|
||||
&& GNUPGHOME=${TEMP_GPG} gpg --batch --quiet --import < test/gnupg-secret-key.asc \
|
||||
&& GNUPGHOME=${TEMP_GPG} gpg --batch --quiet --import < "$srcdir"/test/gnupg-secret-key.asc \
|
||||
&& SESSION_KEY=$(GNUPGHOME=${TEMP_GPG} ./_check_session_keys) \
|
||||
&& [ $SESSION_KEY = 9:0BACD64099D1468AB07C796F0C0AC4851948A658A15B34E803865E9FC635F2F5 ]
|
||||
then
|
||||
|
@ -559,6 +525,154 @@ EOF
|
|||
if [ -n "$TEMP_GPG" -a -d "$TEMP_GPG" ]; then
|
||||
rm -rf "$TEMP_GPG"
|
||||
fi
|
||||
|
||||
# see https://github.com/jstedfast/gmime/pull/90
|
||||
# should be fixed in GMime in 3.2.7, but some distros might patch
|
||||
printf "Checking for GMime X.509 certificate validity... "
|
||||
|
||||
cat > _check_x509_validity.c <<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
|
||||
have_gmime=0
|
||||
printf "No.\n"
|
||||
|
@ -583,7 +697,7 @@ fi
|
|||
if ! pkg-config --exists zlib; then
|
||||
${CC} -o compat/gen_zlib_pc "$srcdir"/compat/gen_zlib_pc.c >/dev/null 2>&1 &&
|
||||
compat/gen_zlib_pc > compat/zlib.pc &&
|
||||
PKG_CONFIG_PATH="$PKG_CONFIG_PATH":compat &&
|
||||
PKG_CONFIG_PATH=${PKG_CONFIG_PATH:+$PKG_CONFIG_PATH:}compat &&
|
||||
export PKG_CONFIG_PATH
|
||||
rm -f compat/gen_zlib_pc
|
||||
fi
|
||||
|
@ -650,6 +764,43 @@ if [ $have_python -eq 0 ]; then
|
|||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
have_python3=0
|
||||
if [ $have_python -eq 1 ]; then
|
||||
printf "Checking for python3 (>= 3.5)..."
|
||||
if "$python" -c 'import sys, sysconfig; assert sys.version_info >= (3,5)'; >/dev/null 2>&1; then
|
||||
printf "Yes.\n"
|
||||
have_python3=1
|
||||
else
|
||||
printf "No (will not install CFFI-based python bindings).\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
have_python3_cffi=0
|
||||
have_python3_pytest=0
|
||||
if [ $have_python3 -eq 1 ]; then
|
||||
printf "Checking for python3 cffi and setuptools... "
|
||||
if "$python" -c 'import cffi,setuptools; cffi.FFI().verify()' >/dev/null 2>&1; then
|
||||
printf "Yes.\n"
|
||||
have_python3_cffi=1
|
||||
WITH_PYTHON_DOCS=1
|
||||
else
|
||||
WITH_PYTHON_DOCS=0
|
||||
printf "No (will not install CFFI-based python bindings).\n"
|
||||
fi
|
||||
rm -rf __pycache__ # cffi.FFI().verify() uses this space
|
||||
|
||||
printf "Checking for python3 pytest (>= 3.0)... "
|
||||
conf=$(mktemp)
|
||||
printf "[pytest]\nminversion=3.0\n" > $conf
|
||||
if "$python" -m pytest -c $conf --version >/dev/null 2>&1; then
|
||||
printf "Yes.\n"
|
||||
have_python3_pytest=1
|
||||
else
|
||||
printf "No (will not test CFFI-based python bindings).\n"
|
||||
fi
|
||||
rm -f $conf
|
||||
fi
|
||||
|
||||
printf "Checking for valgrind development files... "
|
||||
if pkg-config --exists valgrind; then
|
||||
printf "Yes.\n"
|
||||
|
@ -677,13 +828,14 @@ if [ -z "${EMACSETCDIR-}" ]; then
|
|||
EMACSETCDIR="\$(prefix)/share/emacs/site-lisp"
|
||||
fi
|
||||
|
||||
printf "Checking if emacs (>= 24) is available... "
|
||||
if emacs --quick --batch --eval '(if (< emacs-major-version 24) (kill-emacs 1))' > /dev/null 2>&1; then
|
||||
if [ $WITH_EMACS = "1" ]; then
|
||||
printf "Checking if emacs (>= 25) is available... "
|
||||
if emacs --quick --batch --eval '(if (< emacs-major-version 25) (kill-emacs 1))' > /dev/null 2>&1; then
|
||||
printf "Yes.\n"
|
||||
have_emacs=1
|
||||
else
|
||||
printf "No (so will not byte-compile emacs code)\n"
|
||||
have_emacs=0
|
||||
else
|
||||
printf "No (disabling emacs related parts of build)\n"
|
||||
WITH_EMACS=0
|
||||
fi
|
||||
fi
|
||||
|
||||
have_doxygen=0
|
||||
|
@ -823,8 +975,8 @@ EOF
|
|||
if [ $have_python -eq 0 ]; then
|
||||
echo " python interpreter"
|
||||
fi
|
||||
if [ $have_xapian -eq 0 -o $have_xapian_compact -eq 0 ]; then
|
||||
echo " Xapian library (>= version 1.2.6, including development files such as headers)"
|
||||
if [ $have_xapian -eq 0 ]; then
|
||||
echo " Xapian library (>= version 1.4.0, including development files such as headers)"
|
||||
echo " https://xapian.org/"
|
||||
fi
|
||||
if [ $have_zlib -eq 0 ]; then
|
||||
|
@ -859,7 +1011,7 @@ On Debian and similar systems:
|
|||
|
||||
Or on Fedora and similar systems:
|
||||
|
||||
sudo yum install xapian-core-devel gmime-devel libtalloc-devel zlib-devel
|
||||
sudo dnf install xapian-core-devel gmime30-devel libtalloc-devel zlib-devel
|
||||
|
||||
On other systems, similar commands can be used, but the details of the
|
||||
package names may be different.
|
||||
|
@ -874,7 +1026,7 @@ to install pkg-config with a command such as:
|
|||
|
||||
sudo apt-get install pkg-config
|
||||
Or:
|
||||
sudo yum install pkgconfig
|
||||
sudo dnf install pkgconfig
|
||||
|
||||
But if pkg-config is not available for your system, then you will need
|
||||
to modify the configure script to manually set the cflags and ldflags
|
||||
|
@ -948,6 +1100,22 @@ else
|
|||
fi
|
||||
rm -f compat/have_timegm
|
||||
|
||||
cat <<EOF > _time_t.c
|
||||
#include <time.h>
|
||||
#include <assert.h>
|
||||
static_assert(sizeof(time_t) >= 8, "sizeof(time_t) < 8");
|
||||
EOF
|
||||
|
||||
printf "Checking for 64 bit time_t... "
|
||||
if ${CC} -c _time_t.c -o /dev/null
|
||||
then
|
||||
printf "Yes.\n"
|
||||
have_64bit_time_t=1
|
||||
else
|
||||
printf "No.\n"
|
||||
have_64bit_time_t=0
|
||||
fi
|
||||
|
||||
printf "Checking for dirent.d_type... "
|
||||
if ${CC} -o compat/have_d_type "$srcdir"/compat/have_d_type.c > /dev/null 2>&1
|
||||
then
|
||||
|
@ -1031,7 +1199,8 @@ for flag in -Wmissing-declarations; do
|
|||
done
|
||||
printf "\n\t%s\n" "${WARN_CFLAGS}"
|
||||
|
||||
rm -f minimal minimal.c _libversion.c _libversion _libversion.sh _check_session_keys.c _check_session_keys
|
||||
rm -f minimal minimal.c _time_t.c _libversion.c _libversion _libversion.sh _check_session_keys.c _check_session_keys _check_x509_validity.c _check_x509_validity \
|
||||
_verify_sig_with_session_key.c _verify_sig_with_session_key
|
||||
|
||||
# construct the Makefile.config
|
||||
cat > Makefile.config <<EOF
|
||||
|
@ -1165,9 +1334,6 @@ BASH_ABSOLUTE = ${bash_absolute}
|
|||
HAVE_PERL = ${have_perl}
|
||||
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
|
||||
HAVE_SPHINX=${have_sphinx}
|
||||
|
||||
|
@ -1187,7 +1353,7 @@ desktop_dir = \$(prefix)/share/applications
|
|||
bash_completion_dir = ${BASHCOMPLETIONDIR:=\$(prefix)/share/bash-completion/completions}
|
||||
|
||||
# The directory to which zsh completions files should be installed
|
||||
zsh_completion_dir = ${ZSHCOMLETIONDIR:=\$(prefix)/share/zsh/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
|
||||
# build its own version)
|
||||
|
@ -1204,6 +1370,12 @@ HAVE_GETLINE = ${have_getline}
|
|||
# building/testing ruby bindings.
|
||||
HAVE_RUBY_DEV = ${have_ruby_dev}
|
||||
|
||||
# Is the python cffi package available?
|
||||
HAVE_PYTHON3_CFFI = ${have_python3_cffi}
|
||||
|
||||
# Is the python pytest package available?
|
||||
HAVE_PYTHON3_PYTEST = ${have_python3_pytest}
|
||||
|
||||
# Whether the strcasestr function is available (if not, then notmuch will
|
||||
# build its own version)
|
||||
HAVE_STRCASESTR = ${have_strcasestr}
|
||||
|
@ -1219,14 +1391,8 @@ HAVE_TIMEGM = ${have_timegm}
|
|||
# Whether struct dirent has d_type (if not, then notmuch will use stat)
|
||||
HAVE_D_TYPE = ${have_d_type}
|
||||
|
||||
# Whether the Xapian version in use supports compaction
|
||||
HAVE_XAPIAN_COMPACT = ${have_xapian_compact}
|
||||
|
||||
# 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 to have Xapian retry lock
|
||||
HAVE_XAPIAN_DB_RETRY_LOCK = ${WITH_RETRY_LOCK}
|
||||
|
||||
# Whether the getpwuid_r function is standards-compliant
|
||||
# (if not, then notmuch will #define _POSIX_PTHREAD_SEMANTICS
|
||||
|
@ -1250,9 +1416,6 @@ LINKER_RESOLVES_LIBRARY_DEPENDENCIES = ${linker_resolves_library_dependencies}
|
|||
XAPIAN_CXXFLAGS = ${xapian_cxxflags}
|
||||
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
|
||||
GMIME_CFLAGS = ${gmime_cflags}
|
||||
GMIME_LDFLAGS = ${gmime_ldflags}
|
||||
|
@ -1305,9 +1468,7 @@ COMMON_CONFIGURE_CFLAGS = \\
|
|||
-DHAVE_D_TYPE=\$(HAVE_D_TYPE) \\
|
||||
-DSTD_GETPWUID=\$(STD_GETPWUID) \\
|
||||
-DSTD_ASCTIME=\$(STD_ASCTIME) \\
|
||||
-DHAVE_XAPIAN_COMPACT=\$(HAVE_XAPIAN_COMPACT) \\
|
||||
-DSILENCE_XAPIAN_DEPRECATION_WARNINGS \\
|
||||
-DHAVE_XAPIAN_FIELD_PROCESSOR=\$(HAVE_XAPIAN_FIELD_PROCESSOR) \\
|
||||
-DHAVE_XAPIAN_DB_RETRY_LOCK=\$(HAVE_XAPIAN_DB_RETRY_LOCK)
|
||||
|
||||
CONFIGURE_CFLAGS = \$(COMMON_CONFIGURE_CFLAGS)
|
||||
|
@ -1324,17 +1485,14 @@ cat > sh.config <<EOF
|
|||
|
||||
NOTMUCH_SRCDIR='${NOTMUCH_SRCDIR}'
|
||||
|
||||
# Whether the Xapian version in use supports compaction
|
||||
NOTMUCH_HAVE_XAPIAN_COMPACT=${have_xapian_compact}
|
||||
# Whether to have Xapian retry lock
|
||||
NOTMUCH_HAVE_XAPIAN_DB_RETRY_LOCK=${WITH_RETRY_LOCK}
|
||||
|
||||
# Whether the Xapian version in use supports field processors
|
||||
NOTMUCH_HAVE_XAPIAN_FIELD_PROCESSOR=${have_xapian_field_processor}
|
||||
# Whether GMime can verify X.509 certificate validity
|
||||
NOTMUCH_GMIME_X509_CERT_VALIDITY=${gmime_x509_cert_validity}
|
||||
|
||||
# Whether the Xapian version in use supports lock retry
|
||||
NOTMUCH_HAVE_XAPIAN_DB_RETRY_LOCK=${have_xapian_db_retry_lock}
|
||||
|
||||
# Which backend will Xapian use by default?
|
||||
NOTMUCH_DEFAULT_XAPIAN_BACKEND=${default_xapian_backend}
|
||||
# Whether GMime can verify signatures when decrypting with a session key:
|
||||
NOTMUCH_GMIME_VERIFY_WITH_SESSION_KEY=${gmime_verify_with_session_key}
|
||||
|
||||
# do we have man pages?
|
||||
NOTMUCH_HAVE_MAN=$((have_sphinx))
|
||||
|
@ -1343,6 +1501,9 @@ NOTMUCH_HAVE_MAN=$((have_sphinx))
|
|||
NOTMUCH_HAVE_BASH=${have_bash}
|
||||
NOTMUCH_BASH_ABSOLUTE=${bash_absolute}
|
||||
|
||||
# Whether time_t is 64 bits (or more)
|
||||
NOTMUCH_HAVE_64BIT_TIME_T=${have_64bit_time_t}
|
||||
|
||||
# Whether perl exists, and if so where
|
||||
NOTMUCH_HAVE_PERL=${have_perl}
|
||||
NOTMUCH_PERL_ABSOLUTE=${perl_absolute}
|
||||
|
@ -1357,10 +1518,27 @@ NOTMUCH_RUBY=${RUBY}
|
|||
# building/testing ruby bindings.
|
||||
NOTMUCH_HAVE_RUBY_DEV=${have_ruby_dev}
|
||||
|
||||
# Is the python cffi package available?
|
||||
NOTMUCH_HAVE_PYTHON3_CFFI=${have_python3_cffi}
|
||||
|
||||
# Is the python pytest package available?
|
||||
NOTMUCH_HAVE_PYTHON3_PYTEST=${have_python3_pytest}
|
||||
|
||||
# Platform we are run on
|
||||
PLATFORM=${platform}
|
||||
EOF
|
||||
|
||||
{
|
||||
echo "# Generated by configure, run from doc/conf.py"
|
||||
if [ $WITH_EMACS = "1" ]; then
|
||||
echo "tags.add('WITH_EMACS')"
|
||||
fi
|
||||
if [ $WITH_PYTHON_DOCS = "1" ]; then
|
||||
echo "tags.add('WITH_PYTHON')"
|
||||
fi
|
||||
printf "rsti_dir = '%s'\n" "$(cd emacs && pwd -P)"
|
||||
} > sphinx.config
|
||||
|
||||
# Finally, after everything configured, inform the user how to continue.
|
||||
cat <<EOF
|
||||
|
||||
|
|
|
@ -15,11 +15,11 @@ README.html: README
|
|||
markdown $< > $@
|
||||
|
||||
install: all
|
||||
mkdir -p $(DESTDIR)$(prefix)/bin
|
||||
mkdir -p $(DESTDIR)$(prefix)/bin $(DESTDIR)$(mandir)/man1 $(DESTDIR)$(sysconfdir)/Muttrc.d
|
||||
sed "1s|^#!.*|#! $(PERL_ABSOLUTE)|" < $(NAME) > $(DESTDIR)$(prefix)/bin/$(NAME)
|
||||
chmod 755 $(DESTDIR)$(prefix)/bin/$(NAME)
|
||||
install -D -m 644 $(NAME).1 $(DESTDIR)$(mandir)/man1/$(NAME).1
|
||||
install -D -m 644 $(NAME).rc $(DESTDIR)$(sysconfdir)/Muttrc.d/$(NAME).rc
|
||||
install -m 644 $(NAME).1 $(DESTDIR)$(mandir)/man1/
|
||||
install -m 644 $(NAME).rc $(DESTDIR)$(sysconfdir)/Muttrc.d/
|
||||
|
||||
clean:
|
||||
rm -f notmuch-mutt.1 README.html
|
||||
|
|
|
@ -12,6 +12,7 @@ use strict;
|
|||
use warnings;
|
||||
|
||||
use File::Path;
|
||||
use File::Basename;
|
||||
use Getopt::Long qw(:config no_getopt_compat);
|
||||
use Mail::Header;
|
||||
use Mail::Box::Maildir;
|
||||
|
@ -41,16 +42,17 @@ sub search($$$) {
|
|||
my ($maildir, $remove_dups, $query) = @_;
|
||||
my $dup_option = "";
|
||||
|
||||
$query = shell_quote($query);
|
||||
|
||||
if ($remove_dups) {
|
||||
$dup_option = "--duplicate=1";
|
||||
}
|
||||
my @args = qw/notmuch search --output=files/;
|
||||
push @args, "--duplicate=1" if $remove_dups;
|
||||
push @args, $query;
|
||||
|
||||
empty_maildir($maildir);
|
||||
system("notmuch search --output=files $dup_option $query"
|
||||
. " | sed -e 's: :\\\\ :g'"
|
||||
. " | xargs -r -I searchoutput ln -s searchoutput $maildir/cur/");
|
||||
open my $pipe, '-|', @args or die "Running @args failed: $!\n";
|
||||
while (<$pipe>) {
|
||||
chomp;
|
||||
my $ln = "$maildir/cur/" . basename $_;
|
||||
symlink $_, "$ln" or warn "Failed to symlink '$_', '$ln': $!\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub prompt($$) {
|
||||
|
|
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
|
||||
|
||||
* 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>
|
||||
Uploaders:
|
||||
Jameson Graef Rollins <jrollins@finestructure.net>,
|
||||
David Bremner <bremner@debian.org>
|
||||
Build-Conflicts: ruby1.8, gdb-minimal, gdb [ia64 mips mips64el]
|
||||
David Bremner <bremner@debian.org>,
|
||||
Build-Conflicts:
|
||||
gdb [ia64 mips mips64el],
|
||||
gdb-minimal,
|
||||
ruby1.8,
|
||||
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~),
|
||||
texinfo
|
||||
Standards-Version: 4.1.3
|
||||
debhelper-compat (= 13),
|
||||
dh-elpa (>= 1.3),
|
||||
dh-python,
|
||||
desktop-file-utils,
|
||||
doxygen,
|
||||
dpkg-dev (>= 1.17.14),
|
||||
dtach (>= 0.8) <!nocheck>,
|
||||
emacs-nox | emacs-gtk | emacs-lucid | emacs25-nox | emacs25 (>=25~) | emacs25-lucid (>=25~) | emacs24-nox | emacs24 (>=24~) | emacs24-lucid (>=24~),
|
||||
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/
|
||||
Vcs-Git: https://git.notmuchmail.org/git/notmuch -b release
|
||||
Vcs-Browser: https://git.notmuchmail.org/git/notmuch
|
||||
Rules-Requires-Root: no
|
||||
|
||||
Package: notmuch
|
||||
Architecture: any
|
||||
Depends: libnotmuch5 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends}
|
||||
Recommends: elpa-notmuch | notmuch-vim | notmuch-mutt | alot, gnupg-agent, gpgsm
|
||||
Depends:
|
||||
libnotmuch5 (= ${binary:Version}),
|
||||
${misc:Depends},
|
||||
${shlibs:Depends},
|
||||
Recommends:
|
||||
elpa-notmuch | notmuch-vim | notmuch-mutt | alot,
|
||||
gnupg-agent,
|
||||
gpgsm,
|
||||
Suggests:
|
||||
mailscripts
|
||||
Description: thread-based email index, search and tagging
|
||||
Notmuch is a system for indexing, searching, reading, and tagging
|
||||
large collections of email messages in maildir or mh format. It uses
|
||||
|
@ -48,8 +66,11 @@ Description: thread-based email index, search and tagging
|
|||
Package: libnotmuch5
|
||||
Section: libs
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||
Pre-Depends: ${misc:Pre-Depends}
|
||||
Depends:
|
||||
${misc:Depends},
|
||||
${shlibs:Depends},
|
||||
Pre-Depends:
|
||||
${misc:Pre-Depends},
|
||||
Description: thread-based email index, search and tagging (runtime)
|
||||
Notmuch is a system for indexing, searching, reading, and tagging
|
||||
large collections of email messages in maildir or mh format. It uses
|
||||
|
@ -62,7 +83,9 @@ Description: thread-based email index, search and tagging (runtime)
|
|||
Package: libnotmuch-dev
|
||||
Section: libdevel
|
||||
Architecture: any
|
||||
Depends: ${misc:Depends}, libnotmuch5 (= ${binary:Version})
|
||||
Depends:
|
||||
libnotmuch5 (= ${binary:Version}),
|
||||
${misc:Depends},
|
||||
Description: thread-based email index, search and tagging (development)
|
||||
Notmuch is a system for indexing, searching, reading, and tagging
|
||||
large collections of email messages in maildir or mh format. It uses
|
||||
|
@ -75,7 +98,29 @@ Description: thread-based email index, search and tagging (development)
|
|||
Package: python3-notmuch
|
||||
Architecture: all
|
||||
Section: python
|
||||
Depends: ${misc:Depends}, ${python3:Depends}, libnotmuch5 (>= ${source:Version})
|
||||
Depends:
|
||||
libnotmuch5 (>= ${source:Version}),
|
||||
${misc:Depends},
|
||||
${python3:Depends},
|
||||
Description: Python 3 legacy interface to the notmuch mail search and index library
|
||||
Notmuch is a system for indexing, searching, reading, and tagging
|
||||
large collections of email messages in maildir or mh format. It uses
|
||||
the Xapian library to provide fast, full-text search with a very
|
||||
convenient search syntax.
|
||||
.
|
||||
This package provides a legacy Python 3 interface to the notmuch
|
||||
functionality, directly interfacing with a shared notmuch library.
|
||||
.
|
||||
New projects are encouraged to use python3-notmuch2 instead.
|
||||
|
||||
Package: python3-notmuch2
|
||||
Architecture: any
|
||||
Section: python
|
||||
Depends:
|
||||
libnotmuch5 (>= ${source:Version}),
|
||||
${misc:Depends},
|
||||
${python3:Depends},
|
||||
${shlibs:Depends},
|
||||
Description: Python 3 interface to the notmuch mail search and index library
|
||||
Notmuch is a system for indexing, searching, reading, and tagging
|
||||
large collections of email messages in maildir or mh format. It uses
|
||||
|
@ -83,12 +128,17 @@ Description: Python 3 interface to the notmuch mail search and index library
|
|||
convenient search syntax.
|
||||
.
|
||||
This package provides a Python 3 interface to the notmuch
|
||||
functionality, directly interfacing with a shared notmuch library.
|
||||
functionality using CFFI bindings, which interface with a shared
|
||||
notmuch library.
|
||||
.
|
||||
This is the preferred way to use notmuch via Python.
|
||||
|
||||
Package: ruby-notmuch
|
||||
Architecture: any
|
||||
Section: ruby
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||
Depends:
|
||||
${misc:Depends},
|
||||
${shlibs:Depends},
|
||||
Description: Ruby interface to the notmuch mail search and index library
|
||||
Notmuch is a system for indexing, searching, reading, and tagging
|
||||
large collections of email messages in maildir or mh format. It uses
|
||||
|
@ -98,16 +148,12 @@ Description: Ruby interface to the notmuch mail search and index library
|
|||
This package provides a Ruby interface to the notmuch
|
||||
functionality, directly interfacing with a shared notmuch library.
|
||||
|
||||
Package: notmuch-emacs
|
||||
Section: oldlibs
|
||||
Architecture: all
|
||||
Depends: elpa-notmuch, ${misc:Depends}
|
||||
Description: thread-based email index, search and tagging (transitional package)
|
||||
This dummy package help ease transition to the new package elpa-notmuch
|
||||
|
||||
Package: elpa-notmuch
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${elpa:Depends}
|
||||
Depends:
|
||||
${elpa:Depends},
|
||||
${misc:Depends},
|
||||
Suggests: elpa-mailscripts
|
||||
Description: thread-based email index, search and tagging (emacs interface)
|
||||
Notmuch is a system for indexing, searching, reading, and tagging
|
||||
large collections of email messages in maildir or mh format. It uses
|
||||
|
@ -119,10 +165,18 @@ Description: thread-based email index, search and tagging (emacs interface)
|
|||
|
||||
Package: notmuch-vim
|
||||
Architecture: all
|
||||
Breaks: notmuch (<<0.6~254~)
|
||||
Replaces: notmuch (<<0.6~254~)
|
||||
Depends: ${misc:Depends}, notmuch, vim-addon-manager, vim-ruby, ruby-notmuch
|
||||
Recommends: ruby-mail
|
||||
Breaks:
|
||||
notmuch (<<0.6~254~),
|
||||
Replaces:
|
||||
notmuch (<<0.6~254~),
|
||||
Depends:
|
||||
notmuch,
|
||||
ruby-notmuch,
|
||||
vim-addon-manager,
|
||||
vim-ruby,
|
||||
${misc:Depends},
|
||||
Recommends:
|
||||
ruby-mail,
|
||||
Description: thread-based email index, search and tagging (vim interface)
|
||||
Notmuch is a system for indexing, searching, reading, and tagging
|
||||
large collections of email messages in maildir or mh format. It uses
|
||||
|
@ -135,12 +189,18 @@ Description: thread-based email index, search and tagging (vim interface)
|
|||
Package: notmuch-mutt
|
||||
Architecture: all
|
||||
Depends:
|
||||
libmail-box-perl,
|
||||
libmailtools-perl,
|
||||
libstring-shellquote-perl,
|
||||
libterm-readline-gnu-perl,
|
||||
notmuch (>= 0.4),
|
||||
libmail-box-perl, libmailtools-perl,
|
||||
libstring-shellquote-perl, libterm-readline-gnu-perl,
|
||||
${misc:Depends}
|
||||
Recommends: mutt
|
||||
Enhances: notmuch, mutt
|
||||
${misc:Depends},
|
||||
${perl:Depends},
|
||||
Recommends:
|
||||
mutt,
|
||||
Enhances:
|
||||
mutt,
|
||||
notmuch,
|
||||
Description: thread-based email index, search and tagging (Mutt interface)
|
||||
notmuch-mutt provides integration among the Mutt mail user agent and
|
||||
the Notmuch mail indexer.
|
||||
|
|
120
debian/copyright
vendored
120
debian/copyright
vendored
|
@ -1,36 +1,100 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: notmuch
|
||||
Source: git://notmuchmail.org/git/notmuch
|
||||
Source: https://git.notmuchmail.org/git/notmuch
|
||||
Upstream-Contact: Notmuch Mailing List <notmuch@notmuchmail.org>
|
||||
|
||||
Files: *
|
||||
Copyright: Copyright 2009 Carl Worth <cworth@cworth.org>
|
||||
Bart Trojanowski <bart@jukie.net>
|
||||
Keith Packard <keithp@keithp.com>
|
||||
Alexander Botero-Lowry <alex.boterolowry@gmail.com>
|
||||
Ingmar Vanhassel <ingmar@exherbo.org>
|
||||
Jed Brown <jed@59A2.org>
|
||||
Jan Janak <jan@ryngle.com>
|
||||
Chris Wilson <chris@chris-wilson.co.uk>
|
||||
Keith Amidon <keith@nicira.com>
|
||||
Aneesh Kumar K.V <aneesh.kumar@linux.vnet.ibm.com>
|
||||
Mikhail Gusarov <dottedmag@dottedmag.net>
|
||||
Jeffrey C. Ollie <jeff@ocjtech.us>
|
||||
Jameson Graef Rollins <jrollins@finestructure.net>
|
||||
Stewart Smith <stewart@flamingspork.com>
|
||||
Adrian Perez <aperez@igalia.com>
|
||||
Kan-Ru Chen <kanru@kanru.info>
|
||||
James Rowe <jnrowe@gmail.com>
|
||||
Eric Anholt <eric@anholt.net>
|
||||
Alec Berryman <alec@thened.net>
|
||||
Tassilo Horn <tassilo@member.fsf.org>
|
||||
Stefan Schmidt <stefan@datenfreihafen.org>
|
||||
Rolland Santimano <rollandsantimano@yahoo.com>
|
||||
Peter Wang <novalazy@gmail.com>
|
||||
Lars Kellogg-Stedman <lars@seas.harvard.edu>
|
||||
Holger Freyther <zecke@selfish.org>
|
||||
David Bremner <bremner@unb.ca>
|
||||
Alexander Botero-Lowry <alexbl@fortitudo.(none)>
|
||||
Copyright: Copyright 2009-2020
|
||||
David Bremner
|
||||
Carl Worth
|
||||
Jani Nikula
|
||||
Austin Clements
|
||||
Daniel Kahn Gillmor
|
||||
Mark Walters
|
||||
Floris Bruynooghe
|
||||
David Edmondson
|
||||
Tomi Ollila
|
||||
Sebastian Spaeth
|
||||
Ali Polatel
|
||||
Michal Sojka
|
||||
Justus Winter
|
||||
Sebastien Binet
|
||||
W. Trevor King
|
||||
Jameson Graef Rollins
|
||||
Felipe Contreras
|
||||
Pieter Praet
|
||||
Peter Feigl
|
||||
Dmitry Kurochkin
|
||||
Peter Wang
|
||||
Daniel Schoepe
|
||||
Gregor Zattler
|
||||
Keith Packard
|
||||
Adam Wolfe Gordon
|
||||
Stefano Zacchiroli
|
||||
Vincent Breitmoser
|
||||
laochailan
|
||||
Ben Gamari
|
||||
Aaron Ecay
|
||||
Jesse Rosenthal
|
||||
l-m-h@web.de
|
||||
Thomas Jost
|
||||
Dirk Hohndel
|
||||
Blake Jones
|
||||
Jonas Bernoulli
|
||||
Damien Cassou
|
||||
Vladimir Panteleev
|
||||
Anton Khirnov
|
||||
Matt Armstrong
|
||||
Örjan Ekeberg
|
||||
Jan Janak
|
||||
Patrick Totzke
|
||||
Chunyang Xu
|
||||
rhn
|
||||
Ruben Pollan
|
||||
Ioan-Adrian Ratiu
|
||||
Ethan Glasser-Camp
|
||||
Todd
|
||||
Chris Wilson
|
||||
William Casarin
|
||||
Yuri Volchkov
|
||||
Cédric Cabessa
|
||||
Mark Anderson
|
||||
Jed Brown
|
||||
Maxime Coste
|
||||
Ludovic LANGE
|
||||
Sebastian Poeplau
|
||||
Mikhail
|
||||
Gaute Hope
|
||||
Keith Amidon
|
||||
martin f. krafft
|
||||
Jeffrey C. Ollie
|
||||
Bart Trojanowski
|
||||
Jameson Rollins
|
||||
Scott Henson
|
||||
Vladimir Marek
|
||||
Servilio Afre Puentes
|
||||
Kevin McCarthy
|
||||
Tomas Carnecky
|
||||
Kevin J. McCarthy
|
||||
Scott Robinson
|
||||
Wael M. Nasreddine
|
||||
Charles Celerier
|
||||
Olly Betts
|
||||
Istvan Marko
|
||||
Florian Klink
|
||||
Thibaut Horel
|
||||
Joel Borggrén-Franck
|
||||
Ingmar Vanhassel
|
||||
Olivier Taïbi
|
||||
Ian Main
|
||||
Alexander Botero-Lowry
|
||||
Luis Ressel
|
||||
Sergei Shilovsky
|
||||
Trevor Jim
|
||||
Jinwoo Lee
|
||||
Uli Scholler
|
||||
Matthew Lear
|
||||
Amadeusz Żołnowski
|
||||
License: GPL-3+
|
||||
|
||||
Files: debian/*
|
||||
|
|
6
debian/elpa-notmuch.elpa
vendored
6
debian/elpa-notmuch.elpa
vendored
|
@ -1,3 +1,3 @@
|
|||
emacs/*.el
|
||||
emacs/notmuch-logo.png
|
||||
debian/tmp/usr/share/info/*
|
||||
debian/tmp/usr/share/emacs/site-lisp/*.el
|
||||
debian/tmp/usr/share/emacs/site-lisp/notmuch-logo.png
|
||||
emacs/notmuch-pkg.el
|
||||
|
|
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#
|
||||
* Build-Depends-Package: libnotmuch-dev
|
||||
notmuch_built_with@Base 0.23~rc0
|
||||
notmuch_config_list_destroy@Base 0.23~rc0
|
||||
notmuch_config_list_key@Base 0.23~rc0
|
||||
|
@ -55,6 +56,7 @@ libnotmuch.so.5 libnotmuch5 #MINVER#
|
|||
notmuch_message_get_filename@Base 0.3
|
||||
notmuch_message_get_filenames@Base 0.5
|
||||
notmuch_message_get_flag@Base 0.3
|
||||
notmuch_message_get_flag_st@Base 0.31~rc0
|
||||
notmuch_message_get_header@Base 0.3
|
||||
notmuch_message_get_message_id@Base 0.3
|
||||
notmuch_message_get_properties@Base 0.23~rc0
|
||||
|
@ -63,6 +65,7 @@ libnotmuch.so.5 libnotmuch5 #MINVER#
|
|||
notmuch_message_get_tags@Base 0.3
|
||||
notmuch_message_get_thread_id@Base 0.3
|
||||
notmuch_message_has_maildir_flag@Base 0.26~rc0
|
||||
notmuch_message_has_maildir_flag_st@Base 0.31~rc0
|
||||
notmuch_message_maildir_flags_to_tags@Base 0.5
|
||||
notmuch_message_properties_destroy@Base 0.23~rc0
|
||||
notmuch_message_properties_key@Base 0.23~rc0
|
||||
|
@ -128,8 +131,6 @@ libnotmuch.so.5 libnotmuch5 #MINVER#
|
|||
(c++)"typeinfo for Xapian::DatabaseError@Base" 0.24~rc0
|
||||
(c++)"typeinfo for Xapian::DatabaseModifiedError@Base" 0.24~rc0
|
||||
(c++|optional=present with Xapian 1.4)"typeinfo for Xapian::QueryParserError@Base" 0.23~rc0
|
||||
(c++)"typeinfo for Xapian::QueryParser::add_valuerangeprocessor(Xapian::ValueRangeProcessor*)::ShimRangeProcessor@Base" 0.25~rc0
|
||||
(c++)"typeinfo name for Xapian::QueryParser::add_valuerangeprocessor(Xapian::ValueRangeProcessor*)::ShimRangeProcessor@Base" 0.25~rc0
|
||||
(c++)"typeinfo name for Xapian::LogicError@Base" 0.6.1
|
||||
(c++)"typeinfo name for Xapian::RuntimeError@Base" 0.6.1
|
||||
(c++)"typeinfo name for Xapian::DocNotFoundError@Base" 0.6.1
|
||||
|
|
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
|
||||
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/plugin
|
||||
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/syntax/notmuch-*.vim usr/share/vim/addons/syntax
|
||||
vim/notmuch.vim usr/share/vim/addons/plugin
|
||||
vim/notmuch.yaml usr/share/vim/registry
|
||||
vim/syntax/notmuch-*.vim usr/share/vim/addons/syntax
|
||||
|
|
2
debian/notmuch.install
vendored
2
debian/notmuch.install
vendored
|
@ -1,5 +1,5 @@
|
|||
usr/bin/notmuch
|
||||
usr/bin/notmuch-emacs-mua
|
||||
usr/share/applications/notmuch-emacs-mua.desktop
|
||||
usr/share/bash-completion
|
||||
usr/share/zsh/vendor-completions
|
||||
emacs/notmuch-emacs-mua.desktop usr/share/applications
|
||||
|
|
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-tag.1.gz
|
||||
usr/share/man/man1/notmuch-reply.1.gz
|
||||
usr/share/man/man1/notmuch-search.1.gz
|
||||
usr/share/man/man1/notmuch-restore.1.gz
|
||||
usr/share/man/man1/notmuch-insert.1.gz
|
||||
usr/share/man/man1/notmuch-show.1.gz
|
||||
usr/share/man/man1/notmuch-compact.1.gz
|
||||
usr/share/man/man1/notmuch-config.1.gz
|
||||
usr/share/man/man1/notmuch-count.1.gz
|
||||
usr/share/man/man1/notmuch-dump.1.gz
|
||||
usr/share/man/man1/notmuch-emacs-mua.1.gz
|
||||
usr/share/man/man1/notmuch-insert.1.gz
|
||||
usr/share/man/man1/notmuch-new.1.gz
|
||||
usr/share/man/man1/notmuch-reindex.1.gz
|
||||
usr/share/man/man1/notmuch-reply.1.gz
|
||||
usr/share/man/man1/notmuch-restore.1.gz
|
||||
usr/share/man/man1/notmuch-search.1.gz
|
||||
usr/share/man/man1/notmuch-setup.1.gz
|
||||
usr/share/man/man1/notmuch-show.1.gz
|
||||
usr/share/man/man1/notmuch-tag.1.gz
|
||||
usr/share/man/man1/notmuch.1.gz
|
||||
usr/share/man/man5/notmuch-hooks.5.gz
|
||||
usr/share/man/man7/notmuch-properties.7.gz
|
||||
usr/share/man/man7/notmuch-search-terms.7.gz
|
||||
|
|
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
|
||||
|
||||
export PYBUILD_NAME=notmuch
|
||||
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
|
||||
|
||||
%:
|
||||
dh $@ --with python3,elpa
|
||||
|
@ -15,19 +15,25 @@ override_dh_auto_configure:
|
|||
--zshcompletiondir=/usr/share/zsh/vendor-completions \
|
||||
--localstatedir=/var
|
||||
|
||||
override_dh_auto_test:
|
||||
dh_auto_test -- V=1
|
||||
|
||||
override_dh_auto_build:
|
||||
dh_auto_build -- V=1
|
||||
dh_auto_build --buildsystem=pybuild --sourcedirectory bindings/python
|
||||
PYBUILD_NAME=notmuch dh_auto_build --buildsystem=pybuild --sourcedirectory bindings/python
|
||||
PYBUILD_NAME=notmuch2 dh_auto_build --buildsystem=pybuild --sourcedirectory bindings/python-cffi
|
||||
$(MAKE) -C contrib/notmuch-mutt
|
||||
|
||||
override_dh_auto_clean:
|
||||
dh_auto_clean
|
||||
dh_auto_clean --buildsystem=pybuild --sourcedirectory bindings/python
|
||||
PYBUILD_NAME=notmuch dh_auto_clean --buildsystem=pybuild --sourcedirectory bindings/python
|
||||
PYBUILD_NAME=notmuch2 dh_auto_clean --buildsystem=pybuild --sourcedirectory bindings/python-cffi
|
||||
dh_auto_clean --sourcedirectory bindings/ruby
|
||||
$(MAKE) -C contrib/notmuch-mutt clean
|
||||
|
||||
override_dh_auto_install:
|
||||
dh_auto_install
|
||||
dh_auto_install --buildsystem=pybuild --sourcedirectory bindings/python
|
||||
PYBUILD_NAME=notmuch dh_auto_install --buildsystem=pybuild --sourcedirectory bindings/python
|
||||
PYBUILD_NAME=notmuch2 dh_auto_install --buildsystem=pybuild --sourcedirectory bindings/python-cffi
|
||||
$(MAKE) -C contrib/notmuch-mutt DESTDIR=$(CURDIR)/debian/tmp install
|
||||
dh_auto_install --sourcedirectory bindings/ruby
|
||||
|
|
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 ());
|
||||
if (readlink (buf, buf2, sizeof (buf2)) != -1 &&
|
||||
strncmp (basename (buf2), "gdb", 3) == 0)
|
||||
{
|
||||
strncmp (basename (buf2), "gdb", 3) == 0) {
|
||||
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
|
||||
functions is the exception, and starts on a new line.
|
||||
|
||||
* Comments are always C-style /* */ block comments. They should start
|
||||
with a capital letter and generally be written in complete
|
||||
sentences. Public library functions are documented immediately
|
||||
before their prototype in lib/notmuch.h. Internal functions are
|
||||
typically documented immediately before their definition.
|
||||
* Opening parens also cuddle, even if the first argument does not fit
|
||||
on the same line.
|
||||
|
||||
* Ternary operators that span a line should be parenthesized like as
|
||||
"a ? (\n b ) : c". This is mainly to keep the indentation tools
|
||||
happy.
|
||||
|
||||
* Comments are always C-style /* */ block comments, with a leading *
|
||||
each line. They should start with a capital letter and generally be
|
||||
written in complete sentences. Public library functions are
|
||||
documented immediately before their prototype in lib/notmuch.h.
|
||||
Internal functions are typically documented immediately before their
|
||||
definition.
|
||||
|
||||
* Code lines should be less than 80 columns and comments should be
|
||||
wrapped at 70 columns.
|
||||
|
|
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 |
|
||||
| r | notmuch-search-reply-to-thread-sender | notmuch-show-reply-sender | notmuch-show-reply-sender |
|
||||
| s | notmuch-search | notmuch-search | notmuch-search |
|
||||
| t | notmuch-search-filter-by-tag | toggle-truncate-lines | |
|
||||
| t | notmuch-search-filter-by-tag | toggle-truncate-lines | notmuch-search-by-tag |
|
||||
| u | | | |
|
||||
| v | | | notmuch-show-view-all-mime-parts |
|
||||
| w | | notmuch-show-save-attachments | notmuch-show-save-attachments |
|
||||
|
|
|
@ -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>
|
||||
# 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>
|
||||
#
|
||||
# dependencies
|
||||
# - python 2.6 for json
|
||||
# - argparse; either python 2.7, or install separately
|
||||
# - python3 or python2.7
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
for f in ./version debian/changelog NEWS "$PV_FILE"
|
||||
for f in ./version.txt debian/changelog NEWS "$PV_FILE"
|
||||
do
|
||||
if [ ! -f "$f" ]; then append_emsg "File '$f' is missing"
|
||||
elif [ ! -r "$f" ]; then append_emsg "File '$f' is unreadable"
|
||||
|
@ -53,7 +53,7 @@ then
|
|||
else
|
||||
echo "Reading './version' file failed (surprisingly!)"
|
||||
exit 1
|
||||
fi < ./version
|
||||
fi < ./version.txt
|
||||
|
||||
readonly VERSION
|
||||
|
||||
|
@ -109,7 +109,7 @@ else
|
|||
fi
|
||||
|
||||
echo -n "Checking that python bindings version is $VERSION... "
|
||||
py_version=`python -c "with open('$PV_FILE') as vf: exec(vf.read()); print(__VERSION__)"`
|
||||
py_version=`python3 -c "with open('$PV_FILE') as vf: exec(vf.read()); print(__VERSION__)"`
|
||||
if [ "$py_version" = "$VERSION" ]
|
||||
then
|
||||
echo Yes.
|
||||
|
@ -178,10 +178,7 @@ esac
|
|||
year=`exec date +%Y`
|
||||
echo -n "Checking that copyright in documentation contains 2009-$year... "
|
||||
# Read the value of variable `copyright' defined in 'doc/conf.py'.
|
||||
# As __file__ is not defined when python command is given from command line,
|
||||
# it is defined before contents of 'doc/conf.py' (which dereferences __file__)
|
||||
# is executed.
|
||||
copyrightline=`exec python -c "with open('doc/conf.py') as cf: __file__ = ''; exec(cf.read()); print(copyright)"`
|
||||
copyrightline=$(grep ^copyright doc/conf.py)
|
||||
case $copyrightline in
|
||||
*2009-$year*)
|
||||
echo Yes. ;;
|
||||
|
|
|
@ -44,28 +44,10 @@ while looking at: " pdir "emacs\n\nexit emacs (y or n)? ")
|
|||
try-notmuch-emacs-directory (concat pdir "emacs/")
|
||||
load-path (cons try-notmuch-emacs-directory load-path)))
|
||||
|
||||
;; they say advice doesn't work for primitives (functions from c source)
|
||||
;; well, these 'before' advice works for emacs 23.1 - 24.5 (at least)
|
||||
;; ...and for our purposes 24.3 is enough (there is no load-prefer-newer there)
|
||||
;; note also that the old, "obsolete" defadvice mechanism was used, but that
|
||||
;; is the only one available for emacs 23 and 24 up to 24.3.
|
||||
|
||||
(if (boundp 'load-prefer-newer)
|
||||
(defadvice require (before before-require activate)
|
||||
(define-advice require
|
||||
(:before (feature &optional _filename _noerror) notmuch)
|
||||
(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")
|
||||
|
||||
|
|
|
@ -117,3 +117,5 @@ align_right_cmt_span = 8 # align comments span this much in func
|
|||
cmt_star_cont = true
|
||||
|
||||
# indent_brace = 0
|
||||
|
||||
indent_class = true
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# -*- makefile -*-
|
||||
# -*- makefile-gmake -*-
|
||||
|
||||
dir := doc
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS := -q
|
||||
SPHINXBUILD = HAVE_EMACS=${HAVE_EMACS} WITH_EMACS=${WITH_EMACS} sphinx-build
|
||||
SPHINXBUILD = sphinx-build
|
||||
DOCBUILDDIR := $(dir)/_build
|
||||
|
||||
# Internal variables.
|
||||
|
@ -29,8 +29,8 @@ MAN1_TEXI := $(patsubst $(srcdir)/doc/man1/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$
|
|||
MAN5_TEXI := $(patsubst $(srcdir)/doc/man5/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$(MAN5_RST))
|
||||
MAN7_TEXI := $(patsubst $(srcdir)/doc/man7/%.rst,$(DOCBUILDDIR)/texinfo/%.texi,$(MAN7_RST))
|
||||
INFO_TEXI_FILES := $(MAN1_TEXI) $(MAN5_TEXI) $(MAN7_TEXI)
|
||||
ifeq ($(HAVE_EMACS)$(WITH_EMACS),11)
|
||||
INFO_TEXI_FILES := $(INFO_TEXI_FILES) $(DOCBUILDDIR)/texinfo/notmuch-emacs.texi
|
||||
ifeq ($(WITH_EMACS),1)
|
||||
INFO_TEXI_FILES += $(DOCBUILDDIR)/texinfo/notmuch-emacs.texi
|
||||
endif
|
||||
|
||||
INFO_INFO_FILES := $(INFO_TEXI_FILES:.texi=.info)
|
||||
|
@ -40,7 +40,7 @@ INFO_INFO_FILES := $(INFO_TEXI_FILES:.texi=.info)
|
|||
.PHONY: install-man build-man apidocs install-apidocs
|
||||
|
||||
%.gz: %
|
||||
rm -f $@ && gzip --stdout $^ > $@
|
||||
rm -f $@ && gzip --no-name --stdout $^ > $@
|
||||
|
||||
ifeq ($(WITH_EMACS),1)
|
||||
$(DOCBUILDDIR)/.roff.stamp sphinx-html sphinx-texinfo: docstring.stamp
|
||||
|
|
35
doc/conf.py
35
doc/conf.py
|
@ -4,6 +4,8 @@
|
|||
import sys
|
||||
import os
|
||||
|
||||
extensions = [ 'sphinx.ext.autodoc' ]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
|
@ -12,16 +14,26 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = u'notmuch'
|
||||
copyright = u'2009-2019, Carl Worth and many others'
|
||||
copyright = u'2009-2020, Carl Worth and many others'
|
||||
|
||||
location = os.path.dirname(__file__)
|
||||
|
||||
for pathdir in ['.', '..']:
|
||||
version_file = os.path.join(location,pathdir,'version')
|
||||
version_file = os.path.join(location,pathdir,'version.txt')
|
||||
if os.path.exists(version_file):
|
||||
with open(version_file,'r') as infile:
|
||||
version=infile.read().replace('\n','')
|
||||
|
||||
# for autodoc
|
||||
sys.path.insert(0, os.path.join(location, '..', 'bindings', 'python-cffi', 'notmuch2'))
|
||||
|
||||
# read generated config
|
||||
for pathdir in ['.', '..']:
|
||||
conf_file = os.path.join(location,pathdir,'sphinx.config')
|
||||
if os.path.exists(conf_file):
|
||||
with open(conf_file,'r') as infile:
|
||||
exec(''.join(infile.readlines()))
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
|
@ -29,12 +41,23 @@ release = version
|
|||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# If we don't have emacs (or the user configured --without-emacs),
|
||||
# don't build the notmuch-emacs docs, as they need emacs to generate
|
||||
# the docstring include files
|
||||
if os.environ.get('HAVE_EMACS') != '1' or os.environ.get('WITH_EMACS') != '1':
|
||||
if tags.has('WITH_EMACS'):
|
||||
# Hacky reimplementation of include to workaround limitations of
|
||||
# sphinx-doc
|
||||
lines = ['.. include:: /../emacs/rstdoc.rsti\n\n'] # in the source tree
|
||||
for file in ('notmuch.rsti', 'notmuch-lib.rsti', 'notmuch-show.rsti', 'notmuch-tag.rsti'):
|
||||
lines.extend(open(rsti_dir+'/'+file))
|
||||
rst_epilog = ''.join(lines)
|
||||
del lines
|
||||
else:
|
||||
# If we don't have emacs (or the user configured --without-emacs),
|
||||
# don't build the notmuch-emacs docs, as they need emacs to generate
|
||||
# the docstring include files
|
||||
exclude_patterns.append('notmuch-emacs.rst')
|
||||
|
||||
if not tags.has('WITH_PYTHON'):
|
||||
exclude_patterns.append('python-bindings.rst')
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
|
|
|
@ -264,12 +264,10 @@ GENERATE_TAGFILE =
|
|||
ALLEXTERNALS = NO
|
||||
EXTERNAL_GROUPS = NO
|
||||
EXTERNAL_PAGES = NO
|
||||
PERL_PATH = /usr/bin/perl
|
||||
#---------------------------------------------------------------------------
|
||||
# Configuration options related to the dot tool
|
||||
#---------------------------------------------------------------------------
|
||||
CLASS_DIAGRAMS = NO
|
||||
MSCGEN_PATH =
|
||||
HIDE_UNDOC_RELATIONS = YES
|
||||
HAVE_DOT = NO
|
||||
DOT_NUM_THREADS = 0
|
||||
|
|
|
@ -26,6 +26,7 @@ Contents:
|
|||
man7/notmuch-search-terms
|
||||
man1/notmuch-show
|
||||
man1/notmuch-tag
|
||||
python-bindings
|
||||
|
||||
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
|
||||
line of the form::
|
||||
|
||||
*section*.\ *item*\ =\ *value*
|
||||
section.item=value
|
||||
|
||||
No additional whitespace surrounds the dot or equals sign
|
||||
characters. In a multiple-value item (a list), the values are
|
||||
|
@ -134,14 +134,6 @@ The available configuration items are described below.
|
|||
|
||||
Default: ``true``.
|
||||
|
||||
**crypto.gpg_path**
|
||||
Name (or full path) of gpg binary to use in verification and
|
||||
decryption of PGP/MIME messages. NOTE: This configuration item is
|
||||
deprecated, and will be ignored if notmuch is built against GMime
|
||||
3.0 or later.
|
||||
|
||||
Default: ``gpg``.
|
||||
|
||||
**index.decrypt** **[STORED IN DATABASE]**
|
||||
Policy for decrypting encrypted messages during indexing. Must be
|
||||
one of: ``false``, ``auto``, ``nostash``, or ``true``.
|
||||
|
@ -206,8 +198,9 @@ The available configuration items are described below.
|
|||
|
||||
**built_with.<name>**
|
||||
Compile time feature <name>. Current possibilities include
|
||||
"compact" (see **notmuch-compact(1)**) and "field_processor" (see
|
||||
**notmuch-search-terms(7)**).
|
||||
"retry_lock" (configure option, included by default).
|
||||
(since notmuch 0.30, "compact" and "field_processor" are
|
||||
always included.)
|
||||
|
||||
**query.<name>** **[STORED IN DATABASE]**
|
||||
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
|
||||
separator. For the cases where it's not ambiguous (in particular
|
||||
excluding boolean options), a space can also be used. The following
|
||||
are all equivalent:
|
||||
separator. Except for boolean options (which would be ambiguous), a
|
||||
space can also be used as a separator. The following are all
|
||||
equivalent:
|
||||
|
||||
::
|
||||
|
||||
|
|
|
@ -109,6 +109,30 @@ of its normal activity.
|
|||
example, an AES-128 key might be stashed in a notmuch property as:
|
||||
``session-key=7:14B16AF65536C28AF209828DFE34C9E0``.
|
||||
|
||||
**index.repaired**
|
||||
|
||||
Some messages arrive in forms that are confusing to view; they can
|
||||
be mangled by mail transport agents, or the sending mail user
|
||||
agent may structure them in a way that is confusing. If notmuch
|
||||
knows how to both detect and repair such a problematic message, it
|
||||
will do so during indexing.
|
||||
|
||||
If it applies a message repair during indexing, it will use the
|
||||
``index.repaired`` property to note the type of repair(s) it
|
||||
performed.
|
||||
|
||||
``index.repaired=skip-protected-headers-legacy-display`` indicates
|
||||
that when indexing the cleartext of an encrypted message, notmuch
|
||||
skipped over a "legacy-display" text/rfc822-headers part that it
|
||||
found in that message, since it was able to index the built-in
|
||||
protected headers directly.
|
||||
|
||||
``index.repaired=mixedup`` indicates the repair of a "Mixed Up"
|
||||
encrypted PGP/MIME message, a mangling typically produced by
|
||||
Microsoft's Exchange MTA. See
|
||||
https://tools.ietf.org/html/draft-dkg-openpgp-pgpmime-message-mangling
|
||||
for more information.
|
||||
|
||||
SEE ALSO
|
||||
========
|
||||
|
||||
|
|
|
@ -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>
|
||||
indicate user-supplied values).
|
||||
|
||||
If notmuch is built with **Xapian Field Processors** (see below) some
|
||||
of the prefixes with <regex> forms can be also used to restrict the
|
||||
results to those whose value matches a regular expression (see
|
||||
Some of the prefixes with <regex> forms can be also used to restrict
|
||||
the results to those whose value matches a regular expression (see
|
||||
**regex(7)**) delimited with //, for example::
|
||||
|
||||
notmuch search 'from:"/bob@.*[.]example[.]com/"'
|
||||
|
@ -87,8 +86,7 @@ thread:<thread-id>
|
|||
of output from **notmuch search**
|
||||
|
||||
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
|
||||
threads containing a message from mallory and one (not necessarily
|
||||
the same message) with Subject containing the word "crypto".
|
||||
|
@ -158,9 +156,7 @@ lastmod:<initial-revision>..<final-revision>
|
|||
|
||||
query:<name>
|
||||
The **query:** prefix allows queries to refer to previously saved
|
||||
queries added with **notmuch-config(1)**. Named queries are only
|
||||
available if notmuch is built with **Xapian Field Processors**
|
||||
(see below).
|
||||
queries added with **notmuch-config(1)**.
|
||||
|
||||
property:<key>=<value>
|
||||
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:<expr>..! can be used as a shorthand for date:<expr>..<expr>. The
|
||||
expansion takes place before interpretation, and thus, for example,
|
||||
date:monday..! matches from the beginning of Monday until the end of
|
||||
Monday.
|
||||
With **Xapian Field Processor** support (see below), non-range
|
||||
date queries such as date:yesterday will work, but otherwise
|
||||
will give unexpected results; if in doubt use date:yesterday..!
|
||||
|
||||
Currently, we do not support spaces in range expressions. You can
|
||||
Currently, spaces in range expressions are not supported. You can
|
||||
replace the spaces with '\_', or (in most cases) '-', or (in some cases)
|
||||
leave the spaces out altogether. Examples in this man page use spaces
|
||||
for clarity.
|
||||
|
||||
Open-ended ranges are supported (since Xapian 1.2.1), i.e. it's possible
|
||||
to specify date:..<until> or date:<since>.. to not limit the start or
|
||||
end time, respectively. Pre-1.2.1 Xapian does not report an error on
|
||||
open ended ranges, but it does not work as expected either.
|
||||
Open-ended ranges are supported. I.e. it's possible to specify
|
||||
date:..<until> or date:<since>.. to not limit the start or
|
||||
end time, respectively.
|
||||
|
||||
Single expression
|
||||
-----------------
|
||||
|
||||
date:<expr> works as a shorthand for date:<expr>..<expr>.
|
||||
For example, date:monday matches from the beginning of Monday until
|
||||
the end of Monday.
|
||||
|
||||
Relative date and time
|
||||
----------------------
|
||||
|
@ -446,24 +440,6 @@ Time zones
|
|||
|
||||
Some time zone codes, e.g. UTC, EET.
|
||||
|
||||
XAPIAN FIELD PROCESSORS
|
||||
=======================
|
||||
|
||||
Certain optional features of the notmuch query processor rely on the
|
||||
presence of the Xapian field processor API. You can determine if your
|
||||
notmuch was built against a sufficiently recent version of Xapian by running
|
||||
|
||||
::
|
||||
|
||||
% notmuch config get built_with.field_processor
|
||||
|
||||
Currently the following features require field processor support:
|
||||
|
||||
- non-range date queries, e.g. "date:today"
|
||||
- named queries e.g. "query:my_special_query"
|
||||
- regular expression searches, e.g. "subject:/^\\[SPAM\\]/"
|
||||
- thread subqueries, e.g. "thread:{from:bob}"
|
||||
|
||||
SEE ALSO
|
||||
========
|
||||
|
||||
|
|
|
@ -377,13 +377,3 @@ suffix exist it will be read instead (just one of these, chosen in this
|
|||
order). Most often users create ``~/.emacs.d/notmuch-config.el`` and just
|
||||
work with it. If Emacs was invoked with the ``-q`` or ``--no-init-file``
|
||||
options, ``notmuch-init-file`` is not read.
|
||||
|
||||
.. include:: ../emacs/rstdoc.rsti
|
||||
|
||||
.. include:: ../emacs/notmuch.rsti
|
||||
|
||||
.. include:: ../emacs/notmuch-lib.rsti
|
||||
|
||||
.. include:: ../emacs/notmuch-show.rsti
|
||||
|
||||
.. include:: ../emacs/notmuch-tag.rsti
|
||||
|
|
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
|
||||
emacs_sources := \
|
||||
|
@ -47,7 +47,7 @@ emacs_images := \
|
|||
emacs_bytecode = $(emacs_sources:.el=.elc)
|
||||
emacs_docstrings = $(emacs_sources:.el=.rsti)
|
||||
|
||||
ifneq ($(HAVE_SPHINX)$(HAVE_EMACS),11)
|
||||
ifneq ($(HAVE_SPHINX)$(WITH_EMACS),11)
|
||||
docstring.stamp:
|
||||
@echo "Missing prerequisites, not collecting docstrings"
|
||||
else
|
||||
|
@ -60,7 +60,7 @@ endif
|
|||
# the byte compiler may load an old .elc file when processing a
|
||||
# "require" or we may fail to rebuild a .elc that depended on a macro
|
||||
# from an updated file.
|
||||
ifeq ($(HAVE_EMACS),1)
|
||||
ifeq ($(WITH_EMACS),1)
|
||||
$(dir)/.eldeps: $(dir)/Makefile.local $(dir)/make-deps.el $(emacs_sources)
|
||||
$(call quiet,EMACS) --directory emacs -batch -l make-deps.el \
|
||||
-f batch-make-deps $(emacs_sources) > $@.tmp && \
|
||||
|
@ -82,7 +82,7 @@ $(dir)/notmuch-lib.elc: $(dir)/notmuch-version.elc
|
|||
endif
|
||||
CLEAN+=$(dir)/.eldeps $(dir)/.eldeps.tmp $(dir)/.eldeps.x
|
||||
|
||||
ifeq ($(HAVE_EMACS),1)
|
||||
ifeq ($(WITH_EMACS),1)
|
||||
%.elc: %.el $(global_deps)
|
||||
$(call quiet,EMACS) --directory emacs -batch -f batch-byte-compile $<
|
||||
%.rsti: %.el
|
||||
|
@ -103,10 +103,8 @@ endif
|
|||
rm -r .elpa-build
|
||||
|
||||
ifeq ($(WITH_EMACS),1)
|
||||
ifeq ($(HAVE_EMACS),1)
|
||||
all: $(emacs_bytecode) $(emacs_docstrings)
|
||||
install-emacs: $(emacs_bytecode)
|
||||
endif
|
||||
|
||||
install: install-emacs
|
||||
endif
|
||||
|
@ -115,7 +113,7 @@ endif
|
|||
install-emacs: $(emacs_sources) $(emacs_images)
|
||||
mkdir -p "$(DESTDIR)$(emacslispdir)"
|
||||
install -m0644 $(emacs_sources) "$(DESTDIR)$(emacslispdir)"
|
||||
ifeq ($(HAVE_EMACS),1)
|
||||
ifeq ($(WITH_EMACS),1)
|
||||
install -m0644 $(emacs_bytecode) "$(DESTDIR)$(emacslispdir)"
|
||||
endif
|
||||
mkdir -p "$(DESTDIR)$(emacsetcdir)"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
;;; coolj.el --- automatically wrap long lines -*- coding:utf-8 -*-
|
||||
|
||||
;; Copyright (C) 2000, 2001, 2004, 2005, 2006, 2007, 2008, 2009 Free Software Foundation, Inc.
|
||||
;; Copyright (C) 2000, 2001, 2004-2009 Free Software Foundation, Inc.
|
||||
|
||||
;; Authors: Kai Grossjohann <Kai.Grossjohann@CS.Uni-Dortmund.DE>
|
||||
;; Alex Schroeder <alex@gnu.org>
|
||||
|
@ -107,8 +107,8 @@ not need to be wrapped, move point to the next line and return t."
|
|||
If the line should not be broken, return nil; point remains on the
|
||||
line."
|
||||
(move-to-column fill-column)
|
||||
(if (and (re-search-forward "[^ ]" (line-end-position) 1)
|
||||
(> (current-column) fill-column))
|
||||
(and (re-search-forward "[^ ]" (line-end-position) 1)
|
||||
(> (current-column) fill-column)
|
||||
;; This line is too long. Can we break it?
|
||||
(or (coolj-find-break-backward prefix)
|
||||
(progn (move-to-column fill-column)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue