diff options
| author | David Bremner <bremner@debian.org> | 2011-12-16 16:46:20 -0400 |
|---|---|---|
| committer | David Bremner <bremner@debian.org> | 2011-12-16 16:46:20 -0400 |
| commit | 90259bf961eeacb89dfd2e73526a931e530cabd8 (patch) | |
| tree | c8c5d57ebba3f82e372e8d2670257ac01d68fca4 | |
| parent | 8c0cb84ecce40ded56f9c551b2ef791caa9be7cf (diff) | |
| parent | 07bb8b9e895541006eca88430925f1c6524c4708 (diff) | |
Merge commit 'debian/0.10.2-1' into squeeze-backports
Conflicts:
debian/changelog
160 files changed, 8086 insertions, 2243 deletions
diff --git a/.dir-locals.el b/.dir-locals.el index cbdb1f97..aea630bd 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,7 +1,23 @@ ; emacs local configuration settings for notmuch source ; surmised by dkg on 2010-11-23 13:43:18-0500 +; amended by amdragon on 2011-06-06 -((c-mode . ((indent-tabs-mode . t) - (tab-width . 8) - (c-basic-offset . 4) - (c-file-style . "linux")))) +((c-mode + (indent-tabs-mode . t) + (tab-width . 8) + (c-basic-offset . 4) + (c-file-style . "linux")) + (c++-mode + (indent-tabs-mode . t) + (tab-width . 8) + (c-basic-offset . 4) + (c-file-style . "linux")) + (emacs-lisp-mode + (indent-tabs-mode . t) + (tab-width . 8)) + (shell-mode + (indent-tabs-mode . t) + (tab-width . 8) + (sh-basic-offset . 4) + (sh-indentation . 4)) + ) @@ -4,7 +4,8 @@ TAGS tags *cscope* .deps -notmuch +/notmuch +notmuch.sym notmuch-shared notmuch.1.gz libnotmuch.so* @@ -3,7 +3,7 @@ all: # List all subdirectories here. Each contains its own Makefile.local -subdirs = compat completion emacs lib test +subdirs = compat completion emacs lib util test # We make all targets depend on the Makefiles themselves. global_deps = Makefile Makefile.config Makefile.local \ diff --git a/Makefile.local b/Makefile.local index 8a8832da..d97fa618 100644 --- a/Makefile.local +++ b/Makefile.local @@ -15,23 +15,31 @@ IS_GIT=$(shell if [ -d .git ] ; then echo yes ; else echo no; fi) VERSION:=$(shell cat ${srcdir}/version) ifneq ($(MAKECMDGOALS),release) ifneq ($(MAKECMDGOALS),release-message) +ifneq ($(MAKECMDGOALS),pre-release) ifeq ($(IS_GIT),yes) -VERSION:=$(shell git describe --match '[0-9.]*') +VERSION:=$(shell git describe --match '[0-9.]*'|sed -e s/_/~/ -e s/-/+/ -e s/-/~/) endif endif endif +endif + +UPSTREAM_TAG=$(subst ~,_,$(VERSION)) +DEB_TAG=debian/$(UPSTREAM_TAG)-1 RELEASE_HOST=notmuchmail.org RELEASE_DIR=/srv/notmuchmail.org/www/releases RELEASE_URL=http://notmuchmail.org/releases TAR_FILE=$(PACKAGE)-$(VERSION).tar.gz +DEB_TAR_FILE=$(PACKAGE)_$(VERSION).orig.tar.gz SHA1_FILE=$(TAR_FILE).sha1 GPG_FILE=$(SHA1_FILE).asc +PV_FILE=bindings/python/notmuch/version.py + # Smash together user's values with our extra values FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CFLAGS) $(WARN_CFLAGS) $(CONFIGURE_CFLAGS) $(extra_cflags) FINAL_CXXFLAGS = $(CXXFLAGS) $(WARN_CXXFLAGS) $(CONFIGURE_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) -FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Llib -lnotmuch $(AS_NEEDED_LDFLAGS) $(GMIME_LDFLAGS) $(TALLOC_LDFLAGS) +FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Lutil -lutil -Llib -lnotmuch $(AS_NEEDED_LDFLAGS) $(GMIME_LDFLAGS) $(TALLOC_LDFLAGS) FINAL_NOTMUCH_LINKER = CC ifneq ($(LINKER_RESOLVES_LIBRARY_DEPENDENCIES),1) FINAL_NOTMUCH_LDFLAGS += $(CONFIGURE_LDFLAGS) @@ -60,7 +68,13 @@ endif endif $(TAR_FILE): - git archive --format=tar --prefix=$(PACKAGE)-$(VERSION)/ HEAD > $(TAR_FILE).tmp + if git tag -v $(VERSION) >/dev/null 2>&1; then \ + ref=$(VERSION); \ + else \ + ref="HEAD" ; \ + echo "Warning: No signed tag for $(VERSION)"; \ + fi ; \ + git archive --format=tar --prefix=$(PACKAGE)-$(VERSION)/ $$ref > $(TAR_FILE).tmp echo $(VERSION) > version.tmp tar --append -f $(TAR_FILE).tmp --transform s_^_$(PACKAGE)-$(VERSION)/_ --transform 's_.tmp$$__' version.tmp rm version.tmp @@ -87,23 +101,46 @@ dist: $(TAR_FILE) .PHONY: release release: verify-source-tree-and-version $(MAKE) VERSION=$(VERSION) verify-newer + $(MAKE) VERSION=$(VERSION) clean $(MAKE) VERSION=$(VERSION) test - rm -rf ./debian-build - git-buildpackage - cp debian-build/notmuch_$(VERSION).tar.gz notmuch-$(VERSION).tar.gz + git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG) $(MAKE) VERSION=$(VERSION) $(GPG_FILE) - scp $(TAR_FILE) $(SHA1_FILE) $(GPG_FILE) $(RELEASE_HOST):$(RELEASE_DIR) - ssh $(RELEASE_HOST) "rm -f $(RELEASE_DIR)/LATEST-$(PACKAGE)-[0-9]* && ln -s $(TAR_FILE) $(RELEASE_DIR)/LATEST-$(PACKAGE)-$(VERSION)" + ln -sf $(TAR_FILE) $(DEB_TAR_FILE) + pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG) + git tag -s -m "$(PACKAGE) Debian $(VERSION)-1 upload (same as $(VERSION))" $(DEB_TAG) mkdir -p releases mv $(TAR_FILE) $(SHA1_FILE) $(GPG_FILE) releases - (cd debian-build; dput *.changes) - mv debian-build/* releases - rmdir debian-build - git tag -s -m "$(PACKAGE) $(VERSION) release" $(VERSION) - git push origin $(VERSION) $(MAKE) VERSION=$(VERSION) release-message > $(PACKAGE)-$(VERSION).announce +ifeq ($(REALLY_UPLOAD),yes) + git push origin $(VERSION) + cd releases && scp $(TAR_FILE) $(SHA1_FILE) $(GPG_FILE) $(RELEASE_HOST):$(RELEASE_DIR) + ssh $(RELEASE_HOST) "rm -f $(RELEASE_DIR)/LATEST-$(PACKAGE)-[0-9]* ; ln -s $(TAR_FILE) $(RELEASE_DIR)/LATEST-$(PACKAGE)-$(VERSION)" +endif @echo "Please send a release announcement using $(PACKAGE)-$(VERSION).announce as a template." +.PHONY: pre-release +pre-release: + $(MAKE) VERSION=$(VERSION) clean + $(MAKE) VERSION=$(VERSION) test + git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG) + git tag -s -m "$(PACKAGE) Debian $(VERSION)-1 upload (same as $(VERSION))" $(DEB_TAG) + $(MAKE) VERSION=$(VERSION) $(TAR_FILE) + ln -sf $(TAR_FILE) $(DEB_TAR_FILE) + pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG) + mkdir -p releases + mv $(TAR_FILE) $(DEB_TAR_FILE) releases + +.PHONY: debian-snapshot +debian-snapshot: TMPFILE := $(shell mktemp) +debian-snapshot: + make VERSION=$(VERSION) clean + cp debian/changelog $(TMPFILE) + EDITOR=/bin/true dch -b -v $(VERSION)+1 -D UNRELEASED 'test build, not for upload' + echo '3.0 (native)' > debian/source/format + debuild -us -uc + mv -f $(TMPFILE) debian/changelog + echo '3.0 (quilt)' > debian/source/format + .PHONY: release-message release-message: @echo "To: notmuch@notmuchmail.org" @@ -142,7 +179,7 @@ release-message: verify-source-tree-and-version: verify-no-dirty-code .PHONY: verify-no-dirty-code -verify-no-dirty-code: verify-version-debian +verify-no-dirty-code: verify-version-debian verify-version-python ifeq ($(IS_GIT),yes) @printf "Checking that source tree is clean..." ifneq ($(shell git ls-files -m),) @@ -161,13 +198,22 @@ endif .PHONY: verify-version-debian verify-version-debian: verify-version-components - @echo -n "Checking that Debian package version is $(VERSION)..." - @if [ "$(VERSION)" != $$(dpkg-parsechangelog | grep ^Version | awk '{print $$2}') ] ; then \ + @echo -n "Checking that Debian package version is $(VERSION)-1..." + @if [ "$(VERSION)-1" != $$(dpkg-parsechangelog | grep ^Version | awk '{print $$2}') ] ; then \ (echo "No." && \ echo "Please edit version and debian/changelog to have consistent versions." && false) \ fi @echo "Good." +.PHONY: verify-version-python +verify-version-python: verify-version-components + @echo -n "Checking that python bindings version is $(VERSION)..." + @if [ "$(VERSION)" != $$(python -c "execfile('$(PV_FILE)'); print __VERSION__") ] ; then \ + (echo "No." && \ + echo "Please edit version and $(PV_FILE) to have consistent versions." && false) \ + fi + @echo "Good." + .PHONY: verify-version-components verify-version-components: @echo -n "Checking that $(VERSION) consists only of digits and periods..." @@ -180,11 +226,16 @@ verify-version-components: .PHONY: verify-newer verify-newer: @echo -n "Checking that no $(VERSION) release already exists..." - @ssh $(RELEASE_HOST) test ! -e $(RELEASE_DIR)/$(TAR_FILE) \ - || (echo "Ouch." && echo "Found: $(RELEASE_HOST):$(RELEASE_DIR)/$(TAR_FILE)" \ - && echo "Refusing to replace an existing release." \ - && echo "Don't forget to update \"version\" as described in RELEASING before release." && false) - @echo "Good." + @wget -q -O /dev/null $(RELEASE_URL)/$(TAR_FILE) ; \ + case $$? in \ + 8) echo "Good." ;; \ + 0) echo "Ouch."; \ + echo "Found: $(RELEASE_URL)/$(TAR_FILE)"; \ + echo "Refusing to replace an existing release."; \ + echo "Don't forget to update \"version\" as described in RELEASING before release." ; \ + false ;; \ + *) echo "An unexpected error occured"; \ + false;; esac # The user has not set any verbosity, default to quiet mode and inform the # user how to enable verbose compiles. @@ -248,12 +299,11 @@ notmuch_client_srcs = \ notmuch-time.c \ query-string.c \ show-message.c \ - json.c \ - xutil.c + json.c notmuch_client_modules = $(notmuch_client_srcs:.c=.o) -notmuch: $(notmuch_client_modules) lib/libnotmuch.a +notmuch: $(notmuch_client_modules) lib/libnotmuch.a util/libutil.a $(call quiet,CXX $(CFLAGS)) $^ $(FINAL_LIBNOTMUCH_LDFLAGS) -o $@ notmuch-shared: $(notmuch_client_modules) lib/$(LINKER_NAME) @@ -1,3 +1,254 @@ +Notmuch 0.10.2 (2011-12-04) +=========================== + +Bug-fix release. +---------------- + +Fix crash in python bindings. + + The python bindings did not call g_type_init, which caused crashes + for some, but not all users. + +Notmuch 0.10.1 (2011-11-25) +=========================== + +Bug-fix release. +---------------- + +Fix --help argument + + Argument processing changes in 0.10 introduced a bug where "notmuch + --help" crashed while "notmuch help" worked fine. This is fixed in + 0.10.1. + +Notmuch 0.10 (2011-11-23) +========================= + +New build and testing features +------------------------------ + +Emacs tests are now done in dtach. This means that dtach is now +needed to run the notmuch test suite, at least until the checking for +prerequisites is improved. + +Full test coverage of the stashing feature in Emacs. + +New command-line features +------------------------- + +Add "notmuch restore --accumulate" option + + The --accumulate switch causes the union of the existing and new tags to be + applied, instead of replacing each message's tags as they are read in from + the dump file. + +Add search terms to "notmuch dump" + + The dump command now takes an optional search term much like notmuch + search/show/tag. The output file argument of dump is deprecated in + favour of using stdout. + +Add "notmuch search" --offset and --limit options + + The search command now takes options --offset=[-]N and --limit=N to limit + the number of results shown. + +Add "notmuch count --output" option + + The count command is now capable of counting threads in addition to + messages. This is selected using the new --output=(threads|messages) option. + +New emacs UI features +--------------------- + +Add tab-completion for notmuch-search and notmuch-search-filter + + These functions now support completion tags for query parts + starting with "tag:". + +Turn "id:MSG-ID" links into buttons associated with notmuch searches + + Text of the form "id:MSG-ID" in mails is now a clickable button that + opens a notmuch search for the given message id. + +Add keybinding ('c I') for stashing Message-ID's without an id: prefix + + Reduces manual labour when stashing them for use outside notmuch. + +Do not query on notmuch-search exit + + It is harmless to kill the external notmuch process, so the user + is no longer interrogated when they interrupt a search. + +Performance +----------- + +Emacs now constructs large search buffers more efficiently + +Search avoids opening and parsing message files + + We now store more information in the database so search no longer + has to open every message file to get basic headers. This can + improve search speed by as much as 10X, but taking advantage of this + requires a database rebuild: + + notmuch dump > notmuch.dump + # Backup, then remove notmuch database ($MAIL/.notmuch) + notmuch new + notmuch restore notmuch.dump + +New collection of add-on tools +------------------------------ + +The source directory "contrib" contains tools built on notmuch. These +tools are not part of notmuch, and you should check their individual +licenses. Feel free to report problems with them to the notmuch +mailing list. + +nmbug - share tags with a given prefix + + nmbug helps maintain a git repo containing all tags with a given + prefix (by default "notmuch::"). Tags can be shared by commiting + them to git in one location and restoring in another. + +Notmuch 0.9 (2011-10-01) +======================== + +New, general features +--------------------- + +Correct handling of interruptions during "notmuch new" + + "notmuch new" now operates as a series of small, self-consistent + transactions, so it can correctly resume after an interruption or + crash. Previously, interruption could lose existing tags, fail to + detect messages on resume, or leave the database in a state + temporarily or permanently inconsistent with the mail store. + +Library changes +--------------- + +New functions + + notmuch_database_begin_atomic and notmuch_database_end_atomic allow + multiple database operations to be performed atomically. + + notmuch_database_find_message_by_filename does exactly what it says. + +API changes + + notmuch_database_find_message (and n_d_f_m_by_filename) now return + a status indicator and uses an output parameter for the + message. This change required changing the SONAME of libnotmuch to + libnotmuch.so.2 + +Python bindings changes +----------------------- + + - Re-encode python unicode objects to utf-8 before passing back to + libnotmuch. + - Support Database().begin_atomic()/end_atomic() + - Support Database().find_message_by_filename() + NB! This needs a db opened in READ-WRITE mode currently, or it will crash + the python process. The is a limitation (=bug) of the underlying libnotmuch. + - Fixes where we would not throw NotmuchErrors when we should (Justus Winter) + - Update for n_d_find_message* API changes (see above). + +Ruby bindings changes +--------------------- + + - Wrap new library functions notmuch_database_{begin,end}_atomic. + - Add new exception Notmuch::UnbalancedAtomicError. + - Rename destroy to destroy! according to Ruby naming conventions. + - Update for n_d_find_message* API changes (see above). + +Emacs improvements +------------------ + + * Add gpg callback to crypto sigstatus buttons to retrieve/refresh + signing key. + * Add notmuch-show-refresh-view function (and corresponding binding) + to refresh the view of a notmuch-show buffer. + +Reply formatting cleanup +------------------------ + + "notmuch reply" no longer includes notification that non-leafnode + MIME parts are being suppressed. + +Notmuch 0.8 (2011-09-10) +======================== + +Improved handling of message/rfc822 parts + + Both in the CLI and the emacs interface. Output of rfc822 parts now + includes the primary headers, as well as the body and all subparts. + Output of the completely raw rfc822-formatted message, including all + headers, is unfortunately not yet supported (but hopefully will be + soon). + +Improved Build system portability + + Certain parts of the shell script generating notmuch.sym were + specific to the GNU versions of sed and nm. The new version should + be more portable to e.g. OpenBSD. + +Documentation update for Ruby bindings + + Added documentation, typo fixes, and improved support for rdoc. + +Unicode, iterator, PEP8 changes for python bindings + + - PEP8 (code formatting) changes for python files. + - Remove Tags.__len__ ; see 0.6 release notes for motivation. + - Decode headers as UTF8, encode (unicode) database paths as UTF8. + +Notmuch 0.7 (2011-08-01) +======================== + +Vim interface improvements +-------------------------- + +Jason Woofenden provided a number of bug fixes for the Vim interface + + * fix citation/signature fold lengths + * fix cig/cit parsing within multipart/* + * fix on-screen instructions for show-signature + * fix from list reformatting in search view + * fix space key: now archives (did opposite) + +Uwe Kleine-König contributed + + * use full path for sendmail/doc fix + * fix compose temp file name + +Python Bindings changes +----------------------- + +Sebastian Spaeth contributed two changes related to unicode and UTF8: + + * message tags are now explicitly unicode + * query string is encoded as a UTF8 byte string + +Build-System improvements +------------------------ + +Generate notmuch.sym after the relevant object files + + This fixes a bug in parallel building. Thanks to Thomas Jost for the + patch. + +Notmuch 0.6.1 (2011-07-17) +========================== + +Bug-fix release. +---------------- + +Re-export Xapian exception typeinfo symbols. + + It turned out our aggressive symbol hiding caused problems for + people running gcc 4.4.5. + Notmuch 0.6 (2011-07-01) ======================= New, general features @@ -13,8 +264,8 @@ Folder-based searching For example, one might use things such as: folder:spam - folder:2011-* - folder:work/todo + folder:2011-* + folder:work/todo to match any path containing a directory "spam", "work/todo", or containing a directory starting with "2011-", respectively. @@ -159,7 +410,7 @@ Hiding of repeated subjects in collapsed thread view In notmuch-show mode, if a collapsed message has the same subject as its parent, the subject is not shown. - + Automatic detection and hiding of original message in top-posted message When a message contains a line looking something like: @@ -225,7 +476,7 @@ Ruby bindings are now much more complete Including QUERY.sort, QUERY.to_s, MESSAGE.maildir_flags_to_tags, MESSAGE.tags_to_maildir_flags, and MESSAGE.get_filenames -* Python bindings have been upodated and extended +* Python bindings have been updated and extended (docs online at http://packages.python.org/notmuch/) New bindings: @@ -235,16 +486,16 @@ Ruby bindings are now much more complete - Message().__cmp__() and __hash__() These allow, for example: - if msg1 == msg2: ... + if msg1 == msg2: ... As well as set arithmetic on Messages(): - s1, s2= set(msgs1), set(msgs2) + s1, s2= set(msgs1), set(msgs2) s1.union(s2) s2 -= s1 Removed: - - len(Messages()) as it exausted the iterator. + - len(Messages()) as it exhausted the iterator. Use len(list(Messages())) or Query.count_messages() to get the length. @@ -380,9 +631,9 @@ Maildir-flag synchronization ---- ----- 'D' draft 'F' flagged - 'P' passed - 'R' replied - 'S' unread (added when 'S' flag is not present) + 'P' passed + 'R' replied + 'S' unread (added when 'S' flag is not present) The synchronization occurs in both directions, (for example, adding the 'S' flag to a file will cause the "unread" tag to be added, and @@ -1136,4 +1387,3 @@ a performance bug that made notmuch very slow when modifying tags. This would cause distracting pauses when reading mail while notmuch would wait for Xapian when removing the "inbox" and "unread" tags from messages in a thread. - @@ -21,31 +21,26 @@ repository. From here, there are just a few steps to release: See the instructions there for how to increment it. The version should have been updated with any commits that - added API, but do check that that is the case. The command - below can be useful for inspecting header-file changes since - the last release X.Y: + added API _in a non-upwardly compatible_ way, but do check + that that is the case. The command below can be useful for + inspecting header-file changes since the last release X.Y: git diff X.Y..HEAD -- lib/notmuch.h - Note: We currently don't plan to increment - LIBNOTMUCH_VERSION_MAJOR beyond 1, so if there *are* - incompatible changes to the library interface, then - stop. Don't release. Figure out the plan on the notmuch - mailing list. - Commit this change, if any. -3) Update the debian/libnotmuch1.symbols file +3) Update the debian/libnotmuchX.symbols file If the library version changed at all (step 2) it probably means that symbols have changed/been added, in which case the debian symbols file also needs to be updated: dpkg-buildpackage -uc -us - dpkg-gensymbols -plibnotmuch1 | patch -p0 + dpkg-gensymbols -plibnotmuchX | patch -p0 Carefully review the changes to debian/libnotmuch1.symbols to - make sure there are no unexpected changes. + make sure there are no unexpected changes. Remove any debian + versions from symbols. Commit this change, if any. @@ -67,7 +62,13 @@ repository. From here, there are just a few steps to release: be "1.0.1" and a subsequent bug-fix release would be "1.0.2" etc. - Commit this change. + Update bindings/python/notmuch/version.py to match version. + + Update the version in notmuch.1 to match version. + + XXX: Probably these last two steps should be (semi-)automated. + + Commit these changes. 5) Create an entry for the new release in debian/changelog @@ -85,31 +86,24 @@ repository. From here, there are just a few steps to release: 6) Run "make release" which will perform the following steps. - Note: If any problem occurs during the process, (such as a lintian - warning that you decide should be fixed), you can abort at the - prompt for your GPG passphrase and nothing will have been uploaded - yet. + Note: in order to really upload anything, set the make variable + REALLY_UPLOAD=yes * Ensure that the version consists only of digits and periods * Ensure that version and debian/changelog have the same version * Verify that the source tree is clean * Compile the current notmuch code (aborting release if it fails) * Run the notmuch test suite (aborting release if it fails) - * Compile a Debian package - * Copy the tar file from what was made for Debian package - * Generate a .sha1 sum file for the tar file - * Sign the sha1sum using your GPG setup (asks for your GPG password) * Check that no release exists with the current version - * scp the three files to appear on http://notmuchmail.org/releases - * Create a LATEST-notmuch-version file (after deleting any old one) - * Place local copies of the tar, sha1, and gpg files into releases - * Upload the Debian package - * Place a local copy of the Debian package files in releases - * Tag the entire source tree with a tag of the form X.Y.Z, and sign - the tag with your GPG key (asks for your GPG password, and you - may need to set GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL to match - your public-key's setting or this fails.) - * Push that tag + * Make a signed tag + * Generate a tar file from this tag + * Generate a .sha1 sum file for the tar file and GPG sign it. + * Commit a (delta for a) copy of the tar file using pristine-tar + * Tag for the debian version + * if REALLY_UPLOAD=yes + - push the signed tag to the origin + XXX FIXME push debian tag + - scp tarball to web site * Provide some text for the release announcement (see below). 7) Send a message to notmuch@notmuchmail.org to announce the release. @@ -15,7 +15,9 @@ result.) This may require removing the outer array from the current "notmuch search --format=json" results. Fix '*' to work by simply calling '+' or '-' on a region consisting of -the entire buffer. +the entire buffer, (this would avoid one race condition---while still +leaving other race conditions---but could also potentially make '*' a +very expensive operation). Add a global keybinding table for notmuch, and then view-specific tables that add to it. @@ -29,7 +31,7 @@ Make 'notmuch-show-pipe-message have a private history. Add support for a delete keybinding that adds a "deleted" tag to the current message/thread and make searches not return deleted messages -by default, (unless the user asks explicitly for deleted messags in +by default, (unless the user asks explicitly for deleted messages in the search query). Add keybindings for next/previous thread. @@ -119,7 +121,7 @@ Allow configuration for filename patterns that should be ignored when indexing. Replace the "notmuch part --part=id" command with "notmuch show ---part=id", (David Edmonson wants to rewrite some of "notmuch show" to +--part=id", (David Edmondson wants to rewrite some of "notmuch show" to provide more MIME-structure information in its output first). Replace the "notmuch search-tags" command with "notmuch search @@ -265,6 +267,9 @@ large so the detection could be potentially slow). 2. A "notmuch search" syntax could be added to allow the user to find these files, (and perhaps delete them or move them away as appropriate). +Fix filesystem/notmuch-new race condition by not updating database +mtime for a directory if it is the same as the current mtime. + Test suite ---------- Achieve 100% test coverage with the test suite. diff --git a/bindings/python/.gitignore b/bindings/python/.gitignore new file mode 100644 index 00000000..1fbea8a0 --- /dev/null +++ b/bindings/python/.gitignore @@ -0,0 +1,3 @@ +*.py[co] +/docs/build +/docs/html diff --git a/bindings/python/docs/source/index.rst b/bindings/python/docs/source/index.rst index e9f39eb0..73d2a3b0 100644 --- a/bindings/python/docs/source/index.rst +++ b/bindings/python/docs/source/index.rst @@ -21,7 +21,13 @@ Notmuch can be imported as:: or:: - from notmuch import Query,Database + from notmuch import Query, Database + + db = Database('path',create=True) + msgs = Query(db,'from:myself').search_messages() + + for msg in msgs: + print (msg) More information on specific topics can be found on the following pages: @@ -36,8 +42,6 @@ More information on specific topics can be found on the following pages: .. automodule:: notmuch -:todo: Document nmlib,STATUS - :class:`notmuch.Database` -- The underlying notmuch database --------------------------------------------------------------------- @@ -55,6 +59,10 @@ More information on specific topics can be found on the following pages: .. automethod:: upgrade + .. automethod:: begin_atomic + + .. automethod:: end_atomic + .. automethod:: get_directory .. automethod:: add_message @@ -63,13 +71,12 @@ More information on specific topics can be found on the following pages: .. automethod:: find_message + .. automethod:: find_message_by_filename + .. automethod:: get_all_tags .. automethod:: create_query - .. note:: :meth:`create_query` was broken in release - 0.1 and is fixed since 0.1.1. - .. attribute:: Database.MODE Defines constants that are used as the mode in which to open a database. @@ -82,6 +89,7 @@ More information on specific topics can be found on the following pages: .. autoattribute:: db_p + :class:`notmuch.Query` -- A search query ------------------------------------------------- @@ -130,7 +138,7 @@ More information on specific topics can be found on the following pages: .. method:: __len__() - .. note:: :meth:`__len__` was removed in version 0.6 as it exhausted + .. warning:: :meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke list(Messages()). Use the :meth:`Query.count_messages` function or use `len(list(msgs))`. @@ -195,7 +203,12 @@ More information on specific topics can be found on the following pages: .. autoclass:: Tags :members: - .. automethod:: __len__ + .. method:: __len__ + + .. warning:: :meth:`__len__` was removed in version 0.6 as it + exhausted the iterator and broke list(Tags()). Use + :meth:`len(list(msgs))` instead if you need to know the + number of tags. .. automethod:: __str__ diff --git a/bindings/python/docs/source/status_and_errors.rst b/bindings/python/docs/source/status_and_errors.rst index 1d74ba17..bc0d0d23 100644 --- a/bindings/python/docs/source/status_and_errors.rst +++ b/bindings/python/docs/source/status_and_errors.rst @@ -15,9 +15,31 @@ Some methods return a status, indicating if an operation was successful and what :exc:`NotmuchError` -- A Notmuch execution error ------------------------------------------------ -Whenever an error occurs, we throw a special Exception: +Whenever an error occurs, we throw a special Exception :exc:`NotmuchError`, or a more fine grained Exception which is derived from it. This means it is always safe to check for NotmuchErrors if you want to catch all errors. If you are interested in more fine grained exceptions, you can use those below. .. autoexception:: NotmuchError - :members: - This execption inherits directly from :exc:`Exception` and is raised on errors during the notmuch execution. +The following exceptions are all directly derived from NotmuchError. Each of them corresponds to a specific :class:`notmuch.STATUS` value. You can either check the :attr:`status` attribute of a NotmuchError to see if a specific error has occurred, or you can directly check for the following Exception types: + +.. autoexception:: OutOfMemoryError(message=None) + :members: +.. autoexception:: ReadOnlyDatabaseError(message=None) + :members: +.. autoexception:: XapianError(message=None) + :members: +.. autoexception:: FileError(message=None) + :members: +.. autoexception:: FileNotEmailError(message=None) + :members: +.. autoexception:: DuplicateMessageIdError(message=None) + :members: +.. autoexception:: NullPointerError(message=None) + :members: +.. autoexception:: TagTooLongError(message=None) + :members: +.. autoexception:: UnbalancedFreezeThawError(message=None) + :members: +.. autoexception:: UnbalancedAtomicError(message=None) + :members: +.. autoexception:: NotInitializedError(message=None) + :members: diff --git a/bindings/python/notmuch/__init__.py b/bindings/python/notmuch/__init__.py index 4331d9de..539afedf 100644 --- a/bindings/python/notmuch/__init__.py +++ b/bindings/python/notmuch/__init__.py @@ -1,9 +1,10 @@ -"""The :mod:`notmuch` module provides most of the functionality that a user is likely to need. +"""The :mod:`notmuch` module provides most of the functionality that a user is +likely to need. .. note:: The underlying notmuch library is build on a hierarchical memory allocator called talloc. All objects derive from a top-level :class:`Database` object. - + This means that as soon as an object is deleted, all underlying derived objects such as Queries, Messages, Message, and Tags will be freed by the underlying library as well. Accessing these @@ -16,7 +17,7 @@ db = Database('path',create=True) msgs = Query(db,'from:myself').search_messages() - This returns a :class:`Messages` which internally contains a + This returns :class:`Messages` which internally contains a reference to its parent :class:`Query` object. Otherwise the Query() would be immediately freed, taking our *msgs* down with it. @@ -30,7 +31,6 @@ Pretty much the same is valid for all other objects in the hierarchy, such as :class:`Query`, :class:`Messages`, :class:`Message`, and :class:`Tags`. - """ """ @@ -49,13 +49,28 @@ for more details. You should have received a copy of the GNU General Public License along with notmuch. If not, see <http://www.gnu.org/licenses/>. -Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>' +Copyright 2010-2011 Sebastian Spaeth <Sebastian@SSpaeth.de> """ from notmuch.database import Database, Query from notmuch.message import Messages, Message from notmuch.thread import Threads, Thread from notmuch.tag import Tags -from notmuch.globals import nmlib, STATUS, NotmuchError -__LICENSE__="GPL v3+" -__VERSION__='0.6' -__AUTHOR__ ='Sebastian Spaeth <Sebastian@SSpaeth.de>' +from notmuch.globals import ( + nmlib, + STATUS, + NotmuchError, + OutOfMemoryError, + ReadOnlyDatabaseError, + XapianError, + FileError, + FileNotEmailError, + DuplicateMessageIdError, + NullPointerError, + TagTooLongError, + UnbalancedFreezeThawError, + UnbalancedAtomicError, + NotInitializedError +) +from notmuch.version import __VERSION__ +__LICENSE__ = "GPL v3+" +__AUTHOR__ = 'Sebastian Spaeth <Sebastian@SSpaeth.de>' diff --git a/bindings/python/notmuch/database.py b/bindings/python/notmuch/database.py index 5deb2a5d..f4bc53e0 100644 --- a/bindings/python/notmuch/database.py +++ b/bindings/python/notmuch/database.py @@ -19,24 +19,39 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>' import os from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref -from notmuch.globals import nmlib, STATUS, NotmuchError, Enum +from notmuch.globals import (nmlib, STATUS, NotmuchError, NotInitializedError, + NullPointerError, OutOfMemoryError, XapianError, Enum, _str) from notmuch.thread import Threads from notmuch.message import Messages, Message from notmuch.tag import Tags class Database(object): - """Represents a notmuch database (wraps notmuch_database_t) + """The :class:`Database` is the highest-level object that notmuch + provides. It references a notmuch database, and can be opened in + read-only or read-write mode. A :class:`Query` can be derived from + or be applied to a specific database to find messages. Also adding + and removing messages to the database happens via this + object. Modifications to the database are not atmic by default (see + :meth:`begin_atomic`) and once a database has been modified, all + other database objects pointing to the same data-base will throw an + :exc:`XapianError` as the underlying database has been + modified. Close and reopen the database to continue working with it. - .. note:: Do remember that as soon as we tear down this object, - all underlying derived objects such as queries, threads, - messages, tags etc will be freed by the underlying library - as well. Accessing these objects will lead to segfaults and - other unexpected behavior. See above for more details. + .. note:: Any function in this class can and will throw an + :exc:`NotInitializedError` if the database was not + intitialized properly. + + .. note:: Do remember that as soon as we tear down (e.g. via `del + db`) this object, all underlying derived objects such as + queries, threads, messages, tags etc will be freed by the + underlying library as well. Accessing these objects will lead + to segfaults and other unexpected behavior. See above for + more details. """ _std_db_path = None """Class attribute to cache user's default database""" - MODE = Enum(['READ_ONLY','READ_WRITE']) + MODE = Enum(['READ_ONLY', 'READ_WRITE']) """Constants: Mode in which to open the database""" """notmuch_database_get_directory""" @@ -52,7 +67,7 @@ class Database(object): _get_version.restype = c_uint """notmuch_database_open""" - _open = nmlib.notmuch_database_open + _open = nmlib.notmuch_database_open _open.restype = c_void_p """notmuch_database_upgrade""" @@ -61,7 +76,9 @@ class Database(object): """ notmuch_database_find_message""" _find_message = nmlib.notmuch_database_find_message - _find_message.restype = c_void_p + + """notmuch_database_find_message_by_filename""" + _find_message_by_filename = nmlib.notmuch_database_find_message_by_filename """notmuch_database_get_all_tags""" _get_all_tags = nmlib.notmuch_database_get_all_tags @@ -71,9 +88,9 @@ class Database(object): _create = nmlib.notmuch_database_create _create.restype = c_void_p - def __init__(self, path=None, create=False, mode= 0): - """If *path* is `None`, we will try to read a users notmuch - configuration and use his configured database. The location of the + def __init__(self, path=None, create=False, mode=0): + """If *path* is `None`, we will try to read a users notmuch + configuration and use his configured database. The location of the configuration file can be specified through the environment variable *NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`. @@ -84,13 +101,13 @@ class Database(object): above for behavior if `None`) :type path: `str` or `None` :param create: Pass `False` to open an existing, `True` to create a new - database. + database. :type create: bool - :param mode: Mode to open a database in. Is always + :param mode: Mode to open a database in. Is always :attr:`MODE`.READ_WRITE when creating a new one. :type mode: :attr:`MODE` - :returns: Nothing - :exception: :exc:`NotmuchError` in case of failure. + :exception: :exc:`NotmuchError` or derived exception in case of + failure. """ self._db = None if path is None: @@ -100,16 +117,15 @@ class Database(object): Database._std_db_path = self._get_user_default_db() path = Database._std_db_path - assert isinstance(path, basestring), 'Path needs to be a string or None.' if create == False: self.open(path, mode) else: self.create(path) - def _verify_initialized_db(self): - """Raises a NotmuchError in case self._db is still None""" + def _assert_db_is_initialized(self): + """Raises :exc:`NotInitializedError` if self._db is `None`""" if self._db is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + raise NotInitializedError() def create(self, path): """Creates a new notmuch database @@ -125,20 +141,20 @@ class Database(object): :type path: str :returns: Nothing :exception: :exc:`NotmuchError` in case of any failure - (after printing an error message on stderr). + (possibly after printing an error message on stderr). """ if self._db is not None: - raise NotmuchError( - message="Cannot create db, this Database() already has an open one.") + raise NotmuchError(message="Cannot create db, this Database() " + "already has an open one.") - res = Database._create(path, Database.MODE.READ_WRITE) + res = Database._create(_str(path), Database.MODE.READ_WRITE) if res is None: raise NotmuchError( message="Could not create the specified database") self._db = res - def open(self, path, mode= 0): + def open(self, path, mode=0): """Opens an existing database This function is used by __init__() and usually does not need @@ -146,39 +162,29 @@ class Database(object): *notmuch_database_open* function. :param status: Open the database in read-only or read-write mode - :type status: :attr:`MODE` + :type status: :attr:`MODE` :returns: Nothing :exception: Raises :exc:`NotmuchError` in case - of any failure (after printing an error message on stderr). + of any failure (possibly after printing an error message on stderr). """ - - res = Database._open(path, mode) + res = Database._open(_str(path), mode) if res is None: - raise NotmuchError( - message="Could not open the specified database") + raise NotmuchError(message="Could not open the specified database") self._db = res def get_path(self): - """Returns the file path of an open database - - Wraps *notmuch_database_get_path*.""" - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - - return Database._get_path(self._db) + """Returns the file path of an open database""" + self._assert_db_is_initialized() + return Database._get_path(self._db).decode('utf-8') def get_version(self): """Returns the database format version :returns: The database version as positive integer - :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if - the database was not intitialized. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - - return Database._get_version (self._db) + self._assert_db_is_initialized() + return Database._get_version(self._db) def needs_upgrade(self): """Does this database need to be upgraded before writing to it? @@ -189,13 +195,9 @@ class Database(object): etc.) will work unless :meth:`upgrade` is called successfully first. :returns: `True` or `False` - :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if - the database was not intitialized. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - - return notmuch_database_needs_upgrade(self._db) + self._assert_db_is_initialized() + return nmlib.notmuch_database_needs_upgrade(self._db) def upgrade(self): """Upgrades the current database @@ -207,72 +209,109 @@ class Database(object): NOT IMPLEMENTED: The optional progress_notify callback can be used by the caller to provide progress indication to the user. If non-NULL it will be called periodically with - 'progress' as a floating-point value in the range of [0.0..1.0] + 'progress' as a floating-point value in the range of [0.0..1.0] indicating the progress made so far in the upgrade process. :TODO: catch exceptions, document return values and etc... """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - - status = Database._upgrade (self._db, None, None) + self._assert_db_is_initialized() + status = Database._upgrade(self._db, None, None) #TODO: catch exceptions, document return values and etc return status + def begin_atomic(self): + """Begin an atomic database operation + + Any modifications performed between a successful + :meth:`begin_atomic` and a :meth:`end_atomic` will be applied to + the database atomically. Note that, unlike a typical database + transaction, this only ensures atomicity, not durability; + neither begin nor end necessarily flush modifications to disk. + + :returns: :attr:`STATUS`.SUCCESS or raises + + :exception: :exc:`NotmuchError`: + :attr:`STATUS`.XAPIAN_EXCEPTION + Xapian exception occurred; atomic section not entered. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + status = nmlib.notmuch_database_begin_atomic(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return status + + def end_atomic(self): + """Indicate the end of an atomic database operation + + See :meth:`begin_atomic` for details. + + :returns: :attr:`STATUS`.SUCCESS or raises + + :exception: + :exc:`NotmuchError`: + :attr:`STATUS`.XAPIAN_EXCEPTION + A Xapian exception occurred; atomic section not + ended. + :attr:`STATUS`.UNBALANCED_ATOMIC: + end_atomic has been called more times than begin_atomic. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + status = nmlib.notmuch_database_end_atomic(self._db) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return status + def get_directory(self, path): - """Returns a :class:`Directory` of path, + """Returns a :class:`Directory` of path, (creating it if it does not exist(?)) - .. warning:: This call needs a writeable database in - Database.MODE.READ_WRITE mode. The underlying library will exit the + .. warning:: This call needs a writeable database in + :attr:`Database.MODE`.READ_WRITE mode. The underlying library will exit the program if this method is used on a read-only database! - :param path: A str containing the path relative to the path of database - (see :meth:`get_path`), or else should be an absolute path + :param path: An unicode string containing the path relative to the path + of database (see :meth:`get_path`), or else should be an absolute path with initial components that match the path of 'database'. :returns: :class:`Directory` or raises an exception. - :exception: :exc:`NotmuchError` - - STATUS.NOT_INITIALIZED - If the database was not intitialized. - - STATUS.FILE_ERROR - If path is not relative database or absolute with initial + :exception: + :exc:`NotmuchError` with :attr:`STATUS`.FILE_ERROR + If path is not relative database or absolute with initial components same as database. - """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - + self._assert_db_is_initialized() # sanity checking if path is valid, and make path absolute if path[0] == os.sep: # we got an absolute path if not path.startswith(self.get_path()): # but its initial components are not equal to the db path - raise NotmuchError(STATUS.FILE_ERROR, - message="Database().get_directory() called with a wrong absolute path.") + raise NotmuchError(STATUS.FILE_ERROR, + message="Database().get_directory() called " + "with a wrong absolute path.") abs_dirpath = path else: #we got a relative path, make it absolute - abs_dirpath = os.path.abspath(os.path.join(self.get_path(),path)) + abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path)) - dir_p = Database._get_directory(self._db, path); + dir_p = Database._get_directory(self._db, _str(path)) # return the Directory, init it with the absolute path - return Directory(abs_dirpath, dir_p, self) + return Directory(_str(abs_dirpath), dir_p, self) def add_message(self, filename, sync_maildir_flags=False): """Adds a new message to the database - :param filename: should be a path relative to the path of the open - database (see :meth:`get_path`), or else should be an absolute - filename with initial components that match the path of the - database. + :param filename: should be a path relative to the path of the + open database (see :meth:`get_path`), or else should be an + absolute filename with initial components that match the + path of the database. - The file should be a single mail message (not a multi-message mbox) - that is expected to remain at its current location, since the - notmuch database will reference the filename, and will not copy the - entire contents of the file. + The file should be a single mail message (not a + multi-message mbox) that is expected to remain at its + current location, since the notmuch database will reference + the filename, and will not copy the entire contents of the + file. :param sync_maildir_flags: If the message contains Maildir flags, we will -depending on the notmuch configuration- sync @@ -281,43 +320,40 @@ class Database(object): API. You might want to look into the underlying method :meth:`Message.maildir_flags_to_tags`. - :returns: On success, we return + :returns: On success, we return 1) a :class:`Message` object that can be used for things such as adding tags to the just-added message. - 2) one of the following STATUS values: + 2) one of the following :attr:`STATUS` values: - STATUS.SUCCESS + :attr:`STATUS`.SUCCESS Message successfully added to database. - STATUS.DUPLICATE_MESSAGE_ID + :attr:`STATUS`.DUPLICATE_MESSAGE_ID Message has the same message ID as another message already in the database. The new filename was successfully added - to the message in the database. + to the list of the filenames for the existing message. - :rtype: 2-tuple(:class:`Message`, STATUS) + :rtype: 2-tuple(:class:`Message`, :attr:`STATUS`) :exception: Raises a :exc:`NotmuchError` with the following meaning. If such an exception occurs, nothing was added to the database. - STATUS.FILE_ERROR - An error occurred trying to open the file, (such as + :attr:`STATUS`.FILE_ERROR + An error occurred trying to open the file, (such as permission denied, or file not found, etc.). - STATUS.FILE_NOT_EMAIL - The contents of filename don't look like an email message. - STATUS.READ_ONLY_DATABASE + :attr:`STATUS`.FILE_NOT_EMAIL + The contents of filename don't look like an email + message. + :attr:`STATUS`.READ_ONLY_DATABASE Database was opened in read-only mode so no message can be added. - STATUS.NOT_INITIALIZED - The database has not been initialized. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - + self._assert_db_is_initialized() msg_p = c_void_p() status = nmlib.notmuch_database_add_message(self._db, - filename, + _str(filename), byref(msg_p)) - + if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]: raise NotmuchError(status) @@ -329,7 +365,7 @@ class Database(object): return (msg, status) def remove_message(self, filename): - """Removes a message from the given notmuch database + """Removes a message (filename) from the given notmuch database Note that only this particular filename association is removed from the database. If the same message (as determined by the message ID) @@ -338,27 +374,24 @@ class Database(object): is removed for a particular message, the database content for that message will be entirely removed. - :returns: A STATUS value with the following meaning: + :returns: A :attr:`STATUS` value with the following meaning: - STATUS.SUCCESS - The last filename was removed and the message was removed + :attr:`STATUS`.SUCCESS + The last filename was removed and the message was removed from the database. - STATUS.DUPLICATE_MESSAGE_ID - This filename was removed but the message persists in the + :attr:`STATUS`.DUPLICATE_MESSAGE_ID + This filename was removed but the message persists in the database with at least one other filename. :exception: Raises a :exc:`NotmuchError` with the following meaning. - If such an exception occurs, nothing was removed from the database. + If such an exception occurs, nothing was removed from the + database. - STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so no message can be + :attr:`STATUS`.READ_ONLY_DATABASE + Database was opened in read-only mode so no message can be removed. - STATUS.NOT_INITIALIZED - The database has not been initialized. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - + self._assert_db_is_initialized() return nmlib.notmuch_database_remove_message(self._db, filename) @@ -368,36 +401,69 @@ class Database(object): Wraps the underlying *notmuch_database_find_message* function. :param msgid: The message ID - :type msgid: string - :returns: :class:`Message` or `None` if no message is found or - if any xapian exception or out-of-memory situation - occurs. Do note that Xapian Exceptions include - "Database modified" situations, e.g. when the - notmuch database has been modified by - another program in the meantime. A return value of - `None` is therefore no guarantee that the message - does not exist. - :exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if - the database was not intitialized. + :type msgid: unicode or str + :returns: :class:`Message` or `None` if no message is found. + :exception: + :exc:`OutOfMemoryError` + If an Out-of-memory occured while constructing the message. + :exc:`XapianError` + In case of a Xapian Exception. These exceptions + include "Database modified" situations, e.g. when the + notmuch database has been modified by another program + in the meantime. In this case, you should close and + reopen the database and retry. + :exc:`NotInitializedError` if + the database was not intitialized. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() + self._assert_db_is_initialized() + msg_p = c_void_p() + status = Database._find_message(self._db, _str(msgid), byref(msg_p)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return msg_p and Message(msg_p, self) or None + + def find_message_by_filename(self, filename): + """Find a message with the given filename - msg_p = Database._find_message(self._db, msgid) - if msg_p is None: - return None - return Message(msg_p, self) + .. warning:: This call needs a writeable database in + :attr:`Database.MODE`.READ_WRITE mode. The underlying library will + exit the program if this method is used on a read-only + database! + + :returns: If the database contains a message with the given + filename, then a class:`Message:` is returned. This + function returns None if no message is found with the given + filename. + + :exception: + :exc:`OutOfMemoryError` + If an Out-of-memory occured while constructing the message. + :exc:`XapianError` + In case of a Xapian Exception. These exceptions + include "Database modified" situations, e.g. when the + notmuch database has been modified by another program + in the meantime. In this case, you should close and + reopen the database and retry. + :exc:`NotInitializedError` if + the database was not intitialized. + + *Added in notmuch 0.9*""" + self._assert_db_is_initialized() + msg_p = c_void_p() + status = Database._find_message_by_filename(self._db, _str(filename), + byref(msg_p)) + if status != STATUS.SUCCESS: + raise NotmuchError(status) + return msg_p and Message(msg_p, self) or None def get_all_tags(self): """Returns :class:`Tags` with a list of all tags found in the database :returns: :class:`Tags` - :execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error + :execption: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER on error """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - - tags_p = Database._get_all_tags (self._db) + self._assert_db_is_initialized() + tags_p = Database._get_all_tags(self._db) if tags_p == None: raise NotmuchError(STATUS.NULL_POINTER) return Tags(tags_p, self) @@ -420,9 +486,6 @@ class Database(object): This function is a python extension and not in the underlying C API. """ - # Raise a NotmuchError if not initialized - self._verify_initialized_db() - return Query(self, querystring) def __repr__(self): @@ -442,10 +505,10 @@ class Database(object): conf_f = os.getenv('NOTMUCH_CONFIG', os.path.expanduser('~/.notmuch-config')) config.read(conf_f) - if not config.has_option('database','path'): - raise NotmuchError(message= - "No DB path specified and no user default found") - return config.get('database','path') + if not config.has_option('database', 'path'): + raise NotmuchError(message="No DB path specified" + " and no user default found") + return config.get('database', 'path').decode('utf-8') @property def db_p(self): @@ -456,18 +519,19 @@ class Database(object): """ return self._db -#------------------------------------------------------------------------------ + class Query(object): """Represents a search query on an opened :class:`Database`. A query selects and filters a subset of messages from the notmuch database we derive from. - Query() provides an instance attribute :attr:`sort`, which + :class:`Query` provides an instance attribute :attr:`sort`, which contains the sort order (if specified via :meth:`set_sort`) or `None`. - Technically, it wraps the underlying *notmuch_query_t* struct. + Any function in this class may throw an :exc:`NotInitializedError` + in case the underlying query object was not set up correctly. .. note:: Do remember that as soon as we tear down this object, all underlying derived objects such as threads, @@ -476,7 +540,7 @@ class Query(object): other unexpected behavior. See above for more details. """ # constants - SORT = Enum(['OLDEST_FIRST','NEWEST_FIRST','MESSAGE_ID', 'UNSORTED']) + SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED']) """Constants: Sort order in which to return results""" """notmuch_query_create""" @@ -491,7 +555,6 @@ class Query(object): _search_messages = nmlib.notmuch_query_search_messages _search_messages.restype = c_void_p - """notmuch_query_count_messages""" _count_messages = nmlib.notmuch_query_count_messages _count_messages.restype = c_uint @@ -501,54 +564,50 @@ class Query(object): :param db: An open database which we derive the Query from. :type db: :class:`Database` :param querystr: The query string for the message. - :type querystr: str + :type querystr: utf-8 encoded str or unicode """ self._db = None self._query = None self.sort = None self.create(db, querystr) + def _assert_query_is_initialized(self): + """Raises :exc:`NotInitializedError` if self._query is `None`""" + if self._query is None: + raise NotInitializedError() + def create(self, db, querystr): """Creates a new query derived from a Database - This function is utilized by __init__() and usually does not need to + This function is utilized by __init__() and usually does not need to be called directly. :param db: Database to create the query from. :type db: :class:`Database` :param querystr: The query string - :type querystr: str + :type querystr: utf-8 encoded str or unicode :returns: Nothing - :exception: :exc:`NotmuchError` - - * STATUS.NOT_INITIALIZED if db is not inited - * STATUS.NULL_POINTER if the query creation failed - (too little memory) + :exception: + :exc:`NullPointerError` if the query creation failed + (e.g. too little memory). + :exc:`NotInitializedError` if the underlying db was not + intitialized. """ - if db.db_p is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + db._assert_db_is_initialized() # create reference to parent db to keep it alive self._db = db - # create query, return None if too little mem available - query_p = Query._create(db.db_p, querystr) + query_p = Query._create(db.db_p, _str(querystr)) if query_p is None: - NotmuchError(STATUS.NULL_POINTER) + raise NullPointerError self._query = query_p def set_sort(self, sort): """Set the sort order future results will be delivered in - Wraps the underlying *notmuch_query_set_sort* function. - :param sort: Sort order (see :attr:`Query.SORT`) - :returns: Nothing - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not - been initialized. """ - if self._query is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - + self._assert_query_is_initialized() self.sort = sort nmlib.notmuch_query_set_sort(self._query, sort) @@ -556,54 +615,36 @@ class Query(object): """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 + The returned threads are owned by the query and as such, will only be valid until the Query is deleted. The method sets :attr:`Message.FLAG`\.MATCH for those messages that match the query. The method :meth:`Message.get_flag` allows us to get the value of this flag. - Technically, it wraps the underlying - *notmuch_query_search_threads* function. - :returns: :class:`Threads` - :exception: :exc:`NotmuchError` - - * STATUS.NOT_INITIALIZED if query is not inited - * STATUS.NULL_POINTER if search_threads failed + :exception: :exc:`NullPointerError` if search_threads failed """ - if self._query is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - + self._assert_query_is_initialized() threads_p = Query._search_threads(self._query) if threads_p is None: - NotmuchError(STATUS.NULL_POINTER) - - return Threads(threads_p,self) + raise NullPointerError + return Threads(threads_p, self) def search_messages(self): """Filter messages according to the query and return :class:`Messages` in the defined sort order - Technically, it wraps the underlying - *notmuch_query_search_messages* function. - :returns: :class:`Messages` - :exception: :exc:`NotmuchError` - - * STATUS.NOT_INITIALIZED if query is not inited - * STATUS.NULL_POINTER if search_messages failed + :exception: :exc:`NullPointerError` if search_messages failed """ - if self._query is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - + self._assert_query_is_initialized() msgs_p = Query._search_messages(self._query) if msgs_p is None: - NotmuchError(STATUS.NULL_POINTER) - - return Messages(msgs_p,self) + raise NullPointerError + return Messages(msgs_p, self) def count_messages(self): """Estimate the number of messages matching the query @@ -616,22 +657,16 @@ class Query(object): *notmuch_query_count_messages* function. :returns: :class:`Messages` - :exception: :exc:`NotmuchError` - - * STATUS.NOT_INITIALIZED if query is not inited """ - if self._query is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - + self._assert_query_is_initialized() return Query._count_messages(self._query) def __del__(self): """Close and free the Query""" if self._query is not None: - nmlib.notmuch_query_destroy (self._query) + nmlib.notmuch_query_destroy(self._query) -#------------------------------------------------------------------------------ class Directory(object): """Represents a directory entry in the notmuch directory @@ -662,14 +697,14 @@ class Directory(object): _get_child_directories = nmlib.notmuch_directory_get_child_directories _get_child_directories.restype = c_void_p - def _verify_dir_initialized(self): - """Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None""" + def _assert_dir_is_initialized(self): + """Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED) if dir_p is None""" if self._dir_p is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + raise NotmuchError(STATUS.NOT_INITIALIZED) def __init__(self, path, dir_p, parent): """ - :param path: The absolute path of the directory object. + :param path: The absolute path of the directory object as unicode. :param dir_p: The pointer to an internal notmuch_directory_t object. :param parent: The object this Directory is derived from (usually a :class:`Database`). We do not directly use @@ -677,12 +712,12 @@ class Directory(object): this Directory object lives. This keeps the parent object alive. """ + assert isinstance(path, unicode), "Path needs to be an UNICODE object" self._path = path self._dir_p = dir_p self._parent = parent - - def set_mtime (self, mtime): + def set_mtime(self, mtime): """Sets the mtime value of this directory in the database The intention is for the caller to use the mtime to allow efficient @@ -690,36 +725,34 @@ class Directory(object): recommended usage is as follows: * Read the mtime of a directory from the filesystem - + * Call :meth:`Database.add_message` for all mail files in the directory - * Call notmuch_directory_set_mtime with the mtime read from the + * Call notmuch_directory_set_mtime with the mtime read from the filesystem. Then, when wanting to check for updates to the directory in the future, the client can call :meth:`get_mtime` - and know that it only needs to add files if the mtime of the + and know that it only needs to add files if the mtime of the directory and files are newer than the stored timestamp. - .. note:: :meth:`get_mtime` function does not allow the caller + .. note:: :meth:`get_mtime` function does not allow the caller to distinguish a timestamp of 0 from a non-existent timestamp. So don't store a timestamp of 0 unless you are - comfortable with that. + comfortable with that. - :param mtime: A (time_t) timestamp + :param mtime: A (time_t) timestamp :returns: Nothing on success, raising an exception on failure. :exception: :exc:`NotmuchError`: - STATUS.XAPIAN_EXCEPTION + :attr:`STATUS`.XAPIAN_EXCEPTION A Xapian exception occurred, mtime not stored. - STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so directory + :attr:`STATUS`.READ_ONLY_DATABASE + Database was opened in read-only mode so directory mtime cannot be modified. - STATUS.NOT_INITIALIZED + :attr:`STATUS`.NOT_INITIALIZED The directory has not been initialized """ - #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None - self._verify_dir_initialized() - + self._assert_dir_is_initialized() #TODO: make sure, we convert the mtime parameter to a 'c_long' status = Directory._set_mtime(self._dir_p, mtime) @@ -729,53 +762,47 @@ class Directory(object): #fail with Exception otherwise raise NotmuchError(status) - def get_mtime (self): + def get_mtime(self): """Gets the mtime value of this directory in the database Retrieves a previously stored mtime for this directory. - :param mtime: A (time_t) timestamp + :param mtime: A (time_t) timestamp :returns: Nothing on success, raising an exception on failure. :exception: :exc:`NotmuchError`: - STATUS.NOT_INITIALIZED + :attr:`STATUS`.NOT_INITIALIZED The directory has not been initialized """ - #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None - self._verify_dir_initialized() - - return Directory._get_mtime (self._dir_p) + self._assert_dir_is_initialized() + return Directory._get_mtime(self._dir_p) # Make mtime attribute a property of Directory() mtime = property(get_mtime, set_mtime, doc="""Property that allows getting and setting of the Directory *mtime* (read-write) - See :meth:`get_mtime` and :meth:`set_mtime` for usage and + See :meth:`get_mtime` and :meth:`set_mtime` for usage and possible exceptions.""") def get_child_files(self): """Gets a Filenames iterator listing all the filenames of messages in the database within the given directory. - + The returned filenames will be the basename-entries only (not complete paths. """ - #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None - self._verify_dir_initialized() - + self._assert_dir_is_initialized() files_p = Directory._get_child_files(self._dir_p) return Filenames(files_p, self) def get_child_directories(self): """Gets a :class:`Filenames` iterator listing all the filenames of sub-directories in the database within the given directory - + The returned filenames will be the basename-entries only (not complete paths. """ - #Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None - self._verify_dir_initialized() - + self._assert_dir_is_initialized() files_p = Directory._get_child_directories(self._dir_p) return Filenames(files_p, self) @@ -793,10 +820,9 @@ class Directory(object): if self._dir_p is not None: nmlib.notmuch_directory_destroy(self._dir_p) -#------------------------------------------------------------------------------ + class Filenames(object): - """An iterator over File- or Directory names that are stored in the database - """ + """An iterator over File- or Directory names stored in the database""" #notmuch_filenames_get _get = nmlib.notmuch_filenames_get @@ -826,26 +852,26 @@ class Filenames(object): self._files_p = None raise StopIteration - file = Filenames._get (self._files_p) + file = Filenames._get(self._files_p) nmlib.notmuch_filenames_move_to_next(self._files_p) return file def __len__(self): """len(:class:`Filenames`) returns the number of contained files - .. note:: As this iterates over the files, we will not be able to + .. note:: As this iterates over the files, we will not be able to iterate over them again! So this will fail:: #THIS FAILS files = Database().get_directory('').get_child_files() if len(files) > 0: #this 'exhausts' msgs - # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!! + # next line raises NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)!!! for file in files: print file """ if self._files_p is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - i=0 + i = 0 while nmlib.notmuch_filenames_valid(self._files_p): nmlib.notmuch_filenames_move_to_next(self._files_p) i += 1 diff --git a/bindings/python/notmuch/filename.py b/bindings/python/notmuch/filename.py index 20b90e98..de4d785a 100644 --- a/bindings/python/notmuch/filename.py +++ b/bindings/python/notmuch/filename.py @@ -19,19 +19,20 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>' from ctypes import c_char_p from notmuch.globals import nmlib, STATUS, NotmuchError -#------------------------------------------------------------------------------ + class Filenames(object): """Represents a list of filenames as returned by notmuch - This object contains the Filenames iterator. The main function is as_generator() which will return a generator so we can do a Filenamesth an iterator over a list of notmuch filenames. Do - note that the underlying library only provides a one-time iterator - (it cannot reset the iterator to the start). Thus iterating over - the function will "exhaust" the list of tags, and a subsequent - iteration attempt will raise a :exc:`NotmuchError` - STATUS.NOT_INITIALIZED. Also note, that any function that uses - iteration (nearly all) will also exhaust the tags. So both:: + This object contains the Filenames iterator. The main function is + as_generator() which will return a generator so we can do a Filenamesth an + iterator over a list of notmuch filenames. Do note that the underlying + library only provides a one-time iterator (it cannot reset the iterator to + the start). Thus iterating over the function will "exhaust" the list of + tags, and a subsequent iteration attempt will raise a :exc:`NotmuchError` + STATUS.NOT_INITIALIZED. Also note, that any function that uses iteration + (nearly all) will also exhaust the tags. So both:: - for name in filenames: print name + for name in filenames: print name as well as:: @@ -67,7 +68,7 @@ class Filenames(object): once all derived objects are dead. """ if files_p is None: - NotmuchError(STATUS.NULL_POINTER) + raise NotmuchError(STATUS.NULL_POINTER) self._files = files_p #save reference to parent object so we keep it alive @@ -81,14 +82,12 @@ class Filenames(object): if self._files is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - if not nmlib.notmuch_filenames_valid(self._files): - self._files = None - return + while nmlib.notmuch_filenames_valid(self._files): + yield Filenames._get(self._files) + nmlib.notmuch_filenames_move_to_next(self._files) + + self._files = None - file = Filenames._get (self._files) - nmlib.notmuch_filenames_move_to_next(self._files) - yield file - def __str__(self): """Represent Filenames() as newline-separated list of full paths @@ -105,4 +104,4 @@ class Filenames(object): def __del__(self): """Close and free the notmuch filenames""" if self._files is not None: - nmlib.notmuch_filenames_destroy (self._files) + nmlib.notmuch_filenames_destroy(self._files) diff --git a/bindings/python/notmuch/globals.py b/bindings/python/notmuch/globals.py index c675d044..de1db161 100644 --- a/bindings/python/notmuch/globals.py +++ b/bindings/python/notmuch/globals.py @@ -23,18 +23,18 @@ from ctypes.util import find_library #----------------------------------------------------------------------------- #package-global instance of the notmuch library try: - nmlib = CDLL("libnotmuch.so.1") + nmlib = CDLL("libnotmuch.so.2") except: raise ImportError("Could not find shared 'notmuch' library.") -#----------------------------------------------------------------------------- + class Enum(object): """Provides ENUMS as "code=Enum(['a','b','c'])" where code.a=0 etc...""" def __init__(self, names): for number, name in enumerate(names): setattr(self, name, number) -#----------------------------------------------------------------------------- + class Status(Enum): """Enum with a string representation of a notmuch_status_t value.""" _status2str = nmlib.notmuch_status_to_string @@ -49,10 +49,10 @@ class Status(Enum): @classmethod def status2str(self, status): - """Get a string representation of a notmuch_status_t value.""" + """Get a string representation of a notmuch_status_t value.""" # define strings for custom error messages if status == STATUS.NOT_INITIALIZED: - return "Operation on uninitialized object impossible." + return "Operation on uninitialized object impossible." return str(Status._status2str(status)) STATUS = Status(['SUCCESS', @@ -65,8 +65,12 @@ STATUS = Status(['SUCCESS', 'NULL_POINTER', 'TAG_TOO_LONG', 'UNBALANCED_FREEZE_THAW', + 'UNBALANCED_ATOMIC', 'NOT_INITIALIZED']) -"""STATUS is a class, whose attributes provide constants that serve as return indicators for notmuch functions. Currently the following ones are defined. For possible return values and specific meaning for each method, see the method description. +"""STATUS is a class, whose attributes provide constants that serve as return +indicators for notmuch functions. Currently the following ones are defined. For +possible return values and specific meaning for each method, see the method +description. * SUCCESS * OUT_OF_MEMORY @@ -78,17 +82,103 @@ STATUS = Status(['SUCCESS', * NULL_POINTER * TAG_TOO_LONG * UNBALANCED_FREEZE_THAW + * UNBALANCED_ATOMIC * NOT_INITIALIZED - Invoke the class method `notmuch.STATUS.status2str` with a status value as argument to receive a human readable string""" -STATUS.__name__ = 'STATUS' +Invoke the class method `notmuch.STATUS.status2str` with a status value as +argument to receive a human readable string""" +STATUS.__name__ = 'STATUS' class NotmuchError(Exception): + """Is initiated with a (notmuch.STATUS[, message=None]). It will not + return an instance of the class NotmuchError, but a derived instance + of a more specific Error Message, e.g. OutOfMemoryError. Each status + but SUCCESS has a corresponding subclassed Exception.""" + + @classmethod + def get_exc_subclass(cls, status): + """Returns a fine grained Exception() type,detailing the error status""" + subclasses = { + STATUS.OUT_OF_MEMORY: OutOfMemoryError, + STATUS.READ_ONLY_DATABASE: ReadOnlyDatabaseError, + STATUS.XAPIAN_EXCEPTION: XapianError, + STATUS.FILE_ERROR: FileError, + STATUS.FILE_NOT_EMAIL: FileNotEmailError, + STATUS.DUPLICATE_MESSAGE_ID: DuplicateMessageIdError, + STATUS.NULL_POINTER: NullPointerError, + STATUS.TAG_TOO_LONG: TagTooLongError, + STATUS.UNBALANCED_FREEZE_THAW: UnbalancedFreezeThawError, + STATUS.UNBALANCED_ATOMIC: UnbalancedAtomicError, + STATUS.NOT_INITIALIZED: NotInitializedError + } + assert 0 < status <= len(subclasses) + return subclasses[status] + + def __new__(cls, *args, **kwargs): + """Return a correct subclass of NotmuchError if needed + + We return a NotmuchError instance if status is None (or 0) and a + subclass that inherits from NotmuchError depending on the + 'status' parameter otherwise.""" + # get 'status'. Passed in as arg or kwarg? + status = args[0] if len(args) else kwargs.get('status', None) + # no 'status' or cls is subclass already, return 'cls' instance + if not status or cls != NotmuchError: + return super(NotmuchError, cls).__new__(cls) + subclass = cls.get_exc_subclass(status) # which class to use? + return subclass.__new__(subclass, *args, **kwargs) + def __init__(self, status=None, message=None): - """Is initiated with a (notmuch.STATUS[,message=None])""" - super(NotmuchError, self).__init__(message, status) + self.status = status + self.message = message def __str__(self): - if self.args[0] is not None: return self.args[0] - else: return STATUS.status2str(self.args[1]) + if self.message is not None: + return self.message + elif self.status is not None: + return STATUS.status2str(self.status) + else: + return 'Unknown error' + +# List of Subclassed exceptions that correspond to STATUS values and are +# subclasses of NotmuchError. +class OutOfMemoryError(NotmuchError): + status = STATUS.OUT_OF_MEMORY +class ReadOnlyDatabaseError(NotmuchError): + status = STATUS.READ_ONLY_DATABASE +class XapianError(NotmuchError): + status = STATUS.XAPIAN_EXCEPTION +class FileError(NotmuchError): + status = STATUS.FILE_ERROR +class FileNotEmailError(NotmuchError): + status = STATUS.FILE_NOT_EMAIL +class DuplicateMessageIdError(NotmuchError): + status = STATUS.DUPLICATE_MESSAGE_ID +class NullPointerError(NotmuchError): + status = STATUS.NULL_POINTER +class TagTooLongError(NotmuchError): + status = STATUS.TAG_TOO_LONG +class UnbalancedFreezeThawError(NotmuchError): + status = STATUS.UNBALANCED_FREEZE_THAW +class UnbalancedAtomicError(NotmuchError): + status = STATUS.UNBALANCED_ATOMIC +class NotInitializedError(NotmuchError): + """Derived from NotmuchError, this occurs if the underlying data + structure (e.g. database is not initialized (yet) or an iterator has + been exhausted. You can test for NotmuchError with .status = + STATUS.NOT_INITIALIZED""" + status = STATUS.NOT_INITIALIZED + + + +def _str(value): + """Ensure a nicely utf-8 encoded string to pass to libnotmuch + + C++ code expects strings to be well formatted and + unicode strings to have no null bytes.""" + if not isinstance(value, basestring): + raise TypeError("Expected str or unicode, got %s" % str(type(value))) + if isinstance(value, unicode): + return value.encode('UTF-8') + return value diff --git a/bindings/python/notmuch/message.py b/bindings/python/notmuch/message.py index 763d2c68..4bf90c22 100644 --- a/bindings/python/notmuch/message.py +++ b/bindings/python/notmuch/message.py @@ -18,10 +18,10 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>' Jesse Rosenthal <jrosenthal@jhu.edu> """ - + from ctypes import c_char_p, c_void_p, c_long, c_uint, c_int from datetime import date -from notmuch.globals import nmlib, STATUS, NotmuchError, Enum +from notmuch.globals import nmlib, STATUS, NotmuchError, Enum, _str from notmuch.tag import Tags from notmuch.filename import Filenames import sys @@ -31,7 +31,8 @@ try: import simplejson as json except ImportError: import json -#------------------------------------------------------------------------------ + + class Messages(object): """Represents a list of notmuch messages @@ -63,8 +64,8 @@ class Messages(object): # However it will be kept alive until all retrieved Message() # objects are also deleted. If you do e.g. an explicit del(msgs) # here, the following lines would fail. - - # You can reiterate over *msglist* however as often as you want. + + # You can reiterate over *msglist* however as often as you want. # It is simply a list with :class:`Message`s. print (msglist[0].get_filename()) @@ -110,11 +111,11 @@ class Messages(object): (ie :class:`Query`) these tags are derived from. It saves a reference to it, so we can automatically delete the db object once all derived objects are dead. - :TODO: Make the iterator work more than once and cache the tags in + :TODO: Make the iterator work more than once and cache the tags in the Python object.(?) """ if msgs_p is None: - NotmuchError(STATUS.NULL_POINTER) + raise NotmuchError(STATUS.NULL_POINTER) self._msgs = msgs_p #store parent, so we keep them alive as long as self is alive @@ -133,7 +134,7 @@ class Messages(object): raise NotmuchError(STATUS.NOT_INITIALIZED) # collect all tags (returns NULL on error) - tags_p = Messages._collect_tags (self._msgs) + tags_p = Messages._collect_tags(self._msgs) #reset _msgs as we iterated over it and can do so only once self._msgs = None @@ -153,7 +154,7 @@ class Messages(object): self._msgs = None raise StopIteration - msg = Message(Messages._get (self._msgs), self) + msg = Message(Messages._get(self._msgs), self) nmlib.notmuch_messages_move_to_next(self._msgs) return msg @@ -167,14 +168,14 @@ class Messages(object): def __del__(self): """Close and free the notmuch Messages""" if self._msgs is not None: - nmlib.notmuch_messages_destroy (self._msgs) + nmlib.notmuch_messages_destroy(self._msgs) def print_messages(self, format, indent=0, entire_thread=False): """Outputs messages as needed for 'notmuch show' to sys.stdout :param format: A string of either 'text' or 'json'. :param indent: A number indicating the reply depth of these messages. - :param entire_thread: A bool, indicating whether we want to output + :param entire_thread: A bool, indicating whether we want to output whole threads or only the matching messages. """ if format.lower() == "text": @@ -186,7 +187,7 @@ class Messages(object): set_end = "]" set_sep = ", " else: - raise Exception + raise TypeError("format must be either 'text' or 'json'") first_set = True @@ -195,7 +196,7 @@ class Messages(object): # iterate through all toplevel messages in this thread for msg in self: # if not msg: - # break + # break if not first_set: sys.stdout.write(set_sep) first_set = False @@ -207,10 +208,8 @@ class Messages(object): if (match or entire_thread): if format.lower() == "text": sys.stdout.write(msg.format_message_as_text(indent)) - elif format.lower() == "json": - sys.stdout.write(msg.format_message_as_json(indent)) else: - raise NotmuchError + sys.stdout.write(msg.format_message_as_json(indent)) next_indent = indent + 1 # get replies and print them also out (if there are any) @@ -222,7 +221,7 @@ class Messages(object): sys.stdout.write(set_end) sys.stdout.write(set_end) -#------------------------------------------------------------------------------ + class Message(object): """Represents a single Email message @@ -236,7 +235,7 @@ class Message(object): """notmuch_message_get_filename (notmuch_message_t *message)""" _get_filename = nmlib.notmuch_message_get_filename - _get_filename.restype = c_char_p + _get_filename.restype = c_char_p """return all filenames for a message""" _get_filenames = nmlib.notmuch_message_get_filenames @@ -248,7 +247,7 @@ class Message(object): """notmuch_message_get_message_id (notmuch_message_t *message)""" _get_message_id = nmlib.notmuch_message_get_message_id - _get_message_id.restype = c_char_p + _get_message_id.restype = c_char_p """notmuch_message_get_thread_id""" _get_thread_id = nmlib.notmuch_message_get_thread_id @@ -291,17 +290,16 @@ class Message(object): objects are dead. """ if msg_p is None: - NotmuchError(STATUS.NULL_POINTER) + raise NotmuchError(STATUS.NULL_POINTER) self._msg = msg_p #keep reference to parent, so we keep it alive self._parent = parent - def get_message_id(self): """Returns the message ID - + :returns: String with a message ID - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message is not initialized. """ if self._msg is None: @@ -311,23 +309,24 @@ class Message(object): def get_thread_id(self): """Returns the thread ID - The returned string belongs to 'message' will only be valid for as + The returned string belongs to 'message' will only be valid for as long as the message is valid. This function will not return `None` since Notmuch ensures that every message belongs to a single thread. :returns: String with a thread ID - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message is not initialized. """ if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_thread_id (self._msg); + return Message._get_thread_id(self._msg) def get_replies(self): - """Gets all direct replies to this message as :class:`Messages` iterator + """Gets all direct replies to this message as :class:`Messages` + iterator .. note:: This call only makes sense if 'message' was ultimately obtained from a :class:`Thread` object, (such as @@ -338,20 +337,20 @@ class Message(object): to :meth:`Query.search_messages`), then this function will return `None`. - :returns: :class:`Messages` or `None` if there are no replies to + :returns: :class:`Messages` or `None` if there are no replies to this message. - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message is not initialized. """ if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - msgs_p = Message._get_replies(self._msg); + msgs_p = Message._get_replies(self._msg) if msgs_p is None: return None - return Messages(msgs_p,self) + return Messages(msgs_p, self) def get_date(self): """Returns time_t of the message date @@ -362,7 +361,7 @@ class Message(object): :returns: A time_t timestamp. :rtype: c_unit64 - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message is not initialized. """ if self._msg is None: @@ -370,37 +369,38 @@ class Message(object): return Message._get_date(self._msg) def get_header(self, header): - """Returns a message header - - This returns any message header that is stored in the notmuch database. - This is only a selected subset of headers, which is currently: + """Get the value of the specified header. + + The value will be read from the actual message file, not from + the notmuch database. The header name is case insensitive. - TODO: add stored headers + Returns an empty string ("") if the message does not contain a + header line matching 'header'. :param header: The name of the header to be retrieved. - It is not case-sensitive (TODO: confirm). + It is not case-sensitive. :type header: str :returns: The header value as string :exception: :exc:`NotmuchError` - * STATUS.NOT_INITIALIZED if the message + * STATUS.NOT_INITIALIZED if the message is not initialized. - * STATUS.NULL_POINTER, if no header was found + * STATUS.NULL_POINTER if any error occured. """ if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) #Returns NULL if any error occurs. - header = Message._get_header (self._msg, header) + header = Message._get_header(self._msg, header) if header == None: raise NotmuchError(STATUS.NULL_POINTER) - return header + return header.decode('UTF-8') def get_filename(self): """Returns the file path of the message file :returns: Absolute file path & name of the message file - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message is not initialized. """ if self._msg is None: @@ -415,7 +415,7 @@ class Message(object): not necessarily have identical content.""" if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - + files_p = Message._get_filenames(self._msg) return Filenames(files_p, self).as_generator() @@ -427,10 +427,10 @@ class Message(object): *Message.FLAG.MATCH* for those messages that match the query. This method allows us to get the value of this flag. - :param flag: One of the :attr:`Message.FLAG` values (currently only + :param flag: One of the :attr:`Message.FLAG` values (currently only *Message.FLAG.MATCH* :returns: An unsigned int (0/1), indicating whether the flag is set. - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message is not initialized. """ if self._msg is None: @@ -440,12 +440,12 @@ class Message(object): def set_flag(self, flag, value): """Sets/Unsets a specific flag for this message - :param flag: One of the :attr:`Message.FLAG` values (currently only + :param flag: One of the :attr:`Message.FLAG` values (currently only *Message.FLAG.MATCH* :param value: A bool indicating whether to set or unset the flag. :returns: Nothing - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message is not initialized. """ if self._msg is None: @@ -458,7 +458,7 @@ class Message(object): :returns: A :class:`Tags` iterator. :exception: :exc:`NotmuchError` - * STATUS.NOT_INITIALIZED if the message + * STATUS.NOT_INITIALIZED if the message is not initialized. * STATUS.NULL_POINTER, on error """ @@ -493,10 +493,10 @@ class Message(object): STATUS.NULL_POINTER The 'tag' argument is NULL STATUS.TAG_TOO_LONG - The length of 'tag' is too long + The length of 'tag' is too long (exceeds Message.NOTMUCH_TAG_MAX) STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so message cannot be + Database was opened in read-only mode so message cannot be modified. STATUS.NOT_INITIALIZED The message has not been initialized. @@ -504,7 +504,7 @@ class Message(object): if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - status = nmlib.notmuch_message_add_tag (self._msg, tag) + status = nmlib.notmuch_message_add_tag(self._msg, _str(tag)) # bail out on failure if status != STATUS.SUCCESS: @@ -529,7 +529,7 @@ class Message(object): that this will do nothing when a message is frozen, as tag changes will not be committed to the database yet. - :returns: STATUS.SUCCESS if the tag was successfully removed or if + :returns: STATUS.SUCCESS if the tag was successfully removed or if the message had no such tag. Raises an exception otherwise. :exception: :exc:`NotmuchError`. They have the following meaning: @@ -540,7 +540,7 @@ class Message(object): The length of 'tag' is too long (exceeds NOTMUCH_TAG_MAX) STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so message cannot + Database was opened in read-only mode so message cannot be modified. STATUS.NOT_INITIALIZED The message has not been initialized. @@ -548,7 +548,7 @@ class Message(object): if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - status = nmlib.notmuch_message_remove_tag(self._msg, tag) + status = nmlib.notmuch_message_remove_tag(self._msg, _str(tag)) # bail out on error if status != STATUS.SUCCESS: raise NotmuchError(status) @@ -557,8 +557,6 @@ class Message(object): self.tags_to_maildir_flags() return STATUS.SUCCESS - - def remove_all_tags(self, sync_maildir_flags=False): """Removes all tags from the given message. @@ -579,14 +577,14 @@ class Message(object): :exception: :exc:`NotmuchError`. They have the following meaning: STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so message cannot + Database was opened in read-only mode so message cannot be modified. STATUS.NOT_INITIALIZED The message has not been initialized. """ if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - + status = nmlib.notmuch_message_remove_all_tags(self._msg) # bail out on error @@ -600,8 +598,8 @@ class Message(object): def freeze(self): """Freezes the current state of 'message' within the database - This means that changes to the message state, (via :meth:`add_tag`, - :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be + This means that changes to the message state, (via :meth:`add_tag`, + :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be committed to the database until the message is :meth:`thaw`ed. Multiple calls to freeze/thaw are valid and these calls will @@ -633,14 +631,14 @@ class Message(object): :exception: :exc:`NotmuchError`. They have the following meaning: STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so message cannot + Database was opened in read-only mode so message cannot be modified. STATUS.NOT_INITIALIZED The message has not been initialized. """ if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - + status = nmlib.notmuch_message_freeze(self._msg) if STATUS.SUCCESS == status: @@ -652,7 +650,7 @@ class Message(object): def thaw(self): """Thaws the current 'message' - Thaw the current 'message', synchronizing any changes that may have + Thaw the current 'message', synchronizing any changes that may have occurred while 'message' was frozen into the notmuch database. See :meth:`freeze` for an example of how to use this @@ -667,15 +665,15 @@ class Message(object): :exception: :exc:`NotmuchError`. They have the following meaning: STATUS.UNBALANCED_FREEZE_THAW - An attempt was made to thaw an unfrozen message. - That is, there have been an unbalanced number of calls + An attempt was made to thaw an unfrozen message. + That is, there have been an unbalanced number of calls to :meth:`freeze` and :meth:`thaw`. STATUS.NOT_INITIALIZED The message has not been initialized. """ if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - + status = nmlib.notmuch_message_thaw(self._msg) if STATUS.SUCCESS == status: @@ -684,7 +682,6 @@ class Message(object): raise NotmuchError(status) - def is_match(self): """(Not implemented)""" return self.get_flag(Message.FLAG.MATCH) @@ -697,10 +694,10 @@ class Message(object): '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". @@ -749,11 +746,10 @@ class Message(object): """A message() is represented by a 1-line summary""" msg = {} msg['from'] = self.get_header('from') - msg['tags'] = str(self.get_tags()) + msg['tags'] = self.get_tags() msg['date'] = date.fromtimestamp(self.get_date()) return "%(from)s (%(date)s) (%(tags)s)" % (msg) - def get_message_parts(self): """Output like notmuch show""" fp = open(self.get_filename()) @@ -802,7 +798,7 @@ class Message(object): part_dict["content-type"] = cont_type # NOTE: # Now we emulate the current behaviour, where it ignores - # the html if there's a text representation. + # the html if there's a text representation. # # This is being worked on, but it will be easier to fix # here in the future than to end up with another @@ -813,7 +809,7 @@ class Message(object): else: if cont_type.lower() == "text/plain": part_dict["content"] = msg.get_payload() - elif (cont_type.lower() == "text/html" and + elif (cont_type.lower() == "text/html" and i == 0): part_dict["content"] = msg.get_payload() body.append(part_dict) @@ -858,18 +854,18 @@ class Message(object): parts = format["body"] parts.sort(key=lambda x: x['id']) for p in parts: - if not p.has_key("filename"): + if not "filename" in p: output += "\n\fpart{ " - output += "ID: %d, Content-type: %s\n" % (p["id"], - p["content-type"]) - if p.has_key("content"): + output += "ID: %d, Content-type: %s\n" % (p["id"], + p["content-type"]) + if "content" in p: output += "\n%s\n" % p["content"] else: output += "Non-text part: %s\n" % p["content-type"] - output += "\n\fpart}" + output += "\n\fpart}" else: output += "\n\fattachment{ " - output += "ID: %d, Content-type:%s\n" % (p["id"], + output += "ID: %d, Content-type:%s\n" % (p["id"], p["content-type"]) output += "Attachment: %s\n" % p["filename"] output += "\n\fattachment}\n" @@ -895,7 +891,7 @@ class Message(object): been added or removed, the same messages would not be considered equal (as they do not point to the same set of files any more).""" - res = cmp(self.get_message_id(), other.get_message_id()) + res = cmp(self.get_message_id(), other.get_message_id()) if res: res = cmp(list(self.get_filenames()), list(other.get_filenames())) return res @@ -903,4 +899,4 @@ class Message(object): def __del__(self): """Close and free the notmuch Message""" if self._msg is not None: - nmlib.notmuch_message_destroy (self._msg) + nmlib.notmuch_message_destroy(self._msg) diff --git a/bindings/python/notmuch/tag.py b/bindings/python/notmuch/tag.py index e123b0f3..50e3686b 100644 --- a/bindings/python/notmuch/tag.py +++ b/bindings/python/notmuch/tag.py @@ -19,19 +19,21 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>' from ctypes import c_char_p from notmuch.globals import nmlib, STATUS, NotmuchError -#------------------------------------------------------------------------------ + class Tags(object): """Represents a list of notmuch tags - This object provides an iterator over a list of notmuch tags. Do - note that the underlying library only provides a one-time iterator - (it cannot reset the iterator to the start). Thus iterating over - the function will "exhaust" the list of tags, and a subsequent + This object provides an iterator over a list of notmuch tags (which + are unicode instances). + + Do note that the underlying library only provides a one-time + iterator (it cannot reset the iterator to the start). Thus iterating + over the function will "exhaust" the list of tags, and a subsequent iteration attempt will raise a :exc:`NotmuchError` STATUS.NOT_INITIALIZED. Also note, that any function that uses iteration (nearly all) will also exhaust the tags. So both:: - for tag in tags: print tag + for tag in tags: print tag as well as:: @@ -60,7 +62,7 @@ class Tags(object): valid, we will raise an :exc:`NotmuchError` (STATUS.NULL_POINTER) if it is `None`. :type tags_p: :class:`ctypes.c_void_p` - :param parent: The parent object (ie :class:`Database` or + :param parent: The parent object (ie :class:`Database` or :class:`Message` these tags are derived from, and saves a reference to it, so we can automatically delete the db object once all derived objects are dead. @@ -68,12 +70,12 @@ class Tags(object): cache the tags in the Python object(?) """ if tags_p is None: - NotmuchError(STATUS.NULL_POINTER) + raise NotmuchError(STATUS.NULL_POINTER) self._tags = tags_p #save reference to parent object so we keep it alive self._parent = parent - + def __iter__(self): """ Make Tags an iterator """ return self @@ -81,12 +83,10 @@ class Tags(object): def next(self): if self._tags is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - # No need to call nmlib.notmuch_tags_valid(self._tags); - # Tags._get safely returns None, if there is no more valid tag. - tag = Tags._get (self._tags) - if tag is None: + if not nmlib.notmuch_tags_valid(self._tags): self._tags = None raise StopIteration + tag = Tags._get(self._tags).decode('UTF-8') nmlib.notmuch_tags_move_to_next(self._tags) return tag @@ -101,25 +101,6 @@ class Tags(object): left.""" return nmlib.notmuch_tags_valid(self._tags) > 0 - def __len__(self): - """len(:class:`Tags`) returns the number of contained tags - - .. note:: As this iterates over the tags, we will not be able - to iterate over them again (as in retrieve them)! If - the tags have been exhausted already, this will raise a - :exc:`NotmuchError` STATUS.NOT_INITIALIZED on - subsequent attempts. - """ - if self._tags is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - - i=0 - while nmlib.notmuch_tags_valid(self._msgs): - nmlib.notmuch_tags_move_to_next(self._msgs) - i += 1 - self._tags = None - return i - def __str__(self): """The str() representation of Tags() is a space separated list of tags @@ -134,4 +115,4 @@ class Tags(object): def __del__(self): """Close and free the notmuch tags""" if self._tags is not None: - nmlib.notmuch_tags_destroy (self._tags) + nmlib.notmuch_tags_destroy(self._tags) diff --git a/bindings/python/notmuch/thread.py b/bindings/python/notmuch/thread.py index 2bb30b70..5e08eb31 100644 --- a/bindings/python/notmuch/thread.py +++ b/bindings/python/notmuch/thread.py @@ -23,7 +23,7 @@ from notmuch.message import Messages from notmuch.tag import Tags from datetime import date -#------------------------------------------------------------------------------ + class Threads(object): """Represents a list of notmuch threads @@ -59,13 +59,13 @@ class Threads(object): for thread in threads: threadlist.append(thread) - # threads is "exhausted" now and even len(threads) will raise an + # threads is "exhausted" now and even len(threads) will raise an # exception. # However it will be kept around until all retrieved Thread() objects are - # also deleted. If you did e.g. an explicit del(threads) here, the + # also deleted. If you did e.g. an explicit del(threads) here, the # following lines would fail. - - # You can reiterate over *threadlist* however as often as you want. + + # You can reiterate over *threadlist* however as often as you want. # It is simply a list with Thread objects. print (threadlist[0].get_thread_id()) @@ -91,11 +91,11 @@ class Threads(object): (ie :class:`Query`) these tags are derived from. It saves a reference to it, so we can automatically delete the db object once all derived objects are dead. - :TODO: Make the iterator work more than once and cache the tags in + :TODO: Make the iterator work more than once and cache the tags in the Python object.(?) """ if threads_p is None: - NotmuchError(STATUS.NULL_POINTER) + raise NotmuchError(STATUS.NULL_POINTER) self._threads = threads_p #store parent, so we keep them alive as long as self is alive @@ -113,14 +113,14 @@ class Threads(object): self._threads = None raise StopIteration - thread = Thread(Threads._get (self._threads), self) + thread = Thread(Threads._get(self._threads), self) nmlib.notmuch_threads_move_to_next(self._threads) return thread def __len__(self): """len(:class:`Threads`) returns the number of contained Threads - .. note:: As this iterates over the threads, we will not be able to + .. note:: As this iterates over the threads, we will not be able to iterate over them again! So this will fail:: #THIS FAILS @@ -132,7 +132,7 @@ class Threads(object): if self._threads is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - i=0 + i = 0 # returns 'bool'. On out-of-memory it returns None while nmlib.notmuch_threads_valid(self._threads): nmlib.notmuch_threads_move_to_next(self._threads) @@ -143,7 +143,7 @@ class Threads(object): def __nonzero__(self): """Check if :class:`Threads` contains at least one more valid thread - + The existence of this function makes 'if Threads: foo' work, as that will implicitely call len() exhausting the iterator if __nonzero__ does not exist. This function makes `bool(Threads())` @@ -158,9 +158,9 @@ class Threads(object): def __del__(self): """Close and free the notmuch Threads""" if self._threads is not None: - nmlib.notmuch_messages_destroy (self._threads) + nmlib.notmuch_messages_destroy(self._threads) + -#------------------------------------------------------------------------------ class Thread(object): """Represents a single message thread.""" @@ -206,7 +206,7 @@ class Thread(object): objects are dead. """ if thread_p is None: - NotmuchError(STATUS.NULL_POINTER) + raise NotmuchError(STATUS.NULL_POINTER) self._thread = thread_p #keep reference to parent, so we keep it alive self._parent = parent @@ -218,7 +218,7 @@ class Thread(object): for as long as the thread is valid. :returns: String with a message ID - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread is not initialized. """ if self._thread is None: @@ -231,14 +231,13 @@ class Thread(object): :returns: The number of all messages in the database belonging to this thread. Contrast with :meth:`get_matched_messages`. - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread is not initialized. """ if self._thread is None: raise NotmuchError(STATUS.NOT_INITIALIZED) return nmlib.notmuch_thread_get_total_messages(self._thread) - def get_toplevel_messages(self): """Returns a :class:`Messages` iterator for the top-level messages in 'thread' @@ -246,7 +245,7 @@ class Thread(object): This iterator will not necessarily iterate over all of the messages in the thread. It will only iterate over the messages in the thread which are not replies to other messages in the thread. - + To iterate over all messages in the thread, the caller will need to iterate over the result of :meth:`Message.get_replies` for each top-level message (and do that recursively for the resulting @@ -256,7 +255,7 @@ class Thread(object): :exception: :exc:`NotmuchError` * STATUS.NOT_INITIALIZED if query is not inited - * STATUS.NULL_POINTER if search_messages failed + * STATUS.NULL_POINTER if search_messages failed """ if self._thread is None: raise NotmuchError(STATUS.NOT_INITIALIZED) @@ -264,17 +263,17 @@ class Thread(object): msgs_p = Thread._get_toplevel_messages(self._thread) if msgs_p is None: - NotmuchError(STATUS.NULL_POINTER) + raise NotmuchError(STATUS.NULL_POINTER) - return Messages(msgs_p,self) + return Messages(msgs_p, self) def get_matched_messages(self): """Returns the number of messages in 'thread' that matched the query - :returns: The number of all messages belonging to this thread that + :returns: The number of all messages belonging to this thread that matched the :class:`Query`from which this thread was created. Contrast with :meth:`get_total_messages`. - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread is not initialized. """ if self._thread is None: @@ -288,29 +287,35 @@ class Thread(object): authors of mail messages in the query results that belong to this thread. - The returned string belongs to 'thread' and will only be valid for + The returned string belongs to 'thread' and will only be valid for as long as this Thread() is not deleted. """ if self._thread is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - return Thread._get_authors(self._thread) + authors = Thread._get_authors(self._thread) + if authors is None: + return None + return authors.decode('UTF-8') def get_subject(self): """Returns the Subject of 'thread' - The returned string belongs to 'thread' and will only be valid for + The returned string belongs to 'thread' and will only be valid for as long as this Thread() is not deleted. """ if self._thread is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - return Thread._get_subject(self._thread) + subject = Thread._get_subject(self._thread) + if subject is None: + return None + return subject.decode('UTF-8') def get_newest_date(self): """Returns time_t of the newest message date :returns: A time_t timestamp. :rtype: c_unit64 - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message is not initialized. """ if self._thread is None: @@ -322,7 +327,7 @@ class Thread(object): :returns: A time_t timestamp. :rtype: c_unit64 - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message is not initialized. """ if self._thread is None: @@ -337,14 +342,14 @@ class Thread(object): tags of the messages which matched the search and which belong to this thread. - The :class:`Tags` object is owned by the thread and as such, will only - be valid for as long as this :class:`Thread` is valid (e.g. until the + The :class:`Tags` object is owned by the thread and as such, will only + be valid for as long as this :class:`Thread` is valid (e.g. until the query from which it derived is explicitely deleted). :returns: A :class:`Tags` iterator. :exception: :exc:`NotmuchError` - * STATUS.NOT_INITIALIZED if the thread + * STATUS.NOT_INITIALIZED if the thread is not initialized. * STATUS.NULL_POINTER, on error """ @@ -355,7 +360,7 @@ class Thread(object): if tags_p == None: raise NotmuchError(STATUS.NULL_POINTER) return Tags(tags_p, self) - + def __str__(self): """A str(Thread()) is represented by a 1-line summary""" thread = {} @@ -374,9 +379,15 @@ class Thread(object): thread['subject'] = self.get_subject() thread['tags'] = self.get_tags() - return "thread:%(id)s %(date)12s [%(matched)d/%(total)d] %(authors)s; %(subject)s (%(tags)s)" % (thread) + return "thread:%s %12s [%d/%d] %s; %s (%s)" % (thread['id'], + thread['date'], + thread['matched'], + thread['total'], + thread['authors'], + thread['subject'], + thread['tags']) def __del__(self): """Close and free the notmuch Thread""" if self._thread is not None: - nmlib.notmuch_thread_destroy (self._thread) + nmlib.notmuch_thread_destroy(self._thread) diff --git a/bindings/python/notmuch/version.py b/bindings/python/notmuch/version.py new file mode 100644 index 00000000..fd414b0a --- /dev/null +++ b/bindings/python/notmuch/version.py @@ -0,0 +1,2 @@ +# this file should be kept in sync with ../../../version +__VERSION__ = '0.10.2' diff --git a/bindings/python/setup.py b/bindings/python/setup.py index 1497bc43..286fd196 100644 --- a/bindings/python/setup.py +++ b/bindings/python/setup.py @@ -4,18 +4,11 @@ import os import re from distutils.core import setup -def get_version(): - file = open('notmuch/__init__.py') - try: - for line in file: - if re.match('__VERSION__\s*=\s*',line) != None: - version = line.split('=', 1)[1] - return eval(version, {}, {}) - finally: - file.close() - raise IOError('Unexpected end-of-file') - -__VERSION__=get_version() +# get the notmuch version number without importing the notmuch module +version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'notmuch', 'version.py') +execfile(version_file) +assert __VERSION__, 'Failed to read the notmuch binding version number' setup(name='notmuch', version=__VERSION__, @@ -53,12 +46,11 @@ left of cnotmuch then. Requirements ------------ -You need to have notmuch installed (or rather libnotmuch.so.1). The -release version 0.3 should work fine. Also, notmuch makes use of the -ctypes library, and has only been tested with python 2.5. It will not -work on earlier python versions. +You need to have notmuch installed (or rather libnotmuch.so.1). Also, +notmuch makes use of the ctypes library, and has only been tested with +python >= 2.5. It will not work on earlier python versions. """, - classifiers=['Development Status :: 2 - Pre-Alpha', + classifiers=['Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Programming Language :: Python :: 2', diff --git a/bindings/ruby/database.c b/bindings/ruby/database.c index f4edd773..982fd592 100644 --- a/bindings/ruby/database.c +++ b/bindings/ruby/database.c @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,9 +21,9 @@ #include "defs.h" VALUE -notmuch_rb_database_alloc(VALUE klass) +notmuch_rb_database_alloc (VALUE klass) { - return Data_Wrap_Struct(klass, NULL, NULL, NULL); + return Data_Wrap_Struct (klass, NULL, NULL, NULL); } /* @@ -36,51 +36,46 @@ notmuch_rb_database_alloc(VALUE klass) * The argument :mode specifies the open mode of the database. */ VALUE -notmuch_rb_database_initialize(int argc, VALUE *argv, VALUE self) +notmuch_rb_database_initialize (int argc, VALUE *argv, VALUE self) { const char *path; int create, mode; VALUE pathv, hashv; VALUE modev; -#if !defined(RSTRING_PTR) -#define RSTRING_PTR(v) (RSTRING((v))->ptr) -#endif /* !defined(RSTRING_PTR) */ - /* Check arguments */ - rb_scan_args(argc, argv, "11", &pathv, &hashv); + rb_scan_args (argc, argv, "11", &pathv, &hashv); - SafeStringValue(pathv); - path = RSTRING_PTR(pathv); + SafeStringValue (pathv); + path = RSTRING_PTR (pathv); - if (!NIL_P(hashv)) { - Check_Type(hashv, T_HASH); - create = RTEST(rb_hash_aref(hashv, ID2SYM(ID_db_create))); - modev = rb_hash_aref(hashv, ID2SYM(ID_db_mode)); - if (NIL_P(modev)) - mode = NOTMUCH_DATABASE_MODE_READ_ONLY; - else if (!FIXNUM_P(modev)) - rb_raise(rb_eTypeError, ":mode isn't a Fixnum"); - else { - mode = FIX2INT(modev); - switch (mode) { - case NOTMUCH_DATABASE_MODE_READ_ONLY: - case NOTMUCH_DATABASE_MODE_READ_WRITE: - break; - default: - rb_raise(rb_eTypeError, "Invalid mode"); - } - } - } - else { - create = 0; - mode = NOTMUCH_DATABASE_MODE_READ_ONLY; + if (!NIL_P (hashv)) { + Check_Type (hashv, T_HASH); + create = RTEST (rb_hash_aref (hashv, ID2SYM (ID_db_create))); + modev = rb_hash_aref (hashv, ID2SYM (ID_db_mode)); + if (NIL_P (modev)) + mode = NOTMUCH_DATABASE_MODE_READ_ONLY; + else if (!FIXNUM_P (modev)) + rb_raise (rb_eTypeError, ":mode isn't a Fixnum"); + else { + mode = FIX2INT (modev); + switch (mode) { + case NOTMUCH_DATABASE_MODE_READ_ONLY: + case NOTMUCH_DATABASE_MODE_READ_WRITE: + break; + default: + rb_raise ( rb_eTypeError, "Invalid mode"); + } + } + } else { + create = 0; + mode = NOTMUCH_DATABASE_MODE_READ_ONLY; } - Check_Type(self, T_DATA); - DATA_PTR(self) = create ? notmuch_database_create(path) : notmuch_database_open(path, mode); - if (!DATA_PTR(self)) - rb_raise(notmuch_rb_eDatabaseError, "Failed to open database"); + Check_Type (self, T_DATA); + DATA_PTR (self) = create ? notmuch_database_create (path) : notmuch_database_open (path, mode); + if (!DATA_PTR (self)) + rb_raise (notmuch_rb_eDatabaseError, "Failed to open database"); return self; } @@ -93,15 +88,15 @@ notmuch_rb_database_initialize(int argc, VALUE *argv, VALUE self) * the block. */ VALUE -notmuch_rb_database_open(int argc, VALUE *argv, VALUE klass) +notmuch_rb_database_open (int argc, VALUE *argv, VALUE klass) { VALUE obj; - obj = rb_class_new_instance(argc, argv, klass); - if (!rb_block_given_p()) - return obj; + obj = rb_class_new_instance (argc, argv, klass); + if (!rb_block_given_p ()) + return obj; - return rb_ensure(rb_yield, obj, notmuch_rb_database_close, obj); + return rb_ensure (rb_yield, obj, notmuch_rb_database_close, obj); } /* @@ -110,13 +105,13 @@ notmuch_rb_database_open(int argc, VALUE *argv, VALUE klass) * Close the notmuch database. */ VALUE -notmuch_rb_database_close(VALUE self) +notmuch_rb_database_close (VALUE self) { notmuch_database_t *db; - Data_Get_Notmuch_Database(self, db); - notmuch_database_close(db); - DATA_PTR(self) = NULL; + Data_Get_Notmuch_Database (self, db); + notmuch_database_close (db); + DATA_PTR (self) = NULL; return Qnil; } @@ -127,13 +122,13 @@ notmuch_rb_database_close(VALUE self) * Return the path of the database */ VALUE -notmuch_rb_database_path(VALUE self) +notmuch_rb_database_path (VALUE self) { notmuch_database_t *db; - Data_Get_Notmuch_Database(self, db); + Data_Get_Notmuch_Database (self, db); - return rb_str_new2(notmuch_database_get_path(db)); + return rb_str_new2 (notmuch_database_get_path (db)); } /* @@ -142,13 +137,13 @@ notmuch_rb_database_path(VALUE self) * Return the version of the database */ VALUE -notmuch_rb_database_version(VALUE self) +notmuch_rb_database_version (VALUE self) { notmuch_database_t *db; - Data_Get_Notmuch_Database(self, db); + Data_Get_Notmuch_Database (self, db); - return INT2FIX(notmuch_database_get_version(db)); + return INT2FIX (notmuch_database_get_version (db)); } /* @@ -157,20 +152,20 @@ notmuch_rb_database_version(VALUE self) * Return the +true+ if the database needs upgrading, +false+ otherwise */ VALUE -notmuch_rb_database_needs_upgrade(VALUE self) +notmuch_rb_database_needs_upgrade (VALUE self) { notmuch_database_t *db; - Data_Get_Notmuch_Database(self, db); + Data_Get_Notmuch_Database (self, db); - return notmuch_database_needs_upgrade(db) ? Qtrue : Qfalse; + return notmuch_database_needs_upgrade (db) ? Qtrue : Qfalse; } static void -notmuch_rb_upgrade_notify(void *closure, double progress) +notmuch_rb_upgrade_notify (void *closure, double progress) { - VALUE *block = (VALUE *)closure; - rb_funcall(*block, ID_call, 1, rb_float_new(progress)); + VALUE *block = (VALUE *) closure; + rb_funcall (*block, ID_call, 1, rb_float_new (progress)); } /* @@ -182,24 +177,62 @@ notmuch_rb_upgrade_notify(void *closure, double progress) * floating point value in the range of [0.0..1.0]. */ VALUE -notmuch_rb_database_upgrade(VALUE self) +notmuch_rb_database_upgrade (VALUE self) { notmuch_status_t ret; void (*pnotify) (void *closure, double progress); notmuch_database_t *db; VALUE block; - Data_Get_Notmuch_Database(self, db); + Data_Get_Notmuch_Database (self, db); - if (rb_block_given_p()) { - pnotify = notmuch_rb_upgrade_notify; - block = rb_block_proc(); + if (rb_block_given_p ()) { + pnotify = notmuch_rb_upgrade_notify; + block = rb_block_proc (); } else - pnotify = NULL; + pnotify = NULL; + + ret = notmuch_database_upgrade (db, pnotify, pnotify ? &block : NULL); + notmuch_rb_status_raise (ret); + + return Qtrue; +} + +/* + * call-seq: DB.begin_atomic => nil + * + * Begin an atomic database operation. + */ +VALUE +notmuch_rb_database_begin_atomic (VALUE self) +{ + notmuch_status_t ret; + notmuch_database_t *db; + + Data_Get_Notmuch_Database (self, db); + + ret = notmuch_database_begin_atomic (db); + notmuch_rb_status_raise (ret); - ret = notmuch_database_upgrade(db, pnotify, pnotify ? &block : NULL); - notmuch_rb_status_raise(ret); + return Qtrue; +} + +/* + * call-seq: DB.end_atomic => nil + * + * Indicate the end of an atomic database operation. + */ +VALUE +notmuch_rb_database_end_atomic (VALUE self) +{ + notmuch_status_t ret; + notmuch_database_t *db; + + Data_Get_Notmuch_Database (self, db); + + ret = notmuch_database_end_atomic (db); + notmuch_rb_status_raise (ret); return Qtrue; } @@ -210,26 +243,22 @@ notmuch_rb_database_upgrade(VALUE self) * Retrieve a directory object from the database for 'path' */ VALUE -notmuch_rb_database_get_directory(VALUE self, VALUE pathv) +notmuch_rb_database_get_directory (VALUE self, VALUE pathv) { const char *path; notmuch_directory_t *dir; notmuch_database_t *db; - Data_Get_Notmuch_Database(self, db); + Data_Get_Notmuch_Database (self, db); -#if !defined(RSTRING_PTR) -#define RSTRING_PTR(v) (RSTRING((v))->ptr) -#endif /* !defined(RSTRING_PTR) */ + SafeStringValue (pathv); + path = RSTRING_PTR (pathv); - SafeStringValue(pathv); - path = RSTRING_PTR(pathv); - - dir = notmuch_database_get_directory(db, path); + dir = notmuch_database_get_directory (db, path); if (!dir) - rb_raise(notmuch_rb_eXapianError, "Xapian exception"); + rb_raise (notmuch_rb_eXapianError, "Xapian exception"); - return Data_Wrap_Struct(notmuch_rb_cDirectory, NULL, NULL, dir); + return Data_Wrap_Struct (notmuch_rb_cDirectory, NULL, NULL, dir); } /* @@ -241,30 +270,26 @@ notmuch_rb_database_get_directory(VALUE self, VALUE pathv) * duplicate. */ VALUE -notmuch_rb_database_add_message(VALUE self, VALUE pathv) +notmuch_rb_database_add_message (VALUE self, VALUE pathv) { const char *path; notmuch_status_t ret; notmuch_message_t *message; notmuch_database_t *db; - Data_Get_Notmuch_Database(self, db); - -#if !defined(RSTRING_PTR) -#define RSTRING_PTR(v) (RSTRING((v))->ptr) -#endif /* !defined(RSTRING_PTR) */ + Data_Get_Notmuch_Database (self, db); - SafeStringValue(pathv); - path = RSTRING_PTR(pathv); + SafeStringValue (pathv); + path = RSTRING_PTR (pathv); - ret = notmuch_database_add_message(db, path, &message); - notmuch_rb_status_raise(ret); - return rb_assoc_new(Data_Wrap_Struct(notmuch_rb_cMessage, NULL, NULL, message), + ret = notmuch_database_add_message (db, path, &message); + notmuch_rb_status_raise (ret); + return rb_assoc_new (Data_Wrap_Struct (notmuch_rb_cMessage, NULL, NULL, message), (ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) ? Qtrue : Qfalse); } /* - * call-seq: DB.remove_message(path) => isdup + * call-seq: DB.remove_message (path) => isdup * * Remove a message from the database. * @@ -272,50 +297,94 @@ notmuch_rb_database_add_message(VALUE self, VALUE pathv) * duplicate. */ VALUE -notmuch_rb_database_remove_message(VALUE self, VALUE pathv) +notmuch_rb_database_remove_message (VALUE self, VALUE pathv) { const char *path; notmuch_status_t ret; notmuch_database_t *db; - Data_Get_Notmuch_Database(self, db); + Data_Get_Notmuch_Database (self, db); -#if !defined(RSTRING_PTR) -#define RSTRING_PTR(v) (RSTRING((v))->ptr) -#endif /* !defined(RSTRING_PTR) */ + SafeStringValue (pathv); + path = RSTRING_PTR (pathv); - SafeStringValue(pathv); - path = RSTRING_PTR(pathv); - - ret = notmuch_database_remove_message(db, path); - notmuch_rb_status_raise(ret); + ret = notmuch_database_remove_message (db, path); + notmuch_rb_status_raise (ret); return (ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) ? Qtrue : Qfalse; } /* + * call-seq: DB.find_message(id) => MESSAGE or nil + * + * Find a message by message id. + */ +VALUE +notmuch_rb_database_find_message (VALUE self, VALUE idv) +{ + const char *id; + notmuch_status_t ret; + notmuch_database_t *db; + notmuch_message_t *message; + + Data_Get_Notmuch_Database (self, db); + + SafeStringValue (idv); + id = RSTRING_PTR (idv); + + ret = notmuch_database_find_message (db, id, &message); + notmuch_rb_status_raise (ret); + + if (message) + return Data_Wrap_Struct (notmuch_rb_cMessage, NULL, NULL, message); + return Qnil; +} + +/* + * call-seq: DB.find_message_by_filename(path) => MESSAGE or nil + * + * Find a message by filename. + */ +VALUE +notmuch_rb_database_find_message_by_filename (VALUE self, VALUE pathv) +{ + const char *path; + notmuch_status_t ret; + notmuch_database_t *db; + notmuch_message_t *message; + + Data_Get_Notmuch_Database (self, db); + + SafeStringValue (pathv); + path = RSTRING_PTR (pathv); + + ret = notmuch_database_find_message_by_filename (db, path, &message); + notmuch_rb_status_raise (ret); + + if (message) + return Data_Wrap_Struct (notmuch_rb_cMessage, NULL, NULL, message); + return Qnil; +} + +/* * call-seq: DB.query(query) => QUERY * * Retrieve a query object for the query string 'query' */ VALUE -notmuch_rb_database_query_create(VALUE self, VALUE qstrv) +notmuch_rb_database_query_create (VALUE self, VALUE qstrv) { const char *qstr; notmuch_query_t *query; notmuch_database_t *db; - Data_Get_Notmuch_Database(self, db); - -#if !defined(RSTRING_PTR) -#define RSTRING_PTR(v) (RSTRING((v))->ptr) -#endif /* !defined(RSTRING_PTR) */ + Data_Get_Notmuch_Database (self, db); - SafeStringValue(qstrv); - qstr = RSTRING_PTR(qstrv); + SafeStringValue (qstrv); + qstr = RSTRING_PTR (qstrv); - query = notmuch_query_create(db, qstr); + query = notmuch_query_create (db, qstr); if (!query) - rb_raise(notmuch_rb_eMemoryError, "Out of memory"); + rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return Data_Wrap_Struct(notmuch_rb_cQuery, NULL, NULL, query); + return Data_Wrap_Struct (notmuch_rb_cQuery, NULL, NULL, query); } diff --git a/bindings/ruby/defs.h b/bindings/ruby/defs.h index f00afeff..81f652fb 100644 --- a/bindings/ruby/defs.h +++ b/bindings/ruby/defs.h @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,278 +44,296 @@ VALUE notmuch_rb_eFileNotEmailError; VALUE notmuch_rb_eNullPointerError; VALUE notmuch_rb_eTagTooLongError; VALUE notmuch_rb_eUnbalancedFreezeThawError; +VALUE notmuch_rb_eUnbalancedAtomicError; ID ID_call; ID ID_db_create; ID ID_db_mode; -#define Data_Get_Notmuch_Database(obj, ptr) \ - do { \ - Check_Type(obj, T_DATA); \ - if (DATA_PTR(obj) == NULL) \ - rb_raise(rb_eRuntimeError, "database closed"); \ - Data_Get_Struct(obj, notmuch_database_t, ptr); \ - } while(0) +/* RSTRING_PTR() is new in ruby-1.9 */ +#if !defined(RSTRING_PTR) +# define RSTRING_PTR(v) (RSTRING((v))->ptr) +#endif /* !defined (RSTRING_PTR) */ -#define Data_Get_Notmuch_Directory(obj, ptr) \ - do { \ - Check_Type(obj, T_DATA); \ - if (DATA_PTR(obj) == NULL) \ - rb_raise(rb_eRuntimeError, "directory destroyed"); \ - Data_Get_Struct(obj, notmuch_directory_t, ptr); \ - } while(0) +#define Data_Get_Notmuch_Database(obj, ptr) \ + do { \ + Check_Type ((obj), T_DATA); \ + if (DATA_PTR ((obj)) == NULL) \ + rb_raise (rb_eRuntimeError, "database closed"); \ + Data_Get_Struct ((obj), notmuch_database_t, (ptr)); \ + } while (0) -#define Data_Get_Notmuch_FileNames(obj, ptr) \ - do { \ - Check_Type(obj, T_DATA); \ - if (DATA_PTR(obj) == NULL) \ - rb_raise(rb_eRuntimeError, "filenames destroyed"); \ - Data_Get_Struct(obj, notmuch_filenames_t, ptr); \ - } while(0) +#define Data_Get_Notmuch_Directory(obj, ptr) \ + do { \ + Check_Type ((obj), T_DATA); \ + if (DATA_PTR ((obj)) == NULL) \ + rb_raise (rb_eRuntimeError, "directory destroyed"); \ + Data_Get_Struct ((obj), notmuch_directory_t, (ptr)); \ + } while (0) -#define Data_Get_Notmuch_Query(obj, ptr) \ - do { \ - Check_Type(obj, T_DATA); \ - if (DATA_PTR(obj) == NULL) \ - rb_raise(rb_eRuntimeError, "query destroyed"); \ - Data_Get_Struct(obj, notmuch_query_t, ptr); \ - } while(0) +#define Data_Get_Notmuch_FileNames(obj, ptr) \ + do { \ + Check_Type ((obj), T_DATA); \ + if (DATA_PTR ((obj)) == NULL) \ + rb_raise (rb_eRuntimeError, "filenames destroyed"); \ + Data_Get_Struct ((obj), notmuch_filenames_t, (ptr)); \ + } while (0) -#define Data_Get_Notmuch_Threads(obj, ptr) \ - do { \ - Check_Type(obj, T_DATA); \ - if (DATA_PTR(obj) == NULL) \ - rb_raise(rb_eRuntimeError, "threads destroyed"); \ - Data_Get_Struct(obj, notmuch_threads_t, ptr); \ - } while(0) +#define Data_Get_Notmuch_Query(obj, ptr) \ + do { \ + Check_Type ((obj), T_DATA); \ + if (DATA_PTR ((obj)) == NULL) \ + rb_raise (rb_eRuntimeError, "query destroyed"); \ + Data_Get_Struct ((obj), notmuch_query_t, (ptr)); \ + } while (0) -#define Data_Get_Notmuch_Messages(obj, ptr) \ - do { \ - Check_Type(obj, T_DATA); \ - if (DATA_PTR(obj) == NULL) \ - rb_raise(rb_eRuntimeError, "messages destroyed"); \ - Data_Get_Struct(obj, notmuch_messages_t, ptr); \ - } while(0) +#define Data_Get_Notmuch_Threads(obj, ptr) \ + do { \ + Check_Type ((obj), T_DATA); \ + if (DATA_PTR ((obj)) == NULL) \ + rb_raise (rb_eRuntimeError, "threads destroyed"); \ + Data_Get_Struct ((obj), notmuch_threads_t, (ptr)); \ + } while (0) -#define Data_Get_Notmuch_Thread(obj, ptr) \ - do { \ - Check_Type(obj, T_DATA); \ - if (DATA_PTR(obj) == NULL) \ - rb_raise(rb_eRuntimeError, "thread destroyed"); \ - Data_Get_Struct(obj, notmuch_thread_t, ptr); \ - } while(0) +#define Data_Get_Notmuch_Messages(obj, ptr) \ + do { \ + Check_Type ((obj), T_DATA); \ + if (DATA_PTR ((obj)) == NULL) \ + rb_raise (rb_eRuntimeError, "messages destroyed"); \ + Data_Get_Struct ((obj), notmuch_messages_t, (ptr)); \ + } while (0) -#define Data_Get_Notmuch_Message(obj, ptr) \ - do { \ - Check_Type(obj, T_DATA); \ - if (DATA_PTR(obj) == NULL) \ - rb_raise(rb_eRuntimeError, "message destroyed"); \ - Data_Get_Struct(obj, notmuch_message_t, ptr); \ - } while(0) +#define Data_Get_Notmuch_Thread(obj, ptr) \ + do { \ + Check_Type ((obj), T_DATA); \ + if (DATA_PTR ((obj)) == NULL) \ + rb_raise (rb_eRuntimeError, "thread destroyed"); \ + Data_Get_Struct ((obj), notmuch_thread_t, (ptr)); \ + } while (0) -#define Data_Get_Notmuch_Tags(obj, ptr) \ - do { \ - Check_Type(obj, T_DATA); \ - if (DATA_PTR(obj) == NULL) \ - rb_raise(rb_eRuntimeError, "tags destroyed"); \ - Data_Get_Struct(obj, notmuch_tags_t, ptr); \ - } while(0) +#define Data_Get_Notmuch_Message(obj, ptr) \ + do { \ + Check_Type ((obj), T_DATA); \ + if (DATA_PTR ((obj)) == NULL) \ + rb_raise (rb_eRuntimeError, "message destroyed"); \ + Data_Get_Struct ((obj), notmuch_message_t, (ptr)); \ + } while (0) + +#define Data_Get_Notmuch_Tags(obj, ptr) \ + do { \ + Check_Type ((obj), T_DATA); \ + if (DATA_PTR ((obj)) == NULL) \ + rb_raise (rb_eRuntimeError, "tags destroyed"); \ + Data_Get_Struct ((obj), notmuch_tags_t, (ptr)); \ + } while (0) /* status.c */ void -notmuch_rb_status_raise(notmuch_status_t status); +notmuch_rb_status_raise (notmuch_status_t status); /* database.c */ VALUE -notmuch_rb_database_alloc(VALUE klass); +notmuch_rb_database_alloc (VALUE klass); + +VALUE +notmuch_rb_database_initialize (int argc, VALUE *argv, VALUE klass); + +VALUE +notmuch_rb_database_open (int argc, VALUE *argv, VALUE klass); + +VALUE +notmuch_rb_database_close (VALUE self); + +VALUE +notmuch_rb_database_path (VALUE self); VALUE -notmuch_rb_database_initialize(int argc, VALUE *argv, VALUE klass); +notmuch_rb_database_version (VALUE self); VALUE -notmuch_rb_database_open(int argc, VALUE *argv, VALUE klass); +notmuch_rb_database_needs_upgrade (VALUE self); VALUE -notmuch_rb_database_close(VALUE self); +notmuch_rb_database_upgrade (VALUE self); VALUE -notmuch_rb_database_path(VALUE self); +notmuch_rb_database_begin_atomic (VALUE self); VALUE -notmuch_rb_database_version(VALUE self); +notmuch_rb_database_end_atomic (VALUE self); VALUE -notmuch_rb_database_needs_upgrade(VALUE self); +notmuch_rb_database_get_directory (VALUE self, VALUE pathv); VALUE -notmuch_rb_database_upgrade(VALUE self); +notmuch_rb_database_add_message (VALUE self, VALUE pathv); VALUE -notmuch_rb_database_get_directory(VALUE self, VALUE pathv); +notmuch_rb_database_remove_message (VALUE self, VALUE pathv); VALUE -notmuch_rb_database_add_message(VALUE self, VALUE pathv); +notmuch_rb_database_find_message (VALUE self, VALUE idv); VALUE -notmuch_rb_database_remove_message(VALUE self, VALUE pathv); +notmuch_rb_database_find_message_by_filename (VALUE self, VALUE pathv); VALUE -notmuch_rb_database_query_create(VALUE self, VALUE qstrv); +notmuch_rb_database_query_create (VALUE self, VALUE qstrv); /* directory.c */ VALUE -notmuch_rb_directory_destroy(VALUE self); +notmuch_rb_directory_destroy (VALUE self); VALUE -notmuch_rb_directory_get_mtime(VALUE self); +notmuch_rb_directory_get_mtime (VALUE self); VALUE -notmuch_rb_directory_set_mtime(VALUE self, VALUE mtimev); +notmuch_rb_directory_set_mtime (VALUE self, VALUE mtimev); VALUE -notmuch_rb_directory_get_child_files(VALUE self); +notmuch_rb_directory_get_child_files (VALUE self); VALUE -notmuch_rb_directory_get_child_directories(VALUE self); +notmuch_rb_directory_get_child_directories (VALUE self); /* filenames.c */ VALUE -notmuch_rb_filenames_destroy(VALUE self); +notmuch_rb_filenames_destroy (VALUE self); VALUE -notmuch_rb_filenames_each(VALUE self); +notmuch_rb_filenames_each (VALUE self); /* query.c */ VALUE -notmuch_rb_query_destroy(VALUE self); +notmuch_rb_query_destroy (VALUE self); VALUE -notmuch_rb_query_get_sort(VALUE self); +notmuch_rb_query_get_sort (VALUE self); VALUE -notmuch_rb_query_set_sort(VALUE self, VALUE sortv); +notmuch_rb_query_set_sort (VALUE self, VALUE sortv); VALUE -notmuch_rb_query_get_string(VALUE self); +notmuch_rb_query_get_string (VALUE self); VALUE -notmuch_rb_query_search_threads(VALUE self); +notmuch_rb_query_search_threads (VALUE self); VALUE -notmuch_rb_query_search_messages(VALUE self); +notmuch_rb_query_search_messages (VALUE self); /* threads.c */ VALUE -notmuch_rb_threads_destroy(VALUE self); +notmuch_rb_threads_destroy (VALUE self); VALUE -notmuch_rb_threads_each(VALUE self); +notmuch_rb_threads_each (VALUE self); /* messages.c */ VALUE -notmuch_rb_messages_destroy(VALUE self); +notmuch_rb_messages_destroy (VALUE self); VALUE -notmuch_rb_messages_each(VALUE self); +notmuch_rb_messages_each (VALUE self); VALUE -notmuch_rb_messages_collect_tags(VALUE self); +notmuch_rb_messages_collect_tags (VALUE self); /* thread.c */ VALUE -notmuch_rb_thread_destroy(VALUE self); +notmuch_rb_thread_destroy (VALUE self); VALUE -notmuch_rb_thread_get_thread_id(VALUE self); +notmuch_rb_thread_get_thread_id (VALUE self); VALUE -notmuch_rb_thread_get_total_messages(VALUE self); +notmuch_rb_thread_get_total_messages (VALUE self); VALUE -notmuch_rb_thread_get_toplevel_messages(VALUE self); +notmuch_rb_thread_get_toplevel_messages (VALUE self); VALUE -notmuch_rb_thread_get_matched_messages(VALUE self); +notmuch_rb_thread_get_matched_messages (VALUE self); VALUE -notmuch_rb_thread_get_authors(VALUE self); +notmuch_rb_thread_get_authors (VALUE self); VALUE -notmuch_rb_thread_get_subject(VALUE self); +notmuch_rb_thread_get_subject (VALUE self); VALUE -notmuch_rb_thread_get_oldest_date(VALUE self); +notmuch_rb_thread_get_oldest_date (VALUE self); VALUE -notmuch_rb_thread_get_newest_date(VALUE self); +notmuch_rb_thread_get_newest_date (VALUE self); VALUE -notmuch_rb_thread_get_tags(VALUE self); +notmuch_rb_thread_get_tags (VALUE self); /* message.c */ VALUE -notmuch_rb_message_destroy(VALUE self); +notmuch_rb_message_destroy (VALUE self); VALUE -notmuch_rb_message_get_message_id(VALUE self); +notmuch_rb_message_get_message_id (VALUE self); VALUE -notmuch_rb_message_get_thread_id(VALUE self); +notmuch_rb_message_get_thread_id (VALUE self); VALUE -notmuch_rb_message_get_replies(VALUE self); +notmuch_rb_message_get_replies (VALUE self); VALUE -notmuch_rb_message_get_filename(VALUE self); +notmuch_rb_message_get_filename (VALUE self); VALUE -notmuch_rb_message_get_filenames(VALUE self); +notmuch_rb_message_get_filenames (VALUE self); VALUE -notmuch_rb_message_get_flag(VALUE self, VALUE flagv); +notmuch_rb_message_get_flag (VALUE self, VALUE flagv); VALUE -notmuch_rb_message_set_flag(VALUE self, VALUE flagv, VALUE valuev); +notmuch_rb_message_set_flag (VALUE self, VALUE flagv, VALUE valuev); VALUE -notmuch_rb_message_get_date(VALUE self); +notmuch_rb_message_get_date (VALUE self); VALUE -notmuch_rb_message_get_header(VALUE self, VALUE headerv); +notmuch_rb_message_get_header (VALUE self, VALUE headerv); VALUE -notmuch_rb_message_get_tags(VALUE self); +notmuch_rb_message_get_tags (VALUE self); VALUE -notmuch_rb_message_add_tag(VALUE self, VALUE tagv); +notmuch_rb_message_add_tag (VALUE self, VALUE tagv); VALUE -notmuch_rb_message_remove_tag(VALUE self, VALUE tagv); +notmuch_rb_message_remove_tag (VALUE self, VALUE tagv); VALUE -notmuch_rb_message_remove_all_tags(VALUE self); +notmuch_rb_message_remove_all_tags (VALUE self); VALUE -notmuch_rb_message_maildir_flags_to_tags(VALUE self); +notmuch_rb_message_maildir_flags_to_tags (VALUE self); VALUE -notmuch_rb_message_tags_to_maildir_flags(VALUE self); +notmuch_rb_message_tags_to_maildir_flags (VALUE self); VALUE -notmuch_rb_message_freeze(VALUE self); +notmuch_rb_message_freeze (VALUE self); VALUE -notmuch_rb_message_thaw(VALUE self); +notmuch_rb_message_thaw (VALUE self); /* tags.c */ VALUE -notmuch_rb_tags_destroy(VALUE self); +notmuch_rb_tags_destroy (VALUE self); VALUE -notmuch_rb_tags_each(VALUE self); +notmuch_rb_tags_each (VALUE self); /* init.c */ void -Init_notmuch(void); +Init_notmuch (void); #endif diff --git a/bindings/ruby/directory.c b/bindings/ruby/directory.c index 36dcb8be..303523c2 100644 --- a/bindings/ruby/directory.c +++ b/bindings/ruby/directory.c @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,19 +21,19 @@ #include "defs.h" /* - * call-seq: DIR.destroy => nil + * call-seq: DIR.destroy! => nil * * Destroys the directory, freeing all resources allocated for it. */ VALUE -notmuch_rb_directory_destroy(VALUE self) +notmuch_rb_directory_destroy (VALUE self) { notmuch_directory_t *dir; - Data_Get_Struct(self, notmuch_directory_t, dir); + Data_Get_Struct (self, notmuch_directory_t, dir); - notmuch_directory_destroy(dir); - DATA_PTR(self) = NULL; + notmuch_directory_destroy (dir); + DATA_PTR (self) = NULL; return Qnil; } @@ -45,13 +45,13 @@ notmuch_rb_directory_destroy(VALUE self) * stored. */ VALUE -notmuch_rb_directory_get_mtime(VALUE self) +notmuch_rb_directory_get_mtime (VALUE self) { notmuch_directory_t *dir; - Data_Get_Notmuch_Directory(self, dir); + Data_Get_Notmuch_Directory (self, dir); - return UINT2NUM(notmuch_directory_get_mtime(dir)); + return UINT2NUM (notmuch_directory_get_mtime (dir)); } /* @@ -60,18 +60,18 @@ notmuch_rb_directory_get_mtime(VALUE self) * Store an mtime within the database for the directory object. */ VALUE -notmuch_rb_directory_set_mtime(VALUE self, VALUE mtimev) +notmuch_rb_directory_set_mtime (VALUE self, VALUE mtimev) { notmuch_status_t ret; notmuch_directory_t *dir; - Data_Get_Notmuch_Directory(self, dir); + Data_Get_Notmuch_Directory (self, dir); - if (!FIXNUM_P(mtimev)) - rb_raise(rb_eTypeError, "First argument not a fixnum"); + if (!FIXNUM_P (mtimev)) + rb_raise (rb_eTypeError, "First argument not a fixnum"); - ret = notmuch_directory_set_mtime(dir, FIX2UINT(mtimev)); - notmuch_rb_status_raise(ret); + ret = notmuch_directory_set_mtime (dir, FIX2UINT (mtimev)); + notmuch_rb_status_raise (ret); return Qtrue; } @@ -83,16 +83,16 @@ notmuch_rb_directory_set_mtime(VALUE self, VALUE mtimev) * filenames of messages in the database within the given directory. */ VALUE -notmuch_rb_directory_get_child_files(VALUE self) +notmuch_rb_directory_get_child_files (VALUE self) { notmuch_directory_t *dir; notmuch_filenames_t *fnames; - Data_Get_Notmuch_Directory(self, dir); + Data_Get_Notmuch_Directory (self, dir); - fnames = notmuch_directory_get_child_files(dir); + fnames = notmuch_directory_get_child_files (dir); - return Data_Wrap_Struct(notmuch_rb_cFileNames, NULL, NULL, fnames); + return Data_Wrap_Struct (notmuch_rb_cFileNames, NULL, NULL, fnames); } /* @@ -102,14 +102,14 @@ notmuch_rb_directory_get_child_files(VALUE self) * directories in the database within the given directory. */ VALUE -notmuch_rb_directory_get_child_directories(VALUE self) +notmuch_rb_directory_get_child_directories (VALUE self) { notmuch_directory_t *dir; notmuch_filenames_t *fnames; - Data_Get_Notmuch_Directory(self, dir); + Data_Get_Notmuch_Directory (self, dir); - fnames = notmuch_directory_get_child_directories(dir); + fnames = notmuch_directory_get_child_directories (dir); - return Data_Wrap_Struct(notmuch_rb_cFileNames, NULL, NULL, fnames); + return Data_Wrap_Struct (notmuch_rb_cFileNames, NULL, NULL, fnames); } diff --git a/bindings/ruby/extconf.rb b/bindings/ruby/extconf.rb index a9d9d42b..ccac609c 100644 --- a/bindings/ruby/extconf.rb +++ b/bindings/ruby/extconf.rb @@ -1,7 +1,6 @@ #!/usr/bin/env ruby # coding: utf-8 -# vim: set sw=2 sts=2 et nowrap fenc=utf-8 : -# Copyright 2010 Ali Polatel <alip@exherbo.org> +# Copyright 2010, 2011 Ali Polatel <alip@exherbo.org> # Distributed under the terms of the GNU General Public License v3 require 'mkmf' diff --git a/bindings/ruby/filenames.c b/bindings/ruby/filenames.c index faaa9a0a..e2785903 100644 --- a/bindings/ruby/filenames.c +++ b/bindings/ruby/filenames.c @@ -21,19 +21,19 @@ #include "defs.h" /* - * call-seq: FILENAMES.destroy => nil + * call-seq: FILENAMES.destroy! => nil * * Destroys the filenames, freeing all resources allocated for it. */ VALUE -notmuch_rb_filenames_destroy(VALUE self) +notmuch_rb_filenames_destroy (VALUE self) { notmuch_filenames_t *fnames; - Data_Get_Notmuch_FileNames(self, fnames); + Data_Get_Notmuch_FileNames (self, fnames); - notmuch_filenames_destroy(fnames); - DATA_PTR(self) = NULL; + notmuch_filenames_destroy (fnames); + DATA_PTR (self) = NULL; return Qnil; } @@ -45,14 +45,14 @@ notmuch_rb_filenames_destroy(VALUE self) * parameter. */ VALUE -notmuch_rb_filenames_each(VALUE self) +notmuch_rb_filenames_each (VALUE self) { notmuch_filenames_t *fnames; - Data_Get_Notmuch_FileNames(self, fnames); + Data_Get_Notmuch_FileNames (self, fnames); - for (; notmuch_filenames_valid(fnames); notmuch_filenames_move_to_next(fnames)) - rb_yield(rb_str_new2(notmuch_filenames_get(fnames))); + for (; notmuch_filenames_valid (fnames); notmuch_filenames_move_to_next (fnames)) + rb_yield (rb_str_new2 (notmuch_filenames_get (fnames))); return self; } diff --git a/bindings/ruby/init.c b/bindings/ruby/init.c index aa09c8db..4405f196 100644 --- a/bindings/ruby/init.c +++ b/bindings/ruby/init.c @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,261 +27,293 @@ * * Ruby extension to the <tt>notmuch</tt> mail library. * - * == Constants + * == Classes * - * - Notmuch::MODE_READ_ONLY + * Following are the classes that are most likely to be of interest to + * the user: * - * Open the database in read only mode. - * - * - Notmuch::MODE_READ_WRITE - * - * Open the database in read write mode. - * - * - Notmuch::TAG_MAX - * - * Maximum allowed length of a tag - */ - -/* - * Document-class: Notmuch::Database - * - * Notmuch database interaction - */ - -/* - * Document-class: Notmuch::Directory - * - * Notmuch directory - */ - -/* - * Document-class: Notmuch::FileNames - * - * Notmuch file names - */ - -/* - * Document-class: Notmuch::Query - * - * Notmuch query - */ - -/* - * Document-class: Notmuch::Threads - * - * Notmuch threads - */ - -/* - * Document-class: Notmuch::Messages - * - * Notmuch messages - */ - -/* - * Document-class: Notmuch::Thread - * - * Notmuch thread - */ - -/* - * Document-class: Notmuch::Message - * - * Notmuch message - */ - -/* - * Document-class: Notmuch::Tags - * - * Notmuch tags - */ - -/* - * Document-class: Notmuch::BaseError - * - * Base class for all notmuch exceptions - */ - -/* - * Document-class: Notmuch::DatabaseError - * - * Raised when the database can't be created or opened. - */ - -/* - * Document-class: Notmuch::MemoryError - * - * Raised when notmuch is out of memory + * - Notmuch::Database + * - Notmuch::FileNames + * - Notmuch::Query + * - Notmuch::Threads + * - Notmuch::Messages + * - Notmuch::Thread + * - Notmuch::Message + * - Notmuch::Tags */ -/* - * Document-class: Notmuch::ReadOnlyError - * - * Raised when an attempt was made to write to a database opened in read-only - * mode. - */ - -/* - * Document-class: Notmuch::XapianError - * - * Raised when a Xapian exception occurs - */ - -/* - * Document-class: Notmuch::FileError - * - * Raised when an error occurs trying to read or write to a file. - */ - -/* - * Document-class: Notmuch::FileNotEmailError - * - * Raised when a file is presented that doesn't appear to be an email message. - */ - -/* - * Document-class: Notmuch::NullPointerError - * - * Raised when the user erroneously passes a +NULL+ pointer to a notmuch - * function. - */ - -/* - * Document-class: Notmuch::TagTooLongError - * - * Raised when a tag value is too long (exceeds Notmuch::TAG_MAX) - */ - -/* - * Document-class: Notmuch::UnbalancedFreezeThawError - * - * Raised when the notmuch_message_thaw function has been called more times - * than notmuch_message_freeze. - */ - -#define RDOC_HATE 1 - void -Init_notmuch(void) +Init_notmuch (void) { VALUE mod; - ID_call = rb_intern("call"); - ID_db_create = rb_intern("create"); - ID_db_mode = rb_intern("mode"); - - mod = rb_define_module("Notmuch"); + ID_call = rb_intern ("call"); + ID_db_create = rb_intern ("create"); + ID_db_mode = rb_intern ("mode"); - rb_define_const(mod, "MODE_READ_ONLY", INT2FIX(NOTMUCH_DATABASE_MODE_READ_ONLY)); - rb_define_const(mod, "MODE_READ_WRITE", INT2FIX(NOTMUCH_DATABASE_MODE_READ_WRITE)); - rb_define_const(mod, "SORT_OLDEST_FIRST", INT2FIX(NOTMUCH_SORT_OLDEST_FIRST)); - rb_define_const(mod, "SORT_NEWEST_FIRST", INT2FIX(NOTMUCH_SORT_NEWEST_FIRST)); - rb_define_const(mod, "SORT_MESSAGE_ID", INT2FIX(NOTMUCH_SORT_MESSAGE_ID)); - rb_define_const(mod, "SORT_UNSORTED", INT2FIX(NOTMUCH_SORT_UNSORTED)); - rb_define_const(mod, "MESSAGE_FLAG_MATCH", INT2FIX(NOTMUCH_MESSAGE_FLAG_MATCH)); - rb_define_const(mod, "TAG_MAX", INT2FIX(NOTMUCH_TAG_MAX)); + mod = rb_define_module ("Notmuch"); - notmuch_rb_eBaseError = rb_define_class_under(mod, "BaseError", rb_eStandardError); - notmuch_rb_eDatabaseError = rb_define_class_under(mod, "DatabaseError", notmuch_rb_eBaseError); - notmuch_rb_eMemoryError = rb_define_class_under(mod, "MemoryError", notmuch_rb_eBaseError); - notmuch_rb_eReadOnlyError = rb_define_class_under(mod, "ReadOnlyError", notmuch_rb_eBaseError); - notmuch_rb_eXapianError = rb_define_class_under(mod, "XapianError", notmuch_rb_eBaseError); - notmuch_rb_eFileError = rb_define_class_under(mod, "FileError", notmuch_rb_eBaseError); - notmuch_rb_eFileNotEmailError = rb_define_class_under(mod, "FileNotEmailError", notmuch_rb_eBaseError); - notmuch_rb_eNullPointerError = rb_define_class_under(mod, "NullPointerError", notmuch_rb_eBaseError); - notmuch_rb_eTagTooLongError = rb_define_class_under(mod, "TagTooLongError", notmuch_rb_eBaseError); - notmuch_rb_eUnbalancedFreezeThawError = rb_define_class_under(mod, "UnbalancedFreezeThawError", - notmuch_rb_eBaseError); + /* + * Document-const: Notmuch::MODE_READ_ONLY + * + * Open the database in read only mode + */ + rb_define_const (mod, "MODE_READ_ONLY", INT2FIX (NOTMUCH_DATABASE_MODE_READ_ONLY)); + /* + * Document-const: Notmuch::MODE_READ_WRITE + * + * Open the database in read write mode + */ + rb_define_const (mod, "MODE_READ_WRITE", INT2FIX (NOTMUCH_DATABASE_MODE_READ_WRITE)); + /* + * Document-const: Notmuch::SORT_OLDEST_FIRST + * + * Sort query results by oldest first + */ + rb_define_const (mod, "SORT_OLDEST_FIRST", INT2FIX (NOTMUCH_SORT_OLDEST_FIRST)); + /* + * Document-const: Notmuch::SORT_NEWEST_FIRST + * + * Sort query results by newest first + */ + rb_define_const (mod, "SORT_NEWEST_FIRST", INT2FIX (NOTMUCH_SORT_NEWEST_FIRST)); + /* + * Document-const: Notmuch::SORT_MESSAGE_ID + * + * Sort query results by message id + */ + rb_define_const (mod, "SORT_MESSAGE_ID", INT2FIX (NOTMUCH_SORT_MESSAGE_ID)); + /* + * Document-const: Notmuch::SORT_UNSORTED + * + * Do not sort query results + */ + rb_define_const (mod, "SORT_UNSORTED", INT2FIX (NOTMUCH_SORT_UNSORTED)); + /* + * Document-const: Notmuch::MESSAGE_FLAG_MATCH + * + * Message flag "match" + */ + rb_define_const (mod, "MESSAGE_FLAG_MATCH", INT2FIX (NOTMUCH_MESSAGE_FLAG_MATCH)); + /* + * Document-const: Notmuch::TAG_MAX + * + * Maximum allowed length of a tag + */ + rb_define_const (mod, "TAG_MAX", INT2FIX (NOTMUCH_TAG_MAX)); - notmuch_rb_cDatabase = rb_define_class_under(mod, "Database", rb_cData); - rb_define_alloc_func(notmuch_rb_cDatabase, notmuch_rb_database_alloc); - rb_define_singleton_method(notmuch_rb_cDatabase, "open", notmuch_rb_database_open, -1); - rb_define_method(notmuch_rb_cDatabase, "initialize", notmuch_rb_database_initialize, -1); - rb_define_method(notmuch_rb_cDatabase, "close", notmuch_rb_database_close, 0); - rb_define_method(notmuch_rb_cDatabase, "path", notmuch_rb_database_path, 0); - rb_define_method(notmuch_rb_cDatabase, "version", notmuch_rb_database_version, 0); - rb_define_method(notmuch_rb_cDatabase, "needs_upgrade?", notmuch_rb_database_needs_upgrade, 0); - rb_define_method(notmuch_rb_cDatabase, "upgrade!", notmuch_rb_database_upgrade, 0); - rb_define_method(notmuch_rb_cDatabase, "get_directory", notmuch_rb_database_get_directory, 1); - rb_define_method(notmuch_rb_cDatabase, "add_message", notmuch_rb_database_add_message, 1); - rb_define_method(notmuch_rb_cDatabase, "remove_message", notmuch_rb_database_remove_message, 1); - rb_define_method(notmuch_rb_cDatabase, "query", notmuch_rb_database_query_create, 1); + /* + * Document-class: Notmuch::BaseError + * + * Base class for all notmuch exceptions + */ + notmuch_rb_eBaseError = rb_define_class_under (mod, "BaseError", rb_eStandardError); + /* + * Document-class: Notmuch::DatabaseError + * + * Raised when the database can't be created or opened. + */ + notmuch_rb_eDatabaseError = rb_define_class_under (mod, "DatabaseError", notmuch_rb_eBaseError); + /* + * Document-class: Notmuch::MemoryError + * + * Raised when notmuch is out of memory + */ + notmuch_rb_eMemoryError = rb_define_class_under (mod, "MemoryError", notmuch_rb_eBaseError); + /* + * Document-class: Notmuch::ReadOnlyError + * + * Raised when an attempt was made to write to a database opened in read-only + * mode. + */ + notmuch_rb_eReadOnlyError = rb_define_class_under (mod, "ReadOnlyError", notmuch_rb_eBaseError); + /* + * Document-class: Notmuch::XapianError + * + * Raised when a Xapian exception occurs + */ + notmuch_rb_eXapianError = rb_define_class_under (mod, "XapianError", notmuch_rb_eBaseError); + /* + * Document-class: Notmuch::FileError + * + * Raised when an error occurs trying to read or write to a file. + */ + notmuch_rb_eFileError = rb_define_class_under (mod, "FileError", notmuch_rb_eBaseError); + /* + * Document-class: Notmuch::FileNotEmailError + * + * Raised when a file is presented that doesn't appear to be an email message. + */ + notmuch_rb_eFileNotEmailError = rb_define_class_under (mod, "FileNotEmailError", notmuch_rb_eBaseError); + /* + * Document-class: Notmuch::NullPointerError + * + * Raised when the user erroneously passes a +NULL+ pointer to a notmuch + * function. + */ + notmuch_rb_eNullPointerError = rb_define_class_under (mod, "NullPointerError", notmuch_rb_eBaseError); + /* + * Document-class: Notmuch::TagTooLongError + * + * Raised when a tag value is too long (exceeds Notmuch::TAG_MAX) + */ + notmuch_rb_eTagTooLongError = rb_define_class_under (mod, "TagTooLongError", notmuch_rb_eBaseError); + /* + * Document-class: Notmuch::UnbalancedFreezeThawError + * + * Raised when the notmuch_message_thaw function has been called more times + * than notmuch_message_freeze. + */ + notmuch_rb_eUnbalancedFreezeThawError = rb_define_class_under (mod, "UnbalancedFreezeThawError", + notmuch_rb_eBaseError); + /* + * Document-class: Notmuch::UnbalancedAtomicError + * + * Raised when notmuch_database_end_atomic has been called more times than + * notmuch_database_begin_atomic + */ + notmuch_rb_eUnbalancedAtomicError = rb_define_class_under (mod, "UnbalancedAtomicError", + notmuch_rb_eBaseError); + /* + * Document-class: Notmuch::Database + * + * Notmuch database interaction + */ + notmuch_rb_cDatabase = rb_define_class_under (mod, "Database", rb_cData); + rb_define_alloc_func (notmuch_rb_cDatabase, notmuch_rb_database_alloc); + rb_define_singleton_method (notmuch_rb_cDatabase, "open", notmuch_rb_database_open, -1); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "initialize", notmuch_rb_database_initialize, -1); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "close", notmuch_rb_database_close, 0); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "path", notmuch_rb_database_path, 0); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "version", notmuch_rb_database_version, 0); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "needs_upgrade?", notmuch_rb_database_needs_upgrade, 0); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "upgrade!", notmuch_rb_database_upgrade, 0); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "begin_atomic", notmuch_rb_database_begin_atomic, 0); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "end_atomic", notmuch_rb_database_end_atomic, 0); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "get_directory", notmuch_rb_database_get_directory, 1); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "add_message", notmuch_rb_database_add_message, 1); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "remove_message", notmuch_rb_database_remove_message, 1); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "find_message", + notmuch_rb_database_find_message, 1); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "find_message_by_filename", + notmuch_rb_database_find_message_by_filename, 1); /* in database.c */ + rb_define_method (notmuch_rb_cDatabase, "query", notmuch_rb_database_query_create, 1); /* in database.c */ - notmuch_rb_cDirectory = rb_define_class_under(mod, "Directory", rb_cData); - rb_undef_method(notmuch_rb_cDirectory, "initialize"); - rb_define_method(notmuch_rb_cDirectory, "destroy", notmuch_rb_directory_destroy, 0); - rb_define_method(notmuch_rb_cDirectory, "mtime", notmuch_rb_directory_get_mtime, 0); - rb_define_method(notmuch_rb_cDirectory, "mtime=", notmuch_rb_directory_set_mtime, 1); - rb_define_method(notmuch_rb_cDirectory, "child_files", notmuch_rb_directory_get_child_files, 0); - rb_define_method(notmuch_rb_cDirectory, "child_directories", notmuch_rb_directory_get_child_directories, 0); + /* + * Document-class: Notmuch::Directory + * + * Notmuch directory + */ + notmuch_rb_cDirectory = rb_define_class_under (mod, "Directory", rb_cData); + rb_undef_method (notmuch_rb_cDirectory, "initialize"); + rb_define_method (notmuch_rb_cDirectory, "destroy!", notmuch_rb_directory_destroy, 0); /* in directory.c */ + rb_define_method (notmuch_rb_cDirectory, "mtime", notmuch_rb_directory_get_mtime, 0); /* in directory.c */ + rb_define_method (notmuch_rb_cDirectory, "mtime=", notmuch_rb_directory_set_mtime, 1); /* in directory.c */ + rb_define_method (notmuch_rb_cDirectory, "child_files", notmuch_rb_directory_get_child_files, 0); /* in directory.c */ + rb_define_method (notmuch_rb_cDirectory, "child_directories", notmuch_rb_directory_get_child_directories, 0); /* in directory.c */ - notmuch_rb_cFileNames = rb_define_class_under(mod, "FileNames", rb_cData); - rb_undef_method(notmuch_rb_cFileNames, "initialize"); - rb_define_method(notmuch_rb_cFileNames, "destroy", notmuch_rb_filenames_destroy, 0); - rb_define_method(notmuch_rb_cFileNames, "each", notmuch_rb_filenames_each, 0); - rb_include_module(notmuch_rb_cFileNames, rb_mEnumerable); + /* + * Document-class: Notmuch::FileNames + * + * Notmuch file names + */ + notmuch_rb_cFileNames = rb_define_class_under (mod, "FileNames", rb_cData); + rb_undef_method (notmuch_rb_cFileNames, "initialize"); + rb_define_method (notmuch_rb_cFileNames, "destroy!", notmuch_rb_filenames_destroy, 0); /* in filenames.c */ + rb_define_method (notmuch_rb_cFileNames, "each", notmuch_rb_filenames_each, 0); /* in filenames.c */ + rb_include_module (notmuch_rb_cFileNames, rb_mEnumerable); - notmuch_rb_cQuery = rb_define_class_under(mod, "Query", rb_cData); - rb_undef_method(notmuch_rb_cQuery, "initialize"); - rb_define_method(notmuch_rb_cQuery, "destroy", notmuch_rb_query_destroy, 0); - rb_define_method(notmuch_rb_cQuery, "sort", notmuch_rb_query_get_sort, 0); - rb_define_method(notmuch_rb_cQuery, "sort=", notmuch_rb_query_set_sort, 1); - rb_define_method(notmuch_rb_cQuery, "to_s", notmuch_rb_query_get_string, 0); - rb_define_method(notmuch_rb_cQuery, "search_threads", notmuch_rb_query_search_threads, 0); - rb_define_method(notmuch_rb_cQuery, "search_messages", notmuch_rb_query_search_messages, 0); + /* + * Document-class: Notmuch::Query + * + * Notmuch query + */ + notmuch_rb_cQuery = rb_define_class_under (mod, "Query", rb_cData); + rb_undef_method (notmuch_rb_cQuery, "initialize"); + rb_define_method (notmuch_rb_cQuery, "destroy!", notmuch_rb_query_destroy, 0); /* in query.c */ + rb_define_method (notmuch_rb_cQuery, "sort", notmuch_rb_query_get_sort, 0); /* in query.c */ + rb_define_method (notmuch_rb_cQuery, "sort=", notmuch_rb_query_set_sort, 1); /* in query.c */ + rb_define_method (notmuch_rb_cQuery, "to_s", notmuch_rb_query_get_string, 0); /* in query.c */ + rb_define_method (notmuch_rb_cQuery, "search_threads", notmuch_rb_query_search_threads, 0); /* in query.c */ + rb_define_method (notmuch_rb_cQuery, "search_messages", notmuch_rb_query_search_messages, 0); /* in query.c */ - notmuch_rb_cThreads = rb_define_class_under(mod, "Threads", rb_cData); - rb_undef_method(notmuch_rb_cThreads, "initialize"); - rb_define_method(notmuch_rb_cThreads, "destroy", notmuch_rb_threads_destroy, 0); - rb_define_method(notmuch_rb_cThreads, "each", notmuch_rb_threads_each, 0); - rb_include_module(notmuch_rb_cThreads, rb_mEnumerable); + /* + * Document-class: Notmuch::Threads + * + * Notmuch threads + */ + notmuch_rb_cThreads = rb_define_class_under (mod, "Threads", rb_cData); + rb_undef_method (notmuch_rb_cThreads, "initialize"); + rb_define_method (notmuch_rb_cThreads, "destroy!", notmuch_rb_threads_destroy, 0); /* in threads.c */ + rb_define_method (notmuch_rb_cThreads, "each", notmuch_rb_threads_each, 0); /* in threads.c */ + rb_include_module (notmuch_rb_cThreads, rb_mEnumerable); - notmuch_rb_cMessages = rb_define_class_under(mod, "Messages", rb_cData); - rb_undef_method(notmuch_rb_cMessages, "initialize"); - rb_define_method(notmuch_rb_cMessages, "destroy", notmuch_rb_messages_destroy, 0); - rb_define_method(notmuch_rb_cMessages, "each", notmuch_rb_messages_each, 0); - rb_define_method(notmuch_rb_cMessages, "tags", notmuch_rb_messages_collect_tags, 0); - rb_include_module(notmuch_rb_cMessages, rb_mEnumerable); + /* + * Document-class: Notmuch::Messages + * + * Notmuch messages + */ + notmuch_rb_cMessages = rb_define_class_under (mod, "Messages", rb_cData); + rb_undef_method (notmuch_rb_cMessages, "initialize"); + rb_define_method (notmuch_rb_cMessages, "destroy!", notmuch_rb_messages_destroy, 0); /* in messages.c */ + rb_define_method (notmuch_rb_cMessages, "each", notmuch_rb_messages_each, 0); /* in messages.c */ + rb_define_method (notmuch_rb_cMessages, "tags", notmuch_rb_messages_collect_tags, 0); /* in messages.c */ + rb_include_module (notmuch_rb_cMessages, rb_mEnumerable); - notmuch_rb_cThread = rb_define_class_under(mod, "Thread", rb_cData); - rb_undef_method(notmuch_rb_cThread, "initialize"); - rb_define_method(notmuch_rb_cThread, "destroy", notmuch_rb_thread_destroy, 0); - rb_define_method(notmuch_rb_cThread, "thread_id", notmuch_rb_thread_get_thread_id, 0); - rb_define_method(notmuch_rb_cThread, "total_messages", notmuch_rb_thread_get_total_messages, 0); - rb_define_method(notmuch_rb_cThread, "toplevel_messages", notmuch_rb_thread_get_toplevel_messages, 0); - rb_define_method(notmuch_rb_cThread, "matched_messages", notmuch_rb_thread_get_matched_messages, 0); - rb_define_method(notmuch_rb_cThread, "authors", notmuch_rb_thread_get_authors, 0); - rb_define_method(notmuch_rb_cThread, "subject", notmuch_rb_thread_get_subject, 0); - rb_define_method(notmuch_rb_cThread, "oldest_date", notmuch_rb_thread_get_oldest_date, 0); - rb_define_method(notmuch_rb_cThread, "newest_date", notmuch_rb_thread_get_newest_date, 0); - rb_define_method(notmuch_rb_cThread, "tags", notmuch_rb_thread_get_tags, 0); + /* + * Document-class: Notmuch::Thread + * + * Notmuch thread + */ + notmuch_rb_cThread = rb_define_class_under (mod, "Thread", rb_cData); + rb_undef_method (notmuch_rb_cThread, "initialize"); + rb_define_method (notmuch_rb_cThread, "destroy!", notmuch_rb_thread_destroy, 0); /* in thread.c */ + rb_define_method (notmuch_rb_cThread, "thread_id", notmuch_rb_thread_get_thread_id, 0); /* in thread.c */ + rb_define_method (notmuch_rb_cThread, "total_messages", notmuch_rb_thread_get_total_messages, 0); /* in thread.c */ + rb_define_method (notmuch_rb_cThread, "toplevel_messages", notmuch_rb_thread_get_toplevel_messages, 0); /* in thread.c */ + rb_define_method (notmuch_rb_cThread, "matched_messages", notmuch_rb_thread_get_matched_messages, 0); /* in thread.c */ + rb_define_method (notmuch_rb_cThread, "authors", notmuch_rb_thread_get_authors, 0); /* in thread.c */ + rb_define_method (notmuch_rb_cThread, "subject", notmuch_rb_thread_get_subject, 0); /* in thread.c */ + rb_define_method (notmuch_rb_cThread, "oldest_date", notmuch_rb_thread_get_oldest_date, 0); /* in thread.c */ + rb_define_method (notmuch_rb_cThread, "newest_date", notmuch_rb_thread_get_newest_date, 0); /* in thread.c */ + rb_define_method (notmuch_rb_cThread, "tags", notmuch_rb_thread_get_tags, 0); /* in thread.c */ - notmuch_rb_cMessage = rb_define_class_under(mod, "Message", rb_cData); - rb_undef_method(notmuch_rb_cMessage, "initialize"); - rb_define_method(notmuch_rb_cMessage, "destroy", notmuch_rb_message_destroy, 0); - rb_define_method(notmuch_rb_cMessage, "message_id", notmuch_rb_message_get_message_id, 0); - rb_define_method(notmuch_rb_cMessage, "thread_id", notmuch_rb_message_get_thread_id, 0); - rb_define_method(notmuch_rb_cMessage, "replies", notmuch_rb_message_get_replies, 0); - rb_define_method(notmuch_rb_cMessage, "filename", notmuch_rb_message_get_filename, 0); - rb_define_method(notmuch_rb_cMessage, "filenames", notmuch_rb_message_get_filenames, 0); - rb_define_method(notmuch_rb_cMessage, "get_flag", notmuch_rb_message_get_flag, 1); - rb_define_method(notmuch_rb_cMessage, "set_flag", notmuch_rb_message_set_flag, 2); - rb_define_method(notmuch_rb_cMessage, "date", notmuch_rb_message_get_date, 0); - rb_define_method(notmuch_rb_cMessage, "header", notmuch_rb_message_get_header, 1); - rb_define_alias(notmuch_rb_cMessage, "[]", "header"); - rb_define_method(notmuch_rb_cMessage, "tags", notmuch_rb_message_get_tags, 0); - rb_define_method(notmuch_rb_cMessage, "add_tag", notmuch_rb_message_add_tag, 1); - rb_define_alias(notmuch_rb_cMessage, "<<", "add_tag"); - rb_define_method(notmuch_rb_cMessage, "remove_tag", notmuch_rb_message_remove_tag, 1); - rb_define_method(notmuch_rb_cMessage, "remove_all_tags", notmuch_rb_message_remove_all_tags, 0); - rb_define_method(notmuch_rb_cMessage, "maildir_flags_to_tags", notmuch_rb_message_maildir_flags_to_tags, 0); - rb_define_method(notmuch_rb_cMessage, "tags_to_maildir_flags", notmuch_rb_message_tags_to_maildir_flags, 0); - rb_define_method(notmuch_rb_cMessage, "freeze", notmuch_rb_message_freeze, 0); - rb_define_method(notmuch_rb_cMessage, "thaw", notmuch_rb_message_thaw, 0); + /* + * Document-class: Notmuch::Message + * + * Notmuch message + */ + notmuch_rb_cMessage = rb_define_class_under (mod, "Message", rb_cData); + rb_undef_method (notmuch_rb_cMessage, "initialize"); + rb_define_method (notmuch_rb_cMessage, "destroy!", notmuch_rb_message_destroy, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "message_id", notmuch_rb_message_get_message_id, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "thread_id", notmuch_rb_message_get_thread_id, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "replies", notmuch_rb_message_get_replies, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "filename", notmuch_rb_message_get_filename, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "filenames", notmuch_rb_message_get_filenames, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "get_flag", notmuch_rb_message_get_flag, 1); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "set_flag", notmuch_rb_message_set_flag, 2); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "date", notmuch_rb_message_get_date, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "header", notmuch_rb_message_get_header, 1); /* in message.c */ + rb_define_alias (notmuch_rb_cMessage, "[]", "header"); + rb_define_method (notmuch_rb_cMessage, "tags", notmuch_rb_message_get_tags, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "add_tag", notmuch_rb_message_add_tag, 1); /* in message.c */ + rb_define_alias (notmuch_rb_cMessage, "<<", "add_tag"); + rb_define_method (notmuch_rb_cMessage, "remove_tag", notmuch_rb_message_remove_tag, 1); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "remove_all_tags", notmuch_rb_message_remove_all_tags, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "maildir_flags_to_tags", notmuch_rb_message_maildir_flags_to_tags, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "tags_to_maildir_flags", notmuch_rb_message_tags_to_maildir_flags, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "freeze", notmuch_rb_message_freeze, 0); /* in message.c */ + rb_define_method (notmuch_rb_cMessage, "thaw", notmuch_rb_message_thaw, 0); /* in message.c */ - notmuch_rb_cTags = rb_define_class_under(mod, "Tags", rb_cData); - rb_undef_method(notmuch_rb_cTags, "initialize"); - rb_define_method(notmuch_rb_cTags, "destroy", notmuch_rb_tags_destroy, 0); - rb_define_method(notmuch_rb_cTags, "each", notmuch_rb_tags_each, 0); - rb_include_module(notmuch_rb_cTags, rb_mEnumerable); + /* + * Document-class: Notmuch::Tags + * + * Notmuch tags + */ + notmuch_rb_cTags = rb_define_class_under (mod, "Tags", rb_cData); + rb_undef_method (notmuch_rb_cTags, "initialize"); + rb_define_method (notmuch_rb_cTags, "destroy!", notmuch_rb_tags_destroy, 0); /* in tags.c */ + rb_define_method (notmuch_rb_cTags, "each", notmuch_rb_tags_each, 0); /* in tags.c */ + rb_include_module (notmuch_rb_cTags, rb_mEnumerable); } diff --git a/bindings/ruby/message.c b/bindings/ruby/message.c index 49dbace3..eed4b31b 100644 --- a/bindings/ruby/message.c +++ b/bindings/ruby/message.c @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,19 +21,19 @@ #include "defs.h" /* - * call-seq: MESSAGE.destroy => nil + * call-seq: MESSAGE.destroy! => nil * * Destroys the message, freeing all resources allocated for it. */ VALUE -notmuch_rb_message_destroy(VALUE self) +notmuch_rb_message_destroy (VALUE self) { notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - notmuch_message_destroy(message); - DATA_PTR(self) = NULL; + notmuch_message_destroy (message); + DATA_PTR (self) = NULL; return Qnil; } @@ -44,16 +44,16 @@ notmuch_rb_message_destroy(VALUE self) * Get the message ID of 'message'. */ VALUE -notmuch_rb_message_get_message_id(VALUE self) +notmuch_rb_message_get_message_id (VALUE self) { const char *msgid; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - msgid = notmuch_message_get_message_id(message); + msgid = notmuch_message_get_message_id (message); - return rb_str_new2(msgid); + return rb_str_new2 (msgid); } /* @@ -62,16 +62,16 @@ notmuch_rb_message_get_message_id(VALUE self) * Get the thread ID of 'message'. */ VALUE -notmuch_rb_message_get_thread_id(VALUE self) +notmuch_rb_message_get_thread_id (VALUE self) { const char *tid; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - tid = notmuch_message_get_thread_id(message); + tid = notmuch_message_get_thread_id (message); - return rb_str_new2(tid); + return rb_str_new2 (tid); } /* @@ -80,16 +80,16 @@ notmuch_rb_message_get_thread_id(VALUE self) * Get a Notmuch::Messages enumerable for all of the replies to 'message'. */ VALUE -notmuch_rb_message_get_replies(VALUE self) +notmuch_rb_message_get_replies (VALUE self) { notmuch_messages_t *messages; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - messages = notmuch_message_get_replies(message); + messages = notmuch_message_get_replies (message); - return Data_Wrap_Struct(notmuch_rb_cMessages, NULL, NULL, messages); + return Data_Wrap_Struct (notmuch_rb_cMessages, NULL, NULL, messages); } /* @@ -98,16 +98,16 @@ notmuch_rb_message_get_replies(VALUE self) * Get a filename for the email corresponding to 'message' */ VALUE -notmuch_rb_message_get_filename(VALUE self) +notmuch_rb_message_get_filename (VALUE self) { const char *fname; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - fname = notmuch_message_get_filename(message); + fname = notmuch_message_get_filename (message); - return rb_str_new2(fname); + return rb_str_new2 (fname); } /* @@ -116,52 +116,52 @@ notmuch_rb_message_get_filename(VALUE self) * Get all filenames for the email corresponding to MESSAGE. */ VALUE -notmuch_rb_message_get_filenames(VALUE self) +notmuch_rb_message_get_filenames (VALUE self) { notmuch_filenames_t *fnames; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - fnames = notmuch_message_get_filenames(message); + fnames = notmuch_message_get_filenames (message); - return Data_Wrap_Struct(notmuch_rb_cFileNames, NULL, NULL, fnames); + return Data_Wrap_Struct (notmuch_rb_cFileNames, NULL, NULL, fnames); } /* - * call-seq: MESSAGE.get_flag(flag) => true or false + * call-seq: MESSAGE.get_flag (flag) => true or false * * Get a value of a flag for the email corresponding to 'message' */ VALUE -notmuch_rb_message_get_flag(VALUE self, VALUE flagv) +notmuch_rb_message_get_flag (VALUE self, VALUE flagv) { notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - if (!FIXNUM_P(flagv)) - rb_raise(rb_eTypeError, "Flag not a Fixnum"); + if (!FIXNUM_P (flagv)) + rb_raise (rb_eTypeError, "Flag not a Fixnum"); - return notmuch_message_get_flag(message, FIX2INT(flagv)) ? Qtrue : Qfalse; + return notmuch_message_get_flag (message, FIX2INT (flagv)) ? Qtrue : Qfalse; } /* - * call-seq: MESSAGE.set_flag(flag, value) => nil + * call-seq: MESSAGE.set_flag (flag, value) => nil * * Set a value of a flag for the email corresponding to 'message' */ VALUE -notmuch_rb_message_set_flag(VALUE self, VALUE flagv, VALUE valuev) +notmuch_rb_message_set_flag (VALUE self, VALUE flagv, VALUE valuev) { notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - if (!FIXNUM_P(flagv)) - rb_raise(rb_eTypeError, "Flag not a Fixnum"); + if (!FIXNUM_P (flagv)) + rb_raise (rb_eTypeError, "Flag not a Fixnum"); - notmuch_message_set_flag(message, FIX2INT(flagv), RTEST(valuev)); + notmuch_message_set_flag (message, FIX2INT (flagv), RTEST (valuev)); return Qnil; } @@ -172,40 +172,36 @@ notmuch_rb_message_set_flag(VALUE self, VALUE flagv, VALUE valuev) * Get the date of 'message' */ VALUE -notmuch_rb_message_get_date(VALUE self) +notmuch_rb_message_get_date (VALUE self) { notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - return UINT2NUM(notmuch_message_get_date(message)); + return UINT2NUM (notmuch_message_get_date (message)); } /* - * call-seq: MESSAGE.header(name) => String + * call-seq: MESSAGE.header (name) => String * * Get the value of the specified header from 'message' */ VALUE -notmuch_rb_message_get_header(VALUE self, VALUE headerv) +notmuch_rb_message_get_header (VALUE self, VALUE headerv) { const char *header, *value; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); -#if !defined(RSTRING_PTR) -#define RSTRING_PTR(v) (RSTRING((v))->ptr) -#endif /* !defined(RSTRING_PTR) */ + SafeStringValue (headerv); + header = RSTRING_PTR (headerv); - SafeStringValue(headerv); - header = RSTRING_PTR(headerv); - - value = notmuch_message_get_header(message, header); + value = notmuch_message_get_header (message, header); if (!value) - rb_raise(notmuch_rb_eMemoryError, "Out of memory"); + rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return rb_str_new2(value); + return rb_str_new2 (value); } /* @@ -214,70 +210,62 @@ notmuch_rb_message_get_header(VALUE self, VALUE headerv) * Get a Notmuch::Tags enumerable for all of the tags of 'message'. */ VALUE -notmuch_rb_message_get_tags(VALUE self) +notmuch_rb_message_get_tags (VALUE self) { notmuch_message_t *message; notmuch_tags_t *tags; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - tags = notmuch_message_get_tags(message); + tags = notmuch_message_get_tags (message); if (!tags) - rb_raise(notmuch_rb_eMemoryError, "Out of memory"); + rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return Data_Wrap_Struct(notmuch_rb_cTags, NULL, NULL, tags); + return Data_Wrap_Struct (notmuch_rb_cTags, NULL, NULL, tags); } /* - * call-seq: MESSAGE.add_tag(tag) => true + * call-seq: MESSAGE.add_tag (tag) => true * * Add a tag to the 'message' */ VALUE -notmuch_rb_message_add_tag(VALUE self, VALUE tagv) +notmuch_rb_message_add_tag (VALUE self, VALUE tagv) { const char *tag; notmuch_status_t ret; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); - -#if !defined(RSTRING_PTR) -#define RSTRING_PTR(v) (RSTRING((v))->ptr) -#endif /* !defined(RSTRING_PTR) */ + Data_Get_Notmuch_Message (self, message); - SafeStringValue(tagv); - tag = RSTRING_PTR(tagv); + SafeStringValue (tagv); + tag = RSTRING_PTR (tagv); - ret = notmuch_message_add_tag(message, tag); - notmuch_rb_status_raise(ret); + ret = notmuch_message_add_tag (message, tag); + notmuch_rb_status_raise (ret); return Qtrue; } /* - * call-seq: MESSAGE.remove_tag(tag) => true + * call-seq: MESSAGE.remove_tag (tag) => true * * Remove a tag from the 'message' */ VALUE -notmuch_rb_message_remove_tag(VALUE self, VALUE tagv) +notmuch_rb_message_remove_tag (VALUE self, VALUE tagv) { const char *tag; notmuch_status_t ret; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); - -#if !defined(RSTRING_PTR) -#define RSTRING_PTR(v) (RSTRING((v))->ptr) -#endif /* !defined(RSTRING_PTR) */ + Data_Get_Notmuch_Message (self, message); - SafeStringValue(tagv); - tag = RSTRING_PTR(tagv); + SafeStringValue (tagv); + tag = RSTRING_PTR (tagv); - ret = notmuch_message_remove_tag(message, tag); - notmuch_rb_status_raise(ret); + ret = notmuch_message_remove_tag (message, tag); + notmuch_rb_status_raise (ret); return Qtrue; } @@ -288,15 +276,15 @@ notmuch_rb_message_remove_tag(VALUE self, VALUE tagv) * Remove all tags of the 'message' */ VALUE -notmuch_rb_message_remove_all_tags(VALUE self) +notmuch_rb_message_remove_all_tags (VALUE self) { notmuch_status_t ret; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - ret = notmuch_message_remove_all_tags(message); - notmuch_rb_status_raise(ret); + ret = notmuch_message_remove_all_tags (message); + notmuch_rb_status_raise (ret); return Qtrue; } @@ -304,18 +292,18 @@ notmuch_rb_message_remove_all_tags(VALUE self) /* * call-seq: MESSAGE.maildir_flags_to_tags => true * - * Add/remove tags according to maildir flags in the message filename(s) + * Add/remove tags according to maildir flags in the message filename (s) */ VALUE -notmuch_rb_message_maildir_flags_to_tags(VALUE self) +notmuch_rb_message_maildir_flags_to_tags (VALUE self) { notmuch_status_t ret; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - ret = notmuch_message_maildir_flags_to_tags(message); - notmuch_rb_status_raise(ret); + ret = notmuch_message_maildir_flags_to_tags (message); + notmuch_rb_status_raise (ret); return Qtrue; } @@ -323,18 +311,18 @@ notmuch_rb_message_maildir_flags_to_tags(VALUE self) /* * call-seq: MESSAGE.tags_to_maildir_flags => true * - * Rename message filename(s) to encode tags as maildir flags + * Rename message filename (s) to encode tags as maildir flags */ VALUE -notmuch_rb_message_tags_to_maildir_flags(VALUE self) +notmuch_rb_message_tags_to_maildir_flags (VALUE self) { notmuch_status_t ret; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - ret = notmuch_message_tags_to_maildir_flags(message); - notmuch_rb_status_raise(ret); + ret = notmuch_message_tags_to_maildir_flags (message); + notmuch_rb_status_raise (ret); return Qtrue; } @@ -345,15 +333,15 @@ notmuch_rb_message_tags_to_maildir_flags(VALUE self) * Freeze the 'message' */ VALUE -notmuch_rb_message_freeze(VALUE self) +notmuch_rb_message_freeze (VALUE self) { notmuch_status_t ret; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - ret = notmuch_message_freeze(message); - notmuch_rb_status_raise(ret); + ret = notmuch_message_freeze (message); + notmuch_rb_status_raise (ret); return Qtrue; } @@ -364,15 +352,15 @@ notmuch_rb_message_freeze(VALUE self) * Thaw a 'message' */ VALUE -notmuch_rb_message_thaw(VALUE self) +notmuch_rb_message_thaw (VALUE self) { notmuch_status_t ret; notmuch_message_t *message; - Data_Get_Notmuch_Message(self, message); + Data_Get_Notmuch_Message (self, message); - ret = notmuch_message_thaw(message); - notmuch_rb_status_raise(ret); + ret = notmuch_message_thaw (message); + notmuch_rb_status_raise (ret); return Qtrue; } diff --git a/bindings/ruby/messages.c b/bindings/ruby/messages.c index 35b5d146..443a30c9 100644 --- a/bindings/ruby/messages.c +++ b/bindings/ruby/messages.c @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,19 +21,19 @@ #include "defs.h" /* - * call-seq: MESSAGES.destroy => nil + * call-seq: MESSAGES.destroy! => nil * * Destroys the messages, freeing all resources allocated for it. */ VALUE -notmuch_rb_messages_destroy(VALUE self) +notmuch_rb_messages_destroy (VALUE self) { notmuch_messages_t *messages; - Data_Get_Notmuch_Messages(self, messages); + Data_Get_Notmuch_Messages (self, messages); - notmuch_messages_destroy(messages); - DATA_PTR(self) = NULL; + notmuch_messages_destroy (messages); + DATA_PTR (self) = NULL; return Qnil; } @@ -44,16 +44,16 @@ notmuch_rb_messages_destroy(VALUE self) * parameter. */ VALUE -notmuch_rb_messages_each(VALUE self) +notmuch_rb_messages_each (VALUE self) { notmuch_message_t *message; notmuch_messages_t *messages; - Data_Get_Notmuch_Messages(self, messages); + Data_Get_Notmuch_Messages (self, messages); - for (; notmuch_messages_valid(messages); notmuch_messages_move_to_next(messages)) { - message = notmuch_messages_get(messages); - rb_yield(Data_Wrap_Struct(notmuch_rb_cMessage, NULL, NULL, message)); + for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) { + message = notmuch_messages_get (messages); + rb_yield (Data_Wrap_Struct (notmuch_rb_cMessage, NULL, NULL, message)); } return self; @@ -65,16 +65,16 @@ notmuch_rb_messages_each(VALUE self) * Collect tags from the messages */ VALUE -notmuch_rb_messages_collect_tags(VALUE self) +notmuch_rb_messages_collect_tags (VALUE self) { notmuch_tags_t *tags; notmuch_messages_t *messages; - Data_Get_Notmuch_Messages(self, messages); + Data_Get_Notmuch_Messages (self, messages); - tags = notmuch_messages_collect_tags(messages); + tags = notmuch_messages_collect_tags (messages); if (!tags) - rb_raise(notmuch_rb_eMemoryError, "Out of memory"); + rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return Data_Wrap_Struct(notmuch_rb_cTags, NULL, NULL, tags); + return Data_Wrap_Struct (notmuch_rb_cTags, NULL, NULL, tags); } diff --git a/bindings/ruby/query.c b/bindings/ruby/query.c index ef9e1a07..74fd5858 100644 --- a/bindings/ruby/query.c +++ b/bindings/ruby/query.c @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,19 +21,19 @@ #include "defs.h" /* - * call-seq: QUERY.destroy => nil + * call-seq: QUERY.destroy! => nil * * Destroys the query, freeing all resources allocated for it. */ VALUE -notmuch_rb_query_destroy(VALUE self) +notmuch_rb_query_destroy (VALUE self) { notmuch_query_t *query; - Data_Get_Notmuch_Query(self, query); + Data_Get_Notmuch_Query (self, query); - notmuch_query_destroy(query); - DATA_PTR(self) = NULL; + notmuch_query_destroy (query); + DATA_PTR (self) = NULL; return Qnil; } @@ -44,13 +44,13 @@ notmuch_rb_query_destroy(VALUE self) * Get sort type of the +QUERY+ */ VALUE -notmuch_rb_query_get_sort(VALUE self) +notmuch_rb_query_get_sort (VALUE self) { notmuch_query_t *query; - Data_Get_Notmuch_Query(self, query); + Data_Get_Notmuch_Query (self, query); - return FIX2INT(notmuch_query_get_sort(query)); + return FIX2INT (notmuch_query_get_sort (query)); } /* @@ -59,16 +59,16 @@ notmuch_rb_query_get_sort(VALUE self) * Set sort type of the +QUERY+ */ VALUE -notmuch_rb_query_set_sort(VALUE self, VALUE sortv) +notmuch_rb_query_set_sort (VALUE self, VALUE sortv) { notmuch_query_t *query; - Data_Get_Notmuch_Query(self, query); + Data_Get_Notmuch_Query (self, query); - if (!FIXNUM_P(sortv)) - rb_raise(rb_eTypeError, "Not a Fixnum"); + if (!FIXNUM_P (sortv)) + rb_raise (rb_eTypeError, "Not a Fixnum"); - notmuch_query_set_sort(query, FIX2UINT(sortv)); + notmuch_query_set_sort (query, FIX2UINT (sortv)); return Qnil; } @@ -79,13 +79,13 @@ notmuch_rb_query_set_sort(VALUE self, VALUE sortv) * Get query string of the +QUERY+ */ VALUE -notmuch_rb_query_get_string(VALUE self) +notmuch_rb_query_get_string (VALUE self) { notmuch_query_t *query; - Data_Get_Notmuch_Query(self, query); + Data_Get_Notmuch_Query (self, query); - return rb_str_new2(notmuch_query_get_query_string(query)); + return rb_str_new2 (notmuch_query_get_query_string (query)); } /* @@ -94,18 +94,18 @@ notmuch_rb_query_get_string(VALUE self) * Search for threads */ VALUE -notmuch_rb_query_search_threads(VALUE self) +notmuch_rb_query_search_threads (VALUE self) { notmuch_query_t *query; notmuch_threads_t *threads; - Data_Get_Notmuch_Query(self, query); + Data_Get_Notmuch_Query (self, query); - threads = notmuch_query_search_threads(query); + threads = notmuch_query_search_threads (query); if (!threads) - rb_raise(notmuch_rb_eMemoryError, "Out of memory"); + rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return Data_Wrap_Struct(notmuch_rb_cThreads, NULL, NULL, threads); + return Data_Wrap_Struct (notmuch_rb_cThreads, NULL, NULL, threads); } /* @@ -114,16 +114,16 @@ notmuch_rb_query_search_threads(VALUE self) * Search for messages */ VALUE -notmuch_rb_query_search_messages(VALUE self) +notmuch_rb_query_search_messages (VALUE self) { notmuch_query_t *query; notmuch_messages_t *messages; - Data_Get_Notmuch_Query(self, query); + Data_Get_Notmuch_Query (self, query); - messages = notmuch_query_search_messages(query); + messages = notmuch_query_search_messages (query); if (!messages) - rb_raise(notmuch_rb_eMemoryError, "Out of memory"); + rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return Data_Wrap_Struct(notmuch_rb_cMessages, NULL, NULL, messages); + return Data_Wrap_Struct (notmuch_rb_cMessages, NULL, NULL, messages); } diff --git a/bindings/ruby/rdoc.sh b/bindings/ruby/rdoc.sh index 0e86818a..1e867ff9 100755 --- a/bindings/ruby/rdoc.sh +++ b/bindings/ruby/rdoc.sh @@ -1,10 +1,17 @@ #!/bin/sh -# vim: set sw=4 et sts=4 tw=80 : -# RDoc fails to document C extension split into many files. -# This is a hack to generate documentation properly. +if test -z "$RDOC"; then + RDOC=rdoc + if which rdoc19 >/dev/null 2>&1; then + RDOC=rdoc19 + fi +fi -rm -fr ruby -cat *.c > rdoc-sucks.c -rdoc --main 'Notmuch' --title 'Notmuch Ruby API' --op ruby rdoc-sucks.c -rm -f rdoc-sucks.c +set -e +set -x + +$RDOC --main 'Notmuch' --title 'Notmuch Ruby API' --op ruby *.c + +if test "$1" = "--upload"; then + rsync -avze ssh --delete --partial --progress ruby bach.exherbo.org:public_html/notmuch/ +fi diff --git a/bindings/ruby/status.c b/bindings/ruby/status.c index 3d106ca9..b11fb9fb 100644 --- a/bindings/ruby/status.c +++ b/bindings/ruby/status.c @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,29 +21,31 @@ #include "defs.h" void -notmuch_rb_status_raise(notmuch_status_t status) +notmuch_rb_status_raise (notmuch_status_t status) { switch (status) { case NOTMUCH_STATUS_SUCCESS: case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: - break; + break; case NOTMUCH_STATUS_OUT_OF_MEMORY: - rb_raise(notmuch_rb_eMemoryError, "out of memory"); + rb_raise (notmuch_rb_eMemoryError, "out of memory"); case NOTMUCH_STATUS_READ_ONLY_DATABASE: - rb_raise(notmuch_rb_eReadOnlyError, "read-only database"); + rb_raise (notmuch_rb_eReadOnlyError, "read-only database"); case NOTMUCH_STATUS_XAPIAN_EXCEPTION: - rb_raise(notmuch_rb_eXapianError, "xapian exception"); + rb_raise (notmuch_rb_eXapianError, "xapian exception"); case NOTMUCH_STATUS_FILE_ERROR: - rb_raise(notmuch_rb_eFileError, "failed to read/write file"); + rb_raise (notmuch_rb_eFileError, "failed to read/write file"); case NOTMUCH_STATUS_FILE_NOT_EMAIL: - rb_raise(notmuch_rb_eFileNotEmailError, "file not email"); + rb_raise (notmuch_rb_eFileNotEmailError, "file not email"); case NOTMUCH_STATUS_NULL_POINTER: - rb_raise(notmuch_rb_eNullPointerError, "null pointer"); + rb_raise (notmuch_rb_eNullPointerError, "null pointer"); case NOTMUCH_STATUS_TAG_TOO_LONG: - rb_raise(notmuch_rb_eTagTooLongError, "tag too long"); + rb_raise (notmuch_rb_eTagTooLongError, "tag too long"); case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: - rb_raise(notmuch_rb_eUnbalancedFreezeThawError, "unbalanced freeze/thaw"); + rb_raise (notmuch_rb_eUnbalancedFreezeThawError, "unbalanced freeze/thaw"); + case NOTMUCH_STATUS_UNBALANCED_ATOMIC: + rb_raise (notmuch_rb_eUnbalancedAtomicError, "unbalanced atomic"); default: - rb_raise(notmuch_rb_eBaseError, "unknown notmuch error"); + rb_raise (notmuch_rb_eBaseError, "unknown notmuch error"); } } diff --git a/bindings/ruby/tags.c b/bindings/ruby/tags.c index d7cd941d..e8226ad7 100644 --- a/bindings/ruby/tags.c +++ b/bindings/ruby/tags.c @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,19 +21,19 @@ #include "defs.h" /* - * call-seq: TAGS.destroy => nil + * call-seq: TAGS.destroy! => nil * * Destroys the tags, freeing all resources allocated for it. */ VALUE -notmuch_rb_tags_destroy(VALUE self) +notmuch_rb_tags_destroy (VALUE self) { notmuch_tags_t *tags; - Data_Get_Notmuch_Tags(self, tags); + Data_Get_Notmuch_Tags (self, tags); - notmuch_tags_destroy(tags); - DATA_PTR(self) = NULL; + notmuch_tags_destroy (tags); + DATA_PTR (self) = NULL; return Qnil; } @@ -45,16 +45,16 @@ notmuch_rb_tags_destroy(VALUE self) * parameter. */ VALUE -notmuch_rb_tags_each(VALUE self) +notmuch_rb_tags_each (VALUE self) { const char *tag; notmuch_tags_t *tags; - Data_Get_Notmuch_Tags(self, tags); + Data_Get_Notmuch_Tags (self, tags); - for (; notmuch_tags_valid(tags); notmuch_tags_move_to_next(tags)) { - tag = notmuch_tags_get(tags); - rb_yield(rb_str_new2(tag)); + for (; notmuch_tags_valid (tags); notmuch_tags_move_to_next (tags)) { + tag = notmuch_tags_get (tags); + rb_yield (rb_str_new2 (tag)); } return self; diff --git a/bindings/ruby/thread.c b/bindings/ruby/thread.c index e5c87366..efe5aaf7 100644 --- a/bindings/ruby/thread.c +++ b/bindings/ruby/thread.c @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,19 +21,19 @@ #include "defs.h" /* - * call-seq: THREAD.destroy => nil + * call-seq: THREAD.destroy! => nil * * Destroys the thread, freeing all resources allocated for it. */ VALUE -notmuch_rb_thread_destroy(VALUE self) +notmuch_rb_thread_destroy (VALUE self) { notmuch_thread_t *thread; - Data_Get_Notmuch_Thread(self, thread); + Data_Get_Notmuch_Thread (self, thread); - notmuch_thread_destroy(thread); - DATA_PTR(self) = NULL; + notmuch_thread_destroy (thread); + DATA_PTR (self) = NULL; return Qnil; } @@ -44,16 +44,16 @@ notmuch_rb_thread_destroy(VALUE self) * Returns the thread id */ VALUE -notmuch_rb_thread_get_thread_id(VALUE self) +notmuch_rb_thread_get_thread_id (VALUE self) { const char *tid; notmuch_thread_t *thread; - Data_Get_Notmuch_Thread(self, thread); + Data_Get_Notmuch_Thread (self, thread); - tid = notmuch_thread_get_thread_id(thread); + tid = notmuch_thread_get_thread_id (thread); - return rb_str_new2(tid); + return rb_str_new2 (tid); } /* @@ -62,13 +62,13 @@ notmuch_rb_thread_get_thread_id(VALUE self) * Returns the number of total messages */ VALUE -notmuch_rb_thread_get_total_messages(VALUE self) +notmuch_rb_thread_get_total_messages (VALUE self) { notmuch_thread_t *thread; - Data_Get_Notmuch_Thread(self, thread); + Data_Get_Notmuch_Thread (self, thread); - return INT2FIX(notmuch_thread_get_total_messages(thread)); + return INT2FIX (notmuch_thread_get_total_messages (thread)); } /* @@ -77,18 +77,18 @@ notmuch_rb_thread_get_total_messages(VALUE self) * Get a Notmuch::Messages iterator for the top level messages in thread. */ VALUE -notmuch_rb_thread_get_toplevel_messages(VALUE self) +notmuch_rb_thread_get_toplevel_messages (VALUE self) { notmuch_messages_t *messages; notmuch_thread_t *thread; - Data_Get_Notmuch_Thread(self, thread); + Data_Get_Notmuch_Thread (self, thread); - messages = notmuch_thread_get_toplevel_messages(thread); + messages = notmuch_thread_get_toplevel_messages (thread); if (!messages) - rb_raise(notmuch_rb_eMemoryError, "Out of memory"); + rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return Data_Wrap_Struct(notmuch_rb_cMessages, NULL, NULL, messages); + return Data_Wrap_Struct (notmuch_rb_cMessages, NULL, NULL, messages); } /* @@ -97,13 +97,13 @@ notmuch_rb_thread_get_toplevel_messages(VALUE self) * Get the number of messages in thread that matched the search */ VALUE -notmuch_rb_thread_get_matched_messages(VALUE self) +notmuch_rb_thread_get_matched_messages (VALUE self) { notmuch_thread_t *thread; - Data_Get_Notmuch_Thread(self, thread); + Data_Get_Notmuch_Thread (self, thread); - return INT2FIX(notmuch_thread_get_matched_messages(thread)); + return INT2FIX (notmuch_thread_get_matched_messages (thread)); } /* @@ -112,16 +112,16 @@ notmuch_rb_thread_get_matched_messages(VALUE self) * Get a comma-separated list of the names of the authors. */ VALUE -notmuch_rb_thread_get_authors(VALUE self) +notmuch_rb_thread_get_authors (VALUE self) { const char *authors; notmuch_thread_t *thread; - Data_Get_Notmuch_Thread(self, thread); + Data_Get_Notmuch_Thread (self, thread); - authors = notmuch_thread_get_authors(thread); + authors = notmuch_thread_get_authors (thread); - return rb_str_new2(authors); + return rb_str_new2 (authors); } /* @@ -130,16 +130,16 @@ notmuch_rb_thread_get_authors(VALUE self) * Returns the subject of the thread */ VALUE -notmuch_rb_thread_get_subject(VALUE self) +notmuch_rb_thread_get_subject (VALUE self) { const char *subject; notmuch_thread_t *thread; - Data_Get_Notmuch_Thread(self, thread); + Data_Get_Notmuch_Thread (self, thread); - subject = notmuch_thread_get_subject(thread); + subject = notmuch_thread_get_subject (thread); - return rb_str_new2(subject); + return rb_str_new2 (subject); } /* @@ -148,13 +148,13 @@ notmuch_rb_thread_get_subject(VALUE self) * Get the date of the oldest message in thread. */ VALUE -notmuch_rb_thread_get_oldest_date(VALUE self) +notmuch_rb_thread_get_oldest_date (VALUE self) { notmuch_thread_t *thread; - Data_Get_Notmuch_Thread(self, thread); + Data_Get_Notmuch_Thread (self, thread); - return UINT2NUM(notmuch_thread_get_oldest_date(thread)); + return UINT2NUM (notmuch_thread_get_oldest_date (thread)); } /* @@ -163,13 +163,13 @@ notmuch_rb_thread_get_oldest_date(VALUE self) * Get the date of the newest message in thread. */ VALUE -notmuch_rb_thread_get_newest_date(VALUE self) +notmuch_rb_thread_get_newest_date (VALUE self) { notmuch_thread_t *thread; - Data_Get_Notmuch_Thread(self, thread); + Data_Get_Notmuch_Thread (self, thread); - return UINT2NUM(notmuch_thread_get_newest_date(thread)); + return UINT2NUM (notmuch_thread_get_newest_date (thread)); } /* @@ -178,16 +178,16 @@ notmuch_rb_thread_get_newest_date(VALUE self) * Get a Notmuch::Tags iterator for the tags of the thread */ VALUE -notmuch_rb_thread_get_tags(VALUE self) +notmuch_rb_thread_get_tags (VALUE self) { notmuch_thread_t *thread; notmuch_tags_t *tags; - Data_Get_Notmuch_Thread(self, thread); + Data_Get_Notmuch_Thread (self, thread); - tags = notmuch_thread_get_tags(thread); + tags = notmuch_thread_get_tags (thread); if (!tags) - rb_raise(notmuch_rb_eMemoryError, "Out of memory"); + rb_raise (notmuch_rb_eMemoryError, "Out of memory"); - return Data_Wrap_Struct(notmuch_rb_cTags, NULL, NULL, tags); + return Data_Wrap_Struct (notmuch_rb_cTags, NULL, NULL, tags); } diff --git a/bindings/ruby/threads.c b/bindings/ruby/threads.c index abd51212..3e1fbf5d 100644 --- a/bindings/ruby/threads.c +++ b/bindings/ruby/threads.c @@ -1,6 +1,6 @@ /* The Ruby interface to the notmuch mail library * - * Copyright © 2010 Ali Polatel + * Copyright © 2010, 2011 Ali Polatel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,19 +21,19 @@ #include "defs.h" /* - * call-seq: THREADS.destroy => nil + * call-seq: THREADS.destroy! => nil * * Destroys the threads, freeing all resources allocated for it. */ VALUE -notmuch_rb_threads_destroy(VALUE self) +notmuch_rb_threads_destroy (VALUE self) { notmuch_threads_t *threads; - Data_Get_Struct(self, notmuch_threads_t, threads); + Data_Get_Struct (self, notmuch_threads_t, threads); - notmuch_threads_destroy(threads); - DATA_PTR(self) = NULL; + notmuch_threads_destroy (threads); + DATA_PTR (self) = NULL; return Qnil; } @@ -44,16 +44,16 @@ notmuch_rb_threads_destroy(VALUE self) * parameter. */ VALUE -notmuch_rb_threads_each(VALUE self) +notmuch_rb_threads_each (VALUE self) { notmuch_thread_t *thread; notmuch_threads_t *threads; - Data_Get_Notmuch_Threads(self, threads); + Data_Get_Notmuch_Threads (self, threads); - for (; notmuch_threads_valid(threads); notmuch_threads_move_to_next(threads)) { - thread = notmuch_threads_get(threads); - rb_yield(Data_Wrap_Struct(notmuch_rb_cThread, NULL, NULL, thread)); + for (; notmuch_threads_valid (threads); notmuch_threads_move_to_next (threads)) { + thread = notmuch_threads_get (threads); + rb_yield (Data_Wrap_Struct (notmuch_rb_cThread, NULL, NULL, thread)); } return self; diff --git a/compat/README b/compat/README index cd32c56f..38e2e146 100644 --- a/compat/README +++ b/compat/README @@ -1,4 +1,4 @@ -notmuch/comapt +notmuch/compat This directory consists of two things: diff --git a/completion/Makefile b/completion/Makefile index b6859eac..de492a7c 100644 --- a/completion/Makefile +++ b/completion/Makefile @@ -1,4 +1,4 @@ -# See Makfefile.local for the list of files to be compiled in this +# See Makefile.local for the list of files to be compiled in this # directory. all: $(MAKE) -C .. all @@ -22,7 +22,7 @@ if [ "$srcdir" != "." ]; then fi # Set several defaults (optionally specified by the user in -# environemnt variables) +# environment variables) CC=${CC:-gcc} CXX=${CXX:-g++} CFLAGS=${CFLAGS:--O2} @@ -97,6 +97,7 @@ Fine tuning of some installation directories is available: --mandir=DIR Install man pages to DIR [PREFIX/share/man] --sysconfdir=DIR Read-only single-machine data [PREFIX/etc] --emacslispdir=DIR Emacs code [PREFIX/share/emacs/site-lisp] + --emacsetcdir=DIR Emacs miscellaneous files [PREFIX/share/emacs/site-lisp] --bashcompletiondir=DIR Bash completions files [SYSCONFDIR/bash_completion.d] --zshcompletiondir=DIR Zsh completions files [PREFIX/share/zsh/functions/Completion/Unix] @@ -139,6 +140,8 @@ for option; do SYSCONFDIR="${option#*=}" elif [ "${option%%=*}" = '--emacslispdir' ] ; then EMACSLISPDIR="${option#*=}" + elif [ "${option%%=*}" = '--emacsetcdir' ] ; then + EMACSETCDIR="${option#*=}" elif [ "${option%%=*}" = '--bashcompletiondir' ] ; then BASHCOMPLETIONDIR="${option#*=}" elif [ "${option%%=*}" = '--zshcompletiondir' ] ; then @@ -330,6 +333,14 @@ if [ -z "${EMACSLISPDIR}" ]; then fi fi +if [ -z "${EMACSETCDIR}" ]; then + if pkg-config --exists emacs; then + EMACSETCDIR=$(pkg-config emacs --variable sitepkglispdir) + else + EMACSETCDIR='$(prefix)/share/emacs/site-lisp' + fi +fi + printf "Checking if emacs is available... " if emacs --quick --batch > /dev/null 2>&1; then printf "Yes.\n" @@ -608,12 +619,16 @@ includedir = ${INCLUDEDIR:=\$(prefix)/include} # The directory to which man pages should be installed mandir = ${MANDIR:=\$(prefix)/share/man} -# The directory to which read-only (configuration) filesshould be installed +# The directory to which read-only (configuration) files should be installed sysconfdir = ${SYSCONFDIR:=\$(prefix)/etc} # The directory to which emacs lisp files should be installed emacslispdir=${EMACSLISPDIR} +# The directory to which emacs miscellaneous (machine-independent) files should +# be installed +emacsetcdir=${EMACSETCDIR} + # Whether there's an emacs binary available for byte-compiling HAVE_EMACS = ${have_emacs} diff --git a/contrib/.gitattributes b/contrib/.gitattributes new file mode 100644 index 00000000..3d57a081 --- /dev/null +++ b/contrib/.gitattributes @@ -0,0 +1 @@ +notmuch-deliver export-ignore diff --git a/contrib/nmbug b/contrib/nmbug new file mode 100755 index 00000000..bb0739f8 --- /dev/null +++ b/contrib/nmbug @@ -0,0 +1,621 @@ +#!/usr/bin/env perl +# Copyright (c) 2011 David Bremner +# License: same as notmuch + +use strict; +use warnings; +use File::Temp qw(tempdir); +use Pod::Usage; + +no encoding; + +my $NMBGIT = $ENV{NMBGIT} || $ENV{HOME}.'/.nmbug'; + +$NMBGIT .= '/.git' if (-d $NMBGIT.'/.git'); + +my $TAGPREFIX = $ENV{NMBPREFIX} || 'notmuch::'; + +# magic hash for git +my $EMPTYBLOB = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'; + +# for encoding + +my $ESCAPE_CHAR = '%'; +my $NO_ESCAPE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'. + '0123456789+-_@=.:,'; +my $MUST_ENCODE = qr{[^\Q$NO_ESCAPE\E]}; +my $ESCAPED_RX = qr{$ESCAPE_CHAR([A-Fa-f0-9]{2})}; + +my %command = ( + archive => \&do_archive, + checkout => \&do_checkout, + commit => \&do_commit, + fetch => \&do_fetch, + help => \&do_help, + log => \&do_log, + merge => \&do_merge, + pull => \&do_pull, + push => \&do_push, + status => \&do_status, + ); + +my $subcommand = shift || usage (); + +if (!exists $command{$subcommand}) { + usage (); +} + +&{$command{$subcommand}}(@ARGV); + +sub git_pipe { + my $envref = (ref $_[0] eq 'HASH') ? shift : {}; + my $ioref = (ref $_[0] eq 'ARRAY') ? shift : undef; + my $dir = ($_[0] eq '-|' or $_[0] eq '|-') ? shift : undef; + + unshift @_, 'git'; + $envref->{GIT_DIR} ||= $NMBGIT; + spawn ($envref, defined $ioref ? $ioref : (), defined $dir ? $dir : (), @_); +} + +sub git { + my $fh = git_pipe (@_); + my $str = join ('', <$fh>); + chomp($str); + return $str; +} + +sub spawn { + my $envref = (ref $_[0] eq 'HASH') ? shift : {}; + my $ioref = (ref $_[0] eq 'ARRAY') ? shift : undef; + my $dir = ($_[0] eq '-|' or $_[0] eq '|-') ? shift : '-|'; + + die unless @_; + + if (open my $child, $dir) { + return $child; + } + # child + while (my ($key, $value) = each %{$envref}) { + $ENV{$key} = $value; + } + + if (defined $ioref && $dir eq '-|') { + open my $fh, '|-', @_ or die "open |- @_: $!"; + foreach my $line (@{$ioref}) { + print $fh $line, "\n"; + } + exit 0; + } else { + if ($dir ne '|-') { + open STDIN, '<', '/dev/null' or die "reopening stdin: $!" + } + exec @_; + die "exec @_: $!"; + } +} + + +sub get_tags { + my $prefix = shift; + my @tags; + + my $fh = spawn ('-|', qw/notmuch search --output=tags/, "*") + or die 'error dumping tags'; + + while (<$fh>) { + chomp (); + push @tags, $_ if (m/^$prefix/); + } + return @tags; +} + + +sub do_archive { + system ('git', "--git-dir=$NMBGIT", 'archive', 'HEAD'); +} + + +sub is_committed { + my $status = shift; + return scalar (@{$status->{added}} ) + scalar (@{$status->{deleted}} ) == 0; +} + + +sub do_commit { + my @args = @_; + + my $status = compute_status (); + + if ( is_committed ($status) ) { + print "Nothing to commit\n"; + return; + } + + my $index = read_tree ('HEAD'); + + update_index ($index, $status); + + my $tree = git ( { GIT_INDEX_FILE => $index }, 'write-tree') + or die 'no output from write-tree'; + + my $parent = git ( 'rev-parse', 'HEAD' ) + or die 'no output from rev-parse'; + + my $commit = git ([ @args ], 'commit-tree', $tree, '-p', $parent) + or die 'commit-tree'; + + git ('update-ref', 'HEAD', $commit); + + unlink $index || die "unlink: $!"; + +} + +sub read_tree { + my $treeish = shift; + my $index = $NMBGIT.'/nmbug.index'; + git ({ GIT_INDEX_FILE => $index }, 'read-tree', '--empty'); + git ({ GIT_INDEX_FILE => $index }, 'read-tree', $treeish); + return $index; +} + +sub update_index { + my $index = shift; + my $status = shift; + + my $git = spawn ({ GIT_DIR => $NMBGIT, GIT_INDEX_FILE => $index }, + '|-', qw/git update-index --index-info/) + or die 'git update-index'; + + foreach my $pair (@{$status->{deleted}}) { + index_tags_for_msg ($git, $pair->{id}, 'D', $pair->{tag}); + } + + foreach my $pair (@{$status->{added}}) { + index_tags_for_msg ($git, $pair->{id}, 'A', $pair->{tag}); + } +} + + +sub do_fetch { + my $remote = shift || 'origin'; + + git ('fetch', $remote); +} + + +sub notmuch { + my @args = @_; + system ('notmuch', @args) == 0 or die "notmuch @args failed: $?"; +} + + +sub index_tags { + + my $index = $NMBGIT.'/nmbug.index'; + + my $query = join ' ', map ("tag:$_", get_tags ($TAGPREFIX)); + + my $fh = spawn ('-|', qw/notmuch dump --/, $query) + or die "notmuch dump: $!"; + + git ('read-tree', '--empty'); + my $git = spawn ({ GIT_DIR => $NMBGIT, GIT_INDEX_FILE => $index }, + '|-', qw/git update-index --index-info/) + or die 'git update-index'; + + while (<$fh>) { + m/ ( [^ ]* ) \s+ \( ([^\)]* ) \) /x || die 'syntax error in dump'; + my ($id,$rest) = ($1,$2); + + #strip prefixes before writing + my @tags = grep { s/^$TAGPREFIX//; } split (' ', $rest); + index_tags_for_msg ($git,$id, 'A', @tags); + } + + close $git; + return $index; +} + +sub index_tags_for_msg { + my $fh = shift; + my $msgid = shift; + my $mode = shift; + + my $hash = $EMPTYBLOB; + my $blobmode = '100644'; + + if ($mode eq 'D') { + $blobmode = '0'; + $hash = '0000000000000000000000000000000000000000'; + } + + foreach my $tag (@_) { + my $tagpath = 'tags/' . encode_for_fs ($msgid) . '/' . encode_for_fs ($tag); + print $fh "$blobmode $hash\t$tagpath\n"; + } +} + + +sub do_checkout { + do_sync (action => 'checkout'); +} + + +sub do_sync { + + my %args = @_; + + my $status = compute_status (); + my ($A_action, $D_action); + + if ($args{action} eq 'checkout') { + $A_action = '-'; + $D_action = '+'; + } else { + $A_action = '+'; + $D_action = '-'; + } + + foreach my $pair (@{$status->{added}}) { + + notmuch ('tag', $A_action.$TAGPREFIX.$pair->{tag}, + 'id:'.$pair->{id}); + } + + foreach my $pair (@{$status->{deleted}}) { + notmuch ('tag', $D_action.$TAGPREFIX.$pair->{tag}, + 'id:'.$pair->{id}); + } + +} + + +sub insist_committed { + + my $status=compute_status(); + if ( !is_committed ($status) ) { + print "Uncommitted changes to $TAGPREFIX* tags in notmuch + +For a summary of changes, run 'nmbug status' +To save your changes, run 'nmbug commit' before merging/pull +To discard your changes, run 'nmbug checkout' +"; + exit (1); + } + +} + + +sub do_pull { + my $remote = shift || 'origin'; + + git ( 'fetch', $remote); + + do_merge (); +} + + +sub do_merge { + insist_committed (); + + my $tempwork = tempdir ('/tmp/nmbug-merge.XXXXXX', CLEANUP => 1); + + git ( { GIT_WORK_TREE => $tempwork }, 'checkout', '-f', 'HEAD'); + + git ( { GIT_WORK_TREE => $tempwork }, 'merge', 'FETCH_HEAD'); + + do_checkout (); +} + + +sub do_log { + # we don't want output trapping here, because we want the pager. + system ( 'git', "--git-dir=$NMBGIT", 'log', '--name-status', @_); +} + + +sub do_push { + my $remote = shift || 'origin'; + + git ('push', $remote); +} + + +sub do_status { + my $status = compute_status (); + + my %output = (); + foreach my $pair (@{$status->{added}}) { + $output{$pair->{id}} ||= {}; + $output{$pair->{id}}{$pair->{tag}} = 'A' + } + + foreach my $pair (@{$status->{deleted}}) { + $output{$pair->{id}} ||= {}; + $output{$pair->{id}}{$pair->{tag}} = 'D' + } + + foreach my $pair (@{$status->{missing}}) { + $output{$pair->{id}} ||= {}; + $output{$pair->{id}}{$pair->{tag}} = 'U' + } + + if (is_unmerged ()) { + foreach my $pair (diff_refs ('A')) { + $output{$pair->{id}} ||= {}; + $output{$pair->{id}}{$pair->{tag}} ||= ' '; + $output{$pair->{id}}{$pair->{tag}} .= 'a'; + } + + foreach my $pair (diff_refs ('D')) { + $output{$pair->{id}} ||= {}; + $output{$pair->{id}}{$pair->{tag}} ||= ' '; + $output{$pair->{id}}{$pair->{tag}} .= 'd'; + } + } + + foreach my $id (sort keys %output) { + foreach my $tag (sort keys %{$output{$id}}) { + printf "%s\t%s\t%s\n", $output{$id}{$tag}, $id, $tag; + } + } +} + + +sub is_unmerged { + + return 0 if (! -f $NMBGIT.'/FETCH_HEAD'); + + my $fetch_head = git ('rev-parse', 'FETCH_HEAD'); + my $base = git ( 'merge-base', 'HEAD', 'FETCH_HEAD'); + + return ($base ne $fetch_head); + +} + +sub compute_status { + my %args = @_; + + my @added; + my @deleted; + my @missing; + + my $index = index_tags (); + + my @maybe_deleted = diff_index ($index, 'D'); + + foreach my $pair (@maybe_deleted) { + + my $id = $pair->{id}; + + my $fh = spawn ('-|', qw/notmuch search --output=files/,"id:$id") + or die "searching for $id"; + if (!<$fh>) { + push @missing, $pair; + } else { + push @deleted, $pair; + } + } + + + @added = diff_index ($index, 'A'); + + unlink $index || die "unlink $index: $!"; + + return { added => [@added], deleted => [@deleted], missing => [@missing] }; +} + + +sub diff_index { + my $index = shift; + my $filter = shift; + + my $fh = git_pipe ({ GIT_INDEX_FILE => $index }, + qw/diff-index --cached/, + "--diff-filter=$filter", qw/--name-only HEAD/ ); + + return unpack_diff_lines ($fh); +} + + +sub diff_refs { + my $filter = shift; + my $ref1 = shift || 'HEAD'; + my $ref2 = shift || 'FETCH_HEAD'; + + my $fh= git_pipe ( 'diff', "--diff-filter=$filter", '--name-only', + $ref1, $ref2); + + return unpack_diff_lines ($fh); +} + + +sub unpack_diff_lines { + my $fh = shift; + + my @found; + while(<$fh>) { + chomp (); + my ($id,$tag) = m|tags/ ([^/]+) / ([^/]+) |x; + + $id = decode_from_fs ($id); + $tag = decode_from_fs ($tag); + + push @found, { id => $id, tag => $tag }; + } + + return @found; +} + + +sub encode_for_fs { + my $str = shift; + + $str =~ s/($MUST_ENCODE)/"$ESCAPE_CHAR".sprintf ("%02x",ord ($1))/ge; + return $str; +} + + +sub decode_from_fs { + my $str = shift; + + $str =~ s/$ESCAPED_RX/ chr (hex ($1))/eg; + + return $str; + +} + + +sub usage { + pod2usage (); + exit (1); +} + + +sub do_help { + pod2usage ( -verbose => 2 ); + exit (0); +} + +__END__ + +=head1 NAME + +nmbug - manage notmuch tags about notmuch + +=head1 SYNOPSIS + +nmbug subcommand [options] + +B<nmbug help> for more help + +=head1 OPTIONS + +=head2 Most common commands + +=over 8 + +=item B<commit> [message] + +Commit appropriately prefixed tags from the notmuch database to +git. Any extra arguments are used (one per line) as a commit message. + +=item B<push> [remote] + +push local nmbug git state to remote repo + +=item B<pull> [remote] + +pull (merge) remote repo changes to notmuch. B<pull> is equivalent to +B<fetch> followed by B<merge>. + +=back + +=head2 Other Useful Commands + +=over 8 + +=item B<checkout> + +Update the notmuch database from git. This is mainly useful to discard +your changes in notmuch relative to git. + +=item B<fetch> [remote] + +Fetch changes from the remote repo (see merge to bring those changes +into notmuch). + +=item B<help> [subcommand] + +print help [for subcommand] + +=item B<log> [parameters] + +A simple wrapper for git log. After running C<nmbug fetch>, you can +inspect the changes with C<nmbug log HEAD..FETCH_HEAD> + +=item B<merge> + +Merge changes from FETCH_HEAD into HEAD, and load the result into +notmuch. + +=item B<status> + +Show pending updates in notmuch or git repo. See below for more +information about the output format. + +=back + +=head2 Less common commands + +=over 8 + +=item B<archive> + +Dump a tar archive (using git archive) of the current nmbug tag set. + +=back + +=head1 STATUS FORMAT + +B<nmbug status> prints lines of the form + + ng Message-Id tag + +where n is a single character representing notmuch database status + +=over 8 + +=item B<A> + +Tag is present in notmuch database, but not committed to nmbug +(equivalently, tag has been deleted in nmbug repo, e.g. by a pull, but +not restored to notmuch database). + +=item B<D> + +Tag is present in nmbug repo, but not restored to notmuch database +(equivalently, tag has been deleted in notmuch) + +=item B<U> + +Message is unknown (missing from local notmuch database) + +=back + +The second character (if present) represents a difference between remote +git and local. Typically C<nmbug fetch> needs to be run to update this. + +=over 8 + + +=item B<a> + +Tag is present in remote, but not in local git. + + +=item B<d> + +Tag is present in local git, but not in remote git. + + +=back + +=head1 DUMP FORMAT + +Each tag $tag for message with Message-Id $id is written to +an empty file + + tags/encode($id)/encode($tag) + +The encoding preserves alphanumerics, and the characters "+-_@=.:," +(not the quotes). All other octets are replaced with '%' followed by +a two digit hex number. + +=head1 ENVIRONMENT + +B<NMBGIT> specifies the location of the git repository used by nmbug. +If not specified $HOME/.nmbug is used. + +B<NMBPREFIX> specifies the prefix in the notmuch database for tags of +interest to nmbug. If not specified 'notmuch::' is used. diff --git a/contrib/notmuch-deliver/.gitignore b/contrib/notmuch-deliver/.gitignore new file mode 100644 index 00000000..6971ef20 --- /dev/null +++ b/contrib/notmuch-deliver/.gitignore @@ -0,0 +1,42 @@ +# gitignore for notmuch-deliver +.*.swp +*~ +*.[oa] +*.py[co] + +tags + +*.lo +*.la + +.deps +.libs +Makefile.in +Makefile + +gmon.out +gprof* + +/INSTALL +/aclocal.m4 +/autom4te.cache +/config.guess +/config.h +/config.h.in +/config.log +/config.sub +/config.status +/configure +/compile +/depcomp +/install-sh +/missing +/stamp-h1 +/libtool +/ltmain.sh + +m4/libtool.m4 +m4/lt*.m4 + +maildrop/numlib/config.h +src/notmuch-deliver diff --git a/contrib/notmuch-deliver/.mailmap b/contrib/notmuch-deliver/.mailmap new file mode 100644 index 00000000..95f1131d --- /dev/null +++ b/contrib/notmuch-deliver/.mailmap @@ -0,0 +1 @@ +Ali Polatel <alip@exherbo.org> <alip@penguen.ev> diff --git a/contrib/notmuch-deliver/COPYING b/contrib/notmuch-deliver/COPYING new file mode 100644 index 00000000..3912109b --- /dev/null +++ b/contrib/notmuch-deliver/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/contrib/notmuch-deliver/Makefile.am b/contrib/notmuch-deliver/Makefile.am new file mode 100644 index 00000000..365558ab --- /dev/null +++ b/contrib/notmuch-deliver/Makefile.am @@ -0,0 +1,8 @@ +CLEANFILES= *~ +MAINTAINERCLEANFILES= Makefile.in configure aclocal.m4 \ + config.h config.h.in INSTALL +ACLOCAL_AMFLAGS= -I m4 +AUTOMAKE_OPTIONS= dist-bzip2 no-dist-gzip std-options foreign +SUBDIRS= maildrop/numlib src . + +doc_DATA= README.mkd diff --git a/contrib/notmuch-deliver/README.mkd b/contrib/notmuch-deliver/README.mkd new file mode 100644 index 00000000..f1f2144f --- /dev/null +++ b/contrib/notmuch-deliver/README.mkd @@ -0,0 +1,49 @@ +## About +`notmuch-deliver` is a [maildir](http://cr.yp.to/proto/maildir.html) delivery +tool. + +## Overview +`notmuch-deliver` is a [maildir](http://cr.yp.to/proto/maildir.html) delivery +tool for [notmuch](http://notmuchmail.org) mail indexer. It reads from standard +input, delivers the mail to the specified maildir and adds it to the notmuch +database. This is meant as a convenient alternative to running `notmuch new` +after mail delivery. + +## Usage +Here's a simple example for [maildrop](http://www.courier-mta.org/maildrop/): + + # Deliver local mail to $MAILDIR/.Local and add local tag. + if (/^From: root/:h) + { + to "|notmuch-deliver -f -t local Local" + } + + # Deliver lkml mail to $MAILDIR/.Lkml, add lkml tag and remove inbox tag. + if (/^List-Id: linux-kernel@vger.kernel.org/:h) + { + to "|notmuch-deliver -f -t lkml -r inbox Lkml" + } + + # Deliver the rest to $MAILDIR, adding personal tag + to "|notmuch-deliver -t personal" + +## Requirements +- [notmuch](http://notmuchmail.org) shared library +- [GLib](http://library.gnome.org/devel/glib/)-2.16 or newer + +## Contribute +Clone [git://github.com/alip/notmuch-deliver.git](git://github.com/alip/notmuch-deliver.git). +Format patches are preferred. Either send a mail to me or poke me on IRC. +My personal e-mail address is [alip@exherbo.org](mailto:alip@exherbo.org). +I'm available on IRC as `alip` on [Freenode](http://freenode.net) and [OFTC](http://www.oftc.net). + +## License +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 of the License. + +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. + +<!-- vim: set ft=mkd spell spelllang=en sw=4 sts=4 et : --> diff --git a/contrib/notmuch-deliver/autogen.sh b/contrib/notmuch-deliver/autogen.sh new file mode 100755 index 00000000..2c660bcf --- /dev/null +++ b/contrib/notmuch-deliver/autogen.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# vim: set sw=4 et sts=4 tw=80 : + +die() { + echo "$@" >&2 + exit 1 +} + +echo ">>> libtoolize --copy --force --automake" +libtoolize --copy --force --automake || die "libtoolize failed" +echo ">>> rm -f config.cache" +rm -f config.cache +echo ">>> aclocal -I m4" +aclocal -I m4 || die "aclocal failed" +echo ">>> autoheader" +autoheader || die "autoheader failed" +echo ">>> autoconf" +autoconf || die "autoconf failed" +echo ">>> automake --foreign --add-missing --copy" +automake --foreign --add-missing --copy || die "automake failed" diff --git a/contrib/notmuch-deliver/configure.ac b/contrib/notmuch-deliver/configure.ac new file mode 100644 index 00000000..4deb6587 --- /dev/null +++ b/contrib/notmuch-deliver/configure.ac @@ -0,0 +1,156 @@ +dnl vim: set sw=4 sts=4 ts=4 noet ft=config foldmethod=marker foldmarker={{{,}}} : + +dnl {{{ Program, version +AC_PREREQ(2.59) +AC_INIT([src/main.c]) +AC_CANONICAL_SYSTEM + +VERSION_MAJOR=0 +VERSION_MINOR=1 +VERSION_FULL="$VERSION_MAJOR.$VERSION_MINOR" +VERSION="$VERSION_FULL" + +AC_SUBST([VERSION_MAJOR]) +AC_SUBST([VERSION_MINOR]) +AC_SUBST([VERSION_FULL]) + +AM_INIT_AUTOMAKE(notmuch-deliver, [$VERSION_FULL]) +m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES([yes])]) + +dnl {{{ Git revision +AC_MSG_CHECKING([for git head]) +if test -d "${GIT_DIR:-${ac_top_srcdir:-./}/.git}" ; then + GITHEAD=`git describe 2>/dev/null` + if test -z ${GITHEAD} ; then + GITHEAD=`git rev-parse HEAD` + fi + if test -n "`git diff-index -m --name-only HEAD`" ; then + GITHEAD=${GITHEAD}-dirty + fi + if test -n "${GITHEAD}" ; then + GITHEAD="-${GITHEAD}" + fi +fi +AC_MSG_RESULT([$GITHEAD]) +AC_SUBST([GITHEAD]) +dnl }}} +dnl }}} + +dnl {{{ Toolchain checks +AC_USE_SYSTEM_EXTENSIONS +AC_PROG_CC +AC_PROG_CC_C99 +if test x"$ac_cv_prog_cc_c99" = x"no"; then + AC_MSG_ERROR([notmuch-deliver requires a C compiler that supports ISO C99!]) +fi +AC_PROG_LIBTOOL +AC_PROG_LN_S +AC_PROG_INSTALL +AC_PROG_MAKE_SET +dnl }}} + +dnl {{{ Check for headers +AC_HEADER_DIRENT +AC_HEADER_STDC +AC_HEADER_TIME +AC_HEADER_SYS_WAIT +AC_CHECK_HEADERS([sys/stat.h unistd.h sysexits.h utime.h]) +dnl }}} + +dnl {{{ Check for typedefs, structures and compiler characteristics +AC_C_CONST +AC_TYPE_OFF_T +AC_TYPE_SIZE_T +AC_TYPE_UID_T +AC_TYPE_PID_T +AC_STRUCT_TM +dnl }}} + +dnl {{{ Check for library functions +AC_CHECK_FUNCS([setgroups initgroups symlink readlink strcasecmp utime utimes splice]) +dnl }}} + +dnl {{{ gethostname() +AC_CACHE_CHECK([for missing gethostname prototype], + maildir_cv_SYS_GETHOSTNAME, + AC_TRY_COMPILE([ +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif + +int gethostname(int,int); +],,[maildir_cv_SYS_GETHOSTNAME=yes], [maildir_cv_SYS_GETHOSTNAME=no])) + +if test x"$maildir_cv_SYS_GETHOSTNAME" = x"no" ; then + AC_DEFINE_UNQUOTED(HAS_GETHOSTNAME, 1, [Whether gethostname() is prototyped]) +fi +dnl }}} + +dnl {{{ Check for maildir target separator +if test "$target_os" = "cygwin" ; then + AC_DEFINE_UNQUOTED(MDIRSEP, "!", [Maildir target separator]) +else + AC_DEFINE_UNQUOTED(MDIRSEP, ":", [Maildir target separator]) +fi +dnl }}} + +dnl {{{ Make pkg-config work +PKG_PROG_PKG_CONFIG([0.9.0]) +dnl }}} + +dnl {{{ Check for libraries +GLIB_REQUIRED=2.16 + +PKG_CHECK_MODULES([glib], [glib-2.0 >= $GLIB_REQUIRED],, + [AC_MSG_ERROR([notmuch-deliver requires glib-$GLIB_REQUIRED or newer])]) +AC_CHECK_LIB(notmuch, notmuch_database_create,, + [AC_MSG_ERROR([notmuch-deliver requires notmuch mail indexing library])]) +dnl }}} + +dnl {{{ Extra CFLAGS +NOTMUCH_DELIVER_CFLAGS= +WANTED_CFLAGS="-Wall -W -Wextra -Wvla -Wformat=2 -Wformat-security -Wformat-nonliteral -Winit-self -Wfloat-equal -Wno-deprecated-declarations -Wmissing-declarations -Wmissing-noreturn -Wmissing-prototypes -Wredundant-decls -Wshadow -Wpointer-arith -Wstrict-prototypes -Wcast-qual -Wwrite-strings -pedantic" +for flag in $WANTED_CFLAGS ; do + AX_CHECK_COMPILER_FLAGS([$flag], [NOTMUCH_DELIVER_CFLAGS="$NOTMUCH_DELIVER_CFLAGS $flag"],) +done +dnl }}} + +dnl {{{ Profiling +AC_MSG_CHECKING([whether gprof symbols are wanted]) +AC_ARG_ENABLE([gprof], + [AS_HELP_STRING([--enable-gprof], + [Add gprof symbols (-pg) (for debugging)])], + PROFILE="$enableval", + PROFILE="no") +AC_MSG_RESULT([$PROFILE]) +if test x"$PROFILE" = x"yes" ; then + AX_CHECK_COMPILER_FLAGS([-pg],, AC_MSG_ERROR([Your compiler doesn't support -pg flag])) + NOTMUCH_DELIVER_CFLAGS="$NOTMUCH_DELIVER_CFLAGS -pg" + LDFLAGS="$LDFLAGS -pg" +fi +AC_MSG_CHECKING([whether coverage testing should be enabled]) +AC_ARG_ENABLE([gcov], + [AS_HELP_STRING([--enable-gcov], + [add gcov support (for debugging)])], + COVERAGE="$enableval", + COVERAGE="no") +AC_MSG_RESULT([$COVERAGE]) +if test x"$COVERAGE" = x"yes" ; then + AX_CHECK_COMPILER_FLAGS([-fprofile-arcs],, + AC_MSG_ERROR([Your compiler doesn't support -fprofile-arcs flag])) + AX_CHECK_COMPILER_FLAGS([-ftest-coverage],, + AC_MSG_ERROR([Your compiler doesn't support -ftest-coverage flag])) + NOTMUCH_DELIVER_CFLAGS="$NOTMUCH_DELIVER_CFLAGS -fprofile-arcs -ftest-coverage" +fi +AC_SUBST([NOTMUCH_DELIVER_CFLAGS]) +dnl }}} + +dnl {{{ Output +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_LINKS([maildrop/numlib/config.h:config.h]) +AC_OUTPUT( + Makefile + maildrop/numlib/Makefile + src/Makefile + ) +dnl }}} diff --git a/contrib/notmuch-deliver/m4/ax_check_compiler_flags.m4 b/contrib/notmuch-deliver/m4/ax_check_compiler_flags.m4 new file mode 100644 index 00000000..7da8324b --- /dev/null +++ b/contrib/notmuch-deliver/m4/ax_check_compiler_flags.m4 @@ -0,0 +1,74 @@ +# =========================================================================== +# http://www.nongnu.org/autoconf-archive/ax_check_compiler_flags.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CHECK_COMPILER_FLAGS(FLAGS, [ACTION-SUCCESS], [ACTION-FAILURE]) +# +# DESCRIPTION +# +# Check whether the given compiler FLAGS work with the current language's +# compiler, or whether they give an error. (Warnings, however, are +# ignored.) +# +# ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on +# success/failure. +# +# LICENSE +# +# Copyright (c) 2009 Steven G. Johnson <stevenj@alum.mit.edu> +# Copyright (c) 2009 Matteo Frigo +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see <http://www.gnu.org/licenses/>. +# +# As a special exception, the respective Autoconf Macro's copyright owner +# gives unlimited permission to copy, distribute and modify the configure +# scripts that are the output of Autoconf when processing the Macro. You +# need not follow the terms of the GNU General Public License when using +# or distributing such scripts, even though portions of the text of the +# Macro appear in them. The GNU General Public License (GPL) does govern +# all other use of the material that constitutes the Autoconf Macro. +# +# This special exception to the GPL applies to versions of the Autoconf +# Macro released by the Autoconf Archive. When you make and distribute a +# modified version of the Autoconf Macro, you may extend this special +# exception to the GPL to apply to your modified version as well. + +AC_DEFUN([AX_CHECK_COMPILER_FLAGS], +[AC_PREREQ(2.59) dnl for _AC_LANG_PREFIX +AC_MSG_CHECKING([whether _AC_LANG compiler accepts $1]) +dnl Some hackery here since AC_CACHE_VAL can't handle a non-literal varname: +AS_LITERAL_IF([$1], + [AC_CACHE_VAL(AS_TR_SH(ax_cv_[]_AC_LANG_ABBREV[]_flags_[$1]), [ + ax_save_FLAGS=$[]_AC_LANG_PREFIX[]FLAGS + _AC_LANG_PREFIX[]FLAGS="$1" + AC_COMPILE_IFELSE([AC_LANG_PROGRAM()], + AS_TR_SH(ax_cv_[]_AC_LANG_ABBREV[]_flags_[$1])=yes, + AS_TR_SH(ax_cv_[]_AC_LANG_ABBREV[]_flags_[$1])=no) + _AC_LANG_PREFIX[]FLAGS=$ax_save_FLAGS])], + [ax_save_FLAGS=$[]_AC_LANG_PREFIX[]FLAGS + _AC_LANG_PREFIX[]FLAGS="$1" + AC_COMPILE_IFELSE([AC_LANG_PROGRAM()], + eval AS_TR_SH(ax_cv_[]_AC_LANG_ABBREV[]_flags_[$1])=yes, + eval AS_TR_SH(ax_cv_[]_AC_LANG_ABBREV[]_flags_[$1])=no) + _AC_LANG_PREFIX[]FLAGS=$ax_save_FLAGS]) +eval ax_check_compiler_flags=$AS_TR_SH(ax_cv_[]_AC_LANG_ABBREV[]_flags_[$1]) +AC_MSG_RESULT($ax_check_compiler_flags) +if test "x$ax_check_compiler_flags" = xyes; then + m4_default([$2], :) +else + m4_default([$3], :) +fi +])dnl AX_CHECK_COMPILER_FLAGS diff --git a/contrib/notmuch-deliver/maildrop/maildir/Makefile.am b/contrib/notmuch-deliver/maildrop/maildir/Makefile.am new file mode 100644 index 00000000..2495a83b --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/maildir/Makefile.am @@ -0,0 +1,202 @@ +#$Id: Makefile.am,v 1.59 2009/06/27 16:32:38 mrsam Exp $ +# +# Copyright 1998 - 2005 Double Precision, Inc. See COPYING for +# distribution information. + + +noinst_LTLIBRARIES=libmaildir.la + + +DOCS= deliverquota.html.in deliverquota.8.in \ + maildiracl.1.in \ + maildir.html maildir.5 \ + maildiracl.html.in \ + maildirmake.html.in maildirmake.1.in maildirquota.html maildirquota.7 \ + maildirkw.html maildirkw.1 + +if HAVE_SGML +BUILT_SOURCES=maildirsharedrc.h maildirfilterconfig.h quotawarnmsg.h \ + mailbot.h autoresponsequota.h $(noinst_DATA) $(DOCS) +else +BUILT_SOURCES=maildirsharedrc.h maildirfilterconfig.h quotawarnmsg.h \ + mailbot.h autoresponsequota.h $(noinst_DATA) +endif + +noinst_DATA=deliverquota.html maildirmake.html deliverquota.8 maildirmake.1 \ + maildiracl.html maildiracl.1 maildir.libdeps + +libmaildir_la_SOURCES=autoresponse.c autoresponse.h \ + maildiraclt.c maildiraclt.h \ + maildircache.c maildircache.h \ + maildircreate.c maildircreate.h \ + maildirfilename.c maildirgetnew.c \ + maildirfilter.c maildirfilter2.c \ + maildirfilter.h maildirfiltertypelist.h\ + maildirflags.c maildirmkdir.c \ + maildirgetquota.c maildirgetquota.h \ + maildirinfo.c maildirinfo.h \ + maildirkeywords.c maildirkeywords2.c maildirkeywords3.c \ + maildirkeywords4.cpp \ + maildirkeywords.h maildirlist.c maildirlock.c \ + maildirmake2.c \ + maildirnewshared.c maildirnewshared.h maildirnewshared2.c \ + maildiropen.c maildirparsequota.c \ + maildirpath.c maildirpurgetmp.c maildirmisc.h \ + maildirrename.c \ + maildirsearch.c maildirsearchC.cpp maildirsearch.h \ + maildirshared.c maildirshared2.c maildirdelfolder.c\ + maildirquota.c maildirquota.h maildirrequota.c maildirrequota.h \ + maildirwatch.c maildirwatch.h loginexec.c loginexec.h + +noinst_PROGRAMS=deliverquota maildirmake testmaildirfilter maildirkwtest \ + maildirkw maildiracl maildiraclttest + +deliverquota_SOURCES=deliverquota.c +deliverquota_DEPENDENCIES=libmaildir.la ../rfc822/librfc822.la \ + ../numlib/libnumlib.la +deliverquota_LDADD=libmaildir.la ../rfc822/librfc822.la ../numlib/libnumlib.la +deliverquota_LDFLAGS=-static + +maildirmake_SOURCES=maildirmake.c +maildirmake_DEPENDENCIES=libmaildir.la ../numlib/libnumlib.la \ + ../rfc822/librfc822.la +maildirmake_LDADD=libmaildir.la ../numlib/libnumlib.la \ + ../rfc822/librfc822.la +maildirmake_LDFLAGS=-static + +testmaildirfilter_SOURCES=maildirfiltertypelist.h testmaildirfilter.c +testmaildirfilter_DEPENDENCIES=libmaildir.la ../numlib/libnumlib.la +testmaildirfilter_LDADD=libmaildir.la ../numlib/libnumlib.la @LIBPCRE@ +testmaildirfilter_LDFLAGS=-static + +maildirkwtest_SOURCES=maildirkwtest.c +maildirkwtest_LDADD=libmaildir.la +maildirkwtest_DEPENDENCIES=libmaildir.la +maildirkwtest_LDFLAGS=-static + +maildirkw_SOURCES=maildirkw.c +maildirkw_LDADD=libmaildir.la ../liblock/liblock.la ../numlib/libnumlib.la \ + `cat maildir.libdeps` +maildirkw_DEPENDENCIES=libmaildir.la ../liblock/liblock.la \ + ../numlib/libnumlib.la maildir.libdeps +maildirkw_LDFLAGS=-static + +maildiracl=maildiracl.c +maildiracl_LDADD=libmaildir.la ../liblock/liblock.la ../numlib/libnumlib.la \ + `cat maildir.libdeps` +maildiracl_DEPENDENCIES=libmaildir.la ../liblock/liblock.la \ + ../numlib/libnumlib.la maildir.libdeps +maildiracl_LDFLAGS=-static + +maildiraclttest_SOURCES=testmaildiraclt.c +maildiraclttest_LDADD=libmaildir.la ../liblock/liblock.la \ + ../numlib/libnumlib.la `cat maildir.libdeps` +maildiraclttest_DEPENDENCIES=libmaildir.la ../liblock/liblock.la \ + ../numlib/libnumlib.la maildir.libdeps +maildiraclttest_LDFLAGS=-static + +EXTRA_DIST=README.maildirquota.html README.maildirquota.txt \ + README.imapkeywords.html \ + README.maildirfilter.html \ + README.sharedfolders.html README.sharedfolders.txt \ + maildirkwtest.txt \ + testsuite testsuite.txt testsuite2 testsuite2.txt quotawarnmsg $(DOCS) + +HTML2TXT=links -dump -no-numbering + +README.maildirquota.txt: README.maildirquota.html + $(HTML2TXT) README.maildirquota.html >README.maildirquota.txt + +README.sharedfolders.txt: README.sharedfolders.html + $(HTML2TXT) README.sharedfolders.html >README.sharedfolders.txt + +mailbot.h: config.status + echo '#define MAILBOT "@MAILBOT@"' >mailbot.h + +maildirsharedrc.h: config.status + echo '#define MAILDIRSHAREDRC "$(sysconfdir)/maildirshared"' >maildirsharedrc.h + +maildirfilterconfig.h: config.status + echo '#define MAILDIRFILTERCONFIG "$(sysconfdir)/maildirfilterconfig"' >maildirfilterconfig.h + +autoresponsequota.h: config.status + echo '#define AUTORESPONSEQUOTA "$(sysconfdir)/autoresponsesquota"' >autoresponsequota.h + +quotawarnmsg.h: config.status + echo '#define QUOTAWARNMSG "$(sysconfdir)/quotawarnmsg"' >quotawarnmsg.h +maildir.libdeps: config.status + echo @LIBFAM@ >maildir.libdeps + +clean-local: + rm -rf maildir.libdeps testmd + +check-am: + @SHELL@ $(srcdir)/testsuite 2>&1 | cmp - $(srcdir)/testsuite.txt + test "@LIBPCRE@" != "" || exit 0 ; @SHELL@ $(srcdir)/testsuite2 2>&1 | cmp - $(srcdir)/testsuite2.txt + LC_ALL=C; export LC_ALL; ./maildirkwtest | cmp -s - $(srcdir)/maildirkwtest.txt + LC_ALL=C; export LC_ALL; ./maildiraclttest + +if HAVE_SGML + +deliverquota.html.in: deliverquota.sgml ../docbook/sgml2html + ../docbook/sgml2html deliverquota.sgml deliverquota.html.in + +deliverquota.8.in: deliverquota.sgml ../docbook/sgml2html + ../docbook/sgml2man deliverquota.sgml deliverquota.8.in + mv deliverquota.8 deliverquota.8.in + +maildirmake.html.in: maildirmake.sgml ../docbook/sgml2html + ../docbook/sgml2html maildirmake.sgml maildirmake.html.in + +maildirmake.1.in: maildirmake.sgml ../docbook/sgml2html + ../docbook/sgml2man maildirmake.sgml maildirmake.1.in + mv maildirmake.1 maildirmake.1.in + +maildiracl.html.in: maildiracl.sgml ../docbook/sgml2html + ../docbook/sgml2html maildiracl.sgml maildiracl.html.in + +maildiracl.1.in: maildiracl.sgml ../docbook/sgml2html + ../docbook/sgml2man maildiracl.sgml maildiracl.1.in + mv maildiracl.1 maildiracl.1.in + +maildirquota.html: maildirquota.sgml ../docbook/sgml2html + ../docbook/sgml2html maildirquota.sgml maildirquota.html + +maildirquota.7: maildirquota.sgml ../docbook/sgml2man + ../docbook/sgml2man maildirquota.sgml maildirquota.7 + +maildir.html: maildir.sgml ../docbook/sgml2html + ../docbook/sgml2html maildir.sgml maildir.html + +maildir.5: maildir.sgml ../docbook/sgml2man + ../docbook/sgml2man maildir.sgml maildir.5 + +maildirkw.html: maildirkw.sgml ../docbook/sgml2html + ../docbook/sgml2html maildirkw.sgml maildirkw.html + +maildirkw.1: maildirkw.sgml ../docbook/sgml2man + ../docbook/sgml2man maildirkw.sgml maildirkw.1 + +endif + +deliverquota.html: deliverquota.html.in + ./config.status --file=$@ + +deliverquota.8: deliverquota.8.in + ./config.status --file=$@ + +maildirmake.html: maildirmake.html.in + ./config.status --file=$@ + +maildirmake.1: maildirmake.1.in + ./config.status --file=$@ + +maildiracl.html: maildiracl.html.in + ./config.status --file=$@ + +maildiracl.1: maildiracl.1.in + ./config.status --file=$@ + +# autoresponse.c: autoresponsequota.h + +DISTCLEANFILES=$(BUILT_SOURCES) diff --git a/contrib/notmuch-deliver/maildrop/maildir/configure.in b/contrib/notmuch-deliver/maildrop/maildir/configure.in new file mode 100644 index 00000000..1e724443 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/maildir/configure.in @@ -0,0 +1,165 @@ +dnl $Id: configure.in,v 1.37 2008/11/27 21:51:16 mrsam Exp $ +dnl Process this file with autoconf to produce a configure script. +dnl +dnl Copyright 1998 - 2001 Double Precision, Inc. See COPYING for +dnl distribution information. + +AC_INIT(maildir, 0.11, [courier-maildrop@lists.sourceforge.net]) + +>confdefs.h # Kill PACKAGE_ macros + +AC_CONFIG_SRCDIR(maildirquota.c) +LPATH="$PATH:/usr/local/bin" +AC_CANONICAL_SYSTEM +AM_INIT_AUTOMAKE([foreign no-define]) +AM_CONFIG_HEADER(config.h) + +dnl Checks for programs. +AC_PROG_AWK +AC_USE_SYSTEM_EXTENSIONS +AC_PROG_CC +AC_PROG_INSTALL +AC_PROG_LN_S +AC_LIBTOOL_DLOPEN +AM_PROG_LIBTOOL + +AC_PROG_CXX + +AC_PATH_PROGS(PERL, perl5 perl, perl, $LPATH) + +if test "$GCC" = "yes" +then + CFLAGS="$CFLAGS -Wall" +fi + +if test "$GXX" = "yes" +then + CXXFLAGS="$CXXFLAGS -Wall" +fi + +CFLAGS="$CFLAGS -I${srcdir}/.. -I.." + +dnl Checks for libraries. + +dnl Checks for header files. +AC_HEADER_DIRENT +AC_HEADER_STDC +AC_HEADER_TIME +AC_CHECK_HEADERS(sys/stat.h sys/wait.h fcntl.h unistd.h sysexits.h utime.h pcre.h pcre/pcre.h) + +AC_CHECK_HEADER([pcre.h], + [LIBPCRE=-lpcre]) + +AC_CHECK_HEADER([pcre/pcre.h], + [LIBPCRE=-lpcre]) + +AC_SUBST(LIBPCRE) + +AC_HEADER_SYS_WAIT + +AC_LANG_CPLUSPLUS +AC_CHECK_HEADERS(vector vector.h) +AC_LANG_C + +dnl Checks for typedefs, structures, and compiler characteristics. +AC_C_CONST +AC_TYPE_OFF_T +AC_TYPE_SIZE_T +AC_TYPE_UID_T +AC_STRUCT_TM + +dnl Checks for library functions. +AC_CHECK_HEADER(fam.h, :, :) +AC_CHECK_FUNCS(symlink readlink strcasecmp utime utimes) +AC_CHECK_LIB(fam, FAMOpen, [ + LIBFAM=-lfam + AC_DEFINE_UNQUOTED(HAVE_FAM,1, + [ Whether libfam.a is available ]) + + AC_CHECK_HEADER(fam.h, : , [ +AC_MSG_WARN([[The development header files and libraries for fam,]]) +AC_MSG_WARN([[the File Alteration Monitor, are not installed.]]) +AC_MSG_WARN([[You appear to have the FAM runtime libraries installed,]]) +AC_MSG_WARN([[so you need to simply install the additional development]]) +AC_MSG_WARN([[package for your operating system.]]) +AC_MSG_ERROR([[FAM development libraries not found.]]) ] + ) + ]) + +AC_SUBST(LIBFAM) + +echo "$LIBFAM" >maildir.libdeps + +AC_CACHE_CHECK([for missing gethostname prototype],maildir_cv_SYS_GETHOSTNAME, + +AC_TRY_COMPILE([ +#if HAVE_UNISTD_H +#include <unistd.h> +#endif + +extern "C" int gethostname(int,int); +],[ +],maildir_cv_SYS_GETHOSTNAME=yes,maildir_cv_SYS_GETHOSTNAME=no +) + +) + +if test $maildir_cv_SYS_GETHOSTNAME = "no" +then + AC_DEFINE_UNQUOTED(HAS_GETHOSTNAME,1, + [ Whether gethostname() is prototyped ]) +fi + +AC_ARG_WITH(db, [ --with-db=gdbm Use the GDBM library. + --with-db=db Use the libdb.a library.], + db="$withval", db="") + +if test "$db" = "no" +then + db="" +fi + +if test "$db" != "" +then + AC_DEFINE_UNQUOTED(HAVE_DBOBJ,1, + [ Whether the top-level configure script defined dbobj ]) +fi + +AC_ARG_WITH(trashquota, [ --with-trashquota Count deleted messages as part of the quota], + trashquota="$withval", + trashquota="no") + +if test "$trashquota" = "yes" +then + AC_DEFINE_UNQUOTED(TRASHQUOTA,1, + [ Whether to count deleted messages towards the maildir quota ]) +fi + +test "x$prefix" = xNONE && prefix=$ac_default_prefix +test "x$exec_prefix" = xNONE && exec_prefix='${prefix}' +eval "prefix=$prefix" +eval "exec_prefix=$exec_prefix" +eval "sysconfdir=$sysconfdir" + +AC_PATH_PROGS(MAILBOT, mailbot, mailbot, $LPATH) + +if test -d $srcdir/../courier +then + MAILBOT="$bindir/mailbot" + AC_DEFINE_UNQUOTED(HAVE_COURIER,1, + [ Whether building the full Courier suite. ]) +fi + +AC_SUBST(MAILBOT) +AM_CONDITIONAL(HAVE_SGML, test -d ${srcdir}/../docbook) + +if test "$target_os" = "cygwin" +then + AC_DEFINE_UNQUOTED(MDIRSEP, "!", + [ Maildir target separator ]) +else + AC_DEFINE_UNQUOTED(MDIRSEP, ":", + [ Maildir target separator ]) +fi + +AC_OUTPUT(Makefile sharedindexinstall sharedindexsplit) diff --git a/contrib/notmuch-deliver/maildrop/maildir/maildircreate.c b/contrib/notmuch-deliver/maildrop/maildir/maildircreate.c new file mode 100644 index 00000000..74030f41 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/maildir/maildircreate.c @@ -0,0 +1,241 @@ +/* +** Copyright 1998 - 2003 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#include "maildircreate.h" +#include "maildirmisc.h" +#include <sys/types.h> +#if HAVE_SYS_STAT_H +#include <sys/stat.h> +#endif + +#if TIME_WITH_SYS_TIME +#include <sys/time.h> +#include <time.h> +#else +#if HAVE_SYS_TIME_H +#include <sys/time.h> +#else +#include <time.h> +#endif +#endif +#if HAVE_SYS_STAT_H +#include <sys/stat.h> +#endif + +#if HAVE_UNISTD_H +#include <unistd.h> +#endif +#include <string.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <fcntl.h> +#include "numlib/numlib.h" + + +static const char rcsid[]="$Id: maildircreate.c,v 1.6 2003/01/26 04:07:03 mrsam Exp $"; + +FILE *maildir_tmpcreate_fp(struct maildir_tmpcreate_info *info) +{ + int fd=maildir_tmpcreate_fd(info); + FILE *fp; + + if (fd < 0) + return NULL; + + fp=fdopen(fd, "w+"); + + if (fp == NULL) + { + close(fd); + return NULL; + } + + return fp; +} + +static int maildir_tmpcreate_fd_do(struct maildir_tmpcreate_info *info); + +#define KEEPTRYING (60 * 60) +#define SLEEPFOR 3 + +int maildir_tmpcreate_fd(struct maildir_tmpcreate_info *info) +{ + int i; + + if (!info->doordie) + return (maildir_tmpcreate_fd_do(info)); + + for (i=0; i<KEEPTRYING / SLEEPFOR; i++) + { + int fd=maildir_tmpcreate_fd_do(info); + + if (fd >= 0 || errno != EAGAIN) + return fd; + + sleep(SLEEPFOR); + } + + return -1; +} + +static int maildir_tmpcreate_fd_do(struct maildir_tmpcreate_info *info) +{ + const char *maildir=info->maildir; + const char *uniq=info->uniq; + const char *hostname=info->hostname; + + char hostname_buf[256]; + char time_buf[NUMBUFSIZE]; + char usec_buf[NUMBUFSIZE]; + char pid_buf[NUMBUFSIZE]; + char len_buf[NUMBUFSIZE+3]; + char dev_buf[NUMBUFSIZE]; + char ino_buf[NUMBUFSIZE]; + struct timeval tv; + + struct stat stat_buf; + int fd; + + if (!maildir) + maildir="."; + if (!uniq) + uniq=""; + + if (!hostname || !*hostname) + { + hostname_buf[sizeof(hostname_buf)-1]=0; + if (gethostname(hostname_buf, sizeof(hostname_buf)-1) < 0) + strcpy(hostname_buf, "localhost"); + hostname=hostname_buf; + } + + gettimeofday(&tv, NULL); + + libmail_str_time_t(tv.tv_sec, time_buf); + libmail_str_time_t(tv.tv_usec, usec_buf); + libmail_str_pid_t(getpid(), pid_buf); + len_buf[0]=0; + if (info->msgsize > 0) + { + strcpy(len_buf, ",S="); + libmail_str_size_t(info->msgsize, len_buf+3); + } + + if (info->tmpname) + free(info->tmpname); + + info->tmpname=malloc(strlen(maildir)+strlen(uniq)+ + strlen(hostname)+strlen(time_buf)+ + strlen(usec_buf)+ + strlen(pid_buf)+strlen(len_buf)+100); + + if (!info->tmpname) + { + maildir_tmpcreate_free(info); + return -1; + } + + strcpy(info->tmpname, maildir); + strcat(info->tmpname, "/tmp/"); + strcat(info->tmpname, time_buf); + strcat(info->tmpname, ".M"); + strcat(info->tmpname, usec_buf); + strcat(info->tmpname, "P"); + strcat(info->tmpname, pid_buf); + + if (*uniq) + strcat(strcat(info->tmpname, "_"), uniq); + strcat(info->tmpname, "."); + strcat(info->tmpname, hostname); + strcat(info->tmpname, len_buf); + + if (stat( info->tmpname, &stat_buf) == 0) + { + maildir_tmpcreate_free(info); + errno=EAGAIN; + return -1; + } + + if (errno != ENOENT) + { + maildir_tmpcreate_free(info); + if (errno == EAGAIN) + errno=EIO; + return -1; + } + + if ((fd=maildir_safeopen_stat(info->tmpname, O_CREAT|O_RDWR|O_TRUNC, + info->openmode, &stat_buf)) < 0) + { + maildir_tmpcreate_free(info); + return -1; + } + + libmail_strh_dev_t(stat_buf.st_dev, dev_buf); + libmail_strh_ino_t(stat_buf.st_ino, ino_buf); + + if (info->newname) + free(info->newname); + + info->newname=malloc(strlen(info->tmpname)+strlen(ino_buf)+ + strlen(dev_buf)+3); + + if (!info->newname) + { + maildir_tmpcreate_free(info); + unlink(info->tmpname); + close(fd); + if (errno == EAGAIN) + errno=EIO; + return -1; + } + + strcpy(info->newname, maildir); + strcat(info->newname, "/new/"); + strcat(info->newname, time_buf); + strcat(info->newname, ".M"); + strcat(info->newname, usec_buf); + strcat(info->newname, "P"); + strcat(info->newname, pid_buf); + strcat(info->newname, "V"); + strcat(info->newname, dev_buf); + strcat(info->newname, "I"); + strcat(info->newname, ino_buf); + if (*uniq) + strcat(strcat(info->newname, "_"), uniq); + strcat(info->newname, "."); + strcat(info->newname, hostname); + strcat(info->newname, len_buf); + + return fd; +} + +void maildir_tmpcreate_free(struct maildir_tmpcreate_info *info) +{ + if (info->tmpname) + free(info->tmpname); + info->tmpname=NULL; + + if (info->newname) + free(info->newname); + info->newname=NULL; +} + +int maildir_movetmpnew(const char *tmpname, const char *newname) +{ + if (link(tmpname, newname) == 0) + { + unlink(tmpname); + return 0; + } + + if (errno != EXDEV) + return -1; + + /* AFS? */ + + return rename(tmpname, newname); +} diff --git a/contrib/notmuch-deliver/maildrop/maildir/maildircreate.h b/contrib/notmuch-deliver/maildrop/maildir/maildircreate.h new file mode 100644 index 00000000..ea1c71ac --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/maildir/maildircreate.h @@ -0,0 +1,52 @@ +#ifndef maildircreate_h +#define maildircreate_h + +/* +** Copyright 1998 - 2003 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif + +#include <stdio.h> + +#ifdef __cplusplus +extern "C" { +#endif + +static const char maildircreate_h_rcsid[]="$Id: maildircreate.h,v 1.10 2006/10/29 00:03:53 mrsam Exp $"; + + /* Create messages in maildirs */ + +struct maildir_tmpcreate_info { + const char *maildir; + unsigned long msgsize; /* If known, 0 otherwise (must use requota later)*/ + const char *uniq; /* You need when creating multiple msgs */ + const char *hostname; /* If known, NULL otherwise */ + int openmode; /* Default open mode */ + int doordie; /* Loop until we get it right. */ + char *tmpname; /* On exit, filename in tmp */ + char *newname; /* On exit, filename in new */ +}; + +#define maildir_tmpcreate_init(i) \ + do \ + { \ + memset( (i), 0, sizeof(*(i))); \ + (i)->openmode=0644; \ + } while(0) + +int maildir_tmpcreate_fd(struct maildir_tmpcreate_info *); +FILE *maildir_tmpcreate_fp(struct maildir_tmpcreate_info *); +void maildir_tmpcreate_free(struct maildir_tmpcreate_info *); + + /* Move created message from tmp to new */ +int maildir_movetmpnew(const char *tmpname, const char *newname); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/contrib/notmuch-deliver/maildrop/maildir/maildirmisc.h b/contrib/notmuch-deliver/maildrop/maildir/maildirmisc.h new file mode 100644 index 00000000..545d11e1 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/maildir/maildirmisc.h @@ -0,0 +1,202 @@ +#ifndef maildirmisc_h +#define maildirmisc_h + +/* +** Copyright 2000-2003 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif + +#if HAVE_SYS_STAT_H +#include <sys/stat.h> +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +static const char maildirmisc_h_rcsid[]="$Id: maildirmisc.h,v 1.18 2006/07/22 02:48:15 mrsam Exp $"; + +/* +** +** Miscellaneous maildir-related code +** +*/ + +/* Some special folders */ + +#define INBOX "INBOX" +#define DRAFTS "Drafts" +#define SENT "Sent" +#define TRASH "Trash" +#define SHARED "shared" + +#define SHAREDSUBDIR "shared-folders" + +#define NEWSHAREDSP "#shared" +#define NEWSHARED "#shared." + +#define PUBLIC "public" /* SMAP */ + +int maildir_make(const char *maildir, int perm, int subdirperm, + int folder); + +int maildir_del(const char *maildir); + +int maildir_del_content(const char *maildir); + +char *maildir_name2dir(const char *maildir, /* DIR location */ + const char *foldername); /* INBOX.name */ + +char *maildir_location(const char *homedir, + const char *maildir); +/* +** Homedir is the account's home directory, "maildir" is where the account's +** default Maildir is configured to be (usually "./Maildir"). Combine the +** two to produce an absolute pathname. +*/ + + +char *maildir_folderdir(const char *, /* maildir */ + const char *); /* folder name */ + /* Returns the directory corresponding to foldername (foldername is + ** checked to make sure that it's a valid name, else we set errno + ** to EINVAL, and return (0). + */ + +char *maildir_filename(const char *, /* maildir */ + const char *, /* folder */ + const char *); /* filename */ + /* + ** Builds the filename to this message, suitable for opening. + ** If the file doesn't appear to be there, search the maildir to + ** see if someone changed the flags, and return the current filename. + */ + +int maildir_safeopen(const char *, /* filename */ + int, /* mode */ + int); /* perm */ + +/* +** Same arguments as open(). When we're accessing a shared maildir, +** prevent someone from playing cute and dumping a bunch of symlinks +** in there. This function will open the indicate file only if the +** last component is not a symlink. +** This is implemented by opening the file with O_NONBLOCK (to prevent +** a DOS attack of someone pointing the symlink to a pipe, causing +** the open to hang), clearing O_NONBLOCK, then stat-int the file +** descriptor, lstating the filename, and making sure that dev/ino +** match. +*/ + +int maildir_semisafeopen(const char *, /* filename */ + int, /* mode */ + int); /* perm */ + +/* +** Same thing, except that we allow ONE level of soft link indirection, +** because we're reading from our own maildir, which points to the +** message in the sharable maildir. +*/ + +int maildir_safeopen_stat(const char *path, int mode, int perm, + struct stat *stat1); + /* Sane as maildir_safeopen(), except that we also initialize a + ** struct stat, saving an extra syscall to the caller. + */ + +int maildir_mkdir(const char *); /* directory */ +/* +** Create maildir including all subdirectories in the path (like mkdir -p) +*/ + +void maildir_purgetmp(const char *); /* maildir */ + /* purges old stuff out of tmp */ + +void maildir_purge(const char *, /* directory */ + unsigned); /* time_t to purge */ + +void maildir_getnew(const char *, /* maildir */ + const char *, /* folder */ + void (*)(const char *, void *), /* Callback function for + ** every moved msg. + */ + void *arg); /* Passthrough callback arg */ + + /* move messages from new to cur */ + +int maildir_deletefolder(const char *, /* maildir */ + const char *); /* folder */ + /* deletes a folder */ + +void maildir_list(const char *maildir, + void (*func)(const char *, void *), + void *voidp); + +void maildir_list_sharable(const char *, /* maildir */ + void (*)(const char *, void *), /* callback function */ + void *); /* 2nd arg to callback func */ + /* list sharable folders */ + +int maildir_shared_subscribe(const char *, /* maildir */ + const char *); /* folder */ + /* subscribe to a shared folder */ + +void maildir_list_shared(const char *, /* maildir */ + void (*)(const char *, void *), /* callback function */ + void *); /* 2nd arg to the callback func */ + /* list subscribed folders */ + +int maildir_shared_unsubscribe(const char *, /* maildir */ + const char *); /* folder */ + /* unsubscribe from a shared folder */ + +char *maildir_shareddir(const char *, /* maildir */ + const char *); /* folder */ + /* + ** Validate and return a path to a shared folder. folderdir must be + ** a name of a valid shared folder. + */ + +void maildir_shared_sync(const char *); /* maildir */ + /* "sync" the shared folder */ + +int maildir_sharedisro(const char *); /* maildir */ + /* maildir is a shared read-only folder */ + +int maildir_unlinksharedmsg(const char *); /* filename */ + /* Remove a message from a shared folder */ + +/* Internal function that reads a symlink */ + +char *maildir_getlink(const char *); + + /* Determine whether the maildir filename has a certain flag */ + +int maildir_hasflag(const char *filename, char); + +#define MAILDIR_DELETED(f) maildir_hasflag((f), 'T') + + /* + ** Hierarchical maildir rename. + */ + +#define MAILDIR_RENAME_FOLDER 1 +#define MAILDIR_RENAME_SUBFOLDERS 2 + +int maildir_rename(const char *maildir, /* Path to the maildir */ + const char *oldname, /* .foldername */ + const char *newname, /* .foldername */ + int flags, /* See above */ + void (*callback_func)(const char *old_path, + const char *new_path) + ); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/contrib/notmuch-deliver/maildrop/maildir/maildirmkdir.c b/contrib/notmuch-deliver/maildrop/maildir/maildirmkdir.c new file mode 100644 index 00000000..754b2c70 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/maildir/maildirmkdir.c @@ -0,0 +1,78 @@ +/* +** Copyright 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif + +#include <sys/types.h> +#include <sys/stat.h> +#include <string.h> +#include <stdlib.h> +#if HAVE_UNISTD_H +#include <unistd.h> +#endif +#include <errno.h> + +#include "maildirmisc.h" + +static const char rcsid[]="$Id: maildirmkdir.c,v 1.2 2002/03/15 03:09:21 mrsam Exp $"; + +int maildir_mkdir(const char *dir) +{ +char *buf, *p; +size_t l; + + if (dir == 0 || dir[0] == 0) + { + errno = EINVAL; + return (-1); + } + l = strlen(dir); + if ((buf = malloc(l + sizeof("/tmp"))) == 0) + { + errno = ENOMEM; + return (-1); + } + strcpy(buf, dir); + strcpy(buf+l, "/cur"); + + /* We do mkdir -p here */ + + p = buf+1; + while ((p = strchr(p, '/')) != 0) + { + *p = '\0'; + if (mkdir(buf, 0700) < 0 && errno != EEXIST) + { + free(buf); + return (-1); + } + *p++ = '/'; + } + + if (mkdir(buf, 0700) < 0 && errno != EEXIST) { + free(buf); + return (-1); + } + strcpy(buf+l, "/new"); + if (mkdir(buf, 0700) < 0 && errno != EEXIST) { + free(buf); + return (-1); + } + /* + * make /tmp last because this is the one we open first - + * the existence of this directory implies the whole + * Maildir structure is complete + */ + strcpy(buf+l, "/tmp"); + if (mkdir(buf, 0700) < 0 && errno != EEXIST) { + free(buf); + return (-1); + } + free(buf); + return (0); +} + diff --git a/contrib/notmuch-deliver/maildrop/maildir/maildiropen.c b/contrib/notmuch-deliver/maildrop/maildir/maildiropen.c new file mode 100644 index 00000000..5071df76 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/maildir/maildiropen.c @@ -0,0 +1,141 @@ +/* +** Copyright 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif + +#include <sys/types.h> +#include <sys/stat.h> +#include <string.h> +#include <stdlib.h> +#include <time.h> +#if HAVE_UNISTD_H +#include <unistd.h> +#endif +#include <stdio.h> +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> + +#include "maildirmisc.h" + +static const char rcsid[]="$Id: maildiropen.c,v 1.8 2003/01/19 16:39:52 mrsam Exp $"; + +char *maildir_getlink(const char *filename) +{ +#if HAVE_READLINK +size_t bufsiz; +char *buf; + + bufsiz=0; + buf=0; + + for (;;) + { + int n; + + if (buf) free(buf); + bufsiz += 256; + if ((buf=malloc(bufsiz)) == 0) + { + perror("malloc"); + return (0); + } + if ((n=readlink(filename, buf, bufsiz)) < 0) + { + free(buf); + return (0); + } + if (n < bufsiz) + { + buf[n]=0; + break; + } + } + return (buf); +#else + return (0); +#endif +} + +int maildir_semisafeopen(const char *path, int mode, int perm) +{ + +#if HAVE_READLINK + +char *l=maildir_getlink(path); + + if (l) + { + int f; + + if (*l != '/') + { + char *q=malloc(strlen(path)+strlen(l)+2); + char *s; + + if (!q) + { + free(l); + return (-1); + } + + strcpy(q, path); + if ((s=strchr(q, '/')) != 0) + s[1]=0; + else *q=0; + strcat(q, l); + free(l); + l=q; + } + + f=maildir_safeopen(l, mode, perm); + + free(l); + return (f); + } +#endif + + return (maildir_safeopen(path, mode, perm)); +} + +int maildir_safeopen(const char *path, int mode, int perm) +{ + struct stat stat1; + + return maildir_safeopen_stat(path, mode, perm, &stat1); +} + +int maildir_safeopen_stat(const char *path, int mode, int perm, + struct stat *stat1) +{ + struct stat stat2; + + int fd=open(path, mode +#ifdef O_NONBLOCK + | O_NONBLOCK +#else + | O_NDELAY +#endif + , perm); + + if (fd < 0) return (fd); + if (fcntl(fd, F_SETFL, (mode & O_APPEND)) || fstat(fd, stat1) + || lstat(path, &stat2)) + { + close(fd); + return (-1); + } + + if (stat1->st_dev != stat2.st_dev || stat1->st_ino != stat2.st_ino) + { + close(fd); + errno=ENOENT; + return (-1); + } + + return (fd); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/Makefile.am b/contrib/notmuch-deliver/maildrop/numlib/Makefile.am new file mode 100644 index 00000000..c0f129fa --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/Makefile.am @@ -0,0 +1,27 @@ +# $Id: Makefile.am,v 1.12 2007/06/30 15:40:53 mrsam Exp $ +# +# Copyright 1998 - 2004 Double Precision, Inc. See COPYING for +# distribution information. + + +CLEANFILES=$(noinst_DATA) +noinst_LTLIBRARIES=libnumlib.la + +libnumlib_la_SOURCES=\ + atotimet.c \ + atouidt.c \ + changeuidgid.c \ + numlib.h \ + strdevt.c \ + strgidt.c \ + strhdevt.c \ + strhinot.c \ + strhpidt.c \ + strhtimet.c \ + strinot.c \ + strofft.c \ + strpidt.c \ + strsize.c \ + strsizet.c \ + strtimet.c \ + struidt.c diff --git a/contrib/notmuch-deliver/maildrop/numlib/atotimet.c b/contrib/notmuch-deliver/maildrop/numlib/atotimet.c new file mode 100644 index 00000000..d494fd22 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/atotimet.c @@ -0,0 +1,14 @@ +/* +** Copyright 2003 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: atotimet.c,v 1.1 2003/08/03 03:09:19 mrsam Exp $"; + +LIBMAIL_STRIMPL(time_t, libmail_strtotime_t, libmail_atotime_t) diff --git a/contrib/notmuch-deliver/maildrop/numlib/atouidt.c b/contrib/notmuch-deliver/maildrop/numlib/atouidt.c new file mode 100644 index 00000000..3c01ecf5 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/atouidt.c @@ -0,0 +1,15 @@ +/* +** Copyright 2003 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: atouidt.c,v 1.1 2004/01/11 02:47:33 mrsam Exp $"; + +LIBMAIL_STRIMPL(uid_t, libmail_strtouid_t, libmail_atouid_t) +LIBMAIL_STRIMPL(gid_t, libmail_strtogid_t, libmail_atogid_t) diff --git a/contrib/notmuch-deliver/maildrop/numlib/changeuidgid.c b/contrib/notmuch-deliver/maildrop/numlib/changeuidgid.c new file mode 100644 index 00000000..56793927 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/changeuidgid.c @@ -0,0 +1,111 @@ +/* +** Copyright 1998 - 2002 Double Precision, Inc. See COPYING for +** distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include <sys/types.h> +#if HAVE_UNISTD_H +#include <unistd.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <grp.h> +#include <pwd.h> +#include <errno.h> + +#include "numlib.h" + +static const char rcsid[]="$Id: changeuidgid.c,v 1.2 2003/01/05 04:01:17 mrsam Exp $"; + +void libmail_changegroup(gid_t gid) +{ + if ( setgid(gid)) + { + perror("setgid"); + exit(1); + } + +#if HAVE_SETGROUPS + if ( getuid() == 0 && setgroups(1, &gid) ) + { + perror("setgroups"); + exit(1); + } +#endif +} + +void libmail_changeuidgid(uid_t uid, gid_t gid) +{ + libmail_changegroup(gid); + if ( setuid(uid)) + { + perror("setuid"); + exit(1); + } +} + +void libmail_changeusername(const char *uname, const gid_t *forcegrp) +{ +struct passwd *pw; +uid_t changeuid; +gid_t changegid; + +/* uname might be a pointer returned from a previous called to getpw(), +** and libc has a problem getting it back. +*/ +char *p=malloc(strlen(uname)+1); + + if (!p) + { + perror("malloc"); + exit(1); + } + strcpy(p, uname); + + errno=ENOENT; + if ((pw=getpwnam(p)) == 0) + { + free(p); + perror("getpwnam"); + exit(1); + } + free(p); + + changeuid=pw->pw_uid; + + if ( !forcegrp ) forcegrp= &pw->pw_gid; + + changegid= *forcegrp; + + if ( setgid( changegid )) + { + perror("setgid"); + exit(1); + } + +#if HAVE_INITGROUPS + if ( getuid() == 0 && initgroups(pw->pw_name, changegid) ) + { + perror("initgroups"); + exit(1); + } +#else +#if HAVE_SETGROUPS + if ( getuid() == 0 && setgroups(1, &changegid) ) + { + perror("setgroups"); + exit(1); + } +#endif +#endif + + if (setuid(changeuid)) + { + perror("setuid"); + exit(1); + } +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/configure.in b/contrib/notmuch-deliver/maildrop/numlib/configure.in new file mode 100644 index 00000000..fc977aa9 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/configure.in @@ -0,0 +1,46 @@ +dnl Process this file with autoconf to produce a configure script. +dnl $Id: configure.in,v 1.7 2010/03/19 01:09:26 mrsam Exp $ +dnl +dnl Copyright 1998 - 2010 Double Precision, Inc. See COPYING for +dnl distribution information. + +AC_PREREQ(2.59) +AC_INIT(numlib, 0.10, courier-users@lists.sourceforge.net) + +>confdefs.h # Kill PACKAGE_ macros + +AC_CONFIG_SRCDIR([atotimet.c]) +AM_CONFIG_HEADER([config.h]) +AM_INIT_AUTOMAKE([foreign no-define]) + +dnl Checks for programs. +AC_PROG_AWK +AC_PROG_CC +AC_PROG_INSTALL +AC_PROG_LN_S +AC_PROG_LIBTOOL + +if test "$GCC" = "yes" +then + CFLAGS="$CFLAGS -Wall" +fi + +dnl Checks for libraries. + +dnl Checks for header files. + +AC_CHECK_HEADERS(unistd.h stdint.h) + +AC_CHECK_TYPE(int64_t, [ : ], + [ + AC_DEFINE_UNQUOTED(int64_t,long long,[default definition of int64_t]) + ]) + +dnl Checks for typedefs, structures, and compiler characteristics. +AC_TYPE_UID_T +AC_TYPE_PID_T + +dnl Checks for library functions. + +AC_CHECK_FUNCS(setgroups initgroups) +AC_OUTPUT(Makefile) diff --git a/contrib/notmuch-deliver/maildrop/numlib/numlib.h b/contrib/notmuch-deliver/maildrop/numlib/numlib.h new file mode 100644 index 00000000..7928aa7e --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/numlib.h @@ -0,0 +1,102 @@ +#ifndef numlib_h +#define numlib_h + +/* +** Copyright 1998 - 2010 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#ifdef __cplusplus +extern "C" { +#endif + +static const char numlib_h_rcsid[]="$Id: numlib.h,v 1.11 2010/03/19 01:09:26 mrsam Exp $"; + +#if HAVE_CONFIG_H +#include "../numlib/config.h" /* VPATH build */ +#endif + +#if HAVE_STDINT_H +#include <stdint.h> +#endif + +#include <sys/types.h> +#include <time.h> + +#define NUMBUFSIZE 60 + +/* Convert various system types to decimal */ + +char *libmail_str_time_t(time_t, char *); +char *libmail_str_off_t(off_t, char *); +char *libmail_str_int64_t(int64_t, char *); +char *libmail_str_pid_t(pid_t, char *); +char *libmail_str_dev_t(dev_t, char *); +char *libmail_str_ino_t(ino_t, char *); +char *libmail_str_uid_t(uid_t, char *); +char *libmail_str_gid_t(gid_t, char *); +char *libmail_str_size_t(size_t, char *); + +char *libmail_str_sizekb(unsigned long, char *); /* X Kb or X Mb */ + +/* Convert selected system types to hex */ + +char *libmail_strh_time_t(time_t, char *); +char *libmail_strh_pid_t(pid_t, char *); +char *libmail_strh_ino_t(ino_t, char *); +char *libmail_strh_dev_t(dev_t, char *); + +/* And, now let's do the reverse */ + +time_t libmail_strtotime_t(const char **); +time_t libmail_atotime_t(const char *); + +uid_t libmail_strtouid_t(const char **); +uid_t libmail_atouid_t(const char *); + +gid_t libmail_strtogid_t(const char **); +gid_t libmail_atogid_t(const char *); + + /* Common macros: */ + +#define LIBMAIL_STRIMPL(type, f1, f2) \ +\ +type f1(const char **p)\ +{\ + type n=0;\ + while ( **p >= '0' && **p <= '9') n=n*10 + (char)(*(*p)++ - '0');\ + return n;\ +}\ +\ +type f2(const char *p)\ +{\ + return f1(&p);\ +} + + +/* +** The following functions are used by root to reset its user and group id +** to the authenticated user's. Various functions are provided to handle +** various situations. +*/ + +void libmail_changegroup(gid_t); /* Set the group id only. Also clear any + ** auxiliary group ids */ + +void libmail_changeuidgid(uid_t, gid_t); + /* Set both user id and group id. Also clear + ** aux group ids */ + +void libmail_changeusername(const char *, const gid_t *); + /* + ** Set the userid to the indicate user's. If second argument is + ** not null, it points to the groupid to set. If it's null, the + ** group id is taken from the passwd file. Auxiliary IDs are set + ** to any aux IDs set for the user in the group file. If there are + ** no aux group IDs for the user, any AUX ids are cleared. + */ + +#ifdef __cplusplus +} +#endif +#endif diff --git a/contrib/notmuch-deliver/maildrop/numlib/strdevt.c b/contrib/notmuch-deliver/maildrop/numlib/strdevt.c new file mode 100644 index 00000000..2e542d54 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strdevt.c @@ -0,0 +1,26 @@ +/* +** Copyright 2003 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strdevt.c,v 1.1 2003/01/26 03:22:40 mrsam Exp $"; + +char *libmail_str_dev_t(dev_t t, char *arg) +{ + char buf[NUMBUFSIZE]; + char *p=buf+sizeof(buf)-1; + + *p=0; + do + { + *--p= '0' + (t % 10); + t=t / 10; + } while(t); + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/strgidt.c b/contrib/notmuch-deliver/maildrop/numlib/strgidt.c new file mode 100644 index 00000000..89472e3b --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strgidt.c @@ -0,0 +1,26 @@ +/* +** Copyright 1998 - 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strgidt.c,v 1.4 2003/01/05 04:01:17 mrsam Exp $"; + +char *libmail_str_gid_t(gid_t t, char *arg) +{ +char buf[NUMBUFSIZE]; +char *p=buf+sizeof(buf)-1; + + *p=0; + do + { + *--p= '0' + (t % 10); + t=t / 10; + } while(t); + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/strhdevt.c b/contrib/notmuch-deliver/maildrop/numlib/strhdevt.c new file mode 100644 index 00000000..98e25e53 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strhdevt.c @@ -0,0 +1,29 @@ +/* +** Copyright 1998 - 2003 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strhdevt.c,v 1.2 2003/03/12 02:45:56 mrsam Exp $"; + +static const char xdigit[]="0123456789ABCDEF"; + +char *libmail_strh_dev_t(dev_t t, char *arg) +{ +char buf[sizeof(t)*2+1]; +char *p=buf+sizeof(buf)-1; +unsigned i; + + *p=0; + for (i=0; i<sizeof(t)*2; i++) + { + *--p= xdigit[t & 15]; + t=t / 16; + } + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/strhinot.c b/contrib/notmuch-deliver/maildrop/numlib/strhinot.c new file mode 100644 index 00000000..fa640915 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strhinot.c @@ -0,0 +1,29 @@ +/* +** Copyright 1998 - 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strhinot.c,v 1.5 2003/03/12 02:45:56 mrsam Exp $"; + +static const char xdigit[]="0123456789ABCDEF"; + +char *libmail_strh_ino_t(ino_t t, char *arg) +{ +char buf[sizeof(t)*2+1]; +char *p=buf+sizeof(buf)-1; +unsigned i; + + *p=0; + for (i=0; i<sizeof(t)*2; i++) + { + *--p= xdigit[t & 15]; + t=t / 16; + } + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/strhpidt.c b/contrib/notmuch-deliver/maildrop/numlib/strhpidt.c new file mode 100644 index 00000000..2723af0b --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strhpidt.c @@ -0,0 +1,29 @@ +/* +** Copyright 1998 - 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strhpidt.c,v 1.5 2003/03/12 02:45:56 mrsam Exp $"; + +static const char xdigit[]="0123456789ABCDEF"; + +char *libmail_strh_pid_t(pid_t t, char *arg) +{ +char buf[sizeof(t)*2+1]; +char *p=buf+sizeof(buf)-1; +unsigned i; + + *p=0; + for (i=0; i<sizeof(t)*2; i++) + { + *--p= xdigit[t & 15]; + t=t / 16; + } + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/strhtimet.c b/contrib/notmuch-deliver/maildrop/numlib/strhtimet.c new file mode 100644 index 00000000..b86b05a2 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strhtimet.c @@ -0,0 +1,29 @@ +/* +** Copyright 1998 - 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strhtimet.c,v 1.5 2003/03/12 02:45:56 mrsam Exp $"; + +static const char xdigit[]="0123456789ABCDEF"; + +char *libmail_strh_time_t(time_t t, char *arg) +{ +char buf[sizeof(t)*2+1]; +char *p=buf+sizeof(buf)-1; +unsigned i; + + *p=0; + for (i=0; i<sizeof(t)*2; i++) + { + *--p= xdigit[t & 15]; + t=t / 16; + } + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/strinot.c b/contrib/notmuch-deliver/maildrop/numlib/strinot.c new file mode 100644 index 00000000..eb544c3c --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strinot.c @@ -0,0 +1,26 @@ +/* +** Copyright 1998 - 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strinot.c,v 1.4 2003/01/05 04:01:17 mrsam Exp $"; + +char *libmail_str_ino_t(ino_t t, char *arg) +{ +char buf[NUMBUFSIZE]; +char *p=buf+sizeof(buf)-1; + + *p=0; + do + { + *--p= '0' + (t % 10); + t=t / 10; + } while(t); + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/strofft.c b/contrib/notmuch-deliver/maildrop/numlib/strofft.c new file mode 100644 index 00000000..3435148e --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strofft.c @@ -0,0 +1,65 @@ +/* +** Copyright 1998 - 2010 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strofft.c,v 1.6 2010/03/19 01:09:26 mrsam Exp $"; + +char *libmail_str_off_t(off_t t, char *arg) +{ + char buf[NUMBUFSIZE]; + char *p=buf+sizeof(buf)-1; + int isneg=0; + + if (t < 0) + { + t= -t; + isneg=1; + } + + *p=0; + do + { + *--p= '0' + (t % 10); + t=t / 10; + } while(t); + + if (isneg) + *--p='-'; + + return (strcpy(arg, p)); +} + +char *libmail_str_int64_t(int64_t t, char *arg) +{ + char buf[NUMBUFSIZE]; + char *p=buf+sizeof(buf)-1; + int isneg=0; + + if (t < 0) + { + t= -t; + isneg=1; + } + + *p=0; + do + { + *--p= '0' + (t % 10); + t=t / 10; + } while(t); + + if (isneg) + *--p='-'; + + return (strcpy(arg, p)); +} + + + diff --git a/contrib/notmuch-deliver/maildrop/numlib/strpidt.c b/contrib/notmuch-deliver/maildrop/numlib/strpidt.c new file mode 100644 index 00000000..12ee9ce1 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strpidt.c @@ -0,0 +1,26 @@ +/* +** Copyright 1998 - 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strpidt.c,v 1.4 2003/01/05 04:01:17 mrsam Exp $"; + +char *libmail_str_pid_t(pid_t t, char *arg) +{ +char buf[NUMBUFSIZE]; +char *p=buf+sizeof(buf)-1; + + *p=0; + do + { + *--p= '0' + (t % 10); + t=t / 10; + } while(t); + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/strsize.c b/contrib/notmuch-deliver/maildrop/numlib/strsize.c new file mode 100644 index 00000000..0a7dcaa8 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strsize.c @@ -0,0 +1,62 @@ +/* +** Copyright 2001 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strsize.c,v 1.2 2003/01/05 04:01:17 mrsam Exp $"; + +static void cat_n(char *buf, unsigned long n) +{ +char bb[NUMBUFSIZE+1]; +char *p=bb+sizeof(bb)-1; + + *p=0; + do + { + *--p = "0123456789"[n % 10]; + n=n/10; + } while (n); + strcat(buf, p); +} + +char *libmail_str_sizekb(unsigned long n, char *sizebuf) +{ + /* If size is less than 1K bytes, display it as 0.xK */ + + if (n < 1024) + { + strcpy(sizebuf, "0."); + cat_n(sizebuf, (int)(10 * n / 1024 )); + strcat(sizebuf, "K"); + } + /* If size is less than 1 meg, display is as xK */ + + else if (n < 1024 * 1024) + { + *sizebuf=0; + cat_n(sizebuf, (unsigned long)(n+512)/1024); + strcat(sizebuf, "K"); + } + + /* Otherwise, display in megabytes */ + + else + { + unsigned long nm=(double)n / (1024.0 * 1024.0) * 10; + + *sizebuf=0; + cat_n( sizebuf, nm / 10); + strcat(sizebuf, "."); + cat_n( sizebuf, nm % 10); + strcat(sizebuf, "M"); + } + + return (sizebuf); +} + diff --git a/contrib/notmuch-deliver/maildrop/numlib/strsizet.c b/contrib/notmuch-deliver/maildrop/numlib/strsizet.c new file mode 100644 index 00000000..d4ec92d3 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strsizet.c @@ -0,0 +1,26 @@ +/* +** Copyright 1998 - 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strsizet.c,v 1.4 2003/01/05 04:01:18 mrsam Exp $"; + +char *libmail_str_size_t(size_t t, char *arg) +{ +char buf[NUMBUFSIZE]; +char *p=buf+sizeof(buf)-1; + + *p=0; + do + { + *--p= '0' + (t % 10); + t=t / 10; + } while(t); + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/strtimet.c b/contrib/notmuch-deliver/maildrop/numlib/strtimet.c new file mode 100644 index 00000000..be7e051b --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/strtimet.c @@ -0,0 +1,26 @@ +/* +** Copyright 1998 - 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: strtimet.c,v 1.4 2003/01/05 04:01:18 mrsam Exp $"; + +char *libmail_str_time_t(time_t t, char *arg) +{ +char buf[NUMBUFSIZE]; +char *p=buf+sizeof(buf)-1; + + *p=0; + do + { + *--p= '0' + (t % 10); + t=t / 10; + } while(t); + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/maildrop/numlib/struidt.c b/contrib/notmuch-deliver/maildrop/numlib/struidt.c new file mode 100644 index 00000000..50f3f742 --- /dev/null +++ b/contrib/notmuch-deliver/maildrop/numlib/struidt.c @@ -0,0 +1,26 @@ +/* +** Copyright 1998 - 2000 Double Precision, Inc. +** See COPYING for distribution information. +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif +#include "numlib.h" +#include <string.h> + +static const char rcsid[]="$Id: struidt.c,v 1.4 2003/01/05 04:01:18 mrsam Exp $"; + +char *libmail_str_uid_t(uid_t t, char *arg) +{ +char buf[NUMBUFSIZE]; +char *p=buf+sizeof(buf)-1; + + *p=0; + do + { + *--p= '0' + (t % 10); + t=t / 10; + } while(t); + return (strcpy(arg, p)); +} diff --git a/contrib/notmuch-deliver/src/Makefile.am b/contrib/notmuch-deliver/src/Makefile.am new file mode 100644 index 00000000..e2ff7baa --- /dev/null +++ b/contrib/notmuch-deliver/src/Makefile.am @@ -0,0 +1,22 @@ +DEFS+= -DGITHEAD=\"$(GITHEAD)\" +AM_CPPFLAGS= \ + -I$(top_builddir)/maildrop/numlib \ + -I$(top_srcdir)/maildrop \ + -I$(top_srcdir)/maildrop/maildir +AM_CFLAGS= @NOTMUCH_DELIVER_CFLAGS@ $(glib_CFLAGS) + +noinst_HEADERS=\ + $(top_srcdir)/maildrop/maildir/maildircreate.h \ + $(top_srcdir)/maildrop/maildir/maildirmisc.h + +bin_PROGRAMS=\ + notmuch-deliver + +notmuch_deliver_SOURCES=\ + $(top_srcdir)/maildrop/maildir/maildircreate.c \ + $(top_srcdir)/maildrop/maildir/maildiropen.c \ + $(top_srcdir)/maildrop/maildir/maildirmkdir.c \ + main.c +notmuch_deliver_LDADD=\ + $(top_builddir)/maildrop/numlib/libnumlib.la \ + $(glib_LIBS) diff --git a/contrib/notmuch-deliver/src/main.c b/contrib/notmuch-deliver/src/main.c new file mode 100644 index 00000000..f7a4eaa6 --- /dev/null +++ b/contrib/notmuch-deliver/src/main.c @@ -0,0 +1,460 @@ +/* vim: set cino= fo=croql sw=8 ts=8 sts=0 noet cin fdm=syntax : */ + +/* + * Copyright (c) 2010 Ali Polatel <alip@exherbo.org> + * Based in part upon deliverquota of maildrop which is: + * Copyright 1998 - 2009 Double Precision, Inc. + * + * This file is part of the notmuch-deliver. notmuch-deliver is free software; + * you can redistribute it and/or modify it under the terms of the GNU General + * Public License version 2, as published by the Free Software Foundation. + * + * notmuch-deliver 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., 59 Temple + * Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <errno.h> +#include <stdio.h> +#include <string.h> +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif +#ifdef HAVE_SPLICE +#include <fcntl.h> +#endif + +#ifdef HAVE_SYSEXITS_H +#include <sysexits.h> +#endif + +#include <glib.h> +#include <notmuch.h> + +#include "maildircreate.h" +#include "maildirmisc.h" + +#ifndef EX_USAGE +#define EX_USAGE 64 +#endif + +#ifndef EX_SOFTWARE +#define EX_SOFTWARE 70 +#endif + +#ifndef EX_OSERR +#define EX_OSERR 71 +#endif + +#ifndef EX_IOERR +#define EX_IOERR 74 +#endif + +#ifndef EX_TEMPFAIL +#define EX_TEMPFAIL 75 +#endif + +#ifndef EX_NOPERM +#define EX_NOPERM 77 +#endif + +#ifndef EX_CONFIG +#define EX_CONFIG 78 +#endif + +static gboolean opt_create, opt_fatal, opt_folder, opt_version; +static gboolean opt_verbose = FALSE; +static gchar **opt_tags = NULL; +static gchar **opt_rtags = NULL; + +static GOptionEntry options[] = { + {"version", 'V', 0, G_OPTION_ARG_NONE, &opt_version, + "Display version", NULL}, + {"verbose", 'v', 0, G_OPTION_ARG_NONE, &opt_verbose, + "Be verbose (useful for debugging)", NULL}, + {"create", 'c', 0, G_OPTION_ARG_NONE, &opt_create, + "Create the maildir if it doesn't exist", NULL}, + {"folder", 'f', 0, G_OPTION_ARG_NONE, &opt_folder, + "Add a dot before FOLDER, e.g: Local => $MAILDIR/.Local", NULL}, + {"tag", 't', 0, G_OPTION_ARG_STRING_ARRAY, &opt_tags, + "Add a tag to the message, may be specified multiple times", "TAG"}, + {"remove-tag", 'r', 0, G_OPTION_ARG_STRING_ARRAY, &opt_rtags, + "Remove a tag from the message, may be specified multiple times", "TAG"}, + {"fatal-add", 0, 0, G_OPTION_ARG_NONE, &opt_fatal, + "If adding the mail to the database fails, unlink it and return non-zero", NULL}, + {NULL, 0, 0, 0, NULL, NULL, NULL}, +}; + +static void +about(void) +{ + printf(PACKAGE"-"VERSION GITHEAD "\n"); +} + +static void +log_handler(G_GNUC_UNUSED const gchar *domain, GLogLevelFlags level, + const gchar *message, G_GNUC_UNUSED gpointer user_data) +{ + g_return_if_fail(message != NULL && message[0] != '\0'); + + if (!opt_verbose && (level & G_LOG_LEVEL_DEBUG)) + return; + + g_printerr(PACKAGE": %s\n", message); +} + +static gboolean +load_keyfile(const gchar *path, gchar **db_path, gchar ***tags) +{ + GKeyFile *fd; + GError *error; + + fd = g_key_file_new(); + error = NULL; + if (!g_key_file_load_from_file(fd, path, G_KEY_FILE_NONE, &error)) { + g_printerr("Failed to parse `%s': %s", path, error->message); + g_error_free(error); + g_key_file_free(fd); + return FALSE; + } + + *db_path = g_key_file_get_string(fd, "database", "path", &error); + if (*db_path == NULL) { + g_critical("Failed to parse database.path from `%s': %s", path, error->message); + g_error_free(error); + g_key_file_free(fd); + return FALSE; + } + + *tags = g_key_file_get_string_list(fd, "new", "tags", NULL, NULL); + + g_key_file_free(fd); + return TRUE; +} + +#ifdef HAVE_SPLICE +static int +save_splice(int fdin, int fdout) +{ + int ret, written, pfd[2]; + + if (pipe(pfd) < 0) { + g_critical("Failed to create pipe: %s", g_strerror(errno)); + return EX_IOERR; + } + + for (;;) { + ret = splice(fdin, NULL, pfd[1], NULL, 4096, 0); + if (!ret) + break; + if (ret < 0) { + g_critical("Splicing data from standard input failed: %s", + g_strerror(errno)); + close(pfd[0]); + close(pfd[1]); + return EX_IOERR; + } + + do { + written = splice(pfd[0], NULL, fdout, NULL, ret, 0); + if (!written) { + g_critical("Splicing data to temporary file failed: internal error"); + close(pfd[0]); + close(pfd[1]); + return EX_IOERR; + } + if (written < 0) { + g_critical("Splicing data to temporary file failed: %s", + g_strerror(errno)); + close(pfd[0]); + close(pfd[1]); + return EX_IOERR; + } + ret -= written; + } while (ret); + } + + close(pfd[0]); + close(pfd[1]); + return 0; +} +#endif /* HAVE_SPLICE */ + +static int +save_readwrite(int fdin, int fdout) +{ + int ret, written; + char buf[4096], *p; + + for (;;) { + ret = read(fdin, buf, 4096); + if (!ret) + break; + if (ret < 0) { + if (errno == EINTR) + continue; + g_critical("Reading from standard input failed: %s", + g_strerror(errno)); + return EX_IOERR; + } + p = buf; + do { + written = write(fdout, p, ret); + if (!written) + return EX_IOERR; + if (written < 0) { + if (errno == EINTR) + continue; + g_critical("Writing to temporary file failed: %s", + g_strerror(errno)); + return EX_IOERR; + } + p += written; + ret -= written; + } while (ret); + } + + return 0; +} + +static int +save_maildir(int fdin, const char *dir, int auto_create, char **path) +{ + int fdout, ret; + struct maildir_tmpcreate_info info; + + maildir_tmpcreate_init(&info); + info.openmode = 0666; + info.maildir = dir; + info.doordie = 1; + + while ((fdout = maildir_tmpcreate_fd(&info)) < 0) + { + if (errno == ENOENT && auto_create && maildir_mkdir(dir) == 0) + { + auto_create = 0; + continue; + } + + g_critical("Failed to create temporary file `%s': %s", + info.tmpname, g_strerror(errno)); + return EX_TEMPFAIL; + } + + g_debug("Reading from standard input and writing to `%s'", info.tmpname); +#ifdef HAVE_SPLICE + ret = g_getenv("NOTMUCH_DELIVER_NO_SPLICE") + ? save_readwrite(fdin, fdout) + : save_splice(fdin, fdout); +#else + ret = save_readwrite(fdin, fdout); +#endif /* HAVE_SPLICE */ + if (ret) + goto fail; + + close(fdout); + g_debug("Moving `%s' to `%s'", info.tmpname, info.newname); + if (maildir_movetmpnew(info.tmpname, info.newname)) { + g_critical("Moving `%s' to `%s' failed: %s", + info.tmpname, info.newname, g_strerror(errno)); + unlink(info.tmpname); + return EX_IOERR; + } + + if (path) + *path = g_strdup(info.newname); + + maildir_tmpcreate_free(&info); + + return 0; + +fail: + g_debug("Unlinking `%s'", info.tmpname); + unlink(info.tmpname); + return EX_IOERR; +} + +static int +add_tags(notmuch_message_t *message, char **tags) +{ + unsigned i; + notmuch_status_t ret; + + if (!tags) + return 0; + + for (i = 0; tags[i]; i++) { + ret = notmuch_message_add_tag(message, tags[i]); + if (ret != NOTMUCH_STATUS_SUCCESS) + g_warning("Failed to add tag `%s': %s", + tags[i], notmuch_status_to_string(ret)); + } + + return i; +} + +static int +rm_tags(notmuch_message_t *message, char **tags) +{ + unsigned i; + notmuch_status_t ret; + + if (!tags) + return 0; + + for (i = 0; tags[i]; i++) { + ret = notmuch_message_remove_tag(message, tags[i]); + if (ret != NOTMUCH_STATUS_SUCCESS) + g_warning("Failed to remove tag `%s': %s", + tags[i], notmuch_status_to_string(ret)); + } + + return i; +} + +static int +save_database(notmuch_database_t *db, const char *path, char **default_tags) +{ + notmuch_status_t ret; + notmuch_message_t *message; + + g_debug("Adding `%s' to notmuch database", path); + ret = notmuch_database_add_message(db, path, &message); + switch (ret) { + case NOTMUCH_STATUS_SUCCESS: + break; + case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: + g_debug("Message is a duplicate, not adding tags"); + return 0; + default: + g_warning("Failed to add `%s' to notmuch database: %s", + path, notmuch_status_to_string(ret)); + return EX_SOFTWARE; + } + + g_debug("Message isn't a duplicate, adding tags"); + add_tags(message, default_tags); + add_tags(message, opt_tags); + rm_tags(message, opt_rtags); + + return 0; +} + +int +main(int argc, char **argv) +{ + int ret; + gchar *conf_path, *db_path, *folder, *maildir, *mail; + gchar **conf_tags; + GOptionContext *ctx; + GError *error = NULL; + notmuch_database_t *db; + + ctx = g_option_context_new("[FOLDER]"); + g_option_context_add_main_entries(ctx, options, PACKAGE); + g_option_context_set_summary(ctx, PACKAGE"-"VERSION GITHEAD" - notmuch delivery tool"); + g_option_context_set_description(ctx, + "\nConfiguration:\n" + " "PACKAGE" uses notmuch's configuration file to determine database path and\n" + " initial tags to add to new messages. You may set NOTMUCH_CONFIG environment\n" + " variable to specify an alternative configuration file.\n" + "\nEnvironment:\n" + " NOTMUCH_CONFIG: Path to notmuch configuration file\n" + " NOTMUCH_DELIVER_NO_SPLICE: Don't use splice() even if it's available\n" + "\nExit codes:\n" + " 0 => Successful run\n" + " 64 => Usage error\n" + " 70 => Failed to open the database\n" + " (or to add to the database if --fatal-add is specified)\n" + " 71 => Input output errors\n" + " (failed to read from standard input)\n" + " (failed to write to temporary file)\n" + " 76 => Failed to open/create maildir\n" + " 78 => Configuration error (wrt .notmuch-config)\n"); + + g_log_set_default_handler(log_handler, NULL); + + if (!g_option_context_parse(ctx, &argc, &argv, &error)) { + g_critical("Option parsing failed: %s", error->message); + g_option_context_free(ctx); + g_error_free(error); + return EX_USAGE; + } + g_option_context_free(ctx); + + if (opt_version) { + about(); + return 0; + } + + if (g_getenv("NOTMUCH_CONFIG")) + conf_path = g_strdup(g_getenv("NOTMUCH_CONFIG")); + else if (g_getenv("HOME")) + conf_path = g_build_filename(g_getenv("HOME"), ".notmuch-config", NULL); + else { + g_critical("Neither NOTMUCH_CONFIG nor HOME set"); + return EX_USAGE; + } + + db_path = NULL; + conf_tags = NULL; + g_debug("Parsing configuration from `%s'", conf_path); + if (!load_keyfile(conf_path, &db_path, &conf_tags)) { + g_free(conf_path); + return EX_CONFIG; + } + g_free(conf_path); + + if ((argc - 1) > 1) { + g_critical("Won't deliver to %d folders", argc - 1); + return EX_USAGE; + } + + if (argc > 1) { + folder = g_strdup_printf("%s%s", opt_folder ? "." : "", argv[1]); + maildir = g_build_filename(db_path, folder, NULL); + g_free(folder); + } + else + maildir = g_strdup(db_path); + + g_debug("Opening notmuch database `%s'", db_path); + db = notmuch_database_open(db_path, NOTMUCH_DATABASE_MODE_READ_WRITE); + g_free(db_path); + if (db == NULL) + return EX_SOFTWARE; + if (notmuch_database_needs_upgrade(db)) { + g_message("Upgrading database"); + notmuch_database_upgrade(db, NULL, NULL); + } + + g_debug("Opening maildir `%s'", maildir); + if ((ret = save_maildir(STDIN_FILENO, maildir, opt_create, &mail)) != 0) { + g_free(maildir); + return ret; + } + g_free(maildir); + + if ((ret = save_database(db, mail, conf_tags)) != 0 && opt_fatal) { + g_warning("Unlinking `%s'", mail); + unlink(mail); + return ret; + } + g_strfreev(conf_tags); + g_strfreev(opt_tags); + g_strfreev(opt_rtags); + g_free(mail); + + notmuch_database_close(db); + + return 0; +} diff --git a/debian/changelog b/debian/changelog index 8964fe03..d30e6451 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,122 @@ +notmuch (0.10.2-1) unstable; urgency=low + + * Upstream bug fix release + - Fix segfault in python bindings due to missing g_type_init call. + + -- David Bremner <bremner@debian.org> Sun, 04 Dec 2011 22:06:46 -0400 + +notmuch (0.10.1-1) unstable; urgency=low + + * Upstream bug fix release. + - Fix segfault on "notmuch --help" + + -- David Bremner <bremner@debian.org> Fri, 25 Nov 2011 12:11:30 -0500 + +notmuch (0.10-1) unstable; urgency=low + + * New upstream release + - search performance improvements + - emacs UI improvements + - new dump/restore features + - new script contrib/nmbug for sharing tags + - see /usr/share/doc/notmuch/NEWS for details + + -- David Bremner <bremner@debian.org> Wed, 23 Nov 2011 07:44:01 -0400 + +notmuch (0.10~rc2-1) experimental; urgency=low + + * New upstream release candidate + - includes patch to avoid long unix domain socket paths in tests + + -- David Bremner <bremner@debian.org> Sat, 19 Nov 2011 08:21:39 -0400 + +notmuch (0.10~rc1-1) experimental; urgency=low + + * New upstream release candidate. + + -- David Bremner <bremner@debian.org> Tue, 15 Nov 2011 19:46:55 -0400 + +notmuch (0.9-1) unstable; urgency=low + + * New upstream release. + * Only doc changes since last release candidate. + * Upload to unstable. + + -- David Bremner <bremner@debian.org> Tue, 11 Oct 2011 21:51:29 -0300 + +notmuch (0.9~rc2-1) experimental; urgency=low + + * Upstream release candidate + * API changes for n_d_find_message and n_d_find_message_by_filename. + - New SONAME for libnotmuch + - bindings changes for ruby and python + * Less non-text parts reported in replies. + * emacs: provide button action to fetch unknown gpg keys + + -- David Bremner <bremner@debian.org> Fri, 07 Oct 2011 18:53:04 -0300 + +notmuch (0.9~rc1-1) experimental; urgency=low + + * Upstream release candidate + - Atomicity improvements, thanks to Austin Clements + - Add missing call to g_type_init, thanks to Aaron Ecay + * notmuch-emacs: add versioned dependency on notmuch. + (Closes: #642240). + + -- David Bremner <bremner@debian.org> Sun, 25 Sep 2011 11:26:01 -0300 + +notmuch (0.8-1) unstable; urgency=low + + * New upstream version. + - Improved handling of message/rfc822 parts + - Improved Build system portability + - Documentation update for Ruby bindings + - Unicode, iterator, PEP8 changes for python bindings + + -- David Bremner <bremner@debian.org> Sat, 10 Sep 2011 08:53:55 -0300 + +notmuch (0.8~rc1-1) experimental; urgency=low + + * Upstream release candidate. + + -- David Bremner <bremner@debian.org> Tue, 06 Sep 2011 22:24:24 -0300 + +notmuch (0.7-1) unstable; urgency=low + + * New upstream release (no changes since 0.7~rc1). + * Upload to unstable. + + -- David Bremner <bremner@debian.org> Mon, 01 Aug 2011 21:46:26 +0200 + +notmuch (0.7~rc1-1) experimental; urgency=low + + * Upstream release candidate. + * Fix for notmuch.sym and parallel build (Thanks to + Thomas Jost) + * Bug fixes from Jason Woofenden for vim interface: + - Fix "space (in show mode) mostly adds tag:inbox and tag:unread + instead of removing them" (Closes: #633009). + - Fix "says press 's'; to toggle signatures, but it's + really 'i'", (Closes: #633115). + - Fix "fix for from list on search pages", (Closes: #633045). + * Bug fixes for vim interface from Uwe Kleine-König + - use full path for sendmail/doc fix + - fix compose temp file name + * Python tag encoding fixes from Sebastian Spaeth. + + -- David Bremner <bremner@debian.org> Fri, 29 Jul 2011 12:16:56 +0200 + +notmuch (0.6.1-1) unstable; urgency=low + + * Properly install README.Debian in notmuch-vim (Closes: #632992). + Thanks to Jason Woofenden for the report. + * Force notmuch to depend on the same version of libnotmuch. Thanks to + Uwe Kleine-König for the patch. + * Export typeinfo for Xapian exceptions from libnotmuch. This fixes + certain mysterious uncaught exception problems. + + -- David Bremner <bremner@debian.org> Sun, 17 Jul 2011 10:20:42 -0300 + notmuch (0.6~bpo60+1) squeeze-backports; urgency=low * Rebuild for squeeze-backports. diff --git a/debian/control b/debian/control index 78c9849b..3252f32c 100644 --- a/debian/control +++ b/debian/control @@ -2,11 +2,21 @@ Source: notmuch Section: mail Priority: extra Maintainer: Carl Worth <cworth@debian.org> -Uploaders: Jameson Graef Rollins <jrollins@finestructure.net>, martin f. krafft <madduck@debian.org>, - David Bremner <bremner@debian.org> -Build-Depends: debhelper (>= 7.0.50~), pkg-config, libxapian-dev, - libgmime-2.4-dev, libtalloc-dev, libz-dev, python-all (>= 2.6.6-3~), - emacs23-nox | emacs23 (>=23~) | emacs23-lucid (>=23~) +Uploaders: + Jameson Graef Rollins <jrollins@finestructure.net>, + martin f. krafft <madduck@debian.org>, + David Bremner <bremner@debian.org> +Build-Depends: + debhelper (>= 7.0.50~), + pkg-config, + libxapian-dev, + libgmime-2.4-dev, + libtalloc-dev, + libz-dev, + python-all (>= 2.6.6-3~), + emacs23-nox | emacs23 (>=23~) | emacs23-lucid (>=23~), + gdb, + dtach Standards-Version: 3.9.2 Homepage: http://notmuchmail.org/ Vcs-Git: git://notmuchmail.org/git/notmuch @@ -15,7 +25,7 @@ Dm-Upload-Allowed: yes Package: notmuch Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends} +Depends: libnotmuch2 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends} Recommends: notmuch-emacs | notmuch-vim Description: thread-based email index, search and tagging Notmuch is a system for indexing, searching, reading, and tagging @@ -25,7 +35,7 @@ Description: thread-based email index, search and tagging . This package contains the notmuch command-line interface -Package: libnotmuch1 +Package: libnotmuch2 Section: libs Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} @@ -41,7 +51,7 @@ Description: thread-based email index, search and tagging (runtime) Package: libnotmuch-dev Section: libdevel Architecture: any -Depends: ${misc:Depends}, libnotmuch1 (= ${binary:Version}) +Depends: ${misc:Depends}, libnotmuch2 (= ${binary:Version}) 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 @@ -54,7 +64,7 @@ Description: thread-based email index, search and tagging (development) Package: python-notmuch Architecture: all Section: python -Depends: ${misc:Depends}, ${python:Depends}, libnotmuch1 +Depends: ${misc:Depends}, ${python:Depends}, libnotmuch2 Description: python 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 @@ -69,7 +79,7 @@ Architecture: all Section: mail Breaks: notmuch (<<0.6~254~) Replaces: notmuch (<<0.6~254~) -Depends: ${misc:Depends}, notmuch, +Depends: ${misc:Depends}, notmuch (>= ${source:Version}), emacs23 (>= 23~) | emacs23-nox (>=23~) | emacs23-lucid (>=23~) Description: thread-based email index, search and tagging (emacs interface) Notmuch is a system for indexing, searching, reading, and tagging diff --git a/debian/libnotmuch1.install b/debian/libnotmuch2.install index da4fc25b..da4fc25b 100644 --- a/debian/libnotmuch1.install +++ b/debian/libnotmuch2.install diff --git a/debian/libnotmuch1.symbols b/debian/libnotmuch2.symbols index 8df6fec1..507600c8 100644 --- a/debian/libnotmuch1.symbols +++ b/debian/libnotmuch2.symbols @@ -1,8 +1,11 @@ -libnotmuch.so.1 libnotmuch1 #MINVER# +libnotmuch.so.2 libnotmuch2 #MINVER# notmuch_database_add_message@Base 0.3 + notmuch_database_begin_atomic@Base 0.9~rc1 notmuch_database_close@Base 0.3 notmuch_database_create@Base 0.3 - notmuch_database_find_message@Base 0.3 + notmuch_database_end_atomic@Base 0.9~rc1 + notmuch_database_find_message@Base 0.9~rc2 + notmuch_database_find_message_by_filename@Base 0.9~rc2 notmuch_database_get_all_tags@Base 0.3 notmuch_database_get_directory@Base 0.3 notmuch_database_get_path@Base 0.3 @@ -44,6 +47,7 @@ libnotmuch.so.1 libnotmuch1 #MINVER# notmuch_messages_move_to_next@Base 0.3 notmuch_messages_valid@Base 0.3 notmuch_query_count_messages@Base 0.3 + notmuch_query_count_threads@Base 0.10~rc1 notmuch_query_create@Base 0.3 notmuch_query_destroy@Base 0.3 notmuch_query_get_query_string@Base 0.4 @@ -70,3 +74,13 @@ libnotmuch.so.1 libnotmuch1 #MINVER# notmuch_threads_get@Base 0.3 notmuch_threads_move_to_next@Base 0.3 notmuch_threads_valid@Base 0.3 + (c++)"typeinfo for Xapian::LogicError@Base" 0.6.1 + (c++)"typeinfo for Xapian::RuntimeError@Base" 0.6.1 + (c++)"typeinfo for Xapian::DocNotFoundError@Base" 0.6.1 + (c++)"typeinfo for Xapian::InvalidArgumentError@Base" 0.6.1 + (c++)"typeinfo for Xapian::Error@Base" 0.6.1 + (c++)"typeinfo name for Xapian::LogicError@Base" 0.6.1 + (c++)"typeinfo name for Xapian::RuntimeError@Base" 0.6.1 + (c++)"typeinfo name for Xapian::DocNotFoundError@Base" 0.6.1 + (c++)"typeinfo name for Xapian::InvalidArgumentError@Base" 0.6.1 + (c++)"typeinfo name for Xapian::Error@Base" 0.6.1 diff --git a/debian/README.notmuch-emacs b/debian/notmuch-emacs.README.Debian index 713523b4..713523b4 100644 --- a/debian/README.notmuch-emacs +++ b/debian/notmuch-emacs.README.Debian diff --git a/debian/README.notmuch-vim b/debian/notmuch-vim.README.Debian index ec052eef..ec052eef 100644 --- a/debian/README.notmuch-vim +++ b/debian/notmuch-vim.README.Debian diff --git a/debian/rules b/debian/rules index 169a8050..956f3f2c 100755 --- a/debian/rules +++ b/debian/rules @@ -1,13 +1,9 @@ #!/usr/bin/make -f -VERSION=$(shell dpkg-parsechangelog | sed -n 's/^Version:\([^-]*\)/\1/p') - %: dh --with python2 $@ override_dh_auto_configure: - cp version debian/version.pre-build - echo $(VERSION) > version dh_auto_configure -- --emacslispdir=/usr/share/emacs/site-lisp/notmuch override_dh_auto_build: @@ -15,7 +11,6 @@ override_dh_auto_build: dh_auto_build --sourcedirectory bindings/python override_dh_auto_clean: - -mv debian/version.pre-build version dh_auto_clean dh_auto_clean --sourcedirectory bindings/python diff --git a/debian/source/format b/debian/source/format index 89ae9db8..163aaf8d 100644 --- a/debian/source/format +++ b/debian/source/format @@ -1 +1 @@ -3.0 (native) +3.0 (quilt) diff --git a/emacs/Makefile b/emacs/Makefile index b6859eac..de492a7c 100644 --- a/emacs/Makefile +++ b/emacs/Makefile @@ -1,4 +1,4 @@ -# See Makfefile.local for the list of files to be compiled in this +# See Makefile.local for the list of files to be compiled in this # directory. all: $(MAKE) -C .. all diff --git a/emacs/Makefile.local b/emacs/Makefile.local index 10227777..0c58b824 100644 --- a/emacs/Makefile.local +++ b/emacs/Makefile.local @@ -20,7 +20,7 @@ emacs_images := \ emacs_bytecode = $(emacs_sources:.el=.elc) -%.elc: %.el +%.elc: %.el $(global_deps) $(call quiet,EMACS) --directory emacs -batch -f batch-byte-compile $< ifeq ($(WITH_EMACS),1) @@ -38,6 +38,7 @@ install-emacs: ifeq ($(HAVE_EMACS),1) install -m0644 $(emacs_bytecode) "$(DESTDIR)$(emacslispdir)" endif - install -m0644 $(emacs_images) "$(DESTDIR)$(emacslispdir)" + mkdir -p "$(DESTDIR)$(emacsetcdir)" + install -m0644 $(emacs_images) "$(DESTDIR)$(emacsetcdir)" CLEAN := $(CLEAN) $(emacs_bytecode) diff --git a/emacs/notmuch-crypto.el b/emacs/notmuch-crypto.el index f03266ff..44fccae0 100644 --- a/emacs/notmuch-crypto.el +++ b/emacs/notmuch-crypto.el @@ -31,8 +31,9 @@ on the success or failure of the verification process and on the validity of user ID of the signer. The effect of setting this variable can be seen temporarily by -viewing a signed or encrypted message with M-RET in notmuch -search." +providing a prefix when viewing a signed or encrypted message, or +by providing a prefix when reloading the message in notmuch-show +mode." :group 'notmuch :type 'boolean) @@ -70,20 +71,26 @@ search." (let* ((status (plist-get sigstatus :status)) (help-msg nil) (label "Signature not processed") - (face 'notmuch-crypto-signature-unknown)) + (face 'notmuch-crypto-signature-unknown) + (button-action '(lambda (button) (message (button-get button 'help-echo))))) (cond ((string= status "good") - ; if userid present, userid has full or greater validity - (if (plist-member sigstatus :userid) - (let ((userid (plist-get sigstatus :userid))) - (setq label (concat "Good signature by: " userid)) - (setq face 'notmuch-crypto-signature-good)) - (let ((fingerprint (concat "0x" (plist-get sigstatus :fingerprint)))) - (setq label (concat "Good signature by key: " fingerprint)) - (setq face 'notmuch-crypto-signature-good-key)))) + (let ((fingerprint (concat "0x" (plist-get sigstatus :fingerprint)))) + ;; if userid present, userid has full or greater validity + (if (plist-member sigstatus :userid) + (let ((userid (plist-get sigstatus :userid))) + (setq label (concat "Good signature by: " userid)) + (setq face 'notmuch-crypto-signature-good)) + (progn + (setq label (concat "Good signature by key: " fingerprint)) + (setq face 'notmuch-crypto-signature-good-key))) + (setq button-action 'notmuch-crypto-sigstatus-good-callback) + (setq help-msg (concat "Click to list key ID 0x" fingerprint ".")))) ((string= status "error") (let ((keyid (concat "0x" (plist-get sigstatus :keyid)))) - (setq label (concat "Unknown key ID " keyid " or unsupported algorithm")))) + (setq label (concat "Unknown key ID " keyid " or unsupported algorithm")) + (setq button-action 'notmuch-crypto-sigstatus-error-callback) + (setq help-msg (concat "Click to retreive key ID " keyid " from keyserver and redisplay.")))) ((string= status "bad") (let ((keyid (concat "0x" (plist-get sigstatus :keyid)))) (setq label (concat "Bad signature (claimed key ID " keyid ")")) @@ -97,10 +104,36 @@ search." 'help-echo help-msg 'face face 'mouse-face face + 'action button-action :notmuch-sigstatus sigstatus :notmuch-from from) (insert "\n"))) +(declare-function notmuch-show-refresh-view "notmuch-show" (&optional crypto-switch)) + +(defun notmuch-crypto-sigstatus-good-callback (button) + (let* ((sigstatus (button-get button :notmuch-sigstatus)) + (fingerprint (concat "0x" (plist-get sigstatus :fingerprint))) + (buffer (get-buffer-create "*notmuch-crypto-gpg-out*")) + (window (display-buffer buffer t nil))) + (with-selected-window window + (with-current-buffer buffer + (call-process "gpg" nil t t "--list-keys" fingerprint)) + (recenter -1)))) + +(defun notmuch-crypto-sigstatus-error-callback (button) + (let* ((sigstatus (button-get button :notmuch-sigstatus)) + (keyid (concat "0x" (plist-get sigstatus :keyid))) + (buffer (get-buffer-create "*notmuch-crypto-gpg-out*")) + (window (display-buffer buffer t nil))) + (with-selected-window window + (with-current-buffer buffer + (call-process "gpg" nil t t "--recv-keys" keyid) + (insert "\n") + (call-process "gpg" nil t t "--list-keys" keyid)) + (recenter -1)) + (notmuch-show-refresh-view))) + (defun notmuch-crypto-insert-encstatus-button (encstatus) (let* ((status (plist-get encstatus :status)) (help-msg nil) diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index 916cda1c..1a76c30a 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -428,7 +428,7 @@ Complete list of currently available key bindings: (let ((found-target-pos nil) (final-target-pos nil)) (let* ((saved-alist - ;; Filter out empty saved seaches if required. + ;; Filter out empty saved searches if required. (if notmuch-show-empty-saved-searches notmuch-saved-searches (loop for elem in notmuch-saved-searches @@ -462,6 +462,10 @@ Complete list of currently available key bindings: (length "Search: "))) :action (lambda (widget &rest ignore) (notmuch-hello-search (widget-value widget)))) + ;; add an invisible space to make `widget-end-of-line' ignore + ;; trailine spaces in the search widget field + (widget-insert " ") + (put-text-property (1- (point)) (point) 'invisible t) (widget-insert "\n") (when notmuch-hello-recent-searches diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index a21dc145..0f856bf0 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -88,7 +88,7 @@ the user hasn't set this variable with the old or new value." (notmuch-config-get "user.primary_email")) (defun notmuch-user-other-email () - "Return the user.primary_email value (as a list) from the notmuch configuration." + "Return the user.other_email value (as a list) from the notmuch configuration." (split-string (notmuch-config-get "user.other_email") "\n")) (defun notmuch-kill-this-buffer () @@ -105,21 +105,6 @@ the user hasn't set this variable with the old or new value." ;; -;; XXX: This should be a generic function in emacs somewhere, not -;; here. -(defun point-invisible-p () - "Return whether the character at point is invisible. - -Here visibility is determined by `buffer-invisibility-spec' and -the invisible property of any overlays for point. It doesn't have -anything to do with whether point is currently being displayed -within the current window." - (let ((prop (get-char-property (point) 'invisible))) - (if (eq buffer-invisibility-spec t) - prop - (or (memq prop buffer-invisibility-spec) - (assq prop buffer-invisibility-spec))))) - (defun notmuch-remove-if-not (predicate list) "Return a copy of LIST with all items not satisfying PREDICATE removed." (let (out) diff --git a/emacs/notmuch-maildir-fcc.el b/emacs/notmuch-maildir-fcc.el index b6c6e2a7..e6788685 100644 --- a/emacs/notmuch-maildir-fcc.el +++ b/emacs/notmuch-maildir-fcc.el @@ -163,7 +163,7 @@ will NOT be removed or replaced." (make-directory (concat path "/new/") t) (make-directory (concat path "/tmp/") t)) ((file-regular-p path) - (error "%s is a file. Can't creat maildir." path)) + (error "%s is a file. Can't create maildir." path)) (t (error "I don't know how to create a maildir here")))) diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el index 274c5dab..8824b080 100644 --- a/emacs/notmuch-mua.el +++ b/emacs/notmuch-mua.el @@ -65,7 +65,7 @@ list." (defun notmuch-mua-add-more-hidden-headers () "Add some headers to the list that are hidden by default." (mapc (lambda (header) - (when (not (member header 'message-hidden-headers)) + (when (not (member header message-hidden-headers)) (push header message-hidden-headers))) notmuch-mua-hidden-headers)) diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index 45298783..d5c95d80 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -27,6 +27,7 @@ (require 'mm-decode) (require 'mailcap) (require 'icalendar) +(require 'goto-addr) (require 'notmuch-lib) (require 'notmuch-query) @@ -110,7 +111,7 @@ any given message." "Use external viewers to view all attachments from the current message." (interactive) (with-current-notmuch-show-message - ; We ovverride the mm-inline-media-tests to indicate which message + ; We override the mm-inline-media-tests to indicate which message ; parts are already sufficiently handled by the original ; presentation of the message in notmuch-show mode. These parts ; will be inserted directly into the temporary buffer of @@ -508,21 +509,26 @@ current buffer, if possible." t) (defun notmuch-show-insert-part-message/rfc822 (msg part content-type nth depth declared-type) - (let* ((message-part (plist-get part :content)) - (inner-parts (plist-get message-part :content))) - (notmuch-show-insert-part-header nth declared-type content-type nil) + (notmuch-show-insert-part-header nth declared-type content-type nil) + (let* ((message (car (plist-get part :content))) + (headers (plist-get message :headers)) + (body (car (plist-get message :body))) + (start (point))) + ;; Override `notmuch-message-headers' to force `From' to be ;; displayed. (let ((notmuch-message-headers '("From" "Subject" "To" "Cc" "Date"))) - (notmuch-show-insert-headers (plist-get part :headers))) + (notmuch-show-insert-headers (plist-get message :headers))) + ;; Blank line after headers to be compatible with the normal ;; message display. (insert "\n") - ;; Show all of the parts. - (mapc (lambda (inner-part) - (notmuch-show-insert-bodypart msg inner-part depth)) - inner-parts)) + ;; Show the body + (notmuch-show-insert-bodypart msg body depth) + + (when notmuch-show-indent-multipart + (indent-rigidly start (point) 1))) t) (defun notmuch-show-insert-part-text/plain (msg part content-type nth depth declared-type) @@ -775,8 +781,32 @@ current buffer, if possible." "Insert the forest of threads FOREST." (mapc '(lambda (thread) (notmuch-show-insert-thread thread 0)) forest)) +(defvar notmuch-show-thread-id nil) +(make-variable-buffer-local 'notmuch-show-thread-id) (defvar notmuch-show-parent-buffer nil) (make-variable-buffer-local 'notmuch-show-parent-buffer) +(defvar notmuch-show-query-context nil) +(make-variable-buffer-local 'notmuch-show-query-context) +(defvar notmuch-show-buffer-name nil) +(make-variable-buffer-local 'notmuch-show-buffer-name) + +(defun notmuch-show-buttonise-links (start end) + "Buttonise URLs and mail addresses between START and END. + +This also turns id:\"<message id>\"-parts into buttons for +a corresponding notmuch search." + (goto-address-fontify-region start end) + (save-excursion + (goto-char start) + (while (re-search-forward "id:\\(\"?\\)[^[:space:]\"]+\\1" end t) + ;; remove the overlay created by goto-address-mode + (remove-overlays (match-beginning 0) (match-end 0) 'goto-address t) + (make-text-button (match-beginning 0) (match-end 0) + 'action `(lambda (arg) + (notmuch-search ,(match-string-no-properties 0))) + 'follow-link t + 'help-echo "Mouse-1, RET: search for this message" + 'face goto-address-mail-face)))) ;;;###autoload (defun notmuch-show (thread-id &optional parent-buffer query-context buffer-name crypto-switch) @@ -791,22 +821,35 @@ The optional QUERY-CONTEXT is a notmuch search term. Only messages from the thread matching this search term are shown if non-nil. -The optional BUFFER-NAME provides the neame of the buffer in +The optional BUFFER-NAME provides the name of the buffer in which the message thread is shown. If it is nil (which occurs when the command is called interactively) the argument to the -function is used. " +function is used. + +The optional CRYPTO-SWITCH toggles the value of the +notmuch-crypto-process-mime customization variable for this show +buffer." (interactive "sNotmuch show: ") - (let ((buffer (get-buffer-create (generate-new-buffer-name - (or buffer-name - (concat "*notmuch-" thread-id "*"))))) - (process-crypto (if crypto-switch - (not notmuch-crypto-process-mime) - notmuch-crypto-process-mime)) - (inhibit-read-only t)) + (let* ((process-crypto (if crypto-switch + (not notmuch-crypto-process-mime) + notmuch-crypto-process-mime))) + (notmuch-show-worker thread-id parent-buffer query-context buffer-name process-crypto))) + +(defun notmuch-show-worker (thread-id parent-buffer query-context buffer-name process-crypto) + (let* ((buffer-name (generate-new-buffer-name + (or buffer-name + (concat "*notmuch-" thread-id "*")))) + (buffer (get-buffer-create buffer-name)) + (inhibit-read-only t)) (switch-to-buffer buffer) (notmuch-show-mode) + + (setq notmuch-show-thread-id thread-id) (setq notmuch-show-parent-buffer parent-buffer) + (setq notmuch-show-query-context query-context) + (setq notmuch-show-buffer-name buffer-name) (setq notmuch-show-process-crypto process-crypto) + (erase-buffer) (goto-char (point-min)) (save-excursion @@ -822,9 +865,8 @@ function is used. " (notmuch-show-insert-forest (notmuch-query-get-threads basic-args)))) - ;; Enable buttonisation of URLs and email addresses in the - ;; buffer. - (goto-address-mode t) + (jit-lock-register #'notmuch-show-buttonise-links) + ;; Act on visual lines rather than logical lines. (visual-line-mode t) @@ -839,6 +881,22 @@ function is used. " (notmuch-show-mark-read))) +(defun notmuch-show-refresh-view (&optional crypto-switch) + "Refresh the current view (with crypto switch if prefix given). + +Kills the current buffer and reruns notmuch show with the same +thread id. If a prefix is given, crypto processing is toggled." + (interactive "P") + (let ((thread-id notmuch-show-thread-id) + (parent-buffer notmuch-show-parent-buffer) + (query-context notmuch-show-query-context) + (buffer-name notmuch-show-buffer-name) + (process-crypto (if crypto-switch + (not notmuch-show-process-crypto) + notmuch-show-process-crypto))) + (notmuch-kill-this-buffer) + (notmuch-show-worker thread-id parent-buffer query-context buffer-name process-crypto))) + (defvar notmuch-show-stash-map (let ((map (make-sparse-keymap))) (define-key map "c" 'notmuch-show-stash-cc) @@ -846,6 +904,7 @@ function is used. " (define-key map "F" 'notmuch-show-stash-filename) (define-key map "f" 'notmuch-show-stash-from) (define-key map "i" 'notmuch-show-stash-message-id) + (define-key map "I" 'notmuch-show-stash-message-id-stripped) (define-key map "s" 'notmuch-show-stash-subject) (define-key map "T" 'notmuch-show-stash-tags) (define-key map "t" 'notmuch-show-stash-to) @@ -870,6 +929,7 @@ function is used. " (define-key map "V" 'notmuch-show-view-raw-message) (define-key map "v" 'notmuch-show-view-all-mime-parts) (define-key map "c" 'notmuch-show-stash-map) + (define-key map "=" 'notmuch-show-refresh-view) (define-key map "h" 'notmuch-show-toggle-headers) (define-key map "-" 'notmuch-show-remove-tag) (define-key map "+" 'notmuch-show-add-tag) @@ -909,7 +969,7 @@ more selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show- without removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread without scrolling through with \\[notmuch-show-advance-and-archive]). -You can add or remove arbitary tags from the current message with +You can add or remove arbitrary tags from the current message with '\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'. All currently available key bindings: @@ -965,14 +1025,6 @@ All currently available key bindings: (notmuch-show-move-to-message-top) t)) -(defun notmuch-show-move-past-invisible-forward () - (while (point-invisible-p) - (forward-char))) - -(defun notmuch-show-move-past-invisible-backward () - (while (point-invisible-p) - (backward-char))) - ;; Functions relating to the visibility of messages and their ;; components. @@ -1101,17 +1153,18 @@ thread, (remove the \"inbox\" tag from each message). Also kill this buffer, and display the next thread from the search from which this thread was originally shown." (interactive) - (let ((end-of-this-message (notmuch-show-message-bottom))) + (let* ((end-of-this-message (notmuch-show-message-bottom)) + (visible-end-of-this-message (1- end-of-this-message))) + (while (invisible-p visible-end-of-this-message) + (setq visible-end-of-this-message + (previous-single-char-property-change visible-end-of-this-message + 'invisible))) (cond ;; Ideally we would test `end-of-this-message' against the result ;; of `window-end', but that doesn't account for the fact that - ;; the end of the message might be hidden, so we have to actually - ;; go to the end, walk back over invisible text and then see if - ;; point is visible. - ((save-excursion - (goto-char (- end-of-this-message 1)) - (notmuch-show-move-past-invisible-backward) - (> (point) (window-end))) + ;; the end of the message might be hidden. + ((and visible-end-of-this-message + (> visible-end-of-this-message (window-end))) ;; The bottom of this message is not visible - scroll. (scroll-up nil)) @@ -1396,6 +1449,11 @@ buffer." (interactive) (notmuch-common-do-stash (notmuch-show-get-message-id))) +(defun notmuch-show-stash-message-id-stripped () + "Copy message ID of current message (sans `id:' prefix) to kill-ring." + (interactive) + (notmuch-common-do-stash (substring (notmuch-show-get-message-id) 4 -1))) + (defun notmuch-show-stash-subject () "Copy Subject field of current message to kill-ring." (interactive) diff --git a/emacs/notmuch-wash.el b/emacs/notmuch-wash.el index 92f07c50..1f420b25 100644 --- a/emacs/notmuch-wash.el +++ b/emacs/notmuch-wash.el @@ -137,7 +137,7 @@ collapse the remaining lines into a button.") (format label-format lines-count))) (defun notmuch-wash-region-to-button (msg beg end type prefix) - "Auxilary function to do the actual making of overlays and buttons + "Auxiliary function to do the actual making of overlays and buttons BEG and END are buffer locations. TYPE should a string, either \"citation\" or \"signature\". PREFIX is some arbitrary text to diff --git a/emacs/notmuch.el b/emacs/notmuch.el index 3311fe8b..c1827cc2 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -64,7 +64,7 @@ ("authors" . "%-20s ") ("subject" . "%s ") ("tags" . "(%s)")) - "Search result formating. Supported fields are: + "Search result formatting. Supported fields are: date, count, authors, subject, tags For example: (setq notmuch-search-result-format \(\(\"authors\" . \"%-40s\"\) @@ -72,6 +72,9 @@ For example: :type '(alist :key-type (string) :value-type (string)) :group 'notmuch) +(defvar notmuch-query-history nil + "Variable to store minibuffer history for notmuch queries") + (defun notmuch-select-tag-with-completion (prompt &rest search-terms) (let ((tag-list (with-output-to-string @@ -218,7 +221,6 @@ For a mouse binding, return nil." (define-key map "-" 'notmuch-search-remove-tag) (define-key map "+" 'notmuch-search-add-tag) (define-key map (kbd "RET") 'notmuch-search-show-thread) - (define-key map (kbd "M-RET") 'notmuch-search-show-thread-crypto-switch) map) "Keymap for \"notmuch search\" buffers.") (fset 'notmuch-search-mode-map notmuch-search-mode-map) @@ -375,7 +377,7 @@ Complete list of currently available key bindings: (make-local-variable 'notmuch-search-target-line) (set (make-local-variable 'notmuch-search-continuation) nil) (set (make-local-variable 'scroll-preserve-screen-position) t) - (add-to-invisibility-spec 'notmuch-search) + (add-to-invisibility-spec (cons 'ellipsis t)) (use-local-map notmuch-search-mode-map) (setq truncate-lines t) (setq major-mode 'notmuch-search-mode @@ -418,13 +420,9 @@ Complete list of currently available key bindings: "Return a list of authors for the current region" (notmuch-search-properties-in-region 'notmuch-search-subject beg end)) -(defun notmuch-search-show-thread-crypto-switch () - (interactive) - (notmuch-search-show-thread t)) - (defun notmuch-search-show-thread (&optional crypto-switch) "Display the currently selected thread." - (interactive) + (interactive "P") (let ((thread-id (notmuch-search-find-thread-id)) (subject (notmuch-search-find-subject))) (if (> (length thread-id) 0) @@ -493,7 +491,7 @@ the messages that are about to be tagged" :group 'notmuch) (defcustom notmuch-after-tag-hook nil - "Hooks that are run before tags of a message are modified. + "Hooks that are run after tags of a message are modified. 'tags' will contain the tags that were added or removed as a list of strings of the form \"+TAG\" or \"-TAG\". @@ -676,9 +674,6 @@ foreground and blue background." (append (overlay-get overlay 'face) attributes))))) notmuch-search-line-faces))) -(defun notmuch-search-isearch-authors-show (overlay) - (remove-from-invisibility-spec (cons (overlay-get overlay 'invisible) t))) - (defun notmuch-search-author-propertize (authors) "Split `authors' into matching and non-matching authors and propertize appropriately. If no boundary between authors and @@ -752,13 +747,11 @@ non-authors is found, assume that all of the authors match." (insert visible-string) (when (not (string= invisible-string "")) (let ((start (point)) - (invis-spec (make-symbol "notmuch-search-authors")) overlay) (insert invisible-string) - (add-to-invisibility-spec (cons invis-spec t)) (setq overlay (make-overlay start (point))) - (overlay-put overlay 'invisible invis-spec) - (overlay-put overlay 'isearch-open-invisible #'notmuch-search-isearch-authors-show))) + (overlay-put overlay 'invisible 'ellipsis) + (overlay-put overlay 'isearch-open-invisible #'delete-overlay))) (insert padding)))) (defun notmuch-search-insert-field (field date count authors subject tags) @@ -836,7 +829,7 @@ non-authors is found, assume that all of the authors match." (defun notmuch-search-operate-all (action) "Add/remove tags from all matching messages. -Tis command adds or removes tags from all messages matching the +This command adds or removes tags from all messages matching the current search terms. When called interactively, this command will prompt for tags to be added or removed. Tags prefixed with '+' will be added and tags prefixed with '-' will be removed. @@ -882,6 +875,36 @@ characters as well as `_.+-'. (concat "*notmuch-search-" query "*")) ))) +(defun notmuch-read-query (prompt) + "Read a notmuch-query from the minibuffer with completion. + +PROMPT is the string to prompt with." + (lexical-let + ((completions + (append (list "folder:" "thread:" "id:" "date:" "from:" "to:" + "subject:" "attachment:") + (mapcar (lambda (tag) + (concat "tag:" tag)) + (process-lines "notmuch" "search" "--output=tags" "*"))))) + (let ((keymap (copy-keymap minibuffer-local-map)) + (minibuffer-completion-table + (completion-table-dynamic + (lambda (string) + ;; generate a list of possible completions for the current input + (cond + ;; this ugly regexp is used to get the last word of the input + ;; possibly preceded by a '(' + ((string-match "\\(^\\|.* (?\\)\\([^ ]*\\)$" string) + (mapcar (lambda (compl) + (concat (match-string-no-properties 1 string) compl)) + (all-completions (match-string-no-properties 2 string) + completions))) + (t (list string))))))) + ;; this was simpler than convincing completing-read to accept spaces: + (define-key keymap (kbd "<tab>") 'minibuffer-complete) + (read-from-minibuffer prompt nil keymap nil + 'notmuch-query-history nil nil)))) + ;;;###autoload (defun notmuch-search (query &optional oldest-first target-thread target-line continuation) "Run \"notmuch search\" with the given query string and display results. @@ -893,7 +916,7 @@ The optional parameters are used as follows: current if it appears in the search results. target-line: The line number to move to if the target thread does not appear in the search results." - (interactive "sNotmuch search: ") + (interactive (list (notmuch-read-query "Notmuch search: "))) (let ((buffer (get-buffer-create (notmuch-search-buffer-title query)))) (switch-to-buffer buffer) (notmuch-search-mode) @@ -918,7 +941,8 @@ The optional parameters are used as follows: "--sort=newest-first") query))) (set-process-sentinel proc 'notmuch-search-process-sentinel) - (set-process-filter proc 'notmuch-search-process-filter)))) + (set-process-filter proc 'notmuch-search-process-filter) + (set-process-query-on-exit-flag proc nil)))) (run-hooks 'notmuch-search-hook))) (defun notmuch-search-refresh-view () @@ -991,7 +1015,7 @@ search." Runs a new search matching only messages that match both the current search results AND the additional query string provided." - (interactive "sFilter search: ") + (interactive (list (notmuch-read-query "Filter search: "))) (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query) (concat "( " query " )") query))) diff --git a/gmime-filter-headers.h b/gmime-filter-headers.h index 47d1d456..1d1a3ebb 100644 --- a/gmime-filter-headers.h +++ b/gmime-filter-headers.h @@ -39,7 +39,7 @@ typedef struct _GMimeFilterHeadersClass GMimeFilterHeadersClass; * @parent_object: parent #GMimeFilter * @saw_nl: previous char was a \n * @line: temporary buffer for line unfolding - * @line_size: size of currently allocated nemory for @line + * @line_size: size of currently allocated memory for @line * @lineptr: pointer to the first unused character in @line * * A filter to decode rfc2047 encoded headers diff --git a/lib/Makefile b/lib/Makefile index b6859eac..de492a7c 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -1,4 +1,4 @@ -# See Makfefile.local for the list of files to be compiled in this +# See Makefile.local for the list of files to be compiled in this # directory. all: $(MAKE) -C .. all diff --git a/lib/Makefile.local b/lib/Makefile.local index 7e2bc87b..57dca702 100644 --- a/lib/Makefile.local +++ b/lib/Makefile.local @@ -5,19 +5,13 @@ # the library interface, (such as the deletion of an API or a major # semantic change that breaks formerly functioning code). # -# Note: We don't currently have plans to increment this at this time. -# If we *do* want to make an incompatible change to the library -# interface, we'll have to decide whether to increment this (creating -# a new soname) or to introduce symbol versioning to be able to -# provide support for both old and new interfaces without having to -# increment this. -LIBNOTMUCH_VERSION_MAJOR = 1 +LIBNOTMUCH_VERSION_MAJOR = 2 # The minor version of the library interface. This should be incremented at # the time of release for any additions to the library interface, # (and when it is incremented, the release version of the library should # be reset to 0). -LIBNOTMUCH_VERSION_MINOR = 3 +LIBNOTMUCH_VERSION_MINOR = 0 # The release version the library interface. This should be incremented at # the time of release if there have been no changes to the interface, (but @@ -47,6 +41,11 @@ endif dir := lib extra_cflags += -I$(srcdir)/$(dir) -fPIC +# The (often-reused) $dir works fine within targets/prerequisites, +# but cannot be used reliably within commands, so copy its value to a +# variable that is not reused. +lib := $(dir) + libnotmuch_c_srcs = \ $(notmuch_compat_srcs) \ $(dir)/filenames.c \ @@ -55,8 +54,7 @@ libnotmuch_c_srcs = \ $(dir)/message-file.c \ $(dir)/messages.c \ $(dir)/sha1.c \ - $(dir)/tags.c \ - $(dir)/xutil.c + $(dir)/tags.c libnotmuch_cxx_srcs = \ $(dir)/database.cc \ @@ -72,13 +70,10 @@ $(dir)/libnotmuch.a: $(libnotmuch_modules) $(call quiet,AR) rcs $@ $^ $(dir)/$(LIBNAME): $(libnotmuch_modules) notmuch.sym - echo $(libnotmuch_modules) - $(call quiet,CXX $(CXXFLAGS)) $(libnotmuch_modules) $(FINAL_LIBNOTMUCH_LDFLAGS) $(LIBRARY_LINK_FLAG) -o $@ + $(call quiet,CXX $(CXXFLAGS)) $(libnotmuch_modules) $(FINAL_LIBNOTMUCH_LDFLAGS) $(LIBRARY_LINK_FLAG) -o $@ util/libutil.a -notmuch.sym: lib/notmuch.h - printf "{\nglobal:\n" > notmuch.sym - sed -n 's/^\s*\(notmuch_[a-z_]*\)\s*(.*/\t\1;/p' $< >> notmuch.sym - printf "local: *;\n};\n" >> notmuch.sym +notmuch.sym: $(srcdir)/$(dir)/notmuch.h $(libnotmuch_modules) + sh $(srcdir)/$(lib)/gen-version-script.sh $< $(libnotmuch_modules) > $@ $(dir)/$(SONAME): $(dir)/$(LIBNAME) ln -sf $(LIBNAME) $@ @@ -88,10 +83,6 @@ $(dir)/$(LINKER_NAME): $(dir)/$(SONAME) install: install-$(dir) -# The (often-reused) $dir works fine within targets/pre-requisites, -# but cannot be used reliably within commands, so copy its value to a -# variable that is not reused. -lib := $(dir) install-$(dir): $(dir)/$(LIBNAME) mkdir -p "$(DESTDIR)$(libdir)/" install -m0644 "$(lib)/$(LIBNAME)" "$(DESTDIR)$(libdir)/" @@ -102,4 +93,6 @@ install-$(dir): $(dir)/$(LIBNAME) $(LIBRARY_INSTALL_POST_COMMAND) SRCS := $(SRCS) $(libnotmuch_c_srcs) $(libnotmuch_cxx_srcs) -CLEAN := $(CLEAN) $(libnotmuch_modules) $(dir)/$(SONAME) $(dir)/$(LINKER_NAME) $(dir)$(LIBNAME) libnotmuch.a notmuch.sym +CLEAN += $(libnotmuch_modules) $(dir)/$(SONAME) $(dir)/$(LINKER_NAME) +CLEAN += $(dir)/$(LIBNAME) $(dir)/libnotmuch.a notmuch.sym +CLEAN += $(dir)/notmuch.h.gch diff --git a/lib/database-private.h b/lib/database-private.h index f7050097..88532d51 100644 --- a/lib/database-private.h +++ b/lib/database-private.h @@ -43,6 +43,7 @@ struct _notmuch_database { notmuch_bool_t needs_upgrade; notmuch_database_mode_t mode; + int atomic_nesting; Xapian::Database *xapian_db; unsigned int last_doc_id; diff --git a/lib/database.cc b/lib/database.cc index 7f79cf47..98f101e6 100644 --- a/lib/database.cc +++ b/lib/database.cc @@ -26,6 +26,7 @@ #include <signal.h> #include <glib.h> /* g_free, GPtrArray, GHashTable */ +#include <glib-object.h> /* g_type_init */ using namespace std; @@ -69,7 +70,7 @@ typedef struct { * * Multiple terms of given prefix: * - * reference: All message IDs from In-Reply-To and Re ferences + * reference: All message IDs from In-Reply-To and References * headers in the message. * * tag: Any tags associated with this message by the user. @@ -137,7 +138,7 @@ typedef struct { * ASCII integer. The initial database version * was 1, (though a schema existed before that * were no "version" database value existed at - * all). Succesive versions are allocated as + * all). Successive versions are allocated as * changes are made to the database (such as by * indexing new fields). * @@ -148,7 +149,7 @@ typedef struct { * incremented for each thread ID. * * thread_id_* A pre-allocated thread ID for a particular - * message. This is actually an arbitarily large + * message. This is actually an arbitrarily large * family of metadata name. Any particular name is * formed by concatenating "thread_id_" with a message * ID (or the SHA1 sum of a message ID if it is very @@ -209,21 +210,6 @@ static prefix_t PROBABILISTIC_PREFIX[]= { { "folder", "XFOLDER"} }; -int -_internal_error (const char *format, ...) -{ - va_list va_args; - - va_start (va_args, format); - - fprintf (stderr, "Internal error: "); - vfprintf (stderr, format, va_args); - - exit (1); - - return 1; -} - const char * _find_prefix (const char *name) { @@ -273,6 +259,8 @@ notmuch_status_to_string (notmuch_status_t status) return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)"; case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: return "Unbalanced number of calls to notmuch_message_freeze/thaw"; + case NOTMUCH_STATUS_UNBALANCED_ATOMIC: + return "Unbalanced number of calls to notmuch_database_begin_atomic/end_atomic"; default: case NOTMUCH_STATUS_LAST_STATUS: return "Unknown error status value"; @@ -358,13 +346,17 @@ _message_id_compressed (void *ctx, const char *message_id) return compressed; } -notmuch_message_t * +notmuch_status_t notmuch_database_find_message (notmuch_database_t *notmuch, - const char *message_id) + const char *message_id, + notmuch_message_t **message_ret) { notmuch_private_status_t status; unsigned int doc_id; + if (message_ret == NULL) + return NOTMUCH_STATUS_NULL_POINTER; + if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX) message_id = _message_id_compressed (notmuch, message_id); @@ -373,14 +365,21 @@ notmuch_database_find_message (notmuch_database_t *notmuch, message_id, &doc_id); if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) - return NULL; + *message_ret = NULL; + else { + *message_ret = _notmuch_message_create (notmuch, notmuch, doc_id, + NULL); + if (*message_ret == NULL) + return NOTMUCH_STATUS_OUT_OF_MEMORY; + } - return _notmuch_message_create (notmuch, notmuch, doc_id, NULL); + return NOTMUCH_STATUS_SUCCESS; } catch (const Xapian::Error &error) { fprintf (stderr, "A Xapian exception occurred finding message: %s.\n", error.get_msg().c_str()); notmuch->exception_reported = TRUE; - return NULL; + *message_ret = NULL; + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; } } @@ -422,7 +421,7 @@ skip_space_and_comments (const char **str) } /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822 - * comments, and the '<' and '>' delimeters. + * comments, and the '<' and '>' delimiters. * * If not NULL, then *next will be made to point to the first character * not parsed, (possibly pointing to the final '\0' terminator. @@ -602,6 +601,9 @@ notmuch_database_open (const char *path, goto DONE; } + /* Initialize the GLib type system and threads */ + g_type_init (); + notmuch = talloc (NULL, notmuch_database_t); notmuch->exception_reported = FALSE; notmuch->path = talloc_strdup (notmuch, path); @@ -611,6 +613,7 @@ notmuch_database_open (const char *path, notmuch->needs_upgrade = FALSE; notmuch->mode = mode; + notmuch->atomic_nesting = 0; try { string last_thread_id; @@ -974,6 +977,61 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, return NOTMUCH_STATUS_SUCCESS; } +notmuch_status_t +notmuch_database_begin_atomic (notmuch_database_t *notmuch) +{ + if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY || + notmuch->atomic_nesting > 0) + goto DONE; + + try { + (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->begin_transaction (false); + } catch (const Xapian::Error &error) { + fprintf (stderr, "A Xapian exception occurred beginning transaction: %s.\n", + error.get_msg().c_str()); + notmuch->exception_reported = TRUE; + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + +DONE: + notmuch->atomic_nesting++; + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_status_t +notmuch_database_end_atomic (notmuch_database_t *notmuch) +{ + Xapian::WritableDatabase *db; + + if (notmuch->atomic_nesting == 0) + return NOTMUCH_STATUS_UNBALANCED_ATOMIC; + + if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY || + notmuch->atomic_nesting > 1) + goto DONE; + + db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db); + try { + db->commit_transaction (); + + /* This is a hack for testing. Xapian never flushes on a + * non-flushed commit, even if the flush threshold is 1. + * However, we rely on flushing to test atomicity. */ + const char *thresh = getenv ("XAPIAN_FLUSH_THRESHOLD"); + if (thresh && atoi (thresh) == 1) + db->flush (); + } catch (const Xapian::Error &error) { + fprintf (stderr, "A Xapian exception occurred committing transaction: %s.\n", + error.get_msg().c_str()); + notmuch->exception_reported = TRUE; + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + +DONE: + notmuch->atomic_nesting--; + return NOTMUCH_STATUS_SUCCESS; +} + /* We allow the user to use arbitrarily long paths for directories. But * we have a term-length limit. So if we exceed that, we'll use the * SHA-1 of the path for the database term. @@ -1149,7 +1207,7 @@ _notmuch_database_filename_to_direntry (void *ctx, /* Given a legal 'path' for the database, return the relative path. * - * The return value will be a pointer to the originl path contents, + * The return value will be a pointer to the original path contents, * and will be either the original string (if 'path' was relative) or * a portion of the string (if path was absolute and begins with the * database path). @@ -1253,7 +1311,9 @@ _get_metadata_thread_id_key (void *ctx, const char *message_id) /* Find the thread ID to which the message with 'message_id' belongs. * - * Always returns a newly talloced string belonging to 'ctx'. + * Note: 'thread_id_ret' must not be NULL! + * On success '*thread_id_ret' is set to a newly talloced string belonging to + * 'ctx'. * * Note: If there is no message in the database with the given * 'message_id' then a new thread_id will be allocated for this @@ -1261,25 +1321,30 @@ _get_metadata_thread_id_key (void *ctx, const char *message_id) * thread ID can be looked up if the message is added to the database * later). */ -static const char * +static notmuch_status_t _resolve_message_id_to_thread_id (notmuch_database_t *notmuch, void *ctx, - const char *message_id) + const char *message_id, + const char **thread_id_ret) { + notmuch_status_t status; notmuch_message_t *message; string thread_id_string; - const char *thread_id; char *metadata_key; Xapian::WritableDatabase *db; - message = notmuch_database_find_message (notmuch, message_id); + status = notmuch_database_find_message (notmuch, message_id, &message); + + if (status) + return status; if (message) { - thread_id = talloc_steal (ctx, notmuch_message_get_thread_id (message)); + *thread_id_ret = talloc_steal (ctx, + notmuch_message_get_thread_id (message)); notmuch_message_destroy (message); - return thread_id; + return NOTMUCH_STATUS_SUCCESS; } /* Message has not been seen yet. @@ -1293,15 +1358,16 @@ _resolve_message_id_to_thread_id (notmuch_database_t *notmuch, thread_id_string = notmuch->xapian_db->get_metadata (metadata_key); if (thread_id_string.empty()) { - thread_id = _notmuch_database_generate_thread_id (notmuch); - db->set_metadata (metadata_key, thread_id); + *thread_id_ret = talloc_strdup (ctx, + _notmuch_database_generate_thread_id (notmuch)); + db->set_metadata (metadata_key, *thread_id_ret); } else { - thread_id = thread_id_string.c_str(); + *thread_id_ret = talloc_strdup (ctx, thread_id_string.c_str()); } talloc_free (metadata_key); - return talloc_strdup (ctx, thread_id); + return NOTMUCH_STATUS_SUCCESS; } static notmuch_status_t @@ -1388,9 +1454,12 @@ _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch, _notmuch_message_add_term (message, "reference", parent_message_id); - parent_thread_id = _resolve_message_id_to_thread_id (notmuch, - message, - parent_message_id); + ret = _resolve_message_id_to_thread_id (notmuch, + message, + parent_message_id, + &parent_thread_id); + if (ret) + goto DONE; if (*thread_id == NULL) { *thread_id = talloc_strdup (message, parent_thread_id); @@ -1476,7 +1545,7 @@ _notmuch_database_link_message_to_children (notmuch_database_t *notmuch, * In all cases, we assign to the current message the first thread_id * found (through either parent or child). We will also merge any * existing, distinct threads where this message belongs to both, - * (which is not uncommon when mesages are processed out of order). + * (which is not uncommon when messages are processed out of order). * * Finally, if no thread ID has been found through parent or child, we * call _notmuch_message_generate_thread_id to generate a new thread @@ -1543,7 +1612,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch, { notmuch_message_file_t *message_file; notmuch_message_t *message = NULL; - notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; + notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS, ret2; notmuch_private_status_t private_status; const char *date, *header; @@ -1561,6 +1630,12 @@ notmuch_database_add_message (notmuch_database_t *notmuch, if (message_file == NULL) return NOTMUCH_STATUS_FILE_ERROR; + /* Adding a message may change many documents. Do this all + * atomically. */ + ret = notmuch_database_begin_atomic (notmuch); + if (ret) + goto DONE; + notmuch_message_file_restrict_headers (message_file, "date", "from", @@ -1654,7 +1729,7 @@ notmuch_database_add_message (notmuch_database_t *notmuch, goto DONE; date = notmuch_message_file_get_header (message_file, "date"); - _notmuch_message_set_date (message, date); + _notmuch_message_set_header_values (message, date, from, subject); _notmuch_message_index_file (message, filename); } else { @@ -1682,6 +1757,12 @@ notmuch_database_add_message (notmuch_database_t *notmuch, if (message_file) notmuch_message_file_close (message_file); + ret2 = notmuch_database_end_atomic (notmuch); + if ((ret == NOTMUCH_STATUS_SUCCESS || + ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) && + ret2 != NOTMUCH_STATUS_SUCCESS) + ret = ret2; + return ret; } @@ -1689,71 +1770,73 @@ notmuch_status_t notmuch_database_remove_message (notmuch_database_t *notmuch, const char *filename) { - Xapian::WritableDatabase *db; + notmuch_status_t status; + notmuch_message_t *message; + + status = notmuch_database_find_message_by_filename (notmuch, filename, + &message); + + if (status == NOTMUCH_STATUS_SUCCESS && message) { + status = _notmuch_message_remove_filename (message, filename); + if (status == NOTMUCH_STATUS_SUCCESS) + _notmuch_message_delete (message); + else if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) + _notmuch_message_sync (message); + + notmuch_message_destroy (message); + } + + return status; +} + +notmuch_status_t +notmuch_database_find_message_by_filename (notmuch_database_t *notmuch, + const char *filename, + notmuch_message_t **message_ret) +{ void *local; const char *prefix = _find_prefix ("file-direntry"); char *direntry, *term; Xapian::PostingIterator i, end; - Xapian::Document document; notmuch_status_t status; - status = _notmuch_database_ensure_writable (notmuch); - if (status) - return status; + if (message_ret == NULL) + return NOTMUCH_STATUS_NULL_POINTER; local = talloc_new (notmuch); - db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db); - try { - status = _notmuch_database_filename_to_direntry (local, notmuch, filename, &direntry); if (status) - return status; + goto DONE; term = talloc_asprintf (local, "%s%s", prefix, direntry); find_doc_ids_for_term (notmuch, term, &i, &end); - for ( ; i != end; i++) { - Xapian::TermIterator j; - notmuch_message_t *message; + if (i != end) { notmuch_private_status_t private_status; - message = _notmuch_message_create (local, notmuch, - *i, &private_status); - if (message == NULL) - return COERCE_STATUS (private_status, - "Inconsistent document ID in datbase."); - - _notmuch_message_remove_filename (message, filename); - _notmuch_message_sync (message); - - /* Take care to find document after sync'ing filename removal. */ - document = find_document_for_doc_id (notmuch, *i); - j = document.termlist_begin (); - j.skip_to (prefix); - - /* Was this the last file-direntry in the message? */ - if (j == document.termlist_end () || - strncmp ((*j).c_str (), prefix, strlen (prefix))) - { - db->delete_document (document.get_docid ()); - status = NOTMUCH_STATUS_SUCCESS; - } else { - status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID; - } + *message_ret = _notmuch_message_create (notmuch, notmuch, *i, + &private_status); + if (*message_ret == NULL) + status = NOTMUCH_STATUS_OUT_OF_MEMORY; } } catch (const Xapian::Error &error) { - fprintf (stderr, "Error: A Xapian exception occurred removing message: %s\n", + fprintf (stderr, "Error: A Xapian exception occurred finding message by filename: %s\n", error.get_msg().c_str()); notmuch->exception_reported = TRUE; status = NOTMUCH_STATUS_XAPIAN_EXCEPTION; } + DONE: talloc_free (local); + if (status && *message_ret) { + notmuch_message_destroy (*message_ret); + *message_ret = NULL; + } return status; } diff --git a/lib/gen-version-script.sh b/lib/gen-version-script.sh new file mode 100644 index 00000000..76670d57 --- /dev/null +++ b/lib/gen-version-script.sh @@ -0,0 +1,28 @@ + +# we go through a bit of work to get the unmangled names of the +# typeinfo symbols because of +# http://sourceware.org/bugzilla/show_bug.cgi?id=10326 + +if [ $# -lt 2 ]; then + echo Usage: $0 header obj1 obj2 obj3 + exit 1; +fi + +HEADER=$1 +shift + +printf '{\nglobal:\n' +nm $* | awk '$1 ~ "^[0-9a-fA-F][0-9a-fA-F]*$" && $3 ~ "Xapian.*Error" {print $3}' | sort | uniq | \ +while read sym; do + demangled=$(c++filt $sym) + case $demangled in + typeinfo*) + printf "\t$sym;\n" + ;; + *) + ;; + esac +done +nm $* | awk '$1 ~ "^[0-9a-fA-F][0-9a-fA-F]*$" && $2 == "T" && $3 ~ "^get(line|delim)$" {print $3 ";"}' +sed -n 's/^[[:space:]]*\(notmuch_[a-z_]*\)[[:space:]]*(.*/ \1;/p' $HEADER +printf "local: *;\n};\n" diff --git a/lib/libsha1.c b/lib/libsha1.c index c39a5a17..5d16f6ab 100644 --- a/lib/libsha1.c +++ b/lib/libsha1.c @@ -174,7 +174,7 @@ void sha1_hash(const unsigned char data[], unsigned long len, sha1_ctx ctx[1]) if((ctx->count[0] += len) < len) ++(ctx->count[1]); - while(len >= space) /* tranfer whole blocks if possible */ + while(len >= space) /* transfer whole blocks if possible */ { memcpy(((unsigned char*)ctx->wbuf) + pos, sp, space); sp += space; len -= space; space = SHA1_BLOCK_SIZE; pos = 0; diff --git a/lib/libsha1.h b/lib/libsha1.h index c1c848fc..56f445a9 100644 --- a/lib/libsha1.h +++ b/lib/libsha1.h @@ -38,7 +38,7 @@ extern "C" { #endif #if 0 -} /* Appleasing Emacs */ +} /* Appeasing Emacs */ #endif #include <stdint.h> diff --git a/lib/message.cc b/lib/message.cc index 2a4954dd..ca7fbf21 100644 --- a/lib/message.cc +++ b/lib/message.cc @@ -216,11 +216,13 @@ _notmuch_message_create_for_message_id (notmuch_database_t *notmuch, unsigned int doc_id; char *term; - *status_ret = NOTMUCH_PRIVATE_STATUS_SUCCESS; - - message = notmuch_database_find_message (notmuch, message_id); + *status_ret = (notmuch_private_status_t) notmuch_database_find_message (notmuch, + message_id, + &message); if (message) return talloc_steal (notmuch, message); + else if (*status_ret) + return NULL; term = talloc_asprintf (NULL, "%s%s", _find_prefix ("id"), message_id); @@ -410,6 +412,21 @@ _notmuch_message_ensure_message_file (notmuch_message_t *message) const char * notmuch_message_get_header (notmuch_message_t *message, const char *header) { + std::string value; + + /* Fetch header from the appropriate xapian value field if + * available */ + if (strcasecmp (header, "from") == 0) + value = message->doc.get_value (NOTMUCH_VALUE_FROM); + else if (strcasecmp (header, "subject") == 0) + value = message->doc.get_value (NOTMUCH_VALUE_SUBJECT); + else if (strcasecmp (header, "message-id") == 0) + value = message->doc.get_value (NOTMUCH_VALUE_MESSAGE_ID); + + if (!value.empty()) + return talloc_strdup (message, value.c_str ()); + + /* Otherwise fall back to parsing the file */ _notmuch_message_ensure_message_file (message); if (message->message_file == NULL) return NULL; @@ -501,6 +518,9 @@ _notmuch_message_add_filename (notmuch_message_t *message, * This change will not be reflected in the database until the next * call to _notmuch_message_sync. * + * If this message still has other filenames, returns + * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID. + * * Note: This function does not remove a document from the database, * even if the specified filename is the only filename for this * message. For that functionality, see @@ -531,6 +551,8 @@ _notmuch_message_remove_filename (notmuch_message_t *message, "file-direntry", direntry); status = COERCE_STATUS (private_status, "Unexpected error from _notmuch_message_remove_term"); + if (status) + return status; /* Re-synchronize "folder:" terms for this message. This requires: * 1. removing all "folder:" terms @@ -588,6 +610,9 @@ _notmuch_message_remove_filename (notmuch_message_t *message, if (strncmp ((*i).c_str (), direntry_prefix, direntry_prefix_len)) break; + /* Indicate that there are filenames remaining. */ + status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID; + direntry = (*i).c_str (); direntry += direntry_prefix_len; @@ -785,8 +810,10 @@ notmuch_message_set_author (notmuch_message_t *message, } void -_notmuch_message_set_date (notmuch_message_t *message, - const char *date) +_notmuch_message_set_header_values (notmuch_message_t *message, + const char *date, + const char *from, + const char *subject) { time_t time_value; @@ -799,6 +826,8 @@ _notmuch_message_set_date (notmuch_message_t *message, message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP, Xapian::sortable_serialise (time_value)); + message->doc.add_value (NOTMUCH_VALUE_FROM, from); + message->doc.add_value (NOTMUCH_VALUE_SUBJECT, subject); } /* Synchronize changes made to message->doc out into the database. */ @@ -814,6 +843,22 @@ _notmuch_message_sync (notmuch_message_t *message) db->replace_document (message->doc_id, message->doc); } +/* Delete a message document from the database. */ +notmuch_status_t +_notmuch_message_delete (notmuch_message_t *message) +{ + notmuch_status_t status; + Xapian::WritableDatabase *db; + + status = _notmuch_database_ensure_writable (message->notmuch); + if (status) + return status; + + db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db); + db->delete_document (message->doc_id); + return NOTMUCH_STATUS_SUCCESS; +} + /* Ensure that 'message' is not holding any file object open. Future * calls to various functions will still automatically open the * message file as needed. @@ -862,7 +907,7 @@ _notmuch_message_add_term (notmuch_message_t *message, /* Parse 'text' and add a term to 'message' for each parsed word. Each * term will be added both prefixed (if prefix_name is not NULL) and - * also unprefixed). */ + * also non-prefixed). */ notmuch_private_status_t _notmuch_message_gen_terms (notmuch_message_t *message, const char *prefix_name, @@ -1280,7 +1325,8 @@ notmuch_message_tags_to_maildir_flags (notmuch_message_t *message) new_status = _notmuch_message_remove_filename (message, filename); /* Hold on to only the first error. */ - if (! status && new_status) { + if (! status && new_status + && new_status != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) { status = new_status; continue; } diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h index 02e24ee8..60a932fc 100644 --- a/lib/notmuch-private.h +++ b/lib/notmuch-private.h @@ -47,6 +47,7 @@ NOTMUCH_BEGIN_DECLS #include <talloc.h> #include "xutil.h" +#include "error_util.h" #pragma GCC visibility push(hidden) @@ -60,25 +61,6 @@ NOTMUCH_BEGIN_DECLS #define STRNCMP_LITERAL(var, literal) \ strncmp ((var), (literal), sizeof (literal) - 1) -/* There's no point in continuing when we've detected that we've done - * something wrong internally (as opposed to the user passing in a - * bogus value). - * - * Note that PRINTF_ATTRIBUTE comes from talloc.h - */ -int -_internal_error (const char *format, ...) PRINTF_ATTRIBUTE (1, 2); - -/* There's no point in continuing when we've detected that we've done - * something wrong internally (as opposed to the user passing in a - * bogus value). - * - * Note that __location__ comes from talloc.h. - */ -#define INTERNAL_ERROR(format, ...) \ - _internal_error (format " (%s).\n", \ - ##__VA_ARGS__, __location__) - #define unused(x) x __attribute__ ((unused)) #ifdef __cplusplus @@ -111,7 +93,9 @@ _internal_error (const char *format, ...) PRINTF_ATTRIBUTE (1, 2); typedef enum { NOTMUCH_VALUE_TIMESTAMP = 0, - NOTMUCH_VALUE_MESSAGE_ID + NOTMUCH_VALUE_MESSAGE_ID, + NOTMUCH_VALUE_FROM, + NOTMUCH_VALUE_SUBJECT } notmuch_value_t; /* Xapian (with flint backend) complains if we provide a term longer @@ -287,12 +271,16 @@ void _notmuch_message_ensure_thread_id (notmuch_message_t *message); void -_notmuch_message_set_date (notmuch_message_t *message, - const char *date); - +_notmuch_message_set_header_values (notmuch_message_t *message, + const char *date, + const char *from, + const char *subject); void _notmuch_message_sync (notmuch_message_t *message); +notmuch_status_t +_notmuch_message_delete (notmuch_message_t *message); + void _notmuch_message_close (notmuch_message_t *message); diff --git a/lib/notmuch.h b/lib/notmuch.h index e508309e..9f23a106 100644 --- a/lib/notmuch.h +++ b/lib/notmuch.h @@ -81,6 +81,9 @@ typedef int notmuch_bool_t; * NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: The notmuch_message_thaw * function has been called more times than notmuch_message_freeze. * + * NOTMUCH_STATUS_UNBALANCED_ATOMIC: notmuch_database_end_atomic has + * been called more times than notmuch_database_begin_atomic. + * * And finally: * * NOTMUCH_STATUS_LAST_STATUS: Not an actual status value. Just a way @@ -97,13 +100,14 @@ typedef enum _notmuch_status { NOTMUCH_STATUS_NULL_POINTER, NOTMUCH_STATUS_TAG_TOO_LONG, NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW, + NOTMUCH_STATUS_UNBALANCED_ATOMIC, NOTMUCH_STATUS_LAST_STATUS } notmuch_status_t; /* Get a string representation of a notmuch_status_t value. * - * The result is readonly. + * The result is read-only. */ const char * notmuch_status_to_string (notmuch_status_t status); @@ -214,6 +218,42 @@ notmuch_database_upgrade (notmuch_database_t *database, double progress), void *closure); +/* Begin an atomic database operation. + * + * Any modifications performed between a successful begin and a + * notmuch_database_end_atomic will be applied to the database + * atomically. Note that, unlike a typical database transaction, this + * only ensures atomicity, not durability; neither begin nor end + * necessarily flush modifications to disk. + * + * Atomic sections may be nested. begin_atomic and end_atomic must + * always be called in pairs. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: Successfully entered atomic section. + * + * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred; + * atomic section not entered. + */ +notmuch_status_t +notmuch_database_begin_atomic (notmuch_database_t *notmuch); + +/* Indicate the end of an atomic database operation. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: Successfully completed atomic section. + * + * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred; + * atomic section not ended. + * + * NOTMUCH_STATUS_UNBALANCED_ATOMIC: The database is not currently in + * an atomic section. + */ +notmuch_status_t +notmuch_database_end_atomic (notmuch_database_t *notmuch); + /* Retrieve a directory object from the database for 'path'. * * Here, 'path' should be a path relative to the path of 'database' @@ -226,9 +266,10 @@ notmuch_directory_t * notmuch_database_get_directory (notmuch_database_t *database, const char *path); -/* Add a new message to the given notmuch database. +/* Add a new message to the given notmuch database or associate an + * additional filename with an existing message. * - * Here,'filename' should be a path relative to the path of + * Here, 'filename' should be a path relative to the path of * 'database' (see notmuch_database_get_path), or else should be an * absolute filename with initial components that match the path of * 'database'. @@ -238,6 +279,10 @@ notmuch_database_get_directory (notmuch_database_t *database, * notmuch database will reference the filename, and will not copy the * entire contents of the file. * + * If another message with the same message ID already exists in the + * database, rather than creating a new message, this adds 'filename' + * to the list of the filenames for the existing message. + * * If 'message' is not NULL, then, on successful return * (NOTMUCH_STATUS_SUCCESS or NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) '*message' * will be initialized to a message object that can be used for things @@ -255,7 +300,7 @@ notmuch_database_get_directory (notmuch_database_t *database, * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: Message has the same message * ID as another message already in the database. The new * filename was successfully added to the message in the database - * (if not already present). + * (if not already present) and the existing message is returned. * * NOTMUCH_STATUS_FILE_ERROR: an error occurred trying to open the * file, (such as permission denied, or file not found, @@ -272,14 +317,14 @@ notmuch_database_add_message (notmuch_database_t *database, const char *filename, notmuch_message_t **message); -/* Remove a message from the given notmuch database. +/* Remove a message filename from the given notmuch database. If the + * message has no more filenames, remove the message. * - * Note that only this particular filename association is removed from - * the database. If the same message (as determined by the message ID) - * is still available via other filenames, then the message will - * persist in the database for those filenames. When the last filename - * is removed for a particular message, the database content for that - * message will be entirely removed. + * If the same message (as determined by the message ID) is still + * available via other filenames, then the message will persist in the + * database for those filenames. When the last filename is removed for + * a particular message, the database content for that message will be + * entirely removed. * * Return value: * @@ -302,19 +347,57 @@ notmuch_database_remove_message (notmuch_database_t *database, /* Find a message with the given message_id. * - * If the database contains a message with the given message_id, then - * a new notmuch_message_t object is returned. The caller should call - * notmuch_message_destroy when done with the message. + * If a message with the given message_id is found then, on successful return + * (NOTMUCH_STATUS_SUCCESS) '*message' will be initialized to a message + * object. The caller should call notmuch_message_destroy when done with the + * message. + * + * On any failure or when the message is not found, this function initializes + * '*message' to NULL. This means, when NOTMUCH_STATUS_SUCCESS is returned, the + * caller is supposed to check '*message' for NULL to find out whether the + * message with the given message_id was found. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: Successful return, check '*message'. * - * This function returns NULL in the following situations: + * NOTMUCH_STATUS_NULL_POINTER: The given 'message' argument is NULL * - * * No message is found with the given message_id - * * An out-of-memory situation occurs - * * A Xapian exception occurs + * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory, creating message object + * + * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred */ -notmuch_message_t * +notmuch_status_t notmuch_database_find_message (notmuch_database_t *database, - const char *message_id); + const char *message_id, + notmuch_message_t **message); + +/* Find a message with the given filename. + * + * If the database contains a message with the given filename then, on + * successful return (NOTMUCH_STATUS_SUCCESS) '*message' will be initialized to + * a message object. The caller should call notmuch_message_destroy when done + * with the message. + * + * On any failure or when the message is not found, this function initializes + * '*message' to NULL. This means, when NOTMUCH_STATUS_SUCCESS is returned, the + * caller is supposed to check '*message' for NULL to find out whether the + * message with the given filename is found. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: Successful return, check '*message' + * + * NOTMUCH_STATUS_NULL_POINTER: The given 'message' argument is NULL + * + * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory, creating the message object + * + * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred + */ +notmuch_status_t +notmuch_database_find_message_by_filename (notmuch_database_t *notmuch, + const char *filename, + notmuch_message_t **message); /* Return a list of all tags found in the database. * @@ -510,7 +593,7 @@ notmuch_threads_move_to_next (notmuch_threads_t *threads); * * It's not strictly necessary to call this function. All memory from * the notmuch_threads_t object will be reclaimed when the - * containg query object is destroyed. + * containing query object is destroyed. */ void notmuch_threads_destroy (notmuch_threads_t *threads); @@ -526,6 +609,20 @@ notmuch_threads_destroy (notmuch_threads_t *threads); unsigned notmuch_query_count_messages (notmuch_query_t *query); +/* Return the number of threads matching a search. + * + * This function performs a search and returns the number of unique thread IDs + * in the matching messages. This is the same as number of threads matching a + * search. + * + * Note that this is a significantly heavier operation than + * notmuch_query_count_messages(). + * + * If an error occurs, this function may return 0. + */ +unsigned +notmuch_query_count_threads (notmuch_query_t *query); + /* Get the thread ID of 'thread'. * * The returned string belongs to 'thread' and as such, should not be diff --git a/lib/query.cc b/lib/query.cc index 6f02b04b..b6c0f12d 100644 --- a/lib/query.cc +++ b/lib/query.cc @@ -457,3 +457,47 @@ notmuch_query_count_messages (notmuch_query_t *query) return count; } + +unsigned +notmuch_query_count_threads (notmuch_query_t *query) +{ + notmuch_messages_t *messages; + GHashTable *hash; + unsigned int count; + notmuch_sort_t sort; + + sort = query->sort; + query->sort = NOTMUCH_SORT_UNSORTED; + messages = notmuch_query_search_messages (query); + query->sort = sort; + if (messages == NULL) + return 0; + + hash = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); + if (hash == NULL) { + talloc_free (messages); + return 0; + } + + while (notmuch_messages_valid (messages)) { + notmuch_message_t *message = notmuch_messages_get (messages); + const char *thread_id = notmuch_message_get_thread_id (message); + char *thread_id_copy = talloc_strdup (messages, thread_id); + if (unlikely (thread_id_copy == NULL)) { + notmuch_message_destroy (message); + count = 0; + goto DONE; + } + g_hash_table_insert (hash, thread_id_copy, NULL); + notmuch_message_destroy (message); + notmuch_messages_move_to_next (messages); + } + + count = g_hash_table_size (hash); + + DONE: + g_hash_table_unref (hash); + talloc_free (messages); + + return count; +} diff --git a/notmuch-client.h b/notmuch-client.h index 63be3374..b50cb38b 100644 --- a/notmuch-client.h +++ b/notmuch-client.h @@ -63,6 +63,7 @@ typedef struct notmuch_show_format { const char *header_start; void (*header) (const void *ctx, notmuch_message_t *message); + void (*header_message_part) (GMimeMessage *message); const char *header_end; const char *body_start; void (*part_start) (GMimeObject *part, diff --git a/notmuch-config.c b/notmuch-config.c index 6e4c5c4c..1a7ed580 100644 --- a/notmuch-config.c +++ b/notmuch-config.c @@ -69,7 +69,7 @@ static const char maildir_config_comment[] = "\tsynchronize_flags Valid values are true and false.\n" "\n" "\tIf true, then the following maildir flags (in message filenames)\n" - "\twill be syncrhonized with the corresponding notmuch tags:\n" + "\twill be synchronized with the corresponding notmuch tags:\n" "\n" "\t\tFlag Tag\n" "\t\t---- -------\n" @@ -735,6 +735,8 @@ notmuch_config_command_set (void *ctx, char *item, int argc, char *argv[]) int notmuch_config_command (void *ctx, int argc, char *argv[]) { + argc--; argv++; /* skip subcommand argument */ + if (argc < 2) { fprintf (stderr, "Error: notmuch config requires at least two arguments.\n"); return 1; diff --git a/notmuch-count.c b/notmuch-count.c index 39f08c6c..20ce3342 100644 --- a/notmuch-count.c +++ b/notmuch-count.c @@ -29,45 +29,26 @@ notmuch_count_command (void *ctx, int argc, char *argv[]) notmuch_query_t *query; char *query_str; int i; -#if 0 - char *opt, *end; - int i, first = 0, max_threads = -1; - notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST; -#endif + notmuch_bool_t output_messages = TRUE; + + argc--; argv++; /* skip subcommand argument */ for (i = 0; i < argc && argv[i][0] == '-'; i++) { if (strcmp (argv[i], "--") == 0) { i++; break; } -#if 0 - if (STRNCMP_LITERAL (argv[i], "--first=") == 0) { - opt = argv[i] + sizeof ("--first=") - 1; - first = strtoul (opt, &end, 10); - if (*opt == '\0' || *end != '\0') { - fprintf (stderr, "Invalid value for --first: %s\n", opt); - return 1; - } - } else if (STRNCMP_LITERAL (argv[i], "--max-threads=") == 0) { - opt = argv[i] + sizeof ("--max-threads=") - 1; - max_threads = strtoul (opt, &end, 10); - if (*opt == '\0' || *end != '\0') { - fprintf (stderr, "Invalid value for --max-threads: %s\n", opt); - return 1; - } - } else if (STRNCMP_LITERAL (argv[i], "--sort=") == 0) { - opt = argv[i] + sizeof ("--sort=") - 1; - if (strcmp (opt, "oldest-first") == 0) { - sort = NOTMUCH_SORT_OLDEST_FIRST; - } else if (strcmp (opt, "newest-first") == 0) { - sort = NOTMUCH_SORT_NEWEST_FIRST; + if (STRNCMP_LITERAL (argv[i], "--output=") == 0) { + const char *opt = argv[i] + sizeof ("--output=") - 1; + if (strcmp (opt, "threads") == 0) { + output_messages = FALSE; + } else if (strcmp (opt, "messages") == 0) { + output_messages = TRUE; } else { - fprintf (stderr, "Invalid value for --sort: %s\n", opt); + fprintf (stderr, "Invalid value for --output: %s\n", opt); return 1; } - } else -#endif - { + } else { fprintf (stderr, "Unrecognized option: %s\n", argv[i]); return 1; } @@ -101,7 +82,10 @@ notmuch_count_command (void *ctx, int argc, char *argv[]) return 1; } - printf ("%u\n", notmuch_query_count_messages(query)); + if (output_messages) + printf ("%u\n", notmuch_query_count_messages (query)); + else + printf ("%u\n", notmuch_query_count_threads (query)); notmuch_query_destroy (query); notmuch_database_close (notmuch); diff --git a/notmuch-dump.c b/notmuch-dump.c index 7e7bc177..126593d1 100644 --- a/notmuch-dump.c +++ b/notmuch-dump.c @@ -26,10 +26,11 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[]) notmuch_config_t *config; notmuch_database_t *notmuch; notmuch_query_t *query; - FILE *output; + FILE *output = stdout; notmuch_messages_t *messages; notmuch_message_t *message; notmuch_tags_t *tags; + const char* query_str = ""; config = notmuch_config_open (ctx, NULL, NULL); if (config == NULL) @@ -40,23 +41,39 @@ notmuch_dump_command (unused (void *ctx), int argc, char *argv[]) if (notmuch == NULL) return 1; - query = notmuch_query_create (notmuch, ""); - if (query == NULL) { - fprintf (stderr, "Out of memory\n"); - return 1; - } - notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID); + argc--; argv++; /* skip subcommand argument */ - if (argc) { + if (argc && strcmp (argv[0], "--") != 0) { + fprintf (stderr, "Warning: the output file argument of dump is deprecated.\n"); output = fopen (argv[0], "w"); if (output == NULL) { fprintf (stderr, "Error opening %s for writing: %s\n", argv[0], strerror (errno)); return 1; } - } else { - output = stdout; + argc--; + argv++; + } + + if (argc && strcmp (argv[0], "--") == 0){ + argc--; + argv++; + } + + if (argc) { + query_str = query_string_from_args (notmuch, argc, argv); + if (query_str == NULL) { + fprintf (stderr, "Out of memory.\n"); + return 1; + } } + + query = notmuch_query_create (notmuch, query_str); + if (query == NULL) { + fprintf (stderr, "Out of memory\n"); + return 1; + } + notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID); for (messages = notmuch_query_search_messages (query); notmuch_messages_valid (messages); diff --git a/notmuch-new.c b/notmuch-new.c index 744f4ca3..81a93500 100644 --- a/notmuch-new.c +++ b/notmuch-new.c @@ -24,6 +24,7 @@ typedef struct _filename_node { char *filename; + time_t mtime; struct _filename_node *next; } _filename_node_t; @@ -41,14 +42,14 @@ typedef struct { int total_files; int processed_files; - int added_messages; + int added_messages, removed_messages, renamed_messages; struct timeval tv_start; _filename_list_t *removed_files; _filename_list_t *removed_directories; + _filename_list_t *directory_mtimes; notmuch_bool_t synchronize_flags; - _filename_list_t *message_ids_to_sync; } add_files_state_t; static volatile sig_atomic_t do_print_progress = 0; @@ -86,7 +87,7 @@ _filename_list_create (const void *ctx) return list; } -static void +static _filename_node_t * _filename_list_add (_filename_list_t *list, const char *filename) { @@ -99,6 +100,8 @@ _filename_list_add (_filename_list_t *list, *(list->tail) = node; list->tail = &node->next; + + return node; } static void @@ -213,6 +216,7 @@ _entries_resemble_maildir (struct dirent **entries, int count) * information is lost from the database). * * o Tell the database to update its time of 'path' to 'fs_mtime' + * if fs_mtime isn't the current wall-clock time. */ static notmuch_status_t add_files_recursive (notmuch_database_t *notmuch, @@ -230,6 +234,7 @@ add_files_recursive (notmuch_database_t *notmuch, notmuch_directory_t *directory; notmuch_filenames_t *db_files = NULL; notmuch_filenames_t *db_subdirs = NULL; + time_t stat_time; struct stat st; notmuch_bool_t is_maildir, new_directory; const char **tag; @@ -239,6 +244,7 @@ add_files_recursive (notmuch_database_t *notmuch, path, strerror (errno)); return NOTMUCH_STATUS_FILE_ERROR; } + stat_time = time (NULL); /* This is not an error since we may have recursed based on a * symlink to a regular file, not a directory, and we don't know @@ -253,6 +259,25 @@ add_files_recursive (notmuch_database_t *notmuch, new_directory = db_mtime ? FALSE : TRUE; + /* XXX This is a temporary workaround. If we don't update the + * database mtime until after processing messages in this + * directory, then a 0 mtime is *not* sufficient to indicate that + * this directory has no messages or subdirs in the database (for + * example, if an earlier run skipped the mtime update because + * fs_mtime == stat_time, or was interrupted before updating the + * mtime at the end). To address this, we record a (bogus) + * non-zero value before processing any child messages so that a + * later run won't mistake this for a new directory (and, for + * example, fail to detect removed files and subdirs). + * + * A better solution would be for notmuch_database_get_directory + * to indicate if it really created a new directory or not, either + * by a new out-argument, or by recording this information and + * providing an accessor. + */ + if (new_directory) + notmuch_directory_set_mtime (directory, -1); + /* If the database knows about this directory, then we sort based * on strcmp to match the database sorting. Otherwise, we can do * inode-based sorting for faster filesystem operation. */ @@ -373,7 +398,7 @@ add_files_recursive (notmuch_database_t *notmuch, /* If we're looking at a symlink, we only want to add it if it * links to a regular file, (and not to a directory, say). * - * Similarly, if the file is of unknown type (due to filesytem + * Similarly, if the file is of unknown type (due to filesystem * limitations), then we also need to look closer. * * In either case, a stat does the trick. @@ -425,6 +450,12 @@ add_files_recursive (notmuch_database_t *notmuch, fflush (stdout); } + status = notmuch_database_begin_atomic (notmuch); + if (status) { + ret = status; + goto DONE; + } + status = notmuch_database_add_message (notmuch, next, &message); switch (status) { /* success */ @@ -439,11 +470,8 @@ add_files_recursive (notmuch_database_t *notmuch, break; /* Non-fatal issues (go on to next file) */ case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: - /* Defer sync of maildir flags until after old filenames - * are removed in the case of a rename. */ if (state->synchronize_flags == TRUE) - _filename_list_add (state->message_ids_to_sync, - notmuch_message_get_message_id (message)); + notmuch_message_maildir_flags_to_tags (message); break; case NOTMUCH_STATUS_FILE_NOT_EMAIL: fprintf (stderr, "Note: Ignoring non-mail file: %s\n", @@ -462,11 +490,18 @@ add_files_recursive (notmuch_database_t *notmuch, case NOTMUCH_STATUS_NULL_POINTER: case NOTMUCH_STATUS_TAG_TOO_LONG: case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: + case NOTMUCH_STATUS_UNBALANCED_ATOMIC: case NOTMUCH_STATUS_LAST_STATUS: INTERNAL_ERROR ("add_message returned unexpected value: %d", status); goto DONE; } + status = notmuch_database_end_atomic (notmuch); + if (status) { + ret = status; + goto DONE; + } + if (message) { notmuch_message_destroy (message); message = NULL; @@ -509,11 +544,13 @@ add_files_recursive (notmuch_database_t *notmuch, notmuch_filenames_move_to_next (db_subdirs); } - if (! interrupted) { - status = notmuch_directory_set_mtime (directory, fs_mtime); - if (status && ret == NOTMUCH_STATUS_SUCCESS) - ret = status; - } + /* If the directory's mtime is the same as the wall-clock time + * when we stat'ed the directory, we skip updating the mtime in + * the database because a message could be delivered later in this + * same second. This may lead to unnecessary re-scans, but it + * avoids overlooking messages. */ + if (fs_mtime != stat_time) + _filename_list_add (state->directory_mtimes, path)->mtime = fs_mtime; DONE: if (next) @@ -695,18 +732,42 @@ upgrade_print_progress (void *closure, fflush (stdout); } +/* Remove one message filename from the database. */ +static notmuch_status_t +remove_filename (notmuch_database_t *notmuch, + const char *path, + add_files_state_t *add_files_state) +{ + notmuch_status_t status; + notmuch_message_t *message; + status = notmuch_database_begin_atomic (notmuch); + if (status) + return status; + status = notmuch_database_find_message_by_filename (notmuch, path, &message); + if (status || message == NULL) + return status; + status = notmuch_database_remove_message (notmuch, path); + if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) { + add_files_state->renamed_messages++; + if (add_files_state->synchronize_flags == TRUE) + notmuch_message_maildir_flags_to_tags (message); + } else + add_files_state->removed_messages++; + notmuch_message_destroy (message); + notmuch_database_end_atomic (notmuch); + return status; +} + /* Recursively remove all filenames from the database referring to * 'path' (or to any of its children). */ static void _remove_directory (void *ctx, notmuch_database_t *notmuch, const char *path, - int *renamed_files, - int *removed_files) + add_files_state_t *add_files_state) { notmuch_directory_t *directory; notmuch_filenames_t *files, *subdirs; - notmuch_status_t status; char *absolute; directory = notmuch_database_get_directory (notmuch, path); @@ -717,11 +778,7 @@ _remove_directory (void *ctx, { absolute = talloc_asprintf (ctx, "%s/%s", path, notmuch_filenames_get (files)); - status = notmuch_database_remove_message (notmuch, absolute); - if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) - *renamed_files = *renamed_files + 1; - else - *removed_files = *removed_files + 1; + remove_filename (notmuch, absolute, add_files_state); talloc_free (absolute); } @@ -731,7 +788,7 @@ _remove_directory (void *ctx, { absolute = talloc_asprintf (ctx, "%s/%s", path, notmuch_filenames_get (subdirs)); - _remove_directory (ctx, notmuch, absolute, renamed_files, removed_files); + _remove_directory (ctx, notmuch, absolute, add_files_state); talloc_free (absolute); } @@ -752,14 +809,14 @@ notmuch_new_command (void *ctx, int argc, char *argv[]) char *dot_notmuch_path; struct sigaction action; _filename_node_t *f; - int renamed_files, removed_files; - notmuch_status_t status; int i; notmuch_bool_t timer_is_active = FALSE; add_files_state.verbose = 0; add_files_state.output_is_a_tty = isatty (fileno (stdout)); + argc--; argv++; /* skip subcommand argument */ + for (i = 0; i < argc && argv[i][0] == '-'; i++) { if (STRNCMP_LITERAL (argv[i], "--verbose") == 0) { add_files_state.verbose = 1; @@ -774,7 +831,6 @@ notmuch_new_command (void *ctx, int argc, char *argv[]) add_files_state.new_tags = notmuch_config_get_new_tags (config, &add_files_state.new_tags_length); add_files_state.synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config); - add_files_state.message_ids_to_sync = _filename_list_create (ctx); db_path = notmuch_config_get_database_path (config); dot_notmuch_path = talloc_asprintf (ctx, "%s/%s", db_path, ".notmuch"); @@ -825,10 +881,12 @@ notmuch_new_command (void *ctx, int argc, char *argv[]) add_files_state.processed_files = 0; add_files_state.added_messages = 0; + add_files_state.removed_messages = add_files_state.renamed_messages = 0; gettimeofday (&add_files_state.tv_start, NULL); add_files_state.removed_files = _filename_list_create (ctx); add_files_state.removed_directories = _filename_list_create (ctx); + add_files_state.directory_mtimes = _filename_list_create (ctx); if (! debugger_is_active () && add_files_state.output_is_a_tty && ! add_files_state.verbose) { @@ -838,27 +896,20 @@ notmuch_new_command (void *ctx, int argc, char *argv[]) ret = add_files (notmuch, db_path, &add_files_state); - removed_files = 0; - renamed_files = 0; gettimeofday (&tv_start, NULL); - for (f = add_files_state.removed_files->head; f; f = f->next) { - status = notmuch_database_remove_message (notmuch, f->filename); - if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) - renamed_files++; - else - removed_files++; + for (f = add_files_state.removed_files->head; f && !interrupted; f = f->next) { + remove_filename (notmuch, f->filename, &add_files_state); if (do_print_progress) { do_print_progress = 0; generic_print_progress ("Cleaned up", "messages", - tv_start, removed_files + renamed_files, + tv_start, add_files_state.removed_messages + add_files_state.renamed_messages, add_files_state.removed_files->count); } } gettimeofday (&tv_start, NULL); - for (f = add_files_state.removed_directories->head, i = 0; f; f = f->next, i++) { - _remove_directory (ctx, notmuch, f->filename, - &renamed_files, &removed_files); + for (f = add_files_state.removed_directories->head, i = 0; f && !interrupted; f = f->next, i++) { + _remove_directory (ctx, notmuch, f->filename, &add_files_state); if (do_print_progress) { do_print_progress = 0; generic_print_progress ("Cleaned up", "directories", @@ -867,34 +918,18 @@ notmuch_new_command (void *ctx, int argc, char *argv[]) } } - talloc_free (add_files_state.removed_files); - talloc_free (add_files_state.removed_directories); - - /* Now that removals are done (hence the database is aware of all - * renames), we can synchronize maildir_flags to tags for all - * messages that had new filenames appear on this run. */ - gettimeofday (&tv_start, NULL); - if (add_files_state.synchronize_flags) { - _filename_node_t *node; - notmuch_message_t *message; - for (node = add_files_state.message_ids_to_sync->head, i = 0; - node; - node = node->next, i++) - { - message = notmuch_database_find_message (notmuch, node->filename); - notmuch_message_maildir_flags_to_tags (message); - notmuch_message_destroy (message); - if (do_print_progress) { - do_print_progress = 0; - generic_print_progress ( - "Synchronized tags for", "messages", - tv_start, i, add_files_state.message_ids_to_sync->count); - } + for (f = add_files_state.directory_mtimes->head; f && !interrupted; f = f->next) { + notmuch_directory_t *directory; + directory = notmuch_database_get_directory (notmuch, f->filename); + if (directory) { + notmuch_directory_set_mtime (directory, f->mtime); + notmuch_directory_destroy (directory); } } - talloc_free (add_files_state.message_ids_to_sync); - add_files_state.message_ids_to_sync = NULL; + talloc_free (add_files_state.removed_files); + talloc_free (add_files_state.removed_directories); + talloc_free (add_files_state.directory_mtimes); if (timer_is_active) stop_progress_printing_timer (); @@ -925,16 +960,16 @@ notmuch_new_command (void *ctx, int argc, char *argv[]) printf ("No new mail."); } - if (removed_files) { + if (add_files_state.removed_messages) { printf (" Removed %d %s.", - removed_files, - removed_files == 1 ? "message" : "messages"); + add_files_state.removed_messages, + add_files_state.removed_messages == 1 ? "message" : "messages"); } - if (renamed_files) { + if (add_files_state.renamed_messages) { printf (" Detected %d file %s.", - renamed_files, - renamed_files == 1 ? "rename" : "renames"); + add_files_state.renamed_messages, + add_files_state.renamed_messages == 1 ? "rename" : "renames"); } printf ("\n"); diff --git a/notmuch-reply.c b/notmuch-reply.c index 27ef37bd..7ac879f9 100644 --- a/notmuch-reply.c +++ b/notmuch-reply.c @@ -25,12 +25,15 @@ #include "gmime-filter-headers.h" static void +reply_headers_message_part (GMimeMessage *message); + +static void reply_part_content (GMimeObject *part); static const notmuch_show_format_t format_reply = { "", "", NULL, - "", NULL, "", + "", NULL, reply_headers_message_part, ">\n", "", NULL, NULL, @@ -63,12 +66,44 @@ show_reply_headers (GMimeMessage *message) } static void +reply_headers_message_part (GMimeMessage *message) +{ + InternetAddressList *recipients; + const char *recipients_string; + + printf ("> From: %s\n", g_mime_message_get_sender (message)); + recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO); + recipients_string = internet_address_list_to_string (recipients, 0); + if (recipients_string) + printf ("> To: %s\n", + recipients_string); + recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC); + recipients_string = internet_address_list_to_string (recipients, 0); + if (recipients_string) + printf ("> Cc: %s\n", + recipients_string); + printf ("> Subject: %s\n", g_mime_message_get_subject (message)); + printf ("> Date: %s\n", g_mime_message_get_date_as_string (message)); +} + + +static void reply_part_content (GMimeObject *part) { GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part)); GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part); - if (g_mime_content_type_is_type (content_type, "text", "*") && + if (g_mime_content_type_is_type (content_type, "multipart", "*") || + g_mime_content_type_is_type (content_type, "message", "rfc822")) + { + /* Output nothing, since multipart subparts will be handled individually. */ + } + else if (g_mime_content_type_is_type (content_type, "application", "pgp-encrypted") || + g_mime_content_type_is_type (content_type, "application", "pgp-signature")) + { + /* Ignore PGP/MIME cruft parts */ + } + else if (g_mime_content_type_is_type (content_type, "text", "*") && !g_mime_content_type_is_type (content_type, "text", "html")) { GMimeStream *stream_stdout = NULL, *stream_filter = NULL; @@ -278,7 +313,7 @@ add_recipients_from_message (GMimeMessage *reply, * The munging is easy to detect, because it results in a * redundant reply-to header, (with an address that already exists * in either To or Cc). So in this case, we ignore the Reply-To - * field and use the From header. Thie ensures the original sender + * field and use the From header. This ensures the original sender * will get the reply even if not subscribed to the list. Note * that the address in the Reply-To header will always appear in * the reply. @@ -362,7 +397,7 @@ guess_from_received_header (notmuch_config_t *config, notmuch_message_t *message * them indications to which email address this message was * delivered. * The Received: header is special in our get_header function - * and is always concated. + * and is always concatenated. */ received = notmuch_message_get_header (message, "received"); if (received == NULL) @@ -593,6 +628,8 @@ notmuch_reply_command (void *ctx, int argc, char *argv[]) params.part = -1; params.cryptoctx = NULL; + argc--; argv++; /* skip subcommand argument */ + for (i = 0; i < argc && argv[i][0] == '-'; i++) { if (strcmp (argv[i], "--") == 0) { i++; diff --git a/notmuch-restore.c b/notmuch-restore.c index f095f64a..13b4325a 100644 --- a/notmuch-restore.c +++ b/notmuch-restore.c @@ -18,6 +18,8 @@ * Author: Carl Worth <cworth@cworth.org> */ +#include <getopt.h> + #include "notmuch-client.h" int @@ -26,7 +28,8 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[]) notmuch_config_t *config; notmuch_database_t *notmuch; notmuch_bool_t synchronize_flags; - FILE *input; + notmuch_bool_t accumulate = FALSE; + FILE *input = stdin; char *line = NULL; size_t line_size; ssize_t line_len; @@ -44,25 +47,51 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[]) synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config); - if (argc) { - input = fopen (argv[0], "r"); + struct option options[] = { + { "accumulate", no_argument, 0, 'a' }, + { 0, 0, 0, 0} + }; + + int opt; + do { + opt = getopt_long (argc, argv, "", options, NULL); + + switch (opt) { + case 'a': + accumulate = 1; + break; + case '?': + return 1; + break; + } + + } while (opt != -1); + + if (optind < argc) { + input = fopen (argv[optind], "r"); if (input == NULL) { fprintf (stderr, "Error opening %s for reading: %s\n", - argv[0], strerror (errno)); + argv[optind], strerror (errno)); return 1; } - } else { - printf ("No filename given. Reading dump from stdin.\n"); - input = stdin; + optind++; + } + + if (optind < argc) { + fprintf (stderr, + "Cannot read dump from more than one file: %s\n", + argv[optind]); + return 1; } /* Dump output is one line per message. We match a sequence of * non-space characters for the message-id, then one or more * spaces, then a list of space-separated tags as a sequence of * characters within literal '(' and ')'. */ - xregcomp (®ex, - "^([^ ]+) \\(([^)]*)\\)$", - REG_EXTENDED); + if ( xregcomp (®ex, + "^([^ ]+) \\(([^)]*)\\)$", + REG_EXTENDED) ) + INTERNAL_ERROR("compile time constant regex failed."); while ((line_len = getline (&line, &line_size, input)) != -1) { regmatch_t match[3]; @@ -87,10 +116,20 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[]) file_tags = xstrndup (line + match[2].rm_so, match[2].rm_eo - match[2].rm_so); - message = notmuch_database_find_message (notmuch, message_id); - if (message == NULL) { - fprintf (stderr, "Warning: Cannot apply tags to missing message: %s\n", - message_id); + status = notmuch_database_find_message (notmuch, message_id, &message); + if (status || message == NULL) { + fprintf (stderr, "Warning: Cannot apply tags to %smessage: %s\n", + message ? "" : "missing ", message_id); + if (status) + fprintf (stderr, "%s\n", + notmuch_status_to_string(status)); + goto NEXT_LINE; + } + + /* In order to detect missing messages, this check/optimization is + * intentionally done *after* first finding the message. */ + if (accumulate && (file_tags == NULL || *file_tags == '\0')) + { goto NEXT_LINE; } @@ -115,7 +154,9 @@ notmuch_restore_command (unused (void *ctx), int argc, char *argv[]) } notmuch_message_freeze (message); - notmuch_message_remove_all_tags (message); + + if (!accumulate) + notmuch_message_remove_all_tags (message); next = file_tags; while (next) { diff --git a/notmuch-search.c b/notmuch-search.c index faccaf7d..36686d19 100644 --- a/notmuch-search.c +++ b/notmuch-search.c @@ -194,13 +194,22 @@ static int do_search_threads (const search_format_t *format, notmuch_query_t *query, notmuch_sort_t sort, - output_t output) + output_t output, + int offset, + int limit) { notmuch_thread_t *thread; notmuch_threads_t *threads; notmuch_tags_t *tags; time_t date; int first_thread = 1; + int i; + + if (offset < 0) { + offset += notmuch_query_count_threads (query); + if (offset < 0) + offset = 0; + } threads = notmuch_query_search_threads (query); if (threads == NULL) @@ -208,17 +217,22 @@ do_search_threads (const search_format_t *format, fputs (format->results_start, stdout); - for (; - notmuch_threads_valid (threads); - notmuch_threads_move_to_next (threads)) + for (i = 0; + notmuch_threads_valid (threads) && (limit < 0 || i < offset + limit); + notmuch_threads_move_to_next (threads), i++) { int first_tag = 1; + thread = notmuch_threads_get (threads); + + if (i < offset) { + notmuch_thread_destroy (thread); + continue; + } + if (! first_thread) fputs (format->item_sep, stdout); - thread = notmuch_threads_get (threads); - if (output == OUTPUT_THREADS) { format->item_id (thread, "thread:", notmuch_thread_get_thread_id (thread)); @@ -271,12 +285,21 @@ do_search_threads (const search_format_t *format, static int do_search_messages (const search_format_t *format, notmuch_query_t *query, - output_t output) + output_t output, + int offset, + int limit) { notmuch_message_t *message; notmuch_messages_t *messages; notmuch_filenames_t *filenames; int first_message = 1; + int i; + + if (offset < 0) { + offset += notmuch_query_count_messages (query); + if (offset < 0) + offset = 0; + } messages = notmuch_query_search_messages (query); if (messages == NULL) @@ -284,10 +307,13 @@ do_search_messages (const search_format_t *format, fputs (format->results_start, stdout); - for (; - notmuch_messages_valid (messages); - notmuch_messages_move_to_next (messages)) + for (i = 0; + notmuch_messages_valid (messages) && (limit < 0 || i < offset + limit); + notmuch_messages_move_to_next (messages), i++) { + if (i < offset) + continue; + message = notmuch_messages_get (messages); if (output == OUTPUT_FILES) { @@ -394,6 +420,10 @@ notmuch_search_command (void *ctx, int argc, char *argv[]) const search_format_t *format = &format_text; int i, ret; output_t output = OUTPUT_SUMMARY; + int offset = 0; + int limit = -1; /* unlimited */ + + argc--; argv++; /* skip subcommand argument */ for (i = 0; i < argc && argv[i][0] == '-'; i++) { if (strcmp (argv[i], "--") == 0) { @@ -410,6 +440,22 @@ notmuch_search_command (void *ctx, int argc, char *argv[]) fprintf (stderr, "Invalid value for --sort: %s\n", opt); return 1; } + } else if (STRNCMP_LITERAL (argv[i], "--offset=") == 0) { + char *p; + opt = argv[i] + sizeof ("--offset=") - 1; + offset = strtol (opt, &p, 10); + if (*opt == '\0' || p == opt || *p != '\0') { + fprintf (stderr, "Invalid value for --offset: %s\n", opt); + return 1; + } + } else if (STRNCMP_LITERAL (argv[i], "--limit=") == 0) { + char *p; + opt = argv[i] + sizeof ("--limit=") - 1; + limit = strtoul (opt, &p, 10); + if (*opt == '\0' || p == opt || *p != '\0') { + fprintf (stderr, "Invalid value for --limit: %s\n", opt); + return 1; + } } else if (STRNCMP_LITERAL (argv[i], "--format=") == 0) { opt = argv[i] + sizeof ("--format=") - 1; if (strcmp (opt, "text") == 0) { @@ -476,11 +522,11 @@ notmuch_search_command (void *ctx, int argc, char *argv[]) default: case OUTPUT_SUMMARY: case OUTPUT_THREADS: - ret = do_search_threads (format, query, sort, output); + ret = do_search_threads (format, query, sort, output, offset, limit); break; case OUTPUT_MESSAGES: case OUTPUT_FILES: - ret = do_search_messages (format, query, output); + ret = do_search_messages (format, query, output, offset, limit); break; case OUTPUT_TAGS: ret = do_search_tags (notmuch, format, query); diff --git a/notmuch-show.c b/notmuch-show.c index dda83a10..603992a6 100644 --- a/notmuch-show.c +++ b/notmuch-show.c @@ -29,6 +29,9 @@ format_headers_text (const void *ctx, notmuch_message_t *message); static void +format_headers_message_part_text (GMimeMessage *message); + +static void format_part_start_text (GMimeObject *part, int *part_count); @@ -41,7 +44,7 @@ format_part_end_text (GMimeObject *part); static const notmuch_show_format_t format_text = { "", "\fmessage{ ", format_message_text, - "\fheader{\n", format_headers_text, "\fheader}\n", + "\fheader{\n", format_headers_text, format_headers_message_part_text, "\fheader}\n", "\fbody{\n", format_part_start_text, NULL, @@ -63,6 +66,9 @@ format_headers_json (const void *ctx, notmuch_message_t *message); static void +format_headers_message_part_json (GMimeMessage *message); + +static void format_part_start_json (unused (GMimeObject *part), int *part_count); @@ -81,7 +87,7 @@ format_part_end_json (GMimeObject *part); static const notmuch_show_format_t format_json = { "[", "{", format_message_json, - ", \"headers\": {", format_headers_json, "}", + "\"headers\": {", format_headers_json, format_headers_message_part_json, "}", ", \"body\": [", format_part_start_json, format_part_encstatus_json, @@ -102,7 +108,7 @@ format_message_mbox (const void *ctx, static const notmuch_show_format_t format_mbox = { "", "", format_message_mbox, - "", NULL, "", + "", NULL, NULL, "", "", NULL, NULL, @@ -121,7 +127,7 @@ format_part_content_raw (GMimeObject *part); static const notmuch_show_format_t format_raw = { "", "", NULL, - "", NULL, "", + "", NULL, format_headers_message_part_text, "\n", "", NULL, NULL, @@ -216,7 +222,7 @@ format_message_json (const void *ctx, notmuch_message_t *message, unused (int in json_quote_str (ctx_quote, notmuch_tags_get (tags))); first = 0; } - printf("]"); + printf("], "); talloc_free (ctx_quote); } @@ -326,6 +332,7 @@ format_message_mbox (const void *ctx, fclose (file); } + static void format_headers_text (const void *ctx, notmuch_message_t *message) { @@ -346,6 +353,27 @@ format_headers_text (const void *ctx, notmuch_message_t *message) } static void +format_headers_message_part_text (GMimeMessage *message) +{ + InternetAddressList *recipients; + const char *recipients_string; + + printf ("From: %s\n", g_mime_message_get_sender (message)); + recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO); + recipients_string = internet_address_list_to_string (recipients, 0); + if (recipients_string) + printf ("To: %s\n", + recipients_string); + recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC); + recipients_string = internet_address_list_to_string (recipients, 0); + if (recipients_string) + printf ("Cc: %s\n", + recipients_string); + printf ("Subject: %s\n", g_mime_message_get_subject (message)); + printf ("Date: %s\n", g_mime_message_get_date_as_string (message)); +} + +static void format_headers_json (const void *ctx, notmuch_message_t *message) { const char *headers[] = { @@ -374,6 +402,40 @@ format_headers_json (const void *ctx, notmuch_message_t *message) talloc_free (ctx_quote); } +static void +format_headers_message_part_json (GMimeMessage *message) +{ + void *ctx = talloc_new (NULL); + void *ctx_quote = talloc_new (ctx); + InternetAddressList *recipients; + const char *recipients_string; + + printf ("%s: %s", + json_quote_str (ctx_quote, "From"), + json_quote_str (ctx_quote, g_mime_message_get_sender (message))); + recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO); + recipients_string = internet_address_list_to_string (recipients, 0); + if (recipients_string) + printf (", %s: %s", + json_quote_str (ctx_quote, "To"), + json_quote_str (ctx_quote, recipients_string)); + recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC); + recipients_string = internet_address_list_to_string (recipients, 0); + if (recipients_string) + printf (", %s: %s", + json_quote_str (ctx_quote, "Cc"), + json_quote_str (ctx_quote, recipients_string)); + printf (", %s: %s", + json_quote_str (ctx_quote, "Subject"), + json_quote_str (ctx_quote, g_mime_message_get_subject (message))); + printf (", %s: %s", + json_quote_str (ctx_quote, "Date"), + json_quote_str (ctx_quote, g_mime_message_get_date_as_string (message))); + + talloc_free (ctx_quote); + talloc_free (ctx); +} + /* Write a MIME text part out to the given stream. * * Both line-ending conversion (CRLF->LF) and charset conversion ( -> @@ -454,19 +516,21 @@ format_part_start_text (GMimeObject *part, int *part_count) static void format_part_content_text (GMimeObject *part) { - GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part); + const char *cid = g_mime_object_get_content_id (part); GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part)); - printf (", Content-type: %s\n", g_mime_content_type_to_string (content_type)); - - if (disposition && - strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0) + if (GMIME_IS_PART (part)) { const char *filename = g_mime_part_get_filename (GMIME_PART (part)); - printf ("Attachment: %s (%s)\n", filename, - g_mime_content_type_to_string (content_type)); + if (filename) + printf (", Filename: %s", filename); } + if (cid) + printf (", Content-id: %s", cid); + + printf (", Content-type: %s\n", g_mime_content_type_to_string (content_type)); + if (g_mime_content_type_is_type (content_type, "text", "*") && !g_mime_content_type_is_type (content_type, "text", "html")) { @@ -591,7 +655,6 @@ format_part_content_json (GMimeObject *part) GMimeStream *stream_memory = g_mime_stream_mem_new (); const char *cid = g_mime_object_get_content_id (part); void *ctx = talloc_new (NULL); - GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (part); GByteArray *part_content; printf (", \"content-type\": %s", @@ -600,12 +663,11 @@ format_part_content_json (GMimeObject *part) if (cid != NULL) printf(", \"content-id\": %s", json_quote_str (ctx, cid)); - if (disposition && - strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0) + if (GMIME_IS_PART (part)) { const char *filename = g_mime_part_get_filename (GMIME_PART (part)); - - printf (", \"filename\": %s", json_quote_str (ctx, filename)); + if (filename) + printf (", \"filename\": %s", json_quote_str (ctx, filename)); } if (g_mime_content_type_is_type (content_type, "text", "*") && @@ -616,11 +678,14 @@ format_part_content_json (GMimeObject *part) printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len)); } - else if (g_mime_content_type_is_type (content_type, "multipart", "*") || - g_mime_content_type_is_type (content_type, "message", "rfc822")) + else if (g_mime_content_type_is_type (content_type, "multipart", "*")) { printf (", \"content\": ["); } + else if (g_mime_content_type_is_type (content_type, "message", "rfc822")) + { + printf (", \"content\": [{"); + } talloc_free (ctx); if (stream_memory) @@ -630,13 +695,12 @@ format_part_content_json (GMimeObject *part) static void format_part_end_json (GMimeObject *part) { - GMimeContentType *content_type; - - content_type = g_mime_object_get_content_type (GMIME_OBJECT (part)); + GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part)); - if (g_mime_content_type_is_type (content_type, "multipart", "*") || - g_mime_content_type_is_type (content_type, "message", "rfc822")) + if (g_mime_content_type_is_type (content_type, "multipart", "*")) printf ("]"); + else if (g_mime_content_type_is_type (content_type, "message", "rfc822")) + printf ("}]"); printf ("}"); } @@ -644,6 +708,9 @@ format_part_end_json (GMimeObject *part) static void format_part_content_raw (GMimeObject *part) { + if (! GMIME_IS_PART (part)) + return; + GMimeStream *stream_stdout; GMimeStream *stream_filter = NULL; GMimeDataWrapper *wrapper; @@ -869,6 +936,8 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[])) params.cryptoctx = NULL; params.decrypt = 0; + argc--; argv++; /* skip subcommand argument */ + for (i = 0; i < argc && argv[i][0] == '-'; i++) { if (strcmp (argv[i], "--") == 0) { i++; diff --git a/notmuch-tag.c b/notmuch-tag.c index 6204ae3c..dded39ea 100644 --- a/notmuch-tag.c +++ b/notmuch-tag.c @@ -65,6 +65,8 @@ notmuch_tag_command (void *ctx, unused (int argc), unused (char *argv[])) return 1; } + argc--; argv++; /* skip subcommand argument */ + for (i = 0; i < argc; i++) { if (strcmp (argv[i], "--") == 0) { i++; @@ -16,7 +16,7 @@ .\" along with this program. If not, see http://www.gnu.org/licenses/ . .\" .\" Author: Carl Worth <cworth@cworth.org> -.TH NOTMUCH 1 2009-10-31 "Notmuch 0.1" +.TH NOTMUCH 1 2011-12-04 "Notmuch 0.10.2" .SH NAME notmuch \- thread-based email index, search, and tagging .SH SYNOPSIS @@ -214,11 +214,26 @@ when sorting by .B newest\-first the threads will be sorted by the newest message in each thread. -.RE -.RS 4 By default, results will be displayed in reverse chronological order, (that is, the newest results will be displayed first). +.RE + +.RS 4 +.TP 4 +.BR \-\-offset=[\-]N + +Skip displaying the first N results. With the leading '\-', start at the Nth +result from the end. +.RE + +.RS 4 +.TP 4 +.BR \-\-limit=N + +Limit the number of displayed results to N. +.RE +.RS 4 See the .B "SEARCH SYNTAX" section below for details of the supported syntax for <search-terms>. @@ -357,14 +372,35 @@ section below for details of the supported syntax for <search-terms>. .RE .RS 4 .TP 4 -.BR count " <search-term>..." +.BR count " [options...] <search-term>..." Count messages matching the search terms. -The number of matching messages is output to stdout. +The number of matching messages (or threads) is output to stdout. + +With no search terms, a count of all messages (or threads) in the database will +be displayed. + +Supported options for +.B count +include +.RS 4 +.TP 4 +.B \-\-output=(messages|threads) + +.RS 4 +.TP 4 +.B messages + +Output the number of matching messages. This is the default. +.RE +.RS 4 +.TP 4 +.B threads -With no search terms, a count of all messages in the database will be -displayed. +Output the number of matching threads. +.RE +.RE .RE .RE @@ -454,25 +490,38 @@ section below for details of the supported syntax for <search-terms>. The .BR dump " and " restore commands can be used to create a textual dump of email tags for backup -purposes, and to restore from that dump +purposes, and to restore from that dump. .RS 4 .TP 4 -.BR dump " [<filename>]" +.BR dump " [<filename>] [--] [<search-terms>]" Creates a plain-text dump of the tags of each message. -The output is to the given filename, if any, or to stdout. +Output is to the given filename, if any, or to stdout. Note that +using the filename argument is deprecated. These tags are the only data in the notmuch database that can't be recreated from the messages themselves. The output of notmuch dump is therefore the only critical thing to backup (and much more friendly to incremental backup than the native database files.) + +With no search terms, a dump of all messages in the database will be +generated. A "--" argument instructs notmuch that the +remaining arguments are search terms. + +See the +.B "SEARCH SYNTAX" +section below for details of the supported syntax for <search-terms>. +.RE + .TP -.BR restore " <filename>" +.BR restore " [--accumulate] [<filename>]" Restores the tags from the given file (see -.BR "notmuch dump" "." +.BR "notmuch dump" ")." + +The input is read from the given filename, if any, or from stdin. Note: The dump file format is specifically chosen to be compatible with the format of files produced by sup-dump. @@ -480,11 +529,15 @@ So if you've previously been using sup for mail, then the .B "notmuch restore" command provides you a way to import all of your tags (or labels as sup calls them). + +The --accumulate switch causes the union of the existing and new tags to be +applied, instead of replacing each message's tags as they are read in from the +dump file. .RE The .B part -command can used to output a single part of a multi-part MIME message. +command can used to output a single part of a multipart MIME message. .RS 4 .TP 4 @@ -641,13 +694,13 @@ expression). Finally, results can be restricted to only messages within a particular time range, (based on the Date: header) with a syntax of: - <intial-timestamp>..<final-timestamp> + <initial-timestamp>..<final-timestamp> Each timestamp is a number representing the number of seconds since 1970\-01\-01 00:00:00 UTC. This is not the most convenient means of expressing date ranges, but until notmuch is fixed to accept a more convenient form, one can use the date program to construct -timestamps. For example, with the bash shell the folowing syntax would +timestamps. For example, with the bash shell the following syntax would specify a date range to return messages from 2009\-10\-01 until the current time: @@ -48,7 +48,7 @@ static int notmuch_help_command (void *ctx, int argc, char *argv[]); static const char search_terms_help[] = - "\tSeveral notmuch commands accept a comman syntax for search\n" + "\tSeveral notmuch commands accept a common syntax for search\n" "\tterms.\n" "\n" "\tThe search terms can consist of free-form text (and quoted\n" @@ -222,6 +222,15 @@ static command_t commands[] = { "\t\t(oldest-first) or reverse chronological order\n" "\t\t(newest-first), which is the default.\n" "\n" + "\t--offset=[-]N\n" + "\n" + "\t\tSkip displaying the first N results. With the leading '-',\n" + "\t\tstart at the Nth result from the end.\n" + "\n" + "\t--limit=N\n" + "\n" + "\t\tLimit the number of displayed results to N.\n" + "\n" "\tSee \"notmuch help search-terms\" for details of the search\n" "\tterms syntax." }, { "show", notmuch_show_command, @@ -319,12 +328,24 @@ static command_t commands[] = { "\tSee \"notmuch help search-terms\" for details of the search\n" "\tterms syntax." }, { "count", notmuch_count_command, - "<search-terms> [...]", + "[options...] <search-terms> [...]", "Count messages matching the search terms.", - "\tThe number of matching messages is output to stdout.\n" + "\tThe number of matching messages (or threads) is output to stdout.\n" + "\n" + "\tWith no search terms, a count of all messages (or threads) in\n" + "\tthe database will be displayed.\n" + "\n" + "\tSupported options for count include:\n" + "\n" + "\t--output=(messages|threads)\n" "\n" - "\tWith no search terms, a count of all messages in the database\n" - "\twill be displayed.\n" + "\t\tmessages (default)\n" + "\n" + "\t\tOutput the number of matching messages.\n" + "\n" + "\t\tthreads\n" + "\n" + "\t\tOutput the number of matching threads.\n" "\n" "\tSee \"notmuch help search-terms\" for details of the search\n" "\tterms syntax." }, @@ -375,22 +396,35 @@ static command_t commands[] = { "\tSee \"notmuch help search-terms\" for details of the search\n" "\tterms syntax." }, { "dump", notmuch_dump_command, - "[<filename>]", + "[<filename>] [--] [<search-terms>]", "Create a plain-text dump of the tags for each message.", "\tOutput is to the given filename, if any, or to stdout.\n" + "\tNote that using the filename argument is deprecated.\n" + "\n" "\tThese tags are the only data in the notmuch database\n" "\tthat can't be recreated from the messages themselves.\n" "\tThe output of notmuch dump is therefore the only\n" "\tcritical thing to backup (and much more friendly to\n" - "\tincremental backup than the native database files.)" }, + "\tincremental backup than the native database files.)\n" + "\n" + "\tWith no search terms, a dump of all messages in the\n" + "\tdatabase will be generated. A \"--\" argument instructs\n" + "\tnotmuch that the remaining arguments are search terms.\n" + "\n" + "\tSee \"notmuch help search-terms\" for the search-term syntax.\n" + }, { "restore", notmuch_restore_command, - "<filename>", + "[--accumulate] [<filename>]", "Restore the tags from the given dump file (see 'dump').", + "\tInput is read from the given filename, if any, or from stdin.\n" "\tNote: The dump file format is specifically chosen to be\n" "\tcompatible with the format of files produced by sup-dump.\n" "\tSo if you've previously been using sup for mail, then the\n" "\t\"notmuch restore\" command provides you a way to import\n" - "\tall of your tags (or labels as sup calls them)." }, + "\tall of your tags (or labels as sup calls them).\n" + "\tThe --accumulate switch causes the union of the existing and new\n" + "\ttags to be applied, instead of replacing each message's tags as\n" + "\tthey are read in from the dump file."}, { "config", notmuch_config_command, "[get|set] <section>.<item> [value ...]", "Get or set settings in the notmuch configuration file.", @@ -456,6 +490,8 @@ notmuch_help_command (unused (void *ctx), int argc, char *argv[]) command_t *command; unsigned int i; + argc--; argv++; /* Ignore "help" */ + if (argc == 0) { printf ("The notmuch mail system.\n\n"); usage (stdout); @@ -579,12 +615,13 @@ main (int argc, char *argv[]) local = talloc_new (NULL); g_mime_init (0); + g_type_init (); if (argc == 1) return notmuch (local); if (STRNCMP_LITERAL (argv[1], "--help") == 0) - return notmuch_help_command (NULL, 0, NULL); + return notmuch_help_command (NULL, argc - 1, &argv[1]); if (STRNCMP_LITERAL (argv[1], "--version") == 0) { printf ("notmuch " STRINGIFY(NOTMUCH_VERSION) "\n"); @@ -629,7 +666,7 @@ main (int argc, char *argv[]) command = &commands[i]; if (strcmp (argv[1], command->name) == 0) - return (command->function) (local, argc - 2, &argv[2]); + return (command->function) (local, argc - 1, &argv[1]); } fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n", diff --git a/packaging/debian b/packaging/debian index a5866601..c8e8ddd4 100644 --- a/packaging/debian +++ b/packaging/debian @@ -1,2 +1,2 @@ -The debian packaging exists in the top-level "debian" directory within -this source-code respository. +The Debian packaging exists in the top-level "debian" directory within +this source-code repository. diff --git a/show-message.c b/show-message.c index 7a4bbc25..d83f04ec 100644 --- a/show-message.c +++ b/show-message.c @@ -149,9 +149,21 @@ show_message_part (GMimeObject *part, if (selected) state->in_zone = 1; + if (selected || (!selected && state->in_zone)) { + fputs (format->header_start, stdout); + if (format->header_message_part) + format->header_message_part (mime_message); + fputs (format->header_end, stdout); + + fputs (format->body_start, stdout); + } + show_message_part (g_mime_message_get_mime_part (mime_message), state, format, params, TRUE); + if (selected || (!selected && state->in_zone)) + fputs (format->body_end, stdout); + if (selected) state->in_zone = 0; } diff --git a/test/Makefile b/test/Makefile index b6859eac..de492a7c 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,4 +1,4 @@ -# See Makfefile.local for the list of files to be compiled in this +# See Makefile.local for the list of files to be compiled in this # directory. all: $(MAKE) -C .. all diff --git a/test/README b/test/README index 07da480b..2481f16d 100644 --- a/test/README +++ b/test/README @@ -41,6 +41,15 @@ The following command-line options are available when running tests: As the names depend on the tests' file names, it is safe to run the tests with this option in parallel. +--root=<dir>:: + This runs the testsuites specified under a seperate directory. + However, caution is advised, as not all tests are maintained + with this relocation in mind, so some tests may behave + differently. + + Pointing this argument at a tmpfs filesystem can improve the + speed of the test suite for some users. + When invoking the test suite via "make test" any of the above options can be specified as follows: @@ -123,20 +132,19 @@ library for your script to use. <script>. If it yields success, test is considered successful. <message> should state what it is testing. - test_expect_failure <message> <script> - - This is NOT the opposite of test_expect_success, but is used - to mark a test that demonstrates a known breakage. Unlike - the usual test_expect_success tests, which say "ok" on - success and "FAIL" on failure, this will say "FIXED" on - success and "still broken" on failure. Failures from these - tests won't cause -i (immediate) to stop. - test_begin_subtest <message> Set the test description message for a subsequent test_expect_equal invocation (see below). + test_subtest_known_broken + + Mark the current test as broken. Such tests are expected to fail. + Unlike the normal tests, which say "PASS" on success and "FAIL" on + failure, these will say "FIXED" on success and "BROKEN" on failure. + Failures from these tests won't cause -i (immediate) to stop. A + test must call this before any test_expect_* function. + test_expect_equal <output> <expected> This is an often-used convenience function built on top of @@ -147,12 +155,12 @@ library for your script to use. will generate a failure and print the difference of the two strings. - test_expect_equal_failure <output> <expected> + test_expect_equal_file <output> <expected> - This works similar to test_expect_equal (see above) but is used to - mark a test that demonstrates a known breakage, (that is, the - author of the test expectes "output" and "expected" to differ until - the breakage is fixed). See test_expect_failure for details. + Identical to test_exepect_equal, except that <output> and + <expected> are files instead of strings. This is a much more + robust method to compare formatted textual information, since it + also notices whitespace and closing newline differences. test_debug <script> @@ -165,9 +173,13 @@ library for your script to use. This function executes the provided emacs lisp script within emacs. The script can be a sequence of emacs lisp expressions, - (that is, they will be evaluated within a progn form). The lisp - expressions can call `message' to generate output on stdout to be - examined by the calling test script. + (that is, they will be evaluated within a progn form). Emacs + stdout and stderr is not available, the common way to get output + is to save it to a file. There are some auxiliary functions + useful in emacs tests provided in test-lib.el. Do not use `setq' + for setting variables in Emacs tests because it affects other + tests that may run in the same Emacs instance. Use `let' instead + so the scope of the changed variables is limited to a single test. test_done @@ -181,7 +193,7 @@ writing tests: generate_message Generates a message with an optional template. Most tests will - actually prefere to call add_message. See below. + actually prefer to call add_message. See below. add_message @@ -197,6 +209,6 @@ writing tests: This function should be called at the beginning of a test file when a test needs to operate on a non-empty body of messages. It - will intialize the mail database to a known state of 50 sample + will initialize the mail database to a known state of 50 sample messages, (culled from the early history of the notmuch mailing list). diff --git a/test/atomicity b/test/atomicity new file mode 100755 index 00000000..ad7d4a3c --- /dev/null +++ b/test/atomicity @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +test_description='atomicity' +. ./test-lib.sh + +# This script tests the effects of killing and restarting "notmuch +# new" at arbitrary points. If notmuch new is properly atomic, the +# final database contents should be the same regardless of when (or +# if) it is killed and restarted. + +if which gdb 1>/dev/null 2>&1; then + test_set_prereq GDB + +# Create a maildir structure to also stress flag synchronization + mkdir $MAIL_DIR/cur + mkdir $MAIL_DIR/new + mkdir $MAIL_DIR/tmp + mkdir $MAIL_DIR/.remove-dir + + # Prepare the initial database + generate_message [subject]='Duplicate' [filename]='duplicate:2,' [dir]=cur + generate_message [subject]='Remove' [filename]='remove:2,' [dir]=cur + generate_message [subject]='"Remove duplicate"' [filename]='remove-duplicate:2,' [dir]=cur + cp $MAIL_DIR/cur/remove-duplicate:2, $MAIL_DIR/cur/remove-duplicate-copy:2, + generate_message [subject]='Rename' [filename]='rename:2,' [dir]=cur + generate_message [subject]='"Rename duplicate"' [filename]='rename-duplicate:2,' [dir]=cur + generate_message [subject]='"Move 1"' [filename]='move1:2,' [dir]=cur + generate_message [subject]='"Move 2"' [filename]='move2:2,' [dir]=new + generate_message [subject]='Flag' [filename]='flag:2,' [dir]=cur + generate_message [subject]='"Flag duplicate"' [filename]='flag-duplicate:2,' [dir]=cur + cp $MAIL_DIR/cur/flag-duplicate:2, $MAIL_DIR/cur/flag-duplicate-copy:2,F + generate_message [subject]='"Remove directory"' [filename]='remove-directory:2,' [dir]=.remove-dir + generate_message [subject]='"Remove directory duplicate"' [filename]='remove-directory-duplicate:2,' [dir]=.remove-dir + cp $MAIL_DIR/.remove-dir/remove-directory-duplicate:2, $MAIL_DIR/cur/ + notmuch new > /dev/null + + # Make all maildir changes, but *don't* update the database + generate_message [subject]='Added' [filename]='added:2,' [dir]=cur + cp $MAIL_DIR/cur/duplicate:2, $MAIL_DIR/cur/duplicate-copy:2, + generate_message [subject]='"Add duplicate"' [filename]='add-duplicate:2,' [dir]=cur + generate_message [subject]='"Add duplicate copy"' [filename]='add-duplicate-copy:2,' [dir]=cur + rm $MAIL_DIR/cur/remove:2, + rm $MAIL_DIR/cur/remove-duplicate-copy:2, + mv $MAIL_DIR/cur/rename:2, $MAIL_DIR/cur/renamed:2, + mv $MAIL_DIR/cur/rename-duplicate:2, $MAIL_DIR/cur/renamed-duplicate:2, + mv $MAIL_DIR/cur/move1:2, $MAIL_DIR/new/move1:2, + mv $MAIL_DIR/new/move2:2, $MAIL_DIR/cur/move2:2, + mv $MAIL_DIR/cur/flag:2, $MAIL_DIR/cur/flag:2,F + rm $MAIL_DIR/cur/flag-duplicate-copy:2,F + rm $MAIL_DIR/.remove-dir/remove-directory:2, + rm $MAIL_DIR/.remove-dir/remove-directory-duplicate:2, + rmdir $MAIL_DIR/.remove-dir + + # Prepare a snapshot of the updated maildir. The gdb script will + # update the database in this snapshot as it goes. + cp -ra $MAIL_DIR $MAIL_DIR.snap + cp ${NOTMUCH_CONFIG} ${NOTMUCH_CONFIG}.snap + NOTMUCH_CONFIG=${NOTMUCH_CONFIG}.snap notmuch config set database.path $MAIL_DIR.snap + + + + # Execute notmuch new and, at every call to rename, snapshot the + # database, run notmuch new again on the snapshot, and capture the + # results of search. + # + # -tty /dev/null works around a conflict between the 'timeout' wrapper + # and gdb's attempt to control the TTY. + export MAIL_DIR + gdb -tty /dev/null -batch -x $TEST_DIRECTORY/atomicity.gdb notmuch >/dev/null 2>/dev/null + + # Get the final, golden output + notmuch search '*' > expected + + # Check output against golden output + outcount=$(cat outcount) + echo -n > searchall + echo -n > expectall + for ((i = 0; i < $outcount; i++)); do + if ! cmp -s search.$i expected; then + # Find the range of interruptions that match this output + for ((end = $i + 1 ; end < $outcount; end++)); do + if ! cmp -s search.$i search.$end; then + break + fi + done + echo "When interrupted after $test/backtrace.$(expr $i - 1) (abort points $i-$(expr $end - 1))" >> searchall + cat search.$i >> searchall + cat expected >> expectall + echo >> searchall + echo >> expectall + + i=$(expr $end - 1) + fi + done +else + say_color info "%-6s" "WARNING" + echo " Missing test prerequisite GDB" +fi + +test_begin_subtest '"notmuch new" is idempotent under arbitrary aborts' +test_expect_equal_file GDB searchall expectall + +test_expect_success GDB "detected $outcount>10 abort points" "test $outcount -gt 10" + +test_done diff --git a/test/atomicity.gdb b/test/atomicity.gdb new file mode 100644 index 00000000..fd675257 --- /dev/null +++ b/test/atomicity.gdb @@ -0,0 +1,50 @@ +# This gdb script runs notmuch new and simulates killing and +# restarting notmuch new after every Xapian commit. To simulate this +# more efficiently, this script runs notmuch new and, immediately +# after every Xapian commit, it *pauses* the running notmuch new, +# copies the entire database and maildir to a snapshot directory, and +# executes a full notmuch new on that snapshot, comparing the final +# results with the expected output. It can then resume the paused +# notmuch new, which is still running on the original maildir, and +# repeat this process. + +set args new + +# Make Xapian commit after every operation instead of batching +set environment XAPIAN_FLUSH_THRESHOLD = 1 + +# gdb can't keep track of a simple integer. This is me weeping. +shell echo 0 > outcount + +shell touch inodes + +break rename +commands +# As an optimization, only consider snapshots after a Xapian commit. +# Xapian overwrites record.base? as the last step in the commit. +shell echo > gdbcmd +shell stat -c %i $MAIL_DIR/.notmuch/xapian/record.base* > inodes.new +shell if cmp inodes inodes.new; then echo cont > gdbcmd; fi +shell mv inodes.new inodes +source gdbcmd + +# Save a backtrace in case the test does fail +set logging file backtrace +set logging on +backtrace +set logging off +shell mv backtrace backtrace.`cat outcount` + +# Snapshot the database +shell rm -r $MAIL_DIR.snap/.notmuch +shell cp -r $MAIL_DIR/.notmuch $MAIL_DIR.snap/.notmuch +# Restore the mtime of $MAIL_DIR.snap, which we just changed +shell touch -r $MAIL_DIR $MAIL_DIR.snap +# Run notmuch new to completion on the snapshot +shell NOTMUCH_CONFIG=${NOTMUCH_CONFIG}.snap XAPIAN_FLUSH_THRESHOLD=1000 notmuch new > /dev/null +shell NOTMUCH_CONFIG=${NOTMUCH_CONFIG}.snap notmuch search '*' > search.`cat outcount` 2>&1 +shell echo $(expr $(cat outcount) + 1) > outcount +cont +end + +run @@ -51,31 +51,32 @@ test_expect_code 2 'failure to clean up causes the test to fail' ' # Ensure that all tests are being run test_begin_subtest 'Ensure that all available tests will be run by notmuch-test' -eval $(sed -n -e '/^TESTS="$/,/^"$/p' notmuch-test ../notmuch-test) +eval $(sed -n -e '/^TESTS="$/,/^"$/p' notmuch-test $TEST_DIRECTORY/notmuch-test) tests_in_suite=$(for i in $TESTS; do echo $i; done | sort) -available=$(ls -1 ../ | \ +available=$(ls -1 $TEST_DIRECTORY/ | \ sed -r -e "/^(aggregate-results.sh|Makefile|Makefile.local|notmuch-test)/d" \ -e "/^(README|test-lib.sh|test-lib.el|test-results|tmp.*|valgrind|corpus*)/d" \ -e "/^(emacs.expected-output|smtp-dummy|smtp-dummy.c|test-verbose|symbol-test.cc)/d" \ -e "/^(test.expected-output|.*~)/d" \ -e "/^(gnupg-secret-key.asc)/d" \ -e "/^(gnupg-secret-key.NOTE)/d" \ + -e "/^(atomicity.gdb)/d" \ | sort) test_expect_equal "$tests_in_suite" "$available" -EXPECTED=../test.expected-output +EXPECTED=$TEST_DIRECTORY/test.expected-output suppress_diff_date() { sed -e 's/\(.*\-\-\- test-verbose\.4\.\expected\).*/\1/' \ -e 's/\(.*\+\+\+ test-verbose\.4\.\output\).*/\1/' } test_begin_subtest "Ensure that test output is suppressed unless the test fails" -output=$(cd ..; ./test-verbose 2>&1 | suppress_diff_date) +output=$(cd $TEST_DIRECTORY; ./test-verbose 2>&1 | suppress_diff_date) expected=$(cat $EXPECTED/test-verbose-no | suppress_diff_date) test_expect_equal "$output" "$expected" test_begin_subtest "Ensure that -v does not suppress test output" -output=$(cd ..; ./test-verbose -v 2>&1 | suppress_diff_date) +output=$(cd $TEST_DIRECTORY; ./test-verbose -v 2>&1 | suppress_diff_date) expected=$(cat $EXPECTED/test-verbose-yes | suppress_diff_date) # Do not include the results of test-verbose in totals rm $TEST_DIRECTORY/test-results/test-verbose-* diff --git a/test/corpus/cur/51:2, b/test/corpus/cur/51:2, new file mode 100644 index 00000000..f522f69e --- /dev/null +++ b/test/corpus/cur/51:2, @@ -0,0 +1,12 @@ +From: "Aron Griffis" <agriffis@n01se.net> +To: notmuch@notmuchmail.org +Date: Tue, 17 Nov 2009 18:21:38 -0500 +Subject: [notmuch] archive +Message-ID: <20091117232137.GA7669@griffis1.net> + +Just subscribed, I'd like to catch up on the previous postings, +but the archive link seems to be bogus? + +Thanks, +Aron + diff --git a/test/count b/test/count new file mode 100755 index 00000000..300b1714 --- /dev/null +++ b/test/count @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +test_description='"notmuch count" for messages and threads' +. ./test-lib.sh + +add_email_corpus + +SEARCH="\"*\"" + +test_begin_subtest "message count is the default for notmuch count" +test_expect_equal \ + "`notmuch search --output=messages ${SEARCH} | wc -l`" \ + "`notmuch count ${SEARCH}`" + +test_begin_subtest "message count with --output=messages" +test_expect_equal \ + "`notmuch search --output=messages ${SEARCH} | wc -l`" \ + "`notmuch count --output=messages ${SEARCH}`" + +test_begin_subtest "thread count with --output=threads" +test_expect_equal \ + "`notmuch search --output=threads ${SEARCH} | wc -l`" \ + "`notmuch count --output=threads ${SEARCH}`" + +test_begin_subtest "thread count is the default for notmuch search" +test_expect_equal \ + "`notmuch search ${SEARCH} | wc -l`" \ + "`notmuch count --output=threads ${SEARCH}`" + +SEARCH="from:cworth and not from:cworth" +test_begin_subtest "count with no matching messages" +test_expect_equal \ + "0" \ + "`notmuch count --output=messages ${SEARCH}`" + +test_begin_subtest "count with no matching threads" +test_expect_equal \ + "0" \ + "`notmuch count --output=threads ${SEARCH}`" + +test_done diff --git a/test/crypto b/test/crypto index 8e920167..0af4aa8a 100755 --- a/test/crypto +++ b/test/crypto @@ -12,7 +12,7 @@ add_gnupg_home () local output [ -d ${GNUPGHOME} ] && return mkdir -m 0700 "$GNUPGHOME" - gpg --no-tty --import <../gnupg-secret-key.asc >"$GNUPGHOME"/import.log 2>&1 + gpg --no-tty --import <$TEST_DIRECTORY/gnupg-secret-key.asc >"$GNUPGHOME"/import.log 2>&1 test_debug "cat $GNUPGHOME/import.log" if (gpg --quick-random --version >/dev/null 2>&1) ; then echo quick-random >> "$GNUPGHOME"/gpg.conf @@ -168,8 +168,7 @@ Non-text part: application/pgp-encrypted part{ ID: 4, Content-type: text/plain This is a test encrypted message. part} -attachment{ ID: 5, Content-type: application/octet-stream -Attachment: TESTATTACHMENT (application/octet-stream) +attachment{ ID: 5, Filename: TESTATTACHMENT, Content-type: application/octet-stream Non-text part: application/octet-stream attachment} part} @@ -310,15 +309,13 @@ expected='From: Notmuch Test Suite <test_suite@notmuchmail.org> Subject: Re: test encrypted message 002 On 01 Jan 2000 12:00:00 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote: -Non-text part: multipart/encrypted -Non-text part: application/pgp-encrypted > This is another test encrypted message.' test_expect_equal \ "$output" \ "$expected" test_begin_subtest "signature verification with revoked key" -# generate revokation certificate and load it to revoke key +# generate revocation certificate and load it to revoke key echo "y 1 Notmuch Test Suite key revocation (automated) $(date '+%F_%T%z') diff --git a/test/dump-restore b/test/dump-restore index a4de3706..439e9980 100755 --- a/test/dump-restore +++ b/test/dump-restore @@ -4,21 +4,82 @@ test_description="\"notmuch dump\" and \"notmuch restore\"" add_email_corpus -test_expect_success "Dumping all tags" "generate_message && -notmuch new && -notmuch dump dump.expected" +test_expect_success 'Dumping all tags' \ + 'generate_message && + notmuch new && + notmuch dump > dump.expected' -test_begin_subtest "Clearing all tags" -sed -e "s/(\([^(]*\))$/()/" < dump.expected > clear.expected -notmuch restore clear.expected -notmuch dump clear.actual -test_expect_equal "$(< clear.actual)" "$(< clear.expected)" +# The use of from:cworth is rather arbitrary: it matches some of the +# email corpus' messages, but not all of them. -test_begin_subtest "Restoring original tags" -notmuch restore dump.expected -notmuch dump dump.actual -test_expect_equal "$(< dump.actual)" "$(< dump.expected)" +test_expect_success 'Dumping all tags II' \ + 'notmuch tag +ABC +DEF -- from:cworth && + notmuch dump > dump-ABC_DEF.expected && + ! cmp dump.expected dump-ABC_DEF.expected' -test_expect_success "Restore with nothing to do" "notmuch restore dump.expected" +test_expect_success 'Clearing all tags' \ + 'sed -e "s/(\([^(]*\))$/()/" < dump.expected > clear.expected && + notmuch restore clear.expected && + notmuch dump > clear.actual && + test_cmp clear.expected clear.actual' + +test_expect_success 'Accumulate original tags' \ + 'notmuch tag +ABC +DEF -- from:cworth && + notmuch restore --accumulate < dump.expected && + notmuch dump > dump.actual && + test_cmp dump-ABC_DEF.expected dump.actual' + +test_expect_success 'Restoring original tags' \ + 'notmuch restore dump.expected && + notmuch dump > dump.actual && + test_cmp dump.expected dump.actual' + +test_expect_success 'Restore with nothing to do' \ + 'notmuch restore < dump.expected && + notmuch dump > dump.actual && + test_cmp dump.expected dump.actual' + +test_expect_success 'Restore with nothing to do, II' \ + 'notmuch restore --accumulate dump.expected && + notmuch dump > dump.actual && + test_cmp dump.expected dump.actual' + +test_expect_success 'Restore with nothing to do, III' \ + 'notmuch restore --accumulate < clear.expected && + notmuch dump > dump.actual && + test_cmp dump.expected dump.actual' + +# notmuch restore currently only considers the first argument. +test_expect_success 'Invalid restore invocation' \ + 'test_must_fail notmuch restore dump.expected another_one' + +test_begin_subtest "dump outfile" +notmuch dump dump-outfile.actual +test_expect_equal_file dump.expected dump-outfile.actual + +test_begin_subtest "dump outfile # deprecated" +test_expect_equal "Warning: the output file argument of dump is deprecated."\ + "$(notmuch dump /dev/null 2>&1)" + +test_begin_subtest "dump outfile --" +notmuch dump dump-1-arg-dash.actual -- +test_expect_equal_file dump.expected dump-1-arg-dash.actual + +# Note, we assume all messages from cworth have a message-id +# containing cworth.org + +grep 'cworth[.]org' dump.expected > dump-cworth.expected + +test_begin_subtest "dump -- from:cworth" +notmuch dump -- from:cworth > dump-dash-cworth.actual +test_expect_equal_file dump-cworth.expected dump-dash-cworth.actual + +test_begin_subtest "dump outfile from:cworth" +notmuch dump dump-outfile-cworth.actual from:cworth +test_expect_equal_file dump-cworth.expected dump-outfile-cworth.actual + +test_begin_subtest "dump outfile -- from:cworth" +notmuch dump dump-outfile-dash-inbox.actual -- from:cworth +test_expect_equal_file dump-cworth.expected dump-outfile-dash-inbox.actual test_done @@ -1,91 +1,130 @@ #!/usr/bin/env bash + test_description="emacs interface" . test-lib.sh -EXPECTED=../emacs.expected-output +EXPECTED=$TEST_DIRECTORY/emacs.expected-output add_email_corpus test_begin_subtest "Basic notmuch-hello view in emacs" -test_emacs '(notmuch-hello) (princ (buffer-string))' >OUTPUT +test_emacs '(notmuch-hello) + (test-output)' test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello test_begin_subtest "Saved search with 0 results" -test_emacs '(setq notmuch-show-empty-saved-searches t) (setq notmuch-saved-searches '\''(("inbox" . "tag:inbox") ("unread" . "tag:unread") ("empty" . "tag:doesnotexist"))) (notmuch-hello) (princ (buffer-string))' >OUTPUT +test_emacs '(let ((notmuch-show-empty-saved-searches t) + (notmuch-saved-searches + '\''(("inbox" . "tag:inbox") + ("unread" . "tag:unread") + ("empty" . "tag:doesnotexist")))) + (notmuch-hello) + (test-output))' test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-with-empty test_begin_subtest "No saved searches displayed (all with 0 results)" -test_emacs '(setq notmuch-saved-searches '\''(("empty" . "tag:doesnotexist"))) (notmuch-hello) (princ (buffer-string))' >OUTPUT +test_emacs '(let ((notmuch-saved-searches + '\''(("empty" . "tag:doesnotexist")))) + (notmuch-hello) + (test-output))' test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-no-saved-searches test_begin_subtest "Basic notmuch-search view in emacs" -test_emacs '(notmuch-search "tag:inbox") (notmuch-test-wait) (princ (buffer-string))' >OUTPUT +test_emacs '(notmuch-search "tag:inbox") + (notmuch-test-wait) + (test-output)' test_expect_equal_file OUTPUT $EXPECTED/notmuch-search-tag-inbox test_begin_subtest "Navigation of notmuch-hello to search results" -test_emacs '(notmuch-hello) (goto-char (point-min)) (re-search-forward "inbox") (widget-button-press (point)) (notmuch-test-wait) (princ (buffer-string))' >OUTPUT +test_emacs '(notmuch-hello) + (goto-char (point-min)) + (re-search-forward "inbox") + (widget-button-press (point)) + (notmuch-test-wait) + (test-output)' test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-view-inbox test_begin_subtest "Basic notmuch-show view in emacs" maildir_storage_thread=$(notmuch search --output=threads id:20091117190054.GU3165@dottiness.seas.harvard.edu) -test_emacs "(notmuch-show \"$maildir_storage_thread\") (princ (buffer-string))" >OUTPUT +test_emacs "(notmuch-show \"$maildir_storage_thread\") + (test-output)" test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-thread-maildir-storage test_begin_subtest "notmuch-show for message with invalid From" -add_message "[subject]=\"message-with-invalid-from\"" "[from]=\"\\\"Invalid \\\" From\\\" <test_suite@notmuchmail.org>\"" +add_message "[subject]=\"message-with-invalid-from\"" \ + "[from]=\"\\\"Invalid \\\" From\\\" <test_suite@notmuchmail.org>\"" thread=$(notmuch search --output=threads subject:message-with-invalid-from) -output=$(test_emacs "(notmuch-show \"$thread\") (princ (buffer-string))") -test_expect_equal "$output" \ -'"Invalid " From" <test_suite@notmuchmail.org> (2001-01-05) (inbox) +test_emacs "(notmuch-show \"$thread\") + (test-output)" +cat <<EOF >EXPECTED +"Invalid " From" <test_suite@notmuchmail.org> (2001-01-05) (inbox) Subject: message-with-invalid-from To: Notmuch Test Suite <test_suite@notmuchmail.org> Date: Tue, 05 Jan 2001 15:43:57 -0000 -This is just a test message (#1)' +This is just a test message (#1) +EOF +test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "Navigation of notmuch-search to thread view" -test_emacs '(notmuch-search "tag:inbox") (notmuch-test-wait) (goto-char (point-min)) (re-search-forward "Working with Maildir") (notmuch-search-show-thread) (notmuch-test-wait) (princ (buffer-string))' >OUTPUT +test_emacs '(notmuch-search "tag:inbox") + (notmuch-test-wait) + (goto-char (point-min)) + (re-search-forward "Working with Maildir") + (notmuch-search-show-thread) + (notmuch-test-wait) + (test-output)' test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-thread-maildir-storage test_begin_subtest "Add tag from search view" os_x_darwin_thread=$(notmuch search --output=threads id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com) -test_emacs "(notmuch-search \"$os_x_darwin_thread\") (notmuch-test-wait) (notmuch-search-add-tag \"tag-from-search-view\")" +test_emacs "(notmuch-search \"$os_x_darwin_thread\") + (notmuch-test-wait) + (notmuch-search-add-tag \"tag-from-search-view\")" output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag-from-search-view unread)" test_begin_subtest "Remove tag from search view" -test_emacs "(notmuch-search \"$os_x_darwin_thread\") (notmuch-test-wait) (notmuch-search-remove-tag \"tag-from-search-view\")" +test_emacs "(notmuch-search \"$os_x_darwin_thread\") + (notmuch-test-wait) + (notmuch-search-remove-tag \"tag-from-search-view\")" output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)" test_begin_subtest "Add tag from notmuch-show view" -test_emacs "(notmuch-show \"$os_x_darwin_thread\") (notmuch-show-add-tag \"tag-from-show-view\")" +test_emacs "(notmuch-show \"$os_x_darwin_thread\") + (notmuch-show-add-tag \"tag-from-show-view\")" output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox tag-from-show-view unread)" test_begin_subtest "Remove tag from notmuch-show view" -test_emacs "(notmuch-show \"$os_x_darwin_thread\") (notmuch-show-remove-tag \"tag-from-show-view\")" +test_emacs "(notmuch-show \"$os_x_darwin_thread\") + (notmuch-show-remove-tag \"tag-from-show-view\")" output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)" test_begin_subtest "Message with .. in Message-Id:" add_message [id]=123..456@example '[subject]="Message with .. in Message-Id"' -test_emacs '(notmuch-search "id:\"123..456@example\"") (notmuch-test-wait) (notmuch-search-add-tag "search-add") (notmuch-search-add-tag "search-remove") (notmuch-search-remove-tag "search-remove") (notmuch-show "id:\"123..456@example\"") (notmuch-test-wait) (notmuch-show-add-tag "show-add") (notmuch-show-add-tag "show-remove") (notmuch-show-remove-tag "show-remove")' +test_emacs '(notmuch-search "id:\"123..456@example\"") + (notmuch-test-wait) + (notmuch-search-add-tag "search-add") + (notmuch-search-add-tag "search-remove") + (notmuch-search-remove-tag "search-remove") + (notmuch-show "id:\"123..456@example\"") + (notmuch-test-wait) + (notmuch-show-add-tag "show-add") + (notmuch-show-add-tag "show-remove") + (notmuch-show-remove-tag "show-remove")' output=$(notmuch search 'id:"123..456@example"' | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Message with .. in Message-Id (inbox search-add show-add)" test_begin_subtest "Sending a message via (fake) SMTP" - -# Before we can send a message, we have to prepare the FCC maildir -mkdir -p mail/sent/cur -mkdir -p mail/sent/new -mkdir -p mail/sent/tmp - -../smtp-dummy sent_message & -smtp_dummy_pid=$! -test_emacs "(setq message-send-mail-function 'message-smtpmail-send-it) (setq smtpmail-smtp-server \"localhost\") (setq smtpmail-smtp-service \"25025\") (notmuch-hello) (notmuch-mua-mail) (message-goto-to) (insert \"user@example.com\nDate: Fri, 29 Mar 1974 10:00:00 -0000\") (message-goto-subject) (insert \"Testing message sent via SMTP\") (message-goto-body) (insert \"This is a test that messages are sent via SMTP\") (message-send-and-exit)" >/dev/null 2>&1 -wait ${smtp_dummy_pid} - +emacs_deliver_message \ + 'Testing message sent via SMTP' \ + 'This is a test that messages are sent via SMTP' \ + '(message-goto-to) + (kill-whole-line) + (insert "To: user@example.com\n")' sed \ -e s',^User-Agent: Notmuch/.* Emacs/.*,User-Agent: Notmuch/XXX Emacs/XXX,' \ -e s',^Message-ID: <.*>$,Message-ID: <XXX>,' < sent_message >OUTPUT @@ -93,7 +132,7 @@ cat <<EOF >EXPECTED From: Notmuch Test Suite <test_suite@notmuchmail.org> To: user@example.com Subject: Testing message sent via SMTP -Date: Fri, 29 Mar 1974 10:00:00 -0000 +Date: 01 Jan 2000 12:00:00 -0000 User-Agent: Notmuch/XXX Emacs/XXX Message-ID: <XXX> MIME-Version: 1.0 @@ -106,10 +145,12 @@ test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "Verify that sent messages are saved/searchable (via FCC)" notmuch new > /dev/null output=$(notmuch search 'subject:"testing message sent via SMTP"' | notmuch_search_sanitize) -test_expect_equal "$output" "thread:XXX 1974-03-29 [1/1] Notmuch Test Suite; Testing message sent via SMTP (inbox)" +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; Testing message sent via SMTP (inbox)" test_begin_subtest "notmuch-fcc-dirs set to nil" -test_emacs "(setq notmuch-fcc-dirs nil) (notmuch-mua-mail) (princ (buffer-string))" > OUTPUT +test_emacs "(let ((notmuch-fcc-dirs nil)) + (notmuch-mua-mail) + (test-output))" cat <<EOF >EXPECTED From: Notmuch Test Suite <test_suite@notmuchmail.org> To: @@ -124,7 +165,9 @@ mkdir -p mail/sent-string/new mkdir -p mail/sent-string/tmp test_begin_subtest "notmuch-fcc-dirs set to a string" -test_emacs "(setq notmuch-fcc-dirs \"sent-string\") (notmuch-mua-mail) (princ (buffer-string))" > OUTPUT +test_emacs "(let ((notmuch-fcc-dirs \"sent-string\")) + (notmuch-mua-mail) + (test-output))" cat <<EOF >EXPECTED From: Notmuch Test Suite <test_suite@notmuchmail.org> To: @@ -143,7 +186,11 @@ mkdir -p mail/failure/new mkdir -p mail/failure/tmp test_begin_subtest "notmuch-fcc-dirs set to a list (with match)" -test_emacs "(setq notmuch-fcc-dirs '((\"notmuchmail.org\" . \"sent-list-match\") (\".*\" . \"failure\"))) (notmuch-mua-mail) (princ (buffer-string))" > OUTPUT +test_emacs "(let ((notmuch-fcc-dirs + '((\"notmuchmail.org\" . \"sent-list-match\") + (\".*\" . \"failure\")))) + (notmuch-mua-mail) + (test-output))" cat <<EOF >EXPECTED From: Notmuch Test Suite <test_suite@notmuchmail.org> To: @@ -159,7 +206,11 @@ mkdir -p mail/sent-list-catch-all/new mkdir -p mail/sent-list-catch-all/tmp test_begin_subtest "notmuch-fcc-dirs set to a list (catch-all)" -test_emacs "(setq notmuch-fcc-dirs '((\"example.com\" . \"failure\") (\".*\" . \"sent-list-catch-all\"))) (notmuch-mua-mail) (princ (buffer-string))" > OUTPUT +test_emacs "(let ((notmuch-fcc-dirs + '((\"example.com\" . \"failure\") + (\".*\" . \"sent-list-catch-all\")))) + (notmuch-mua-mail) + (test-output))" cat <<EOF >EXPECTED From: Notmuch Test Suite <test_suite@notmuchmail.org> To: @@ -170,7 +221,11 @@ EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "notmuch-fcc-dirs set to a list (no match)" -test_emacs "(setq notmuch-fcc-dirs '((\"example.com\" . \"failure\") (\"nomatchhere.net\" . \"failure\"))) (notmuch-mua-mail) (princ (buffer-string))" > OUTPUT +test_emacs "(let ((notmuch-fcc-dirs + '((\"example.com\" . \"failure\") + (\"nomatchhere.net\" . \"failure\")))) + (notmuch-mua-mail) + (test-output))" cat <<EOF >EXPECTED From: Notmuch Test Suite <test_suite@notmuchmail.org> To: @@ -180,9 +235,11 @@ EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "Reply within emacs" -# We sed away everything before the ^From in the output to avoid getting -# confused by messages such as "Parsing /home/cworth/.mailrc... done" -test_emacs '(notmuch-search "subject:\"testing message sent via SMTP\"") (notmuch-test-wait) (notmuch-search-reply-to-thread) (princ (buffer-string))' | sed -ne '/^From/,$ p' | sed -e 's/^In-Reply-To: <.*>$/In-Reply-To: <XXX>/' >OUTPUT +test_emacs '(notmuch-search "subject:\"testing message sent via SMTP\"") + (notmuch-test-wait) + (notmuch-search-reply-to-thread) + (test-output)' +sed -i -e 's/^In-Reply-To: <.*>$/In-Reply-To: <XXX>/' OUTPUT cat <<EOF >EXPECTED From: Notmuch Test Suite <test_suite@notmuchmail.org> To: user@example.com @@ -190,24 +247,28 @@ Subject: Re: Testing message sent via SMTP In-Reply-To: <XXX> Fcc: $(pwd)/mail/sent --text follows this line-- -On Fri, 29 Mar 1974 10:00:00 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote: +On 01 Jan 2000 12:00:00 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote: > This is a test that messages are sent via SMTP EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "Save attachment from within emacs using notmuch-show-save-attachments" # save as archive to test that Emacs does not re-compress .gz -echo ./attachment1.gz | test_emacs '(notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com") (notmuch-show-save-attachments)' > /dev/null 2>&1 -test_expect_equal_file "$EXPECTED/attachment" attachment1.gz +test_emacs '(let ((standard-input "\"attachment1.gz\"")) + (notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com") + (notmuch-show-save-attachments))' +test_expect_equal_file attachment1.gz "$EXPECTED/attachment" test_begin_subtest "Save attachment from within emacs using notmuch-show-save-part" # save as archive to test that Emacs does not re-compress .gz -echo ./attachment2.gz | test_emacs '(notmuch-show-save-part "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com" 5)' > /dev/null 2>&1 -test_expect_equal_file "$EXPECTED/attachment" attachment2.gz +test_emacs '(let ((standard-input "\"attachment2.gz\"")) + (notmuch-show-save-part "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com" 5))' +test_expect_equal_file attachment2.gz "$EXPECTED/attachment" test_begin_subtest "View raw message within emacs" -first_line=$(head -n1 $EXPECTED/raw-message-cf0c4d-52ad0a) -test_emacs '(notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com") (notmuch-show-view-raw-message) (princ (buffer-string))' | sed -ne "/$first_line/,\$ p" >OUTPUT +test_emacs '(notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com") + (notmuch-show-view-raw-message) + (test-output)' test_expect_equal_file OUTPUT $EXPECTED/raw-message-cf0c4d-52ad0a test_begin_subtest "Hiding/showing signature in notmuch-show view" @@ -217,7 +278,7 @@ test_emacs "(notmuch-show \"$maildir_storage_thread\") (button-activate (button-at (point))) (search-backward \"Click/Enter to hide.\") (button-activate (button-at (point))) - (princ (buffer-string))" >OUTPUT + (test-output)" test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-thread-maildir-storage test_begin_subtest "Detection and hiding of top-post quoting of message" @@ -245,7 +306,7 @@ Q: Why is top-posting such a bad thing? A: Top-posting. Q: What is the most annoying thing in e-mail?"' test_emacs "(notmuch-show \"top-posting\") - (princ (visible-buffer-string))" >OUTPUT + (test-visible-output)" echo "Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox) Subject: The problem with top-posting To: Notmuch Test Suite <test_suite@notmuchmail.org> @@ -268,19 +329,116 @@ Thanks for the advice! I will be sure to put it to good use. test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "Hiding message in notmuch-show view" -output=$(test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com") - (notmuch-show-toggle-message) - (princ (visible-buffer-string))') -expected=$(cat $EXPECTED/notmuch-show-thread-with-hidden-messages) -test_expect_equal "$output" "$expected" +test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com") + (notmuch-show-toggle-message) + (test-visible-output)' +test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-thread-with-hidden-messages test_begin_subtest "Hiding message with visible citation in notmuch-show view" -output=$(test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com") - (search-forward "Click/Enter to show.") - (button-activate (button-at (point))) - (notmuch-show-toggle-message) - (princ (visible-buffer-string))') -expected=$(cat $EXPECTED/notmuch-show-thread-with-hidden-messages) -test_expect_equal "$output" "$expected" +test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com") + (search-forward "Click/Enter to show.") + (button-activate (button-at (point))) + (notmuch-show-toggle-message) + (test-visible-output)' +test_expect_equal_file OUTPUT $EXPECTED/notmuch-show-thread-with-hidden-messages + +test_begin_subtest "Stashing in notmuch-show" +add_message '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' \ + '[from]="Some One <someone@somewhere.org>"' \ + '[to]="Some One Else <notsomeone@somewhere.org>"' \ + '[cc]="Notmuch <notmuch@notmuchmail.org>"' \ + '[subject]="Stash my stashables"' \ + '[id]="bought"' \ + '[body]="Unable to stash body. Where did you get it in the first place?!?"' +notmuch tag +stashtest id:${gen_msg_id} +test_emacs '(notmuch-show "id:\"bought\"") + (notmuch-show-stash-date) + (notmuch-show-stash-from) + (notmuch-show-stash-to) + (notmuch-show-stash-cc) + (notmuch-show-stash-subject) + (notmuch-show-stash-message-id) + (notmuch-show-stash-message-id-stripped) + (notmuch-show-stash-tags) + (notmuch-show-stash-filename) + (switch-to-buffer + (generate-new-buffer "*test-stashing*")) + (dotimes (i 9) + (yank) + (insert "\n") + (rotate-yank-pointer 1)) + (reverse-region (point-min) (point-max)) + (test-output)' +sed -i -e 's/^.*tmp.emacs\/mail.*$/FILENAME/' OUTPUT +test_expect_equal_file OUTPUT $EXPECTED/emacs-stashing + +test_begin_subtest "Stashing in notmuch-search" +test_emacs '(notmuch-search "id:\"bought\"") + (notmuch-test-wait) + (notmuch-search-stash-thread-id) + (switch-to-buffer + (generate-new-buffer "*test-stashing*")) + (yank) + (test-output)' +sed -i -e 's/^thread:.*$/thread:XXX/' OUTPUT +test_expect_equal $(cat OUTPUT) "thread:XXX" + +test_begin_subtest 'Hiding message following HTML part' +test_subtest_known_broken +id='html-message@notmuchmail.org' +emacs_deliver_message \ + 'HTML message' \ + '' \ + "(message-goto-eoh) + (insert \"Message-ID: <$id>\n\") + (message-goto-body) + (mml-insert-part \"text/html\") + (insert \"<body>This is a test HTML message</body>\")" +emacs_deliver_message \ + 'Reply to HTML message' \ + 'This is a reply to the test HTML message' \ + "(message-goto-eoh) + (insert \"In-Reply-To: <$id>\n\")" +test_emacs "(notmuch-show \"id:$id\") \ + (notmuch-show-next-message) \ + (command-execute (key-binding (kbd \"RET\"))) \ + (test-visible-output)" +test_emacs "(notmuch-show \"id:$id\") \ + (notmuch-show-next-message) \ + (notmuch-show-toggle-message) \ + (test-visible-output \"EXPECTED\")" +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest 'notmuch-show-advance-and-archive with invisible signature' +message1='id:20091118010116.GC25380@dottiness.seas.harvard.edu' +message2='id:1258491078-29658-1-git-send-email-dottedmag@dottedmag.net' +test_emacs "(notmuch-search \"$message1 or $message2\") + (notmuch-test-wait) + (notmuch-search-show-thread) + (goto-char (point-max)) + (redisplay) + (notmuch-show-advance-and-archive) + (test-output)" +test_emacs "(notmuch-show \"$message2\") + (test-output \"EXPECTED\")" +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "Refresh show buffer" +test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com") + (test-visible-output "EXPECTED") + (notmuch-show-refresh-view) + (test-visible-output)' +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "Refresh modified show buffer" +test_subtest_known_broken +test_emacs '(notmuch-show "id:f35dbb950911171438k5df6eb56k77b6c0944e2e79ae@mail.gmail.com") + (notmuch-show-toggle-message) + (notmuch-show-next-message) + (notmuch-show-toggle-message) + (test-visible-output "EXPECTED") + (notmuch-show-refresh-view) + (test-visible-output)' +test_expect_equal_file OUTPUT EXPECTED test_done diff --git a/test/emacs-large-search-buffer b/test/emacs-large-search-buffer index c78ce334..6095e9da 100755 --- a/test/emacs-large-search-buffer +++ b/test/emacs-large-search-buffer @@ -19,16 +19,25 @@ done notmuch new > /dev/null test_begin_subtest "Ensure that emacs doesn't drop results" -expected="$(notmuch search '*' | sed -e 's/^thread:[0-9a-f]* //' -e 's/;//' -e 's/xx*/[BLOB]/') -End of search results." +notmuch search '*' > EXPEXTED +sed -i -e 's/^thread:[0-9a-f]* //' -e 's/;//' -e 's/xx*/[BLOB]/' EXPEXTED +echo 'End of search results.' >> EXPEXTED -output=$(test_emacs '(notmuch-search "*") (notmuch-test-wait) (princ (buffer-string))' | sed -e s', *, ,g' -e 's/xxx*/[BLOB]/g') -test_expect_equal "$output" "$expected" +test_emacs '(notmuch-search "*") + (notmuch-test-wait) + (test-output)' +sed -i -e s', *, ,g' -e 's/xxx*/[BLOB]/g' OUTPUT +test_expect_equal_file OUTPUT EXPEXTED test_begin_subtest "Ensure that emacs doesn't drop error messages" -output=$(test_emacs '(notmuch-search "--this-option-does-not-exist") (notmuch-test-wait) (princ (buffer-string))') -test_expect_equal "$output" "Error: Unexpected output from notmuch search: +test_emacs '(notmuch-search "--this-option-does-not-exist") + (notmuch-test-wait) + (test-output)' +cat <<EOF >EXPEXTED +Error: Unexpected output from notmuch search: Unrecognized option: --this-option-does-not-exist -End of search results. (process returned 1)" +End of search results. (process returned 1) +EOF +test_expect_equal_file OUTPUT EXPEXTED test_done diff --git a/test/emacs.expected-output/emacs-stashing b/test/emacs.expected-output/emacs-stashing new file mode 100644 index 00000000..49235947 --- /dev/null +++ b/test/emacs.expected-output/emacs-stashing @@ -0,0 +1,9 @@ +Sat, 01 Jan 2000 12:00:00 -0000 +Some One <someone@somewhere.org> +Some One Else <notsomeone@somewhere.org> +Notmuch <notmuch@notmuchmail.org> +Stash my stashables +id:"bought" +bought +inbox,stashtest +FILENAME diff --git a/test/emacs.expected-output/notmuch-hello b/test/emacs.expected-output/notmuch-hello index 64b7e42c..48143bd7 100644 --- a/test/emacs.expected-output/notmuch-hello +++ b/test/emacs.expected-output/notmuch-hello @@ -4,7 +4,7 @@ Saved searches: [edit] 50 inbox 50 unread -Search: +Search: [Show all tags] diff --git a/test/emacs.expected-output/notmuch-hello-no-saved-searches b/test/emacs.expected-output/notmuch-hello-no-saved-searches index 7f8206aa..7c09e40b 100644 --- a/test/emacs.expected-output/notmuch-hello-no-saved-searches +++ b/test/emacs.expected-output/notmuch-hello-no-saved-searches @@ -1,6 +1,6 @@ Welcome to notmuch. You have 50 messages. -Search: +Search: [Show all tags] diff --git a/test/emacs.expected-output/notmuch-hello-with-empty b/test/emacs.expected-output/notmuch-hello-with-empty index a9ed6304..2a267c92 100644 --- a/test/emacs.expected-output/notmuch-hello-with-empty +++ b/test/emacs.expected-output/notmuch-hello-with-empty @@ -4,7 +4,7 @@ Saved searches: [edit] 50 inbox 50 unread 0 empty -Search: +Search: [Show all tags] diff --git a/test/help-test b/test/help-test new file mode 100755 index 00000000..9f4b9c79 --- /dev/null +++ b/test/help-test @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +test_description="online help" +. test-lib.sh + +test_expect_success 'notmuch --help' 'notmuch --help' +test_expect_success 'notmuch --help tag' 'notmuch --help tag' +test_expect_success 'notmuch help' 'notmuch help' +test_expect_success 'notmuch help tag' 'notmuch help tag' +test_expect_success 'notmuch --version' 'notmuch --version' + +test_done @@ -23,6 +23,19 @@ add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan output=$(notmuch show --format=json "jsön-show-méssage") test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-utf8-body-sübjéct\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Cc\": \"\", \"Bcc\": \"\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 -0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"jsön-show-méssage\n\"}]}, []]]]" +test_begin_subtest "Show message: json, inline attachment filename" +subject='json-show-inline-attachment-filename' +id="json-show-inline-attachment-filename@notmuchmail.org" +emacs_deliver_message \ + "$subject" \ + 'This is a test message with inline attachment with a filename' \ + "(mml-attach-file \"$TEST_DIRECTORY/README\" nil nil \"inline\") + (message-goto-eoh) + (insert \"Message-ID: <$id>\n\")" +output=$(notmuch show --format=json "id:$id") +filename=$(notmuch search --output=files "id:$id") +test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"filename\": \"$filename\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\"], \"headers\": {\"Subject\": \"$subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"test_suite@notmuchmail.org\", \"Cc\": \"\", \"Bcc\": \"\", \"Date\": \"01 Jan 2000 12:00:00 -0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"multipart/mixed\", \"content\": [{\"id\": 2, \"content-type\": \"text/plain\", \"content\": \"This is a test message with inline attachment with a filename\"}, {\"id\": 3, \"content-type\": \"application/octet-stream\", \"filename\": \"README\"}]}]}, []]]]" + test_begin_subtest "Search message: json, utf-8" add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\"" output=$(notmuch search --format=json "jsön-search-méssage" | notmuch_search_sanitize) diff --git a/test/maildir-sync b/test/maildir-sync index 2b43127e..a60854f8 100755 --- a/test/maildir-sync +++ b/test/maildir-sync @@ -23,7 +23,6 @@ output=$(notmuch search subject:"Adding S flag" | notmuch_search_sanitize) output+=" " mv "${gen_msg_filename}" "${gen_msg_filename}S" -increment_mtime "$(dirname "${gen_msg_filename}")" output+=$(NOTMUCH_NEW) output+=" " @@ -66,7 +65,6 @@ test_expect_equal "$output" '[[[{"id": "adding-replied-tag@notmuch-test-suite", test_expect_success 'notmuch reply works with renamed file (without notmuch new)' 'notmuch reply id:${gen_msg_id}' test_begin_subtest "notmuch new detects no file rename after tag->flag synchronization" -increment_mtime "$(dirname ${gen_msg_filename})" output=$(NOTMUCH_NEW) test_expect_equal "$output" "No new mail." @@ -77,7 +75,6 @@ output=$(cd "$MAIL_DIR/cur"; ls message-to-move*) test_expect_equal "$output" "message-to-move-to-cur:2,S" test_begin_subtest "No rename should be detected by notmuch new" -increment_mtime "$MAIL_DIR/cur" output=$(NOTMUCH_NEW) test_expect_equal "$output" "No new mail." # (*) If notmuch new was not run we've got "Processed 1 file in almost @@ -85,7 +82,7 @@ test_expect_equal "$output" "No new mail." # test created directory document in the database but this document # was not linked as subdirectory of $MAIL_DIR. Therefore notmuch new # could not reach the cur/ directory and its files in it during -# recurive traversal. +# recursive traversal. # # XXX: The above sounds like a bug that should be fixed. If notmuch is # creating new directories in the mail store, then it should be @@ -97,7 +94,6 @@ output=$(notmuch search subject:"Removing S flag" | notmuch_search_sanitize) output+=" " mv "${gen_msg_filename}" "${gen_msg_filename%S}" -increment_mtime "$(dirname "${gen_msg_filename}")" output+=$(NOTMUCH_NEW) output+=" " @@ -110,7 +106,6 @@ test_begin_subtest "Removing info from filename leaves tags unchanged" add_message [subject]='"Message to lose maildir info"' [filename]='message-to-lose-maildir-info' [dir]=cur notmuch tag -unread subject:"Message to lose maildir info" mv "$MAIL_DIR/cur/message-to-lose-maildir-info:2,S" "$MAIL_DIR/cur/message-without-maildir-info" -increment_mtime "$MAIL_DIR/cur" output=$(NOTMUCH_NEW) output+=" " @@ -134,7 +129,6 @@ mv $MAIL_DIR/cur/adding-replied-tag:2,RS $MAIL_DIR/cur/adding-replied-tag:2,S mv $MAIL_DIR/cur/adding-s-flag:2,S $MAIL_DIR/cur/adding-s-flag:2, mv $MAIL_DIR/cur/adding-with-s-flag:2,S $MAIL_DIR/cur/adding-with-s-flag:2,RS mv $MAIL_DIR/cur/message-to-move-to-cur:2,S $MAIL_DIR/cur/message-to-move-to-cur:2,DS -increment_mtime $MAIL_DIR/cur notmuch dump dump.txt NOTMUCH_NEW >/dev/null notmuch restore dump.txt @@ -144,7 +138,6 @@ test_expect_equal "$output" "$expected" test_begin_subtest 'Adding flags to duplicate message tags the mail' add_message [subject]='"Duplicated message"' [dir]=cur [filename]='duplicated-message:2,' cp "$MAIL_DIR/cur/duplicated-message:2," "$MAIL_DIR/cur/duplicated-message-copy:2,RS" -increment_mtime $MAIL_DIR/cur NOTMUCH_NEW > output notmuch search subject:"Duplicated message" | notmuch_search_sanitize >> output test_expect_equal "$(< output)" "No new mail. @@ -152,7 +145,6 @@ thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Duplicated message (inbox repl test_begin_subtest "Adding duplicate message without flags does not remove tags" cp "$MAIL_DIR/cur/duplicated-message-copy:2,RS" "$MAIL_DIR/cur/duplicated-message-another-copy:2," -increment_mtime $MAIL_DIR/cur NOTMUCH_NEW > output notmuch search subject:"Duplicated message" | notmuch_search_sanitize >> output test_expect_equal "$(< output)" "No new mail. diff --git a/test/multipart b/test/multipart index 0879696e..f83526bb 100755 --- a/test/multipart +++ b/test/multipart @@ -2,11 +2,34 @@ test_description="output of multipart message" . ./test-lib.sh +cat <<EOF > embedded_message +From: Carl Worth <cworth@cworth.org> +To: cworth@cworth.org +Subject: html message +Date: Fri, 05 Jan 2001 15:42:57 +0000 +User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu) +Message-ID: <87liy5ap01.fsf@yoom.home.cworth.org> +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="==-=-==" + +--==-=-== +Content-Type: text/html + +<p>This is an embedded message, with a multipart/alternative part.</p> + +--==-=-== +Content-Type: text/plain + +This is an embedded message, with a multipart/alternative part. + +--==-=-==-- +EOF + cat <<EOF > ${MAIL_DIR}/multipart From: Carl Worth <cworth@cworth.org> To: cworth@cworth.org Subject: Multipart message -Date: Tue, 05 Jan 2001 15:43:57 -0000 +Date: Fri, 05 Jan 2001 15:43:57 +0000 User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu) Message-ID: <87liy5ap00.fsf@yoom.home.cworth.org> MIME-Version: 1.0 @@ -20,17 +43,9 @@ Content-Type: multipart/mixed; boundary="=-=-=" Content-Type: message/rfc822 Content-Disposition: inline -From: Carl Worth <cworth@cworth.org> -To: cworth@cworth.org -Subject: html message -Date: Tue, 05 Jan 2001 15:42:57 -0000 -User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu) -Message-ID: <87liy5ap01.fsf@yoom.home.cworth.org> -MIME-Version: 1.0 -Content-Type: text/html - -<p>This is an embedded message, with a single html part.</p> - +EOF +cat embedded_message >> ${MAIL_DIR}/multipart +cat <<EOF >> ${MAIL_DIR}/multipart --=-=-= Content-Disposition: attachment; filename=attachment @@ -61,7 +76,7 @@ cat <<EOF > ${MAIL_DIR}/base64-part-with-crlf From: Carl Worth <cworth@cworth.org> To: cworth@cworth.org Subject: Test message with a BASE64 encoded binary containing CRLF pair -Date: Tue, 05 Jan 2001 15:43:57 -0000 +Date: Fri, 05 Jan 2001 15:43:57 +0000 User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu) Message-ID: <base64-part-with-crlf> MIME-Version: 1.0 @@ -88,7 +103,6 @@ Content-Transfer-Encoding: base64 7w0K --==-=-=-- EOF -increment_mtime "$MAIL_DIR" notmuch new > /dev/null test_begin_subtest "--format=text --part=0, full message" @@ -100,27 +114,39 @@ Carl Worth <cworth@cworth.org> (2001-01-05) (attachment inbox signed unread) Subject: Multipart message From: Carl Worth <cworth@cworth.org> To: cworth@cworth.org -Date: Tue, 05 Jan 2001 15:43:57 -0000 +Date: Fri, 05 Jan 2001 15:43:57 +0000 header} body{ part{ ID: 1, Content-type: multipart/signed part{ ID: 2, Content-type: multipart/mixed part{ ID: 3, Content-type: message/rfc822 -part{ ID: 4, Content-type: text/html +header{ +From: Carl Worth <cworth@cworth.org> +To: cworth@cworth.org +Subject: html message +Date: Fri, 05 Jan 2001 15:42:57 +0000 +header} +body{ +part{ ID: 4, Content-type: multipart/alternative +part{ ID: 5, Content-type: text/html Non-text part: text/html part} +part{ ID: 6, Content-type: text/plain +This is an embedded message, with a multipart/alternative part. +part} part} -attachment{ ID: 5, Content-type: text/plain -Attachment: attachment (text/plain) +body} +part} +attachment{ ID: 7, Filename: attachment, Content-type: text/plain This is a text attachment. attachment} -part{ ID: 6, Content-type: text/plain +part{ ID: 8, Content-type: text/plain And this message is signed. -Carl part} part} -part{ ID: 7, Content-type: application/pgp-signature +part{ ID: 9, Content-type: application/pgp-signature Non-text part: application/pgp-signature part} part} @@ -129,41 +155,42 @@ Non-text part: application/pgp-signature EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=text --part=0, full message" -notmuch show --format=text --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +test_begin_subtest "--format=text --part=1, message body" +notmuch show --format=text --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED -message{ id:87liy5ap00.fsf@yoom.home.cworth.org depth:0 match:1 filename:${MAIL_DIR}/multipart +part{ ID: 1, Content-type: multipart/signed +part{ ID: 2, Content-type: multipart/mixed +part{ ID: 3, Content-type: message/rfc822 header{ -Carl Worth <cworth@cworth.org> (2001-01-05) (attachment inbox signed unread) -Subject: Multipart message From: Carl Worth <cworth@cworth.org> To: cworth@cworth.org -Date: Tue, 05 Jan 2001 15:43:57 -0000 +Subject: html message +Date: Fri, 05 Jan 2001 15:42:57 +0000 header} body{ -part{ ID: 1, Content-type: multipart/signed -part{ ID: 2, Content-type: multipart/mixed -part{ ID: 3, Content-type: message/rfc822 -part{ ID: 4, Content-type: text/html +part{ ID: 4, Content-type: multipart/alternative +part{ ID: 5, Content-type: text/html Non-text part: text/html part} +part{ ID: 6, Content-type: text/plain +This is an embedded message, with a multipart/alternative part. +part} +part} +body} part} -attachment{ ID: 5, Content-type: text/plain -Attachment: attachment (text/plain) +attachment{ ID: 7, Filename: attachment, Content-type: text/plain This is a text attachment. attachment} -part{ ID: 6, Content-type: text/plain +part{ ID: 8, Content-type: text/plain And this message is signed. -Carl part} part} -part{ ID: 7, Content-type: application/pgp-signature +part{ ID: 9, Content-type: application/pgp-signature Non-text part: application/pgp-signature part} part} -body} -message} EOF test_expect_equal_file OUTPUT EXPECTED @@ -172,15 +199,27 @@ notmuch show --format=text --part=2 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OU cat <<EOF >EXPECTED part{ ID: 2, Content-type: multipart/mixed part{ ID: 3, Content-type: message/rfc822 -part{ ID: 4, Content-type: text/html +header{ +From: Carl Worth <cworth@cworth.org> +To: cworth@cworth.org +Subject: html message +Date: Fri, 05 Jan 2001 15:42:57 +0000 +header} +body{ +part{ ID: 4, Content-type: multipart/alternative +part{ ID: 5, Content-type: text/html Non-text part: text/html part} +part{ ID: 6, Content-type: text/plain +This is an embedded message, with a multipart/alternative part. +part} part} -attachment{ ID: 5, Content-type: text/plain -Attachment: attachment (text/plain) +body} +part} +attachment{ ID: 7, Filename: attachment, Content-type: text/plain This is a text attachment. attachment} -part{ ID: 6, Content-type: text/plain +part{ ID: 8, Content-type: text/plain And this message is signed. -Carl @@ -189,40 +228,75 @@ And this message is signed. EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=text --part=3, rfc822 multipart" +test_begin_subtest "--format=text --part=3, rfc822 part" notmuch show --format=text --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED part{ ID: 3, Content-type: message/rfc822 -part{ ID: 4, Content-type: text/html +header{ +From: Carl Worth <cworth@cworth.org> +To: cworth@cworth.org +Subject: html message +Date: Fri, 05 Jan 2001 15:42:57 +0000 +header} +body{ +part{ ID: 4, Content-type: multipart/alternative +part{ ID: 5, Content-type: text/html Non-text part: text/html part} +part{ ID: 6, Content-type: text/plain +This is an embedded message, with a multipart/alternative part. +part} +part} +body} part} EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=text --part=4, html part" +test_begin_subtest "--format=text --part=4, rfc822's multipart" notmuch show --format=text --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED -part{ ID: 4, Content-type: text/html +part{ ID: 4, Content-type: multipart/alternative +part{ ID: 5, Content-type: text/html Non-text part: text/html part} +part{ ID: 6, Content-type: text/plain +This is an embedded message, with a multipart/alternative part. +part} +part} EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=text --part=5, inline attachement" +test_begin_subtest "--format=text --part=5, rfc822's html part" notmuch show --format=text --part=5 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED -attachment{ ID: 5, Content-type: text/plain -Attachment: attachment (text/plain) -This is a text attachment. -attachment} +part{ ID: 5, Content-type: text/html +Non-text part: text/html +part} EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=text --part=6, plain text part" +test_begin_subtest "--format=text --part=6, rfc822's text part" notmuch show --format=text --part=6 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED part{ ID: 6, Content-type: text/plain +This is an embedded message, with a multipart/alternative part. +part} +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--format=text --part=7, inline attachement" +notmuch show --format=text --part=7 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +cat <<EOF >EXPECTED +attachment{ ID: 7, Filename: attachment, Content-type: text/plain +This is a text attachment. +attachment} +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--format=text --part=8, plain text part" +notmuch show --format=text --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +cat <<EOF >EXPECTED +part{ ID: 8, Content-type: text/plain And this message is signed. -Carl @@ -230,10 +304,10 @@ And this message is signed. EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=text --part=7, pgp signature (unverified)" -notmuch show --format=text --part=7 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +test_begin_subtest "--format=text --part=9, pgp signature (unverified)" +notmuch show --format=text --part=9 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED -part{ ID: 7, Content-type: application/pgp-signature +part{ ID: 9, Content-type: application/pgp-signature Non-text part: application/pgp-signature part} EOF @@ -244,50 +318,126 @@ test_expect_success \ "notmuch show --format=text --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org'" test_begin_subtest "--format=json --part=0, full message" -output=$(notmuch show --format=json --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org') -test_expect_equal "$output" \ -'{"id": "87liy5ap00.fsf@yoom.home.cworth.org", "match": true, "filename": "'"${MAIL_DIR}/multipart"'", "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["attachment","inbox","signed","unread"], "headers": {"Subject": "Multipart message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Cc": "", "Bcc": "", "Date": "Tue, 05 Jan 2001 15:43:57 -0000"}, "body": [{"id": 1, "content-type": "multipart/signed", "content": [{"id": 2, "content-type": "multipart/mixed", "content": [{"id": 3, "content-type": "message/rfc822", "content": [{"id": 4, "content-type": "text/html"}]}, {"id": 5, "content-type": "text/plain", "filename": "attachment", "content": "This is a text attachment.\n"}, {"id": 6, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"}]}, {"id": 7, "content-type": "application/pgp-signature"}]}]}' +notmuch show --format=json --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' | sed 's|{"id":|\n{"id":|g' >OUTPUT +echo >>OUTPUT # expect *no* newline at end of output +cat <<EOF >EXPECTED + +{"id": "87liy5ap00.fsf@yoom.home.cworth.org", "match": true, "filename": "${MAIL_DIR}/multipart", "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["attachment","inbox","signed","unread"], "headers": {"Subject": "Multipart message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Cc": "", "Bcc": "", "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [ +{"id": 1, "content-type": "multipart/signed", "content": [ +{"id": 2, "content-type": "multipart/mixed", "content": [ +{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [ +{"id": 4, "content-type": "multipart/alternative", "content": [ +{"id": 5, "content-type": "text/html"}, +{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}, +{"id": 7, "content-type": "text/plain", "filename": "attachment", "content": "This is a text attachment.\n"}, +{"id": 8, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"}]}, +{"id": 9, "content-type": "application/pgp-signature"}]}]} +EOF +test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "--format=json --part=1, message body" -output=$(notmuch show --format=json --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org') -test_expect_equal "$output" \ -'{"id": 1, "content-type": "multipart/signed", "content": [{"id": 2, "content-type": "multipart/mixed", "content": [{"id": 3, "content-type": "message/rfc822", "content": [{"id": 4, "content-type": "text/html"}]}, {"id": 5, "content-type": "text/plain", "filename": "attachment", "content": "This is a text attachment.\n"}, {"id": 6, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"}]}, {"id": 7, "content-type": "application/pgp-signature"}]}' +notmuch show --format=json --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org' | sed 's|{"id":|\n{"id":|g' >OUTPUT +echo >>OUTPUT # expect *no* newline at end of output +cat <<EOF >EXPECTED + +{"id": 1, "content-type": "multipart/signed", "content": [ +{"id": 2, "content-type": "multipart/mixed", "content": [ +{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [ +{"id": 4, "content-type": "multipart/alternative", "content": [ +{"id": 5, "content-type": "text/html"}, +{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}, +{"id": 7, "content-type": "text/plain", "filename": "attachment", "content": "This is a text attachment.\n"}, +{"id": 8, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"}]}, +{"id": 9, "content-type": "application/pgp-signature"}]} +EOF +test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "--format=json --part=2, multipart/mixed" -output=$(notmuch show --format=json --part=2 'id:87liy5ap00.fsf@yoom.home.cworth.org') -test_expect_equal "$output" \ -'{"id": 2, "content-type": "multipart/mixed", "content": [{"id": 3, "content-type": "message/rfc822", "content": [{"id": 4, "content-type": "text/html"}]}, {"id": 5, "content-type": "text/plain", "filename": "attachment", "content": "This is a text attachment.\n"}, {"id": 6, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"}]}' +notmuch show --format=json --part=2 'id:87liy5ap00.fsf@yoom.home.cworth.org' | sed 's|{"id":|\n{"id":|g' >OUTPUT +echo >>OUTPUT # expect *no* newline at end of output +cat <<EOF >EXPECTED + +{"id": 2, "content-type": "multipart/mixed", "content": [ +{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [ +{"id": 4, "content-type": "multipart/alternative", "content": [ +{"id": 5, "content-type": "text/html"}, +{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}, +{"id": 7, "content-type": "text/plain", "filename": "attachment", "content": "This is a text attachment.\n"}, +{"id": 8, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"}]} +EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=json --part=3, rfc822 multipart" -output=$(notmuch show --format=json --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org') -test_expect_equal "$output" \ -'{"id": 3, "content-type": "message/rfc822", "content": [{"id": 4, "content-type": "text/html"}]}' +test_begin_subtest "--format=json --part=3, rfc822 part" +notmuch show --format=json --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' | sed 's|{"id":|\n{"id":|g' >OUTPUT +echo >>OUTPUT # expect *no* newline at end of output +cat <<EOF >EXPECTED + +{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [ +{"id": 4, "content-type": "multipart/alternative", "content": [ +{"id": 5, "content-type": "text/html"}, +{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]} +EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=json --part=4, html part" -output=$(notmuch show --format=json --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org') -test_expect_equal "$output" \ -'{"id": 4, "content-type": "text/html"}' +test_begin_subtest "--format=json --part=4, rfc822's multipart/alternative" +notmuch show --format=json --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org' | sed 's|{"id":|\n{"id":|g' >OUTPUT +echo >>OUTPUT # expect *no* newline at end of output +cat <<EOF >EXPECTED -test_begin_subtest "--format=json --part=5, inline attachment" -output=$(notmuch show --format=json --part=5 'id:87liy5ap00.fsf@yoom.home.cworth.org') -test_expect_equal "$output" \ -'{"id": 5, "content-type": "text/plain", "filename": "attachment", "content": "This is a text attachment.\n"}' +{"id": 4, "content-type": "multipart/alternative", "content": [ +{"id": 5, "content-type": "text/html"}, +{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]} +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--format=json --part=5, rfc822's html part" +notmuch show --format=json --part=5 'id:87liy5ap00.fsf@yoom.home.cworth.org' | sed 's|{"id":|\n{"id":|g' >OUTPUT +echo >>OUTPUT # expect *no* newline at end of output +cat <<EOF >EXPECTED -test_begin_subtest "--format=json --part=6, plain text part" -output=$(notmuch show --format=json --part=6 'id:87liy5ap00.fsf@yoom.home.cworth.org') -test_expect_equal "$output" \ -'{"id": 6, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"}' +{"id": 5, "content-type": "text/html"} +EOF +test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=json --part=7, pgp signature (unverified)" -output=$(notmuch show --format=json --part=7 'id:87liy5ap00.fsf@yoom.home.cworth.org') -test_expect_equal "$output" \ -'{"id": 7, "content-type": "application/pgp-signature"}' +test_begin_subtest "--format=json --part=6, rfc822's text part" +notmuch show --format=json --part=6 'id:87liy5ap00.fsf@yoom.home.cworth.org' | sed 's|{"id":|\n{"id":|g' >OUTPUT +echo >>OUTPUT # expect *no* newline at end of output +cat <<EOF >EXPECTED + +{"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"} +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--format=json --part=7, inline attachment" +notmuch show --format=json --part=7 'id:87liy5ap00.fsf@yoom.home.cworth.org' | sed 's|{"id":|\n{"id":|g' >OUTPUT +echo >>OUTPUT # expect *no* newline at end of output +cat <<EOF >EXPECTED + +{"id": 7, "content-type": "text/plain", "filename": "attachment", "content": "This is a text attachment.\n"} +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--format=json --part=8, plain text part" +notmuch show --format=json --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org' | sed 's|{"id":|\n{"id":|g' >OUTPUT +echo >>OUTPUT # expect *no* newline at end of output +cat <<EOF >EXPECTED + +{"id": 8, "content-type": "text/plain", "content": "And this message is signed.\n\n-Carl\n"} +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--format=json --part=9, pgp signature (unverified)" +notmuch show --format=json --part=9 'id:87liy5ap00.fsf@yoom.home.cworth.org' | sed 's|{"id":|\n{"id":|g' >OUTPUT +echo >>OUTPUT # expect *no* newline at end of output +cat <<EOF >EXPECTED + +{"id": 9, "content-type": "application/pgp-signature"} +EOF +test_expect_equal_file OUTPUT EXPECTED test_expect_success \ - "--format=json --part=8, no part, expect error" \ - "notmuch show --format=json --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org'" + "--format=json --part=10, no part, expect error" \ + "notmuch show --format=json --part=10 'id:87liy5ap00.fsf@yoom.home.cworth.org'" test_begin_subtest "--format=raw" notmuch show --format=raw 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT @@ -302,7 +452,13 @@ notmuch show --format=raw --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUT # output should *not* include newline echo >>OUTPUT cat <<EOF >EXPECTED -<p>This is an embedded message, with a single html part.</p> +From: Carl Worth <cworth@cworth.org> +To: cworth@cworth.org +Subject: html message +Date: Fri, 05 Jan 2001 15:42:57 +0000 + +<p>This is an embedded message, with a multipart/alternative part.</p> +This is an embedded message, with a multipart/alternative part. This is a text attachment. And this message is signed. @@ -320,7 +476,13 @@ test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "--format=raw --part=2, multipart/mixed" notmuch show --format=raw --part=2 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED -<p>This is an embedded message, with a single html part.</p> +From: Carl Worth <cworth@cworth.org> +To: cworth@cworth.org +Subject: html message +Date: Fri, 05 Jan 2001 15:42:57 +0000 + +<p>This is an embedded message, with a multipart/alternative part.</p> +This is an embedded message, with a multipart/alternative part. This is a text attachment. And this message is signed. @@ -328,29 +490,43 @@ And this message is signed. EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=raw --part=3, rfc822 multipart" +test_begin_subtest "--format=raw --part=3, rfc822 part" +test_subtest_known_broken + notmuch show --format=raw --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +test_expect_equal_file OUTPUT embedded_message + +test_begin_subtest "--format=raw --part=4, rfc822's html part" +notmuch show --format=raw --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED -<p>This is an embedded message, with a single html part.</p> +<p>This is an embedded message, with a multipart/alternative part.</p> +This is an embedded message, with a multipart/alternative part. EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=raw --part=4, html part" -notmuch show --format=raw --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +test_begin_subtest "--format=raw --part=5, rfc822's html part" +notmuch show --format=raw --part=5 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED -<p>This is an embedded message, with a single html part.</p> +<p>This is an embedded message, with a multipart/alternative part.</p> EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=raw --part=5, inline attachment" -notmuch show --format=raw --part=5 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +test_begin_subtest "--format=raw --part=6, rfc822's text part" +notmuch show --format=raw --part=6 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +cat <<EOF >EXPECTED +This is an embedded message, with a multipart/alternative part. +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--format=raw --part=7, inline attachment" +notmuch show --format=raw --part=7 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED This is a text attachment. EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=raw --part=6, plain text part" -notmuch show --format=raw --part=6 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +test_begin_subtest "--format=raw --part=8, plain text part" +notmuch show --format=raw --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT cat <<EOF >EXPECTED And this message is signed. @@ -358,8 +534,8 @@ And this message is signed. EOF test_expect_equal_file OUTPUT EXPECTED -test_begin_subtest "--format=raw --part=7, pgp signature (unverified)" -notmuch show --format=raw --part=7 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT +test_begin_subtest "--format=raw --part=9, pgp signature (unverified)" +notmuch show --format=raw --part=9 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT # output should *not* include newline echo >>OUTPUT cat <<EOF >EXPECTED @@ -374,7 +550,7 @@ EOF test_expect_equal_file OUTPUT EXPECTED test_expect_success \ - "--format=raw --part=8, no part, expect error" \ + "--format=raw --part=10, no part, expect error" \ "notmuch show --format=raw --part=8 'id:87liy5ap00.fsf@yoom.home.cworth.org'" test_begin_subtest "--format=mbox" @@ -398,16 +574,18 @@ To: Carl Worth <cworth@cworth.org>, cworth@cworth.org In-Reply-To: <87liy5ap00.fsf@yoom.home.cworth.org> References: <87liy5ap00.fsf@yoom.home.cworth.org> -On Tue, 05 Jan 2001 15:43:57 -0000, Carl Worth <cworth@cworth.org> wrote: -Non-text part: multipart/signed -Non-text part: multipart/mixed -Non-text part: message/rfc822 +On Fri, 05 Jan 2001 15:43:57 +0000, Carl Worth <cworth@cworth.org> wrote: +> From: Carl Worth <cworth@cworth.org> +> To: cworth@cworth.org +> Subject: html message +> Date: Fri, 05 Jan 2001 15:42:57 +0000 +> Non-text part: text/html +> This is an embedded message, with a multipart/alternative part. > This is a text attachment. > And this message is signed. > > -Carl -Non-text part: application/pgp-signature EOF test_expect_equal_file OUTPUT EXPECTED @@ -52,10 +52,8 @@ generate_message tmp_msg_filename=tmp/"$gen_msg_filename" mkdir -p "$(dirname "$tmp_msg_filename")" mv "$gen_msg_filename" "$tmp_msg_filename" -increment_mtime "${MAIL_DIR}" notmuch new > /dev/null mv "$tmp_msg_filename" "$gen_msg_filename" -increment_mtime "${MAIL_DIR}" output=$(NOTMUCH_NEW) test_expect_equal "$output" "Added 1 new message to the database." @@ -65,7 +63,6 @@ test_begin_subtest "Renamed message" generate_message notmuch new > /dev/null mv "$gen_msg_filename" "${gen_msg_filename}"-renamed -increment_mtime "${MAIL_DIR}" output=$(NOTMUCH_NEW) test_expect_equal "$output" "No new mail. Detected 1 file rename." @@ -73,7 +70,6 @@ test_expect_equal "$output" "No new mail. Detected 1 file rename." test_begin_subtest "Deleted message" rm "${gen_msg_filename}"-renamed -increment_mtime "${MAIL_DIR}" output=$(NOTMUCH_NEW) test_expect_equal "$output" "No new mail. Removed 1 message." @@ -87,7 +83,6 @@ generate_message [dir]=dir notmuch new > /dev/null mv "${MAIL_DIR}"/dir "${MAIL_DIR}"/dir-renamed -increment_mtime "${MAIL_DIR}" output=$(NOTMUCH_NEW) test_expect_equal "$output" "No new mail. Detected 3 file renames." @@ -96,7 +91,6 @@ test_expect_equal "$output" "No new mail. Detected 3 file renames." test_begin_subtest "Deleted directory" rm -rf "${MAIL_DIR}"/dir-renamed -increment_mtime "${MAIL_DIR}" output=$(NOTMUCH_NEW) test_expect_equal "$output" "No new mail. Removed 3 messages." @@ -115,7 +109,6 @@ test_expect_equal "$output" "Added 3 new messages to the database." test_begin_subtest "Deleted directory (end of list)" rm -rf "${MAIL_DIR}"/zzz -increment_mtime "${MAIL_DIR}" output=$(NOTMUCH_NEW) test_expect_equal "$output" "No new mail. Removed 3 messages." @@ -139,7 +132,6 @@ external_msg_filename="$PWD"/external/"$(basename "$gen_msg_filename")" mkdir -p "$(dirname "$external_msg_filename")" mv "$gen_msg_filename" "$external_msg_filename" ln -s "$external_msg_filename" "$gen_msg_filename" -increment_mtime "${MAIL_DIR}" output=$(NOTMUCH_NEW) test_expect_equal "$output" "Added 1 new message to the database." @@ -157,7 +149,6 @@ test_expect_equal "$output" "Added 3 new messages to the database." test_begin_subtest "Deleted two-level directory" rm -rf "${MAIL_DIR}"/two -increment_mtime "${MAIL_DIR}" output=$(NOTMUCH_NEW) test_expect_equal "$output" "No new mail. Removed 3 messages." diff --git a/test/notmuch-test b/test/notmuch-test index 79e6267b..113ea7cf 100755 --- a/test/notmuch-test +++ b/test/notmuch-test @@ -18,12 +18,16 @@ cd $(dirname "$0") TESTS=" basic + help-test new + count search search-output search-by-folder search-position-overlap-bug search-insufficient-from-quoting + search-limiting + tagging json multipart thread-naming @@ -42,6 +46,8 @@ TESTS=" crypto symbol-hiding search-folder-coherence + atomicity + python " TESTS=${NOTMUCH_TESTS:=$TESTS} diff --git a/test/python b/test/python new file mode 100755 index 00000000..f737749f --- /dev/null +++ b/test/python @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +test_description="python bindings" +. ./test-lib.sh + +add_email_corpus + +test_begin_subtest "compare thread ids" +LD_LIBRARY_PATH=$TEST_DIRECTORY/../lib \ +PYTHONPATH=$TEST_DIRECTORY/../bindings/python \ +python <<EOF | sort > OUTPUT +import notmuch +db = notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) +q_new = notmuch.Query(db, 'tag:inbox') +for t in q_new.search_threads(): + print t.get_thread_id() +EOF +notmuch search --output=threads tag:inbox | sed s/^thread:// | sort > EXPECTED +test_expect_equal_file OUTPUT EXPECTED +test_done diff --git a/test/search-by-folder b/test/search-by-folder index 4afa483d..5cc2ca8d 100755 --- a/test/search-by-folder +++ b/test/search-by-folder @@ -23,14 +23,12 @@ test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; B test_begin_subtest "After removing duplicate instance of matching path" rm -r "${MAIL_DIR}/bad/news" -increment_mtime "${MAIL_DIR}/bad" notmuch new output=$(notmuch search folder:bad/news | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Bears (inbox unread)" test_begin_subtest "After rename, old path returns nothing" mv "${MAIL_DIR}/duplicate/bad/news" "${MAIL_DIR}/duplicate/bad/olds" -increment_mtime "${MAIL_DIR}/duplicate/bad" notmuch new output=$(notmuch search folder:bad/news | notmuch_search_sanitize) test_expect_equal "$output" "" diff --git a/test/search-folder-coherence b/test/search-folder-coherence index 9c312542..f8119cbb 100755 --- a/test/search-folder-coherence +++ b/test/search-folder-coherence @@ -36,7 +36,6 @@ test_expect_equal "$output" "thread:0000000000000001 2001-01-05 [1/1] Notmuch test_begin_subtest "Remove folder:spam copy of email" rm $dir/spam/$(basename $file_x) -increment_mtime $dir/spam output=$(NOTMUCH_NEW) test_expect_equal "$output" "No new mail. Detected 1 file rename." diff --git a/test/search-limiting b/test/search-limiting new file mode 100755 index 00000000..303762cf --- /dev/null +++ b/test/search-limiting @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +test_description='"notmuch search" --offset and --limit parameters' +. ./test-lib.sh + +add_email_corpus + +for outp in messages threads; do + test_begin_subtest "${outp}: limit does the right thing" + notmuch search --output=${outp} "*" | head -n 20 >expected + notmuch search --output=${outp} --limit=20 "*" >output + test_expect_equal_file expected output + + test_begin_subtest "${outp}: concatenation of limited searches" + notmuch search --output=${outp} "*" | head -n 20 >expected + notmuch search --output=${outp} --limit=10 "*" >output + notmuch search --output=${outp} --limit=10 --offset=10 "*" >>output + test_expect_equal_file expected output + + test_begin_subtest "${outp}: limit larger than result set" + N=`notmuch count --output=${outp} "*"` + notmuch search --output=${outp} "*" >expected + notmuch search --output=${outp} --limit=$((1 + ${N})) "*" >output + test_expect_equal_file expected output + + test_begin_subtest "${outp}: limit = 0" + test_expect_equal "`notmuch search --output=${outp} --limit=0 "*"`" "" + + test_begin_subtest "${outp}: offset does the right thing" + # note: tail -n +N is 1-based + notmuch search --output=${outp} "*" | tail -n +21 >expected + notmuch search --output=${outp} --offset=20 "*" >output + test_expect_equal_file expected output + + test_begin_subtest "${outp}: offset = 0" + notmuch search --output=${outp} "*" >expected + notmuch search --output=${outp} --offset=0 "*" >output + test_expect_equal_file expected output + + test_begin_subtest "${outp}: negative offset" + notmuch search --output=${outp} "*" | tail -n 20 >expected + notmuch search --output=${outp} --offset=-20 "*" >output + test_expect_equal_file expected output + + test_begin_subtest "${outp}: negative offset" + notmuch search --output=${outp} "*" | tail -n 1 >expected + notmuch search --output=${outp} --offset=-1 "*" >output + test_expect_equal_file expected output + + test_begin_subtest "${outp}: negative offset combined with limit" + notmuch search --output=${outp} "*" | tail -n 20 | head -n 10 >expected + notmuch search --output=${outp} --offset=-20 --limit=10 "*" >output + test_expect_equal_file expected output + + test_begin_subtest "${outp}: negative offset combined with equal limit" + notmuch search --output=${outp} "*" | tail -n 20 >expected + notmuch search --output=${outp} --offset=-20 --limit=20 "*" >output + test_expect_equal_file expected output + + test_begin_subtest "${outp}: negative offset combined with large limit" + notmuch search --output=${outp} "*" | tail -n 20 >expected + notmuch search --output=${outp} --offset=-20 --limit=50 "*" >output + test_expect_equal_file expected output + + test_begin_subtest "${outp}: negative offset larger then results" + N=`notmuch count --output=${outp} "*"` + notmuch search --output=${outp} "*" >expected + notmuch search --output=${outp} --offset=-$((1 + ${N})) "*" >output + test_expect_equal_file expected output +done + +test_done diff --git a/test/search-output b/test/search-output index 3c875cd1..10291c3b 100755 --- a/test/search-output +++ b/test/search-output @@ -207,6 +207,7 @@ MAIL_DIR/cur/22:2, MAIL_DIR/cur/21:2, MAIL_DIR/cur/19:2, MAIL_DIR/cur/18:2, +MAIL_DIR/cur/51:2, MAIL_DIR/cur/20:2, MAIL_DIR/cur/17:2, MAIL_DIR/cur/16:2, @@ -263,6 +264,7 @@ cat <<EOF >EXPECTED "MAIL_DIR/cur/21:2,", "MAIL_DIR/cur/19:2,", "MAIL_DIR/cur/18:2,", +"MAIL_DIR/cur/51:2,", "MAIL_DIR/cur/20:2,", "MAIL_DIR/cur/17:2,", "MAIL_DIR/cur/16:2,", diff --git a/test/smtp-dummy.c b/test/smtp-dummy.c index 9da8202f..3801a5e0 100644 --- a/test/smtp-dummy.c +++ b/test/smtp-dummy.c @@ -71,7 +71,7 @@ static int process_command (FILE *peer, FILE *output, const char *command) { if (STRNCMP_LITERAL (command, "EHLO ") == 0) { - fprintf (peer, "502\r\n"); + fprintf (peer, "502 not implemented\r\n"); fflush (peer); } else if (STRNCMP_LITERAL (command, "HELO ") == 0) { fprintf (peer, "250 localhost\r\n"); @@ -159,6 +159,7 @@ main (int argc, char *argv[]) return 1; } + memset (&addr, 0, sizeof (addr)); addr.sin_family = AF_INET; addr.sin_port = htons (25025); addr.sin_addr = *(struct in_addr *) hostinfo->h_addr; diff --git a/test/symbol-hiding b/test/symbol-hiding index bb555245..d0b31aec 100755 --- a/test/symbol-hiding +++ b/test/symbol-hiding @@ -12,13 +12,13 @@ test_description='exception symbol hiding' . ./test-lib.sh run_test(){ - result=$(LD_LIBRARY_PATH=../../lib ./symbol-test 2>&1) + result=$(LD_LIBRARY_PATH=$TEST_DIRECTORY/../lib ./symbol-test 2>&1) } output="A Xapian exception occurred opening database: Couldn't stat 'fakedb/.notmuch/xapian' caught No chert database found at path \`./nonexistant'" -g++ -o symbol-test -I../../lib ../symbol-test.cc -L../../lib -lnotmuch -lxapian +g++ -o symbol-test -I$TEST_DIRECTORY/../lib $TEST_DIRECTORY/symbol-test.cc -L$TEST_DIRECTORY/../lib -lnotmuch -lxapian mkdir -p fakedb/.notmuch test_expect_success 'running test' run_test test_begin_subtest 'checking output' diff --git a/test/tagging b/test/tagging new file mode 100755 index 00000000..77202bf9 --- /dev/null +++ b/test/tagging @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +test_description='"notmuch tag"' +. ./test-lib.sh + +add_message '[subject]=One' +add_message '[subject]=Two' + +test_begin_subtest "Adding tags" +notmuch tag +tag1 +tag2 +tag3 \* +output=$(notmuch search \* | notmuch_search_sanitize) +test_expect_equal "$output" "\ +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag1 tag2 tag3 unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 tag2 tag3 unread)" + +test_begin_subtest "Removing tags" +notmuch tag -tag1 -tag2 \* +output=$(notmuch search \* | notmuch_search_sanitize) +test_expect_equal "$output" "\ +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag3 unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag3 unread)" + +test_expect_code 1 "No tag operations" 'notmuch tag One' +test_expect_code 1 "No query" 'notmuch tag +tag2' + +test_begin_subtest "Redundant tagging" +notmuch tag +tag1 -tag3 One +notmuch tag +tag1 -tag3 \* +output=$(notmuch search \* | notmuch_search_sanitize) +test_expect_equal "$output" "\ +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag1 unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 unread)" + +test_begin_subtest "Special characters in tags" +notmuch tag +':" ' \* +notmuch tag -':" ' Two +output=$(notmuch search \* | notmuch_search_sanitize) +test_expect_equal "$output" "\ +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One (:\" inbox tag1 unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag1 unread)" + +test_done diff --git a/test/test-lib.el b/test/test-lib.el index 94399962..97ae5938 100644 --- a/test/test-lib.el +++ b/test/test-lib.el @@ -20,6 +20,27 @@ ;; ;; Authors: Dmitry Kurochkin <dmitry.kurochkin@gmail.com> +;; `read-file-name' by default uses `completing-read' function to read +;; user input. It does not respect `standard-input' variable which we +;; use in tests to provide user input. So replace it with a plain +;; `read' call. +(setq read-file-name-function (lambda (&rest _) (read))) + +(defun notmuch-test-wait () + "Wait for process completion." + (while (get-buffer-process (current-buffer)) + (sleep-for 0.1))) + +(defun test-output (&optional filename) + "Save current buffer to file FILENAME. Default FILENAME is OUTPUT." + (write-region (point-min) (point-max) (or filename "OUTPUT"))) + +(defun test-visible-output (&optional filename) + "Save visible text in current buffer to file FILENAME. Default +FILENAME is OUTPUT." + (let ((text (visible-buffer-string))) + (with-temp-file (or filename "OUTPUT") (insert text)))) + (defun visible-buffer-string () "Same as `buffer-string', but excludes invisible text." (visible-buffer-substring (point-min) (point-max))) @@ -33,3 +54,10 @@ (setq str (concat str (buffer-substring start next-pos)))) (setq start next-pos))) str)) + +(defun orphan-watchdog (pid) + "Periodically check that the process with id PID is still +running, quit if it terminated." + (if (not (process-attributes pid)) + (kill-emacs) + (run-at-time "1 min" nil 'orphan-watchdog pid))) diff --git a/test/test-lib.sh b/test/test-lib.sh index a59d1c13..cf309f94 100755 --- a/test/test-lib.sh +++ b/test/test-lib.sh @@ -39,7 +39,7 @@ done,*) ;; esac -# Keep the original TERM for say_color +# Keep the original TERM for say_color and test_emacs ORIGINAL_TERM=$TERM # For repeatability, reset the environment to known value. @@ -174,6 +174,7 @@ test_success=0 die () { code=$? + rm -rf "$TEST_TMPDIR" if test -n "$GIT_EXIT_OK" then exit $code @@ -184,6 +185,8 @@ die () { } GIT_EXIT_OK= +# Note: TEST_TMPDIR *NOT* exported! +TEST_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/notmuch-test-$$.XXXXXX") trap 'die' EXIT test_decode_color () { @@ -213,16 +216,6 @@ remove_cr () { tr '\015' Q | sed -e 's/Q$//' } -# Notmuch helper functions -increment_mtime_amount=0 -increment_mtime () -{ - dir="$1" - - increment_mtime_amount=$((increment_mtime_amount + 1)) - touch -d "+${increment_mtime_amount} seconds" "$dir" -} - # Generate a new message in the mail directory, with a unique message # ID and subject. The message is not added to the index. # @@ -364,9 +357,6 @@ Date: ${template[date]} ${additional_headers} ${template[body]} EOF - - # Ensure that the mtime of the containing directory is updated - increment_mtime "$(dirname "${gen_msg_filename}")" } # Generate a new message and add it to the database. @@ -392,9 +382,22 @@ emacs_deliver_message () shift 2 # before we can send a message, we have to prepare the FCC maildir mkdir -p "$MAIL_DIR"/sent/{cur,new,tmp} - ../smtp-dummy sent_message & + $TEST_DIRECTORY/smtp-dummy sent_message & smtp_dummy_pid=$! - test_emacs "(setq message-send-mail-function 'message-smtpmail-send-it) (setq smtpmail-smtp-server \"localhost\") (setq smtpmail-smtp-service \"25025\") (notmuch-hello) (notmuch-mua-mail) (message-goto-to) (insert \"test_suite@notmuchmail.org\nDate: 01 Jan 2000 12:00:00 -0000\") (message-goto-subject) (insert \"${subject}\") (message-goto-body) (insert \"${body}\") $@ (message-send-and-exit)" >/dev/null 2>&1 + test_emacs \ + "(let ((message-send-mail-function 'message-smtpmail-send-it) + (smtpmail-smtp-server \"localhost\") + (smtpmail-smtp-service \"25025\")) + (notmuch-hello) + (notmuch-mua-mail) + (message-goto-to) + (insert \"test_suite@notmuchmail.org\nDate: 01 Jan 2000 12:00:00 -0000\") + (message-goto-subject) + (insert \"${subject}\") + (message-goto-body) + (insert \"${body}\") + $@ + (message-send-and-exit))" wait ${smtp_dummy_pid} notmuch new >/dev/null } @@ -408,12 +411,12 @@ emacs_deliver_message () add_email_corpus () { rm -rf ${MAIL_DIR} - if [ -d ../corpus.mail ]; then - cp -a ../corpus.mail ${MAIL_DIR} + if [ -d $TEST_DIRECTORY/corpus.mail ]; then + cp -a $TEST_DIRECTORY/corpus.mail ${MAIL_DIR} else - cp -a ../corpus ${MAIL_DIR} + cp -a $TEST_DIRECTORY/corpus ${MAIL_DIR} notmuch new >/dev/null - cp -a ${MAIL_DIR} ../corpus.mail + cp -a ${MAIL_DIR} $TEST_DIRECTORY/corpus.mail fi } @@ -424,7 +427,8 @@ test_begin_subtest () error "bug in test script: Missing test_expect_equal in ${BASH_SOURCE[1]}:${BASH_LINENO[0]}" fi test_subtest_name="$1" - # Remember stdout and stderr file descriptios and redirect test + test_subtest_known_broken_= + # Remember stdout and stderr file descriptors and redirect test # output to the previously prepared file descriptors 3 and 4 (see # below) if test "$verbose" != "t"; then exec 4>test.output 3>&4; fi @@ -448,7 +452,7 @@ test_expect_equal () output="$1" expected="$2" - if ! test_skip "$@" + if ! test_skip "$test_subtest_name" then if [ "$output" = "$expected" ]; then test_ok_ "$test_subtest_name" @@ -461,6 +465,7 @@ test_expect_equal () fi } +# Like test_expect_equal, but takes two filenames. test_expect_equal_file () { exec 1>&6 2>&7 # Restore stdout and stderr @@ -471,7 +476,7 @@ test_expect_equal_file () output="$1" expected="$2" - if ! test_skip "$@" + if ! test_skip "$test_subtest_name" then if diff -q "$expected" "$output" >/dev/null ; then test_ok_ "$test_subtest_name" @@ -484,29 +489,6 @@ test_expect_equal_file () fi } -test_expect_equal_failure () -{ - exec 1>&6 2>&7 # Restore stdout and stderr - inside_subtest= - test "$#" = 3 && { prereq=$1; shift; } || prereq= - test "$#" = 2 || - error "bug in the test script: not 2 or 3 parameters to test_expect_equal" - - output="$1" - expected="$2" - if ! test_skip "$@" - then - if [ "$output" = "$expected" ]; then - test_known_broken_ok_ "$test_subtest_name" - else - test_known_broken_failure_ "$test_subtest_name" - testname=$this_test.$test_count - echo "$expected" > $testname.expected - echo "$output" > $testname.output - fi - fi -} - NOTMUCH_NEW () { notmuch new | grep -v -E -e '^Processed [0-9]*( total)? file|Found [0-9]* total file' @@ -568,31 +550,46 @@ test_have_prereq () { # the text_expect_* functions instead. test_ok_ () { + if test "$test_subtest_known_broken_" = "t"; then + test_known_broken_ok_ "$@" + return + fi test_success=$(($test_success + 1)) say_color pass "%-6s" "PASS" echo " $@" } test_failure_ () { + if test "$test_subtest_known_broken_" = "t"; then + test_known_broken_failure_ "$@" + return + fi test_failure=$(($test_failure + 1)) - say_color error "%-6s" "FAIL" - echo " $1" - shift + test_failure_message_ "FAIL" "$@" + test "$immediate" = "" || { GIT_EXIT_OK=t; exit 1; } + return 1 +} + +test_failure_message_ () { + say_color error "%-6s" "$1" + echo " $2" + shift 2 echo "$@" | sed -e 's/^/ /' if test "$verbose" != "t"; then cat test.output; fi - test "$immediate" = "" || { GIT_EXIT_OK=t; exit 1; } } test_known_broken_ok_ () { + test_subtest_known_broken_= test_fixed=$(($test_fixed+1)) say_color pass "%-6s" "FIXED" echo " $@" } test_known_broken_failure_ () { + test_subtest_known_broken_= test_broken=$(($test_broken+1)) - say_color pass "%-6s" "BROKEN" - echo " $@" + test_failure_message_ "BROKEN" "$@" + return 1 } test_debug () { @@ -625,6 +622,7 @@ test_skip () { fi case "$to_skip" in t) + test_subtest_known_broken_= say_color skip >&3 "skipping test: $@" say_color skip "%-6s" "SKIP" echo " $1" @@ -636,20 +634,8 @@ test_skip () { esac } -test_expect_failure () { - test "$#" = 3 && { prereq=$1; shift; } || prereq= - test "$#" = 2 || - error "bug in the test script: not 2 or 3 parameters to test-expect-failure" - if ! test_skip "$@" - then - test_run_ "$2" - if [ "$?" = 0 -a "$eval_ret" = 0 ] - then - test_known_broken_ok_ "$1" - else - test_known_broken_failure_ "$1" - fi - fi +test_subtest_known_broken () { + test_subtest_known_broken_=t } test_expect_success () { @@ -742,7 +728,7 @@ test_external_without_stderr () { fi } -# This is not among top-level (test_expect_success | test_expect_failure) +# This is not among top-level (test_expect_success) # but is a prefix that can be used in the test script, like: # # test_expect_success 'complain and die' ' @@ -816,6 +802,8 @@ test_done () { echo + [ -n "$EMACS_SERVER" ] && test_emacs '(kill-emacs)' + if [ "$test_failure" = "0" ]; then if [ "$test_broken" = "0" ]; then rm -rf "$remove_tmp" @@ -826,25 +814,17 @@ test_done () { fi } -test_emacs () { +emacs_generate_script () { # Construct a little test script here for the benefit of the user, # (who can easily run "run_emacs" to get the same emacs environment # for investigating any failures). - cat <<EOF > run_emacs + cat <<EOF >"$TMP_DIRECTORY/run_emacs" #!/bin/sh export PATH=$PATH export NOTMUCH_CONFIG=$NOTMUCH_CONFIG -# We assume that the user will give a command-line argument only if -# wanting to run in batch mode. -if [ \$# -gt 0 ]; then - BATCH=--batch -fi - # Here's what we are using here: # -# --batch: Quit after given commands and print all (messages) -# # --no-init-file Don't load users ~/.emacs # # --no-site-file Don't load the site-wide startup stuff @@ -852,24 +832,34 @@ fi # --directory Ensure that the local elisp sources are found # # --load Force loading of notmuch.el and test-lib.el -# -# notmuch-test-wait Function for tests to use to wait for process completion -# -# message-signature Avoiding appending user's signature on messages -# -# set-frame-width 80 columns (avoids crazy 10-column default of --batch) -emacs \$BATCH --no-init-file --no-site-file \ - --directory ../../emacs --load notmuch.el \ - --directory .. --load test-lib.el \ - --eval "(defun notmuch-test-wait () - (while (get-buffer-process (current-buffer)) - (sleep-for 0.1)))" \ - --eval "(setq message-signature nil)" \ - --eval "(progn (set-frame-width (window-frame (get-buffer-window)) 80) \$@)" +exec emacs --no-init-file --no-site-file \ + --directory "$TEST_DIRECTORY/../emacs" --load notmuch.el \ + --directory "$TEST_DIRECTORY" --load test-lib.el \ + "\$@" EOF - chmod a+x ./run_emacs - ./run_emacs "$@" + chmod a+x "$TMP_DIRECTORY/run_emacs" +} + +test_emacs () { + if [ -z "$EMACS_SERVER" ]; then + EMACS_SERVER="notmuch-test-suite-$$" + # start a detached session with an emacs server + # user's TERM is given to dtach which assumes a minimally + # VT100-compatible terminal -- and emacs inherits that + TERM=$ORIGINAL_TERM dtach -n "$TEST_TMPDIR/emacs-dtach-socket.$$" \ + sh -c "stty rows 24 cols 80; exec '$TMP_DIRECTORY/run_emacs' \ + --no-window-system \ + --eval '(setq server-name \"$EMACS_SERVER\")' \ + --eval '(server-start)' \ + --eval '(orphan-watchdog $$)'" || return + # wait until the emacs server is up + until test_emacs '()' 2>/dev/null; do + sleep 1 + done + fi + + emacsclient --socket-name="$EMACS_SERVER" --eval "(progn $@)" } @@ -925,11 +915,11 @@ then test ! -d "$symlink_target" && test "#!" != "$(head -c 2 < "$symlink_target")" then - symlink_target=../valgrind.sh + symlink_target=$TEST_DIRECTORY/valgrind.sh fi case "$base" in *.sh|*.perl) - symlink_target=../unprocessed-script + symlink_target=$TEST_DIRECTORY/unprocessed-script esac # create the link, or replace it if it is out of date make_symlink "$symlink_target" "$GIT_VALGRIND/bin/$base" || exit @@ -997,6 +987,8 @@ primary_email=test_suite@notmuchmail.org other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org EOF +emacs_generate_script + # Use -P to resolve symlinks in our working directory so that the cwd # in subprocesses like git equals our $PWD (for pathname comparisons). diff --git a/test/uuencode b/test/uuencode index d0d16bdd..b3e1ac19 100755 --- a/test/uuencode +++ b/test/uuencode @@ -5,11 +5,11 @@ test_description="handling of uuencoded data" add_message [subject]=uuencodetest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' \ '[body]="This message is used to ensure that notmuch correctly handles a message containing a block of uuencoded data. First, we have a marker -this content beforeuudata . Then we beging the uunencoded data itself: +this content beforeuudata . Then we begin the uuencoded data itself: begin 644 bogus-uuencoded-data M0123456789012345678901234567890123456789012345678901234567890 -MOBVIOUSLY, THIS IS NOT ANY SORT OF USEFUL UUNECODED DATA. +MOBVIOUSLY, THIS IS NOT ANY SORT OF USEFUL UUENCODED DATA. MINSTEAD THIS IS JUST A WAY TO ENSURE THAT THIS BLOCK OF DATA MIS CORRECTLY IGNORED WHEN NOTMUCH CREATES ITS INDEX. SO WE MINCLUDE A DURINGUUDATA MARKER THAT SHOULD NOT RESULT IN ANY diff --git a/util/Makefile b/util/Makefile new file mode 100644 index 00000000..fa25832e --- /dev/null +++ b/util/Makefile @@ -0,0 +1,5 @@ +all: + $(MAKE) -C .. all + +.DEFAULT: + $(MAKE) -C .. $@ diff --git a/util/Makefile.local b/util/Makefile.local new file mode 100644 index 00000000..2ff42b3d --- /dev/null +++ b/util/Makefile.local @@ -0,0 +1,11 @@ +# -*- makefile -*- + +dir := util +extra_cflags += -I$(srcdir)/$(dir) + +libutil_c_srcs := $(dir)/xutil.c $(dir)/error_util.c + +libutil_modules := $(libutil_c_srcs:.c=.o) + +$(dir)/libutil.a: $(libutil_modules) + $(call quiet,AR) rcs $@ $^ diff --git a/util/error_util.c b/util/error_util.c new file mode 100644 index 00000000..630d2281 --- /dev/null +++ b/util/error_util.c @@ -0,0 +1,41 @@ +/* error_util.c - internal error utilities for notmuch. + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth <cworth@cworth.org> + */ + +#include <stdlib.h> +#include <stdarg.h> +#include <stdio.h> + +#include "error_util.h" + +int +_internal_error (const char *format, ...) +{ + va_list va_args; + + va_start (va_args, format); + + fprintf (stderr, "Internal error: "); + vfprintf (stderr, format, va_args); + + exit (1); + + return 1; +} + diff --git a/util/error_util.h b/util/error_util.h new file mode 100644 index 00000000..bb158220 --- /dev/null +++ b/util/error_util.h @@ -0,0 +1,45 @@ +/* error_util.h - Provide the INTERNAL_ERROR macro + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth <cworth@cworth.org> + */ + +#ifndef ERROR_UTIL_H +#define ERROR_UTIL_H + +#include <talloc.h> + +/* There's no point in continuing when we've detected that we've done + * something wrong internally (as opposed to the user passing in a + * bogus value). + * + * Note that PRINTF_ATTRIBUTE comes from talloc.h + */ +int +_internal_error (const char *format, ...) PRINTF_ATTRIBUTE (1, 2); + +/* There's no point in continuing when we've detected that we've done + * something wrong internally (as opposed to the user passing in a + * bogus value). + * + * Note that __location__ comes from talloc.h. + */ +#define INTERNAL_ERROR(format, ...) \ + _internal_error (format " (%s).\n", \ + ##__VA_ARGS__, __location__) + +#endif diff --git a/lib/xutil.c b/util/xutil.c index 268225b8..ac496daf 100644 --- a/lib/xutil.c +++ b/util/xutil.c @@ -18,9 +18,11 @@ * Author: Carl Worth <cworth@cworth.org> */ -#include "notmuch-private.h" - #include <stdio.h> +#include <string.h> + +#include "xutil.h" +#include "error_util.h" void * xcalloc (size_t nmemb, size_t size) @@ -97,7 +99,7 @@ xstrndup (const char *s, size_t n) return ret; } -void +int xregcomp (regex_t *preg, const char *regex, int cflags) { int rerr; @@ -108,9 +110,12 @@ xregcomp (regex_t *preg, const char *regex, int cflags) char *error = xmalloc (error_size); regerror (rerr, preg, error, error_size); - INTERNAL_ERROR ("compiling regex %s: %s\n", + fprintf (stderr, "compiling regex %s: %s\n", regex, error); + free (error); + return 1; } + return 0; } int diff --git a/lib/xutil.h b/util/xutil.h index fd77f733..b84e0e25 100644 --- a/lib/xutil.h +++ b/util/xutil.h @@ -25,8 +25,6 @@ #include <sys/types.h> #include <regex.h> -#pragma GCC visibility push(hidden) - /* xutil.c */ void * xcalloc (size_t nmemb, size_t size); @@ -43,13 +41,12 @@ xstrdup (const char *s); char * xstrndup (const char *s, size_t n); -void +/* Returns 0 for successful compilation, 1 otherwise */ +int xregcomp (regex_t *preg, const char *regex, int cflags); int xregexec (const regex_t *preg, const char *string, size_t nmatch, regmatch_t pmatch[], int eflags); -#pragma GCC visibility pop - #endif @@ -1 +1 @@ -0.6 +0.10.2 @@ -7,8 +7,9 @@ Dependencies: notmuch: Naturally, it expects you have notmuch installed and configured. - mailx: - To send mail, notmuch.vim uses the UNIX mailx command. + sendmail: + To send mail, notmuch.vim uses sendmail as default. Most modern MTAs + provide a compatibility binary, and so should work well. To install: @@ -37,7 +38,7 @@ Buffer types: You are presented with the search results when you run :NotMuch. Keybindings: - <Space> - show the selected thread colapsing unmatched items + <Space> - show the selected thread collapsing unmatched items <Enter> - show the entire selected thread a - archive message (remove inbox tag) f - filter the current search terms diff --git a/vim/plugin/notmuch.vim b/vim/plugin/notmuch.vim index c731c471..21985c71 100644 --- a/vim/plugin/notmuch.vim +++ b/vim/plugin/notmuch.vim @@ -25,7 +25,7 @@ let s:notmuch_defaults = { \ 'g:notmuch_cmd': 'notmuch' , - \ 'g:notmuch_sendmail': 'sendmail' , + \ 'g:notmuch_sendmail': '/usr/sbin/sendmail' , \ 'g:notmuch_debug': 0 , \ \ 'g:notmuch_search_newest_first': 1 , @@ -58,7 +58,7 @@ let s:notmuch_defaults = { \ \ 'g:notmuch_compose_insert_mode_start': 1 , \ 'g:notmuch_compose_header_help': 1 , - \ 'g:notmuch_compose_temp_file_dir': '~/.notmuch/compose/' , + \ 'g:notmuch_compose_temp_file_dir': '~/.notmuch/compose' , \ } " defaults for g:notmuch_initial_search_words @@ -149,7 +149,7 @@ let g:notmuch_show_maps = { \ 'b': ':call <SID>NM_show_fold_toggle(''b'', ''bdy'', !g:notmuch_show_fold_bodies)<CR>', \ 'c': ':call <SID>NM_show_fold_toggle(''c'', ''cit'', !g:notmuch_show_fold_citations)<CR>', \ 'h': ':call <SID>NM_show_fold_toggle(''h'', ''hdr'', !g:notmuch_show_fold_headers)<CR>', - \ 'i': ':call <SID>NM_show_fold_toggle(''s'', ''sig'', !g:notmuch_show_fold_signatures)<CR>', + \ 'i': ':call <SID>NM_show_fold_toggle(''i'', ''sig'', !g:notmuch_show_fold_signatures)<CR>', \ \ 'I': ':call <SID>NM_show_mark_read_thread()<CR>', \ 'a': ':call <SID>NM_show_archive_thread()<CR>', @@ -262,12 +262,12 @@ function! s:NM_cmd_search_fmtline(line) return 'ERROR PARSING: ' . a:line endif let max = g:notmuch_search_from_column_width - let flist = [] - for at in split(m[4], ", ") - let p = min([stridx(at, "."), stridx(at, "@")]) - call insert(flist, tolower(at[0:p - 1])) + let flist = {} + for at in split(m[4], '[|,] ') + let p = split(at, '[@.]') + let flist[p[0]] = 1 endfor - let from = join(flist, ", ") + let from = join(keys(flist), ", ") return printf("%-12s %3s %-20.20s | %s (%s)", m[2], m[3], from, m[5], m[6]) endfunction @@ -596,7 +596,7 @@ function! s:NM_show_advance_marking_read_and_archiving() let filter = <SID>NM_combine_tags('tag:', advance_tags, 'OR', '()') \ + ['AND'] \ + <SID>NM_combine_tags('', ids, 'OR', '()') - call map(advance_tags, '"+" . v:val') + call map(advance_tags, '"-" . v:val') call <SID>NM_tag(filter, advance_tags) call <SID>NM_show_next(1, 1) return @@ -747,8 +747,11 @@ function! s:NM_cmd_show_parse(inlines) elseif mode_type == 'cit' if part_end || match(line, g:notmuch_show_citation_regexp) == -1 let outlnum = len(info['disp']) - let foldinfo = [ mode_type, mode_start, outlnum-1, len(info['msgs']), - \ printf('[ %d-line citation. Press "c" to show. ]', outlnum - mode_start) ] + if !part_end + let outlnum = outlnum - 1 + endif + let foldinfo = [ mode_type, mode_start, outlnum, len(info['msgs']), + \ printf('[ %d-line citation. Press "c" to show. ]', 1 + outlnum - mode_start) ] let mode_type = '' endif elseif mode_type == 'sig' @@ -756,8 +759,8 @@ function! s:NM_cmd_show_parse(inlines) if (outlnum - mode_start) > g:notmuch_show_signature_lines_max let mode_type = '' elseif part_end - let foldinfo = [ mode_type, mode_start, outlnum-1, len(info['msgs']), - \ printf('[ %d-line signature. Press "s" to show. ]', outlnum - mode_start) ] + let foldinfo = [ mode_type, mode_start, outlnum, len(info['msgs']), + \ printf('[ %d-line signature. Press "i" to show. ]', 1 + outlnum - mode_start) ] let mode_type = '' endif endif @@ -765,7 +768,7 @@ function! s:NM_cmd_show_parse(inlines) if part_end " FIXME: this is a hack for handling two folds being added for one line - " we should handle addinga fold in a function + " we should handle adding a fold in a function if len(foldinfo) && foldinfo[1] < foldinfo[2] call add(info['folds'], foldinfo[0:3]) let info['foldtext'][foldinfo[1]] = foldinfo[4] @@ -796,7 +799,14 @@ function! s:NM_cmd_show_parse(inlines) endif call add(info['disp'], \ printf('--- %s ---', in_part)) - let part_start = len(info['disp']) + 1 + " We don't yet handle nested parts, so pop + " multipart/* immediately so text/plain + " sub-parts are parsed properly + if match(in_part, '^multipart/') != -1 + let in_part = '' + else + let part_start = len(info['disp']) + 1 + endif endif elseif in_header @@ -1314,7 +1324,7 @@ endfunction function! s:NM_tag(filter, tags) let filter = len(a:filter) ? a:filter : [<SID>NM_search_thread_id()] if !len(filter) - throw 'Eeek! I couldn''t find the thead id!' + throw 'Eeek! I couldn''t find the thread id!' endif let args = ['tag'] call extend(args, a:tags) diff --git a/xutil.c b/xutil.c deleted file mode 100644 index 5f98f3f7..00000000 --- a/xutil.c +++ /dev/null @@ -1,138 +0,0 @@ -/* xutil.c - Various wrapper functions to abort on error. - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth <cworth@cworth.org> - */ - -#include "notmuch-private.h" - -#include <stdio.h> - -void * -xcalloc (size_t nmemb, size_t size) -{ - void *ret; - - ret = calloc (nmemb, size); - if (ret == NULL) { - fprintf (stderr, "Out of memory.\n"); - exit (1); - } - - return ret; -} - -void * -xmalloc (size_t size) -{ - void *ret; - - ret = malloc (size); - if (ret == NULL) { - fprintf (stderr, "Out of memory.\n"); - exit (1); - } - - return ret; -} - -void * -xrealloc (void *ptr, size_t size) -{ - void *ret; - - ret = realloc (ptr, size); - if (ret == NULL) { - fprintf (stderr, "Out of memory.\n"); - exit (1); - } - - return ret; -} - -char * -xstrdup (const char *s) -{ - char *ret; - - ret = strdup (s); - if (ret == NULL) { - fprintf (stderr, "Out of memory.\n"); - exit (1); - } - - return ret; -} - -char * -xstrndup (const char *s, size_t n) -{ - char *ret; - - if (strlen (s) <= n) - n = strlen (s); - - ret = malloc (n + 1); - if (ret == NULL) { - fprintf (stderr, "Out of memory.\n"); - exit (1); - } - memcpy (ret, s, n); - ret[n] = '\0'; - - return ret; -} - -void -xregcomp (regex_t *preg, const char *regex, int cflags) -{ - int rerr; - - rerr = regcomp (preg, regex, cflags); - if (rerr) { - size_t error_size = regerror (rerr, preg, NULL, 0); - char *error = xmalloc (error_size); - - regerror (rerr, preg, error, error_size); - fprintf (stderr, "Internal error: compiling regex %s: %s\n", - regex, error); - exit (1); - } -} - -int -xregexec (const regex_t *preg, const char *string, - size_t nmatch, regmatch_t pmatch[], int eflags) -{ - unsigned int i; - int rerr; - - rerr = regexec (preg, string, nmatch, pmatch, eflags); - if (rerr) - return rerr; - - for (i = 0; i < nmatch; i++) { - if (pmatch[i].rm_so == -1) { - fprintf (stderr, "Internal error: matching regex against %s:" - "Sub-match %d not found\n", - string, i); - exit (1); - } - } - - return 0; -} |
